Merge branch 'release/1.5.12'

This commit is contained in:
Julian Raufelder 2021-02-22 11:47:04 +01:00
commit fc78d428cc
No known key found for this signature in database
GPG Key ID: 17EE71F6634E381D
111 changed files with 1457 additions and 162 deletions

11
.gitignore vendored
View File

@ -31,11 +31,6 @@ build/
local.properties local.properties
# fastlane # fastlane
secret_key_file.json **/fastlane/.env
**/**/fastlane/fastlane/** **/fastlane/metadata/**/images/**
**/**/fastlane/metadata/** **/fastlane/report.xml
**/**/fastlane/report.xml
**/**/fastlane/mappings/**
**/**/fastlane/release_notes/**
**/**/fastlane/latest_versions/**
.env.default

9
Gemfile Normal file
View File

@ -0,0 +1,9 @@
source "https://rubygems.org"
gem "fastlane"
gem "net-sftp"
gem "ed25519"
gem "bcrypt_pbkdf"
plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile')
eval_gemfile(plugins_path) if File.exist?(plugins_path)

221
Gemfile.lock Normal file
View File

@ -0,0 +1,221 @@
GEM
remote: https://rubygems.org/
specs:
CFPropertyList (3.0.3)
addressable (2.7.0)
public_suffix (>= 2.0.2, < 5.0)
apktools (0.7.4)
rubyzip (~> 2.0)
artifactory (3.0.15)
atomos (0.1.3)
aws-eventstream (1.1.0)
aws-partitions (1.426.0)
aws-sdk-core (3.112.0)
aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.239.0)
aws-sigv4 (~> 1.1)
jmespath (~> 1.0)
aws-sdk-kms (1.42.0)
aws-sdk-core (~> 3, >= 3.112.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.88.0)
aws-sdk-core (~> 3, >= 3.112.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.1)
aws-sigv4 (1.2.2)
aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.4)
bcrypt_pbkdf (1.0.1)
claide (1.0.3)
colored (1.2)
colored2 (3.1.2)
commander-fastlane (4.4.6)
highline (~> 1.7.2)
declarative (0.0.20)
declarative-option (0.1.0)
digest-crc (0.6.3)
rake (>= 12.0.0, < 14.0.0)
domain_name (0.5.20190701)
unf (>= 0.0.5, < 1.0.0)
dotenv (2.7.6)
ed25519 (1.2.4)
emoji_regex (3.2.1)
excon (0.79.0)
faraday (1.3.0)
faraday-net_http (~> 1.0)
multipart-post (>= 1.2, < 3)
ruby2_keywords
faraday-cookie_jar (0.0.7)
faraday (>= 0.8.0)
http-cookie (~> 1.0.0)
faraday-net_http (1.0.1)
faraday_middleware (1.0.0)
faraday (~> 1.0)
fastimage (2.2.2)
fastlane (2.174.0)
CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.3, < 3.0.0)
artifactory (~> 3.0)
aws-sdk-s3 (~> 1.0)
babosa (>= 1.0.3, < 2.0.0)
bundler (>= 1.12.0, < 3.0.0)
colored
commander-fastlane (>= 4.4.6, < 5.0.0)
dotenv (>= 2.1.1, < 3.0.0)
emoji_regex (>= 0.1, < 4.0)
excon (>= 0.71.0, < 1.0.0)
faraday (~> 1.0)
faraday-cookie_jar (~> 0.0.6)
faraday_middleware (~> 1.0)
fastimage (>= 2.1.0, < 3.0.0)
gh_inspector (>= 1.1.2, < 2.0.0)
google-api-client (>= 0.37.0, < 0.39.0)
google-cloud-storage (>= 1.15.0, < 2.0.0)
highline (>= 1.7.2, < 2.0.0)
json (< 3.0.0)
jwt (>= 2.1.0, < 3)
mini_magick (>= 4.9.4, < 5.0.0)
multipart-post (~> 2.0.0)
plist (>= 3.1.0, < 4.0.0)
rubyzip (>= 2.0.0, < 3.0.0)
security (= 0.1.3)
simctl (~> 1.6.3)
slack-notifier (>= 2.0.0, < 3.0.0)
terminal-notifier (>= 2.0.0, < 3.0.0)
terminal-table (>= 1.4.5, < 2.0.0)
tty-screen (>= 0.6.3, < 1.0.0)
tty-spinner (>= 0.8.0, < 1.0.0)
word_wrap (~> 1.0.0)
xcodeproj (>= 1.13.0, < 2.0.0)
xcpretty (~> 0.3.0)
xcpretty-travis-formatter (>= 0.0.3)
fastlane-plugin-aws_s3 (1.8.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-api-client (0.38.0)
addressable (~> 2.5, >= 2.5.1)
googleauth (~> 0.9)
httpclient (>= 2.8.1, < 3.0)
mini_mime (~> 1.0)
representable (~> 3.0)
retriable (>= 2.0, < 4.0)
signet (~> 0.12)
google-apis-core (0.2.1)
addressable (~> 2.5, >= 2.5.1)
googleauth (~> 0.14)
httpclient (>= 2.8.1, < 3.0)
mini_mime (~> 1.0)
representable (~> 3.0)
retriable (>= 2.0, < 4.0)
rexml
signet (~> 0.14)
webrick
google-apis-iamcredentials_v1 (0.1.0)
google-apis-core (~> 0.1)
google-apis-storage_v1 (0.2.0)
google-apis-core (~> 0.1)
google-cloud-core (1.5.0)
google-cloud-env (~> 1.0)
google-cloud-errors (~> 1.0)
google-cloud-env (1.4.0)
faraday (>= 0.17.3, < 2.0)
google-cloud-errors (1.0.1)
google-cloud-storage (1.30.0)
addressable (~> 2.5)
digest-crc (~> 0.4)
google-apis-iamcredentials_v1 (~> 0.1)
google-apis-storage_v1 (~> 0.1)
google-cloud-core (~> 1.2)
googleauth (~> 0.9)
mini_mime (~> 1.0)
googleauth (0.15.1)
faraday (>= 0.17.3, < 2.0)
jwt (>= 1.4, < 3.0)
memoist (~> 0.16)
multi_json (~> 1.11)
os (>= 0.9, < 2.0)
signet (~> 0.14)
highline (1.7.10)
http-cookie (1.0.3)
domain_name (~> 0.5)
httpclient (2.8.3)
jmespath (1.4.0)
json (2.5.1)
jwt (2.2.2)
memoist (0.16.2)
mime-types (3.3.1)
mime-types-data (~> 3.2015)
mime-types-data (3.2020.1104)
mini_magick (4.11.0)
mini_mime (1.0.2)
multi_json (1.15.0)
multipart-post (2.0.0)
nanaimo (0.3.0)
naturally (2.2.1)
net-sftp (2.1.2)
net-ssh (>= 2.6.5)
net-ssh (5.2.0)
os (1.1.1)
plist (3.6.0)
public_suffix (4.0.6)
rake (13.0.3)
representable (3.0.4)
declarative (< 0.1.0)
declarative-option (< 0.2.0)
uber (< 0.2.0)
retriable (3.1.2)
rexml (3.2.4)
rouge (2.0.7)
ruby2_keywords (0.0.4)
rubyzip (2.3.0)
security (0.1.3)
signet (0.14.1)
addressable (~> 2.3)
faraday (>= 0.17.3, < 2.0)
jwt (>= 1.5, < 3.0)
multi_json (~> 1.10)
simctl (1.6.8)
CFPropertyList
naturally
slack-notifier (2.3.2)
terminal-notifier (2.0.0)
terminal-table (1.8.0)
unicode-display_width (~> 1.1, >= 1.1.1)
tty-cursor (0.7.1)
tty-screen (0.8.1)
tty-spinner (0.9.3)
tty-cursor (~> 0.7)
uber (0.1.0)
unf (0.1.4)
unf_ext
unf_ext (0.0.7.7)
unicode-display_width (1.7.0)
webrick (1.7.0)
word_wrap (1.0.0)
xcodeproj (1.19.0)
CFPropertyList (>= 2.3.3, < 4.0)
atomos (~> 0.1.3)
claide (>= 1.0.2, < 2.0)
colored2 (~> 3.1)
nanaimo (~> 0.3.0)
xcpretty (0.3.0)
rouge (~> 2.0.7)
xcpretty-travis-formatter (1.0.1)
xcpretty (~> 0.2, >= 0.0.7)
PLATFORMS
ruby
DEPENDENCIES
bcrypt_pbkdf
ed25519
fastlane
fastlane-plugin-aws_s3
fastlane-plugin-get_version_name
net-sftp
BUNDLED WITH
2.2.5

View File

@ -42,6 +42,42 @@ Please read our [contribution guide](.github/CONTRIBUTING.md), if you would like
Help us keep Cryptomator open and inclusive. Please read and follow our [Code of Conduct](.github/CODE_OF_CONDUCT.md). Help us keep Cryptomator open and inclusive. Please read and follow our [Code of Conduct](.github/CODE_OF_CONDUCT.md).
## Deployment
Follow these steps to deploy a release:
1. Check `TODO`/`FIXME` comments
- Create issue for or delete
- Regexp for "Find in Path": `\W(TODO|FIXME)(?! #[0-9]{1,4}:)`
1. Merge translations
1. Check latest dependencies
1. Create release branch
1. Test database migration
1. Smoke-Test changed or added functionality
1. Update version
1. Create and commit release notes
1. Merge in `master`
1. Create tag and execute deploy app using Fastlane
1. Close GitHub-issues or move them to next milestone
1. Close milestone
1. Update version on website ([cryptomator.org/android](https://cryptomator.org/android/))
### Release Notes
Before tagging the release, create and commit the release notes. For Playstore create [fastlane/metadata/android/de-DE/changelogs/default.txt](https://github.com/cryptomator/android/blob/develop/fastlane/metadata/android/de-DE/changelogs/default.txt), [fastlane/metadata/android/en-US/changelogs/default.txt](https://github.com/cryptomator/android/blob/develop/fastlane/metadata/android/en-US/changelogs/default.txt) and for the website create [fastlane/release_notes_apkstore_en.html](https://github.com/cryptomator/android/blob/develop/fastlane/release_notes_apkstore_en.html).
### Deploy app using Fastlane
Deploy production version to Google Play, Website/GitHub-Releases and F-Droid using `fastlane android deploy` or `bundle exec fastlane deploy`
There are further targets and options like `beta`, see [fastlane/README.md](https://github.com/cryptomator/android/blob/develop/fastlane/README.md)
### Initial setup Fastlane
1. Make sure you copied `.default.env` to `.env` in the `fastlane` folder and filled out those variables.
1. Install Ruby (depends on OS, Ubuntu): `sudo apt install ruby-dev`
1. Install fastlane (depends on OS, Ubuntu): `gem install fastlane -N`
## License ## License
This project is dual-licensed under the GPLv3 for FOSS projects as well as a commercial license for independent software vendors and resellers. If you want to modify this application under different conditions, feel free to contact our support team. This project is dual-licensed under the GPLv3 for FOSS projects as well as a commercial license for independent software vendors and resellers. If you want to modify this application under different conditions, feel free to contact our support team.

View File

@ -3,7 +3,7 @@ apply from: 'buildsystem/dependencies.gradle'
apply plugin: "com.vanniktech.android.junit.jacoco" apply plugin: "com.vanniktech.android.junit.jacoco"
buildscript { buildscript {
ext.kotlin_version = '1.4.21' ext.kotlin_version = '1.4.30'
repositories { repositories {
jcenter() jcenter()
mavenCentral() mavenCentral()
@ -42,7 +42,7 @@ allprojects {
ext { ext {
androidApplicationId = 'org.cryptomator' androidApplicationId = 'org.cryptomator'
androidVersionCode = getVersionCode() androidVersionCode = getVersionCode()
androidVersionName = '1.5.11' androidVersionName = '1.5.12'
} }
repositories { repositories {
mavenCentral() mavenCentral()

View File

@ -18,27 +18,26 @@ ext {
// support lib // support lib
androidSupportAnnotationsVersion = '1.1.0' androidSupportAnnotationsVersion = '1.1.0'
androidSupportAppcompatVersion = '1.2.0' androidSupportAppcompatVersion = '1.2.0'
// check https://stackoverflow.com/questions/41025200/android-view-inflateexception-error-inflating-class-android-webkit-webview/57968071#57968071 !!!!!! androidSupportDesignVersion = '1.3.0'
androidSupportDesignVersion = '1.2.1'
// app frameworks and utilities // app frameworks and utilities
rxJavaVersion = '2.2.20' rxJavaVersion = '2.2.21'
rxAndroidVersion = '2.1.1' rxAndroidVersion = '2.1.1'
rxBindingVersion = '2.2.0' rxBindingVersion = '2.2.0'
daggerVersion = '2.31.2' daggerVersion = '2.32'
gsonVersion = '2.8.6' gsonVersion = '2.8.6'
okHttpVersion = '4.9.0' okHttpVersion = '4.9.1'
okHttpDigestVersion = '2.5' okHttpDigestVersion = '2.5'
velocityVersion = '1.7' velocityVersion = '1.7'
timberVersion = '4.7.1' timberVersion = '4.7.1'
zxcvbnVersion = '1.3.3' zxcvbnVersion = '1.3.6'
scaleImageViewVersion = '3.10.0' scaleImageViewVersion = '3.10.0'
@ -58,7 +57,7 @@ ext {
googlePlayServicesVersion = '19.0.0' googlePlayServicesVersion = '19.0.0'
googleClientVersion = '1.31.2' googleClientVersion = '1.31.2'
msgraphVersion = '2.5.0' msgraphVersion = '2.7.1'
msaAuthVersion = '0.10.0' msaAuthVersion = '0.10.0'
commonsCodecVersion = '1.15' commonsCodecVersion = '1.15'
@ -67,7 +66,7 @@ ext {
// testing dependencies // testing dependencies
jUnitVersion = '5.7.0' jUnitVersion = '5.7.1'
jUnit4Version = '4.13.1' jUnit4Version = '4.13.1'
assertJVersion = '1.7.1' assertJVersion = '1.7.1'
mockitoVersion = '3.7.7' mockitoVersion = '3.7.7'
@ -82,13 +81,13 @@ ext {
uiautomatorVersion = '2.2.0' uiautomatorVersion = '2.2.0'
androidxCoreVersion = '1.3.2' androidxCoreVersion = '1.3.2'
androidxFragmentVersion = '1.2.5' androidxFragmentVersion = '1.3.0'
androidxViewpagerVersion = '1.0.0' androidxViewpagerVersion = '1.0.0'
androidxSwiperefreshVersion = '1.1.0' androidxSwiperefreshVersion = '1.1.0'
androidxPreferenceVersion = '1.0.0' // 1.1.0 and 1.1.2 does have a bug with the text size androidxPreferenceVersion = '1.0.0' // 1.1.0 and 1.1.2 does have a bug with the text size
androidxRecyclerViewVersion = '1.1.0' androidxRecyclerViewVersion = '1.1.0'
androidxDocumentfileVersion = '1.0.1' androidxDocumentfileVersion = '1.0.1'
androidxBiometricVersion = '1.0.1' androidxBiometricVersion = '1.1.0'
androidxTestCoreVersion = '1.3.0' androidxTestCoreVersion = '1.3.0'
jsonWebTokenApiVersion = '0.11.2' jsonWebTokenApiVersion = '0.11.2'

View File

@ -74,7 +74,7 @@ android {
} }
greendao { greendao {
schemaVersion 3 schemaVersion 4
} }
configurations.all { configurations.all {

View File

@ -1,7 +1,6 @@
package org.cryptomator.data.cloud.local; package org.cryptomator.data.cloud.local;
import android.content.Context; import android.content.Context;
import android.os.Build;
import org.cryptomator.data.cloud.local.file.LocalStorageContentRepository; import org.cryptomator.data.cloud.local.file.LocalStorageContentRepository;
import org.cryptomator.data.cloud.local.storageaccessframework.LocalStorageAccessFrameworkContentRepository; import org.cryptomator.data.cloud.local.storageaccessframework.LocalStorageAccessFrameworkContentRepository;
@ -43,7 +42,7 @@ public class LocalStorageContentRepositoryFactory implements CloudContentReposit
if (!hasPermissions(WRITE_EXTERNAL_STORAGE, READ_EXTERNAL_STORAGE)) { if (!hasPermissions(WRITE_EXTERNAL_STORAGE, READ_EXTERNAL_STORAGE)) {
throw new NoAuthenticationProvidedException(cloud); throw new NoAuthenticationProvidedException(cloud);
} }
if (((LocalStorageCloud) cloud).rootUri() != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { if (((LocalStorageCloud) cloud).rootUri() != null) {
return new LocalStorageAccessFrameworkContentRepository(context, mimeTypes, (LocalStorageCloud) cloud); return new LocalStorageAccessFrameworkContentRepository(context, mimeTypes, (LocalStorageCloud) cloud);
} else { } else {
return new LocalStorageContentRepository(context, (LocalStorageCloud) cloud); return new LocalStorageContentRepository(context, (LocalStorageCloud) cloud);

View File

@ -152,7 +152,7 @@ public abstract class MSAAuthAndroidAdapter implements IAuthenticationAdapter {
public void onAuthComplete(final LiveStatus status, final LiveConnectSession session, final Object userState) { public void onAuthComplete(final LiveStatus status, final LiveConnectSession session, final Object userState) {
Timber.tag("MSAAuthAndroidAdapter").d(String.format("LiveStatus: %s, LiveConnectSession good?: %s, UserState %s", status, session != null, userState)); Timber.tag("MSAAuthAndroidAdapter").d(String.format("LiveStatus: %s, LiveConnectSession good?: %s, UserState %s", status, session != null, userState));
if (status == LiveStatus.NOT_CONNECTED) { if (status == LiveStatus.NOT_CONNECTED && session.getRefreshToken() == null) {
Timber.tag("MSAAuthAndroidAdapter").d("Received invalid login failure from silent authentication, ignoring."); Timber.tag("MSAAuthAndroidAdapter").d("Received invalid login failure from silent authentication, ignoring.");
return; return;
} }

View File

@ -21,12 +21,14 @@ class DatabaseUpgrades {
public DatabaseUpgrades( // public DatabaseUpgrades( //
Upgrade0To1 upgrade0To1, // Upgrade0To1 upgrade0To1, //
Upgrade1To2 upgrade1To2, // Upgrade1To2 upgrade1To2, //
Upgrade2To3 upgrade2To3) { Upgrade2To3 upgrade2To3, //
Upgrade3To4 upgrade3To4) {
availableUpgrades = defineUpgrades( // availableUpgrades = defineUpgrades( //
upgrade0To1, // upgrade0To1, //
upgrade1To2, // upgrade1To2, //
upgrade2To3); upgrade2To3, //
upgrade3To4);
} }
private Map<Integer, List<DatabaseUpgrade>> defineUpgrades(DatabaseUpgrade... upgrades) { private Map<Integer, List<DatabaseUpgrade>> defineUpgrades(DatabaseUpgrade... upgrades) {

View File

@ -0,0 +1,66 @@
package org.cryptomator.data.db
import org.cryptomator.data.db.Sql.SqlCreateTableBuilder.ForeignKeyBehaviour
import org.cryptomator.data.db.entities.CloudEntityDao
import org.cryptomator.data.db.entities.VaultEntityDao
import org.greenrobot.greendao.database.Database
import org.greenrobot.greendao.internal.DaoConfig
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
internal class Upgrade3To4 @Inject constructor() : DatabaseUpgrade(3, 4) {
override fun internalApplyTo(db: Database, origin: Int) {
db.beginTransaction()
try {
addPositionToVaultSchema(db)
initVaultPositionUsingCurrentSortOrder(db)
db.setTransactionSuccessful()
} finally {
db.endTransaction()
}
}
private fun addPositionToVaultSchema(db: Database) {
Sql.alterTable("VAULT_ENTITY").renameTo("VAULT_ENTITY_OLD").executeOn(db)
Sql.createTable("VAULT_ENTITY") //
.id() //
.optionalInt("FOLDER_CLOUD_ID") //
.optionalText("FOLDER_PATH") //
.optionalText("FOLDER_NAME") //
.requiredText("CLOUD_TYPE") //
.optionalText("PASSWORD") //
.optionalInt("POSITION") //
.foreignKey("FOLDER_CLOUD_ID", "CLOUD_ENTITY", ForeignKeyBehaviour.ON_DELETE_SET_NULL) //
.executeOn(db)
Sql.insertInto("VAULT_ENTITY") //
.select("_id", "FOLDER_CLOUD_ID", "FOLDER_PATH", "FOLDER_NAME", "PASSWORD", "CLOUD_ENTITY.TYPE") //
.columns("_id", "FOLDER_CLOUD_ID", "FOLDER_PATH", "FOLDER_NAME", "PASSWORD", "CLOUD_TYPE") //
.from("VAULT_ENTITY_OLD") //
.join("CLOUD_ENTITY", "VAULT_ENTITY_OLD.FOLDER_CLOUD_ID") //
.executeOn(db)
Sql.dropIndex("IDX_VAULT_ENTITY_FOLDER_PATH_FOLDER_CLOUD_ID").executeOn(db)
Sql.createUniqueIndex("IDX_VAULT_ENTITY_FOLDER_PATH_FOLDER_CLOUD_ID") //
.on("VAULT_ENTITY") //
.asc("FOLDER_PATH") //
.asc("FOLDER_CLOUD_ID") //
.executeOn(db)
Sql.dropTable("VAULT_ENTITY_OLD").executeOn(db)
}
private fun initVaultPositionUsingCurrentSortOrder(db: Database) {
CloudEntityDao(DaoConfig(db, VaultEntityDao::class.java)) //
.loadAll() //
.map {
Sql.update("VAULT_ENTITY") //
.where("_id", Sql.eq(it.id)) //
.set("POSITION", Sql.toInteger(it.id - 1)) //
.executeOn(db)
}
}
}

View File

@ -28,6 +28,8 @@ public class VaultEntity extends DatabaseEntity {
private String password; private String password;
private Integer position;
/** /**
* Convenient call for {@link org.greenrobot.greendao.AbstractDao#refresh(Object)}. * Convenient call for {@link org.greenrobot.greendao.AbstractDao#refresh(Object)}.
* Entity must attached to an entity context. * Entity must attached to an entity context.
@ -152,6 +154,14 @@ public class VaultEntity extends DatabaseEntity {
this.password = password; this.password = password;
} }
public Integer getPosition() {
return this.position;
}
public void setPosition(Integer position) {
this.position = position;
}
/** called by internal mechanisms, do not call yourself. */ /** called by internal mechanisms, do not call yourself. */
@Generated(hash = 674742652) @Generated(hash = 674742652)
public void __setDaoSession(DaoSession daoSession) { public void __setDaoSession(DaoSession daoSession) {
@ -159,14 +169,16 @@ public class VaultEntity extends DatabaseEntity {
myDao = daoSession != null ? daoSession.getVaultEntityDao() : null; myDao = daoSession != null ? daoSession.getVaultEntityDao() : null;
} }
@Generated(hash = 1196809909) @Generated(hash = 825602374)
public VaultEntity(Long id, Long folderCloudId, String folderPath, String folderName, @NotNull String cloudType, String password) { public VaultEntity(Long id, Long folderCloudId, String folderPath, String folderName, @NotNull String cloudType, String password,
Integer position) {
this.id = id; this.id = id;
this.folderCloudId = folderCloudId; this.folderCloudId = folderCloudId;
this.folderPath = folderPath; this.folderPath = folderPath;
this.folderName = folderName; this.folderName = folderName;
this.cloudType = cloudType; this.cloudType = cloudType;
this.password = password; this.password = password;
this.position = position;
} }
@Generated(hash = 691253864) @Generated(hash = 691253864)

View File

@ -30,6 +30,7 @@ public class VaultEntityMapper extends EntityMapper<VaultEntity, Vault> {
.withCloud(cloudFrom(entity)) // .withCloud(cloudFrom(entity)) //
.withCloudType(CloudType.valueOf(entity.getCloudType())) // .withCloudType(CloudType.valueOf(entity.getCloudType())) //
.withSavedPassword(entity.getPassword()) // .withSavedPassword(entity.getPassword()) //
.withPosition(entity.getPosition()) //
.build(); .build();
} }
@ -51,6 +52,7 @@ public class VaultEntityMapper extends EntityMapper<VaultEntity, Vault> {
} }
entity.setCloudType(domainObject.getCloudType().name()); entity.setCloudType(domainObject.getCloudType().name());
entity.setPassword(domainObject.getPassword()); entity.setPassword(domainObject.getPassword());
entity.setPosition(domainObject.getPosition());
return entity; return entity;
} }
} }

View File

@ -9,18 +9,49 @@ import com.google.api.services.drive.DriveScopes;
import org.cryptomator.data.BuildConfig; import org.cryptomator.data.BuildConfig;
import org.cryptomator.domain.exception.FatalBackendException; import org.cryptomator.domain.exception.FatalBackendException;
import org.cryptomator.util.SharedPreferencesHandler;
import java.util.Collections; import java.util.Collections;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.LogRecord;
import java.util.logging.Logger;
import timber.log.Timber;
class GoogleDriveClientFactory { class GoogleDriveClientFactory {
private final Context context; private final Context context;
private final SharedPreferencesHandler sharedPreferencesHandler;
GoogleDriveClientFactory(Context context) { GoogleDriveClientFactory(Context context, SharedPreferencesHandler sharedPreferencesHandler) {
this.context = context; this.context = context;
this.sharedPreferencesHandler = sharedPreferencesHandler;
} }
Drive getClient(String accountName) throws FatalBackendException { Drive getClient(String accountName) throws FatalBackendException {
if(sharedPreferencesHandler.debugMode()) {
Logger.getLogger("com.google.api.client").setLevel(Level.CONFIG);
Logger.getLogger("com.google.api.client").addHandler(new Handler() {
@Override
public void publish(LogRecord record) {
if(record.getMessage().startsWith("-------------- RESPONSE --------------")
|| record.getMessage().startsWith("-------------- REQUEST --------------")
|| record.getMessage().startsWith("{\n \"files\": [\n")) {
Timber.tag("GoogleDriveClient").d(record.getMessage());
}
}
@Override
public void flush() {
}
@Override
public void close() throws SecurityException {
}
});
}
try { try {
FixedGoogleAccountCredential credential = FixedGoogleAccountCredential.usingOAuth2(context, Collections.singleton(DriveScopes.DRIVE)); FixedGoogleAccountCredential credential = FixedGoogleAccountCredential.usingOAuth2(context, Collections.singleton(DriveScopes.DRIVE));
credential.setAccountName(accountName); credential.setAccountName(accountName);

View File

@ -69,7 +69,7 @@ class GoogleDriveImpl {
} }
private Drive client() { private Drive client() {
return new GoogleDriveClientFactory(context) // return new GoogleDriveClientFactory(context, sharedPreferencesHandler) //
.getClient(googleDriveCloud.accessToken()); .getClient(googleDriveCloud.accessToken());
} }

View File

@ -19,7 +19,8 @@ public class Vault implements Serializable {
.withPath(vault.getPath()) // .withPath(vault.getPath()) //
.withUnlocked(vault.isUnlocked()) // .withUnlocked(vault.isUnlocked()) //
.withSavedPassword(vault.getPassword()) // .withSavedPassword(vault.getPassword()) //
.withVersion(vault.getVersion()); .withVersion(vault.getVersion()) //
.withPosition(vault.getPosition());
} }
private final Long id; private final Long id;
@ -30,6 +31,7 @@ public class Vault implements Serializable {
private final boolean unlocked; private final boolean unlocked;
private final String password; private final String password;
private final int version; private final int version;
private final int position;
private Vault(Builder builder) { private Vault(Builder builder) {
this.id = builder.id; this.id = builder.id;
@ -40,6 +42,7 @@ public class Vault implements Serializable {
this.cloudType = builder.cloudType; this.cloudType = builder.cloudType;
this.password = builder.password; this.password = builder.password;
this.version = builder.version; this.version = builder.version;
this.position = builder.position;
} }
public Long getId() { public Long getId() {
@ -74,6 +77,10 @@ public class Vault implements Serializable {
return version; return version;
} }
public int getPosition() {
return position;
}
public static class Builder { public static class Builder {
private Long id = NOT_SET; private Long id = NOT_SET;
@ -84,6 +91,7 @@ public class Vault implements Serializable {
private boolean unlocked; private boolean unlocked;
private String password; private String password;
private int version = -1; private int version = -1;
private int position = -1;
private Builder() { private Builder() {
} }
@ -154,6 +162,11 @@ public class Vault implements Serializable {
return this; return this;
} }
public Builder withPosition(int position) {
this.position = position;
return this;
}
public Vault build() { public Vault build() {
validate(); validate();
return new Vault(this); return new Vault(this);
@ -172,6 +185,9 @@ public class Vault implements Serializable {
if (cloudType == null) { if (cloudType == null) {
throw new IllegalStateException("cloudtype must be set"); throw new IllegalStateException("cloudtype must be set");
} }
if (position == -1) {
throw new IllegalStateException("position must be set");
}
} }
} }

View File

@ -35,7 +35,10 @@ class CreateVault {
CloudFolder vaultFolder = cloudContentRepository.folder(folder, vaultName); CloudFolder vaultFolder = cloudContentRepository.folder(folder, vaultName);
vaultFolder = cloudContentRepository.create(vaultFolder); vaultFolder = cloudContentRepository.create(vaultFolder);
cloudRepository.create(vaultFolder, password); cloudRepository.create(vaultFolder, password);
return vaultRepository.store(aVault().thatIsNew().withNamePathAndCloudFrom(vaultFolder).build()); return vaultRepository.store(aVault() //
.thatIsNew() //
.withNamePathAndCloudFrom(vaultFolder) //
.withPosition(vaultRepository.vaults().size()) //
.build());
} }
} }

View File

@ -6,6 +6,8 @@ import org.cryptomator.domain.repository.VaultRepository;
import org.cryptomator.generator.Parameter; import org.cryptomator.generator.Parameter;
import org.cryptomator.generator.UseCase; import org.cryptomator.generator.UseCase;
import java.util.List;
@UseCase @UseCase
class DeleteVault { class DeleteVault {
@ -18,7 +20,12 @@ class DeleteVault {
} }
public Long execute() throws BackendException { public Long execute() throws BackendException {
return vaultRepository.delete(vault); Long vaultId = vaultRepository.delete(vault);
List<Vault> reorderVaults = MoveVaultHelper.Companion.reorderVaults(vaultRepository);
MoveVaultHelper.Companion.updateVaultsInDatabase(reorderVaults, vaultRepository);
return vaultId;
} }
} }

View File

@ -0,0 +1,51 @@
package org.cryptomator.domain.usecases.vault;
import org.cryptomator.domain.Vault
import org.cryptomator.domain.repository.VaultRepository
import java.util.*
class MoveVaultHelper {
companion object {
fun updateVaultPosition(fromPosition: Int, toPosition: Int, vaultRepository: VaultRepository): List<Vault> {
val vaults = vaultRepository.vaults()
vaults.sortWith(VaultComparator())
if (fromPosition < toPosition) {
for (i in fromPosition until toPosition) {
Collections.swap(vaults, i, i + 1)
}
} else {
for (i in fromPosition downTo toPosition + 1) {
Collections.swap(vaults, i, i - 1)
}
}
return reorderVaults(vaults)
}
private fun reorderVaults(vaults: MutableList<Vault>) : List<Vault> {
for (i in 0 until vaults.size) {
vaults[i] = Vault.aCopyOf(vaults[i]).withPosition(i).build()
}
return vaults;
}
fun reorderVaults(vaultRepository: VaultRepository) : List<Vault> {
val vaults = vaultRepository.vaults()
vaults.sortWith(VaultComparator())
return reorderVaults(vaults)
}
fun updateVaultsInDatabase(vaults: List<Vault>, vaultRepository: VaultRepository): List<Vault> {
vaults.forEach { vault -> vaultRepository.store(vault) }
return vaultRepository.vaults()
}
}
internal class VaultComparator : Comparator<Vault> {
override fun compare(o1: Vault, o2: Vault): Int {
return o1.position - o2.position
}
}
}

View File

@ -0,0 +1,28 @@
package org.cryptomator.domain.usecases.vault;
import org.cryptomator.domain.Vault;
import org.cryptomator.domain.exception.BackendException;
import org.cryptomator.domain.repository.VaultRepository;
import org.cryptomator.generator.Parameter;
import org.cryptomator.generator.UseCase;
import java.util.List;
@UseCase
class MoveVaultPosition {
private final VaultRepository vaultRepository;
private final int fromPosition;
private final int toPosition;
public MoveVaultPosition(VaultRepository vaultRepository, @Parameter Integer fromPosition, @Parameter Integer toPosition) {
this.vaultRepository = vaultRepository;
this.fromPosition = fromPosition;
this.toPosition = toPosition;
}
public List<Vault> execute() throws BackendException {
List<Vault> vaults = MoveVaultHelper.Companion.updateVaultPosition(fromPosition, toPosition, vaultRepository);
return MoveVaultHelper.Companion.updateVaultsInDatabase(vaults, vaultRepository);
}
}

View File

@ -0,0 +1,100 @@
package org.cryptomator.domain.usecases.vault
import org.cryptomator.domain.CloudType
import org.cryptomator.domain.Vault
import org.cryptomator.domain.repository.VaultRepository
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.mockito.Mockito
class MoveVaultHelperTest {
private lateinit var orderedVaults: ArrayList<Vault>
private lateinit var unorderedVaults: ArrayList<Vault>
private lateinit var vaultRepository: VaultRepository
private lateinit var cloudType: CloudType
@Test
fun reorderVaults() {
Mockito.`when`(vaultRepository.vaults()).thenReturn(unorderedVaults)
assertEquals(orderedVaults, MoveVaultHelper.Companion.reorderVaults(vaultRepository), "Failed to reorderVaults")
}
@Test
fun movePositionUp() {
Mockito.`when`(vaultRepository.vaults()).thenReturn(orderedVaults)
val resultList = ArrayList<Vault>()
resultList.add(Vault.aVault().withId(2).withPath("").withCloudType(cloudType).withName("foo 5").withPosition(0).build())
resultList.add(Vault.aVault().withId(3).withPath("").withCloudType(cloudType).withName("foo 10").withPosition(1).build())
resultList.add(Vault.aVault().withId(24).withPath("").withCloudType(cloudType).withName("foo 1").withPosition(2).build())
resultList.add(Vault.aVault().withId(4).withPath("").withCloudType(cloudType).withName("foo 15").withPosition(3).build())
assertEquals(resultList, MoveVaultHelper.Companion.updateVaultPosition(0, 2, vaultRepository), "Failed to movePositionUp")
}
@Test
fun movePositionDown() {
Mockito.`when`(vaultRepository.vaults()).thenReturn(orderedVaults)
val resultList = ArrayList<Vault>()
resultList.add(Vault.aVault().withId(3).withPath("").withCloudType(cloudType).withName("foo 10").withPosition(0).build())
resultList.add(Vault.aVault().withId(24).withPath("").withCloudType(cloudType).withName("foo 1").withPosition(1).build())
resultList.add(Vault.aVault().withId(2).withPath("").withCloudType(cloudType).withName("foo 5").withPosition(2).build())
resultList.add(Vault.aVault().withId(4).withPath("").withCloudType(cloudType).withName("foo 15").withPosition(3).build())
assertEquals(resultList, MoveVaultHelper.Companion.updateVaultPosition(2, 0, vaultRepository), "Failed to movePositionDown")
}
@Test
fun movePositionToSelf() {
Mockito.`when`(vaultRepository.vaults()).thenReturn(orderedVaults)
val resultList = ArrayList<Vault>()
resultList.add(Vault.aVault().withId(24).withPath("").withCloudType(cloudType).withName("foo 1").withPosition(0).build())
resultList.add(Vault.aVault().withId(2).withPath("").withCloudType(cloudType).withName("foo 5").withPosition(1).build())
resultList.add(Vault.aVault().withId(3).withPath("").withCloudType(cloudType).withName("foo 10").withPosition(2).build())
resultList.add(Vault.aVault().withId(4).withPath("").withCloudType(cloudType).withName("foo 15").withPosition(3).build())
assertEquals(resultList, MoveVaultHelper.Companion.updateVaultPosition(1, 1, vaultRepository), "Failed to movePositionToSelf")
}
@Test
fun movePositionOutOfBounds() {
Mockito.`when`(vaultRepository.vaults()).thenReturn(orderedVaults)
Assertions.assertThrows(IndexOutOfBoundsException::class.java) { MoveVaultHelper.Companion.updateVaultPosition(1, 4, vaultRepository) }
}
@Test
fun verifyStoreInVaultRepo() {
Mockito.`when`(vaultRepository.vaults()).thenReturn(orderedVaults)
val result = MoveVaultHelper.Companion.updateVaultsInDatabase(orderedVaults, vaultRepository)
assertEquals(orderedVaults, result, "Failed to verifyStoreInVaultRepo")
orderedVaults.forEach {
Mockito.verify(vaultRepository).store(Mockito.eq(it))
}
}
@BeforeEach
fun setup() {
vaultRepository = Mockito.mock(VaultRepository::class.java)
cloudType = CloudType.LOCAL
unorderedVaults = ArrayList()
unorderedVaults.add(Vault.aVault().withId(24).withPath("").withCloudType(cloudType).withName("foo 1").withPosition(1).build())
unorderedVaults.add(Vault.aVault().withId(3).withPath("").withCloudType(cloudType).withName("foo 10").withPosition(10).build())
unorderedVaults.add(Vault.aVault().withId(2).withPath("").withCloudType(cloudType).withName("foo 5").withPosition(5).build())
unorderedVaults.add(Vault.aVault().withId(4).withPath("").withCloudType(cloudType).withName("foo 15").withPosition(15).build())
orderedVaults = ArrayList()
orderedVaults.add(Vault.aVault().withId(24).withPath("").withCloudType(cloudType).withName("foo 1").withPosition(0).build())
orderedVaults.add(Vault.aVault().withId(2).withPath("").withCloudType(cloudType).withName("foo 5").withPosition(1).build())
orderedVaults.add(Vault.aVault().withId(3).withPath("").withCloudType(cloudType).withName("foo 10").withPosition(2).build())
orderedVaults.add(Vault.aVault().withId(4).withPath("").withCloudType(cloudType).withName("foo 15").withPosition(3).build())
}
}

21
fastlane/.default.env Normal file
View File

@ -0,0 +1,21 @@
# This file is just a template and should remain untouched
# COPY this file to .env and fill out the env variables
GOOGLE_PLAYSTORE_PRIVATE_KEY_FILE_PATH=
SIGNING_KEYSTORE_PATH=
SIGNING_KEYSTORE_PASSWORD=
SIGNING_KEY_ALIAS=
SIGNING_KEY_PASSWORD=
SIGNING_UPDATE_APK_STORE_KEY_PATH=
SIGNING_UPDATE_APK_STORE_PUB_KEY_PATH=
APK_STORE_BASIC_URL=
S3_BUCKET=
S3_ENDPOINT=
S3_REGION=
S3_ACCESS_KEY=
S3_SECRET_ACCESS_KEY=
SLACK_URL=

2
fastlane/Appfile Normal file
View File

@ -0,0 +1,2 @@
json_key_file(ENV["GOOGLE_PLAYSTORE_PRIVATE_KEY_FILE_PATH"])
package_name("org.cryptomator")

183
fastlane/Fastfile Normal file
View File

@ -0,0 +1,183 @@
fastlane_require 'dotenv'
fastlane_require 'jwt'
fastlane_require 'base64'
fastlane_require 'net/sftp'
default_platform(:android)
branch_name = `git rev-parse --abbrev-ref HEAD`
build = `git rev-list --count #{branch_name} | tr -d " \t\n\r"`
build = build.to_i + 1958 # adding 1958 for legacy reasons. Must be in sync with getVersionCode() from build.gradle
version = get_version_name(
gradle_file_path:"build.gradle",
ext_constant_name:"androidVersionName")
version = version.delete "'"
platform :android do |options|
desc "Run all the tests"
lane :test do |options|
gradle(task: "test")
end
desc "Deploy new version to Google Play and APK Store options: beta:false (default)"
lane :deploy do |options|
release_note_path_en = "metadata/android/en-US/changelogs/default.txt"
# use english-change-log for french language too
FileUtils.cp(release_note_path_en, "metadata/android/fr-FR/changelogs/default.txt")
deployToPlaystore(beta:options[:beta])
deployToServer(beta:options[:beta])
deployToFDroid(beta:options[:beta])
slack(
default_payloads: [], # reduce the notification to the minimum
message: ":rocket: Successfully deployed #{version} with code #{build} to the Play Store :cryptomator:",
payload: {
"Changes" => File.read(release_note_path_en)
}
)
end
desc "Deploy new version to Play Store"
lane :deployToPlaystore do |options|
deploy_target = "production"
if options[:beta]
deploy_target = "beta"
end
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"],
}
)
upload_to_play_store(
track: deploy_target,
apk: lane_context[SharedValues::GRADLE_APK_OUTPUT_PATH],
mapping: lane_context[SharedValues::GRADLE_MAPPING_TXT_OUTPUT_PATH],
version_name: version,
version_code: build,
release_status: "draft",
json_key: ENV["GOOGLE_PLAYSTORE_PRIVATE_KEY_FILE_PATH"],
skip_upload_aab: true,
skip_upload_metadata: false,
skip_upload_images: true,
skip_upload_screenshots: true,
metadata_path: "fastlane/metadata/android"
)
FileUtils.cp(lane_context[SharedValues::GRADLE_APK_OUTPUT_PATH], "release/Cryptomator-#{version}_playstore_signed.apk")
end
desc "Deploy new version to server"
lane :deployToServer do |options|
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"],
}
)
FileUtils.cp(lane_context[SharedValues::GRADLE_APK_OUTPUT_PATH], "release/Cryptomator-#{version}.apk")
server_host = ENV["APK_STORE_BASIC_URL"]
base_url = "https://#{server_host}/android/"
apk_url = "#{base_url}#{version}/Cryptomator-#{version}.apk"
release_note_url = "#{base_url}#{version}/release-notes.html"
claims = {
"version": version,
"url": apk_url,
"release_notes": release_note_url
}
private_key = OpenSSL::PKey.read(File.read(ENV["SIGNING_UPDATE_APK_STORE_KEY_PATH"]))
token = JWT.encode claims, private_key, "ES256"
latest_version_filename = "latest-version-#{version}.json"
latest_version_jsn = File.new("latest_versions/#{latest_version_filename}","w")
latest_version_jsn.write(token)
latest_version_jsn.close
if options[:beta]
puts "Skipping deployment to server cause there isn't currently a beta channel"
else
puts "Uploading APK and release note"
aws_s3(
bucket: ENV['S3_BUCKET'],
endpoint: ENV['S3_ENDPOINT'],
region: ENV['S3_REGION'],
access_key: ENV['S3_ACCESS_KEY'],
secret_access_key: ENV['S3_SECRET_ACCESS_KEY'],
path: "android/#{version}",
files: [
"fastlane/release/Cryptomator-#{version}.apk",
"fastlane/release-notes.html"
],
skip_html_upload: true,
apk: ''
)
puts "Uploading #{latest_version_filename} with claims #{claims}"
puts "Rename #{latest_version_filename} to latest-version.json for deployment"
aws_s3(
bucket: ENV['S3_BUCKET'],
endpoint: ENV['S3_ENDPOINT'],
region: ENV['S3_REGION'],
access_key: ENV['S3_ACCESS_KEY'],
secret_access_key: ENV['S3_SECRET_ACCESS_KEY'],
path: "android",
files: [
"fastlane/latest_versions/#{latest_version_filename}"
],
skip_html_upload: true,
apk: ''
)
end
FileUtils.mv("release/Cryptomator-#{version}.apk", "release/Cryptomator-#{version}_signed.apk")
end
desc "Deploy new version to F-Droid"
lane :deployToFDroid do |options|
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"],
}
)
FileUtils.cp(lane_context[SharedValues::GRADLE_APK_OUTPUT_PATH], "release/Cryptomator-#{version}_fdroid_signed.apk")
end
end

6
fastlane/Pluginfile Normal file
View File

@ -0,0 +1,6 @@
# Autogenerated by fastlane
#
# Ensure this file is checked in to source control!
gem 'fastlane-plugin-get_version_name'
gem 'fastlane-plugin-aws_s3'

49
fastlane/README.md Normal file
View File

@ -0,0 +1,49 @@
fastlane documentation
================
# Installation
Make sure you have the latest version of the Xcode command line tools installed:
```
xcode-select --install
```
Install _fastlane_ using
```
[sudo] gem install fastlane -NV
```
or alternatively using `brew install fastlane`
# Available Actions
## Android
### android test
```
fastlane android test
```
Run all the tests
### android deploy
```
fastlane android deploy
```
Deploy new version to Google Play and APK Store options: beta:false (default)
### android deployToPlaystore
```
fastlane android deployToPlaystore
```
Deploy new version to Play Store
### android deployToServer
```
fastlane android deployToServer
```
Deploy new version to server
### android deployToFDroid
```
fastlane android deployToFDroid
```
Deploy new version to F-Droid
----
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).

4
fastlane/latest_versions/.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
# Ignore everything in this directory
*
# Except this file
!.gitignore

View File

@ -0,0 +1,4 @@
- Möglichkeit zum Sortieren der Tresorliste hinzugefügt
- Logging in Google Drive Cloud hinzugefügt
- Verhalten bei Änderung des OneDrive-Passworts verbessert
- CryptoBot-Symbole aufpoliert

View File

@ -0,0 +1,39 @@
Mit Cryptomator liegt der Schlüssel zu deinen Daten bei dir. Durch Cryptomator ver­schlüsselst du deine Daten schnell und unkompliziert. Anschließend lädst du sie geschützt in deinen Lieblingscloudservice hoch.
<b>EINFACH</b>
Cryptomator ist ein einfaches Tool zur digitalen Selbstverteidigung. Es ermöglicht dir, deine Cloud-Daten eigenständig und unabhängig zu schützen.
• Erstelle einen Tresor und vergib ein Passwort
• Keine komplizierte Konfiguration und keine weiteren Accounts notwendig
• Entsperr Tresore mit deinem Fingerabdruck*
* ab Android 6.0 and Smartphones mit Fingerabdruck-Sensor
<b>KOMPATIBEL</b>
Cryptomator ist kompatibel mit den meistgenutzten Cloudspeichern und verfügbar für alle gängigen Betriebssysteme.
• Kompatibel mit Dropbox, Google Drive, OneDrive und WebDAV-basierten Cloudspeicher-Diensten
• Erstelle auch Tresore in Androids lokalem Speicher (funktioniert bspw. zusammen mit Sync-Apps von Drittanbietern)
• Greife auf deine Tresore von allen deinen mobilen Endgeräten und Computern zu
<b>SICHER</b>
Du musst Cryptomator nicht blind vertrauen, denn <a href="https://github.com/cryptomator/android">die App ist quelloffen</a>, was für dich als Nutzer bedeutet, dass jeder den Code einsehen kann.
• Verschlüsselung der Dateiinhalte und Dateinamen mit AES und 256-Bit-Schlüssellänge
• Erhöhte Brute-Force-Resistenz des Tresor-Passworts durch Einsatz von scrypt
• Tresore werden automatisch gesperrt, wenn die App in den Hintergrund geschickt wird
• Verschlüsselungsimplementierung ist öffentlich dokumentiert
<b>PREISGEKRÖNT</b>
Cryptomator wurde mit dem <b>CeBIT Innovation Award 2016 for Usable Security and Privacy</b> ausgezeichnet. Wir freuen uns, hunderttausenden Cryptomator-Nutzern Sicherheit und Privatsphärenschutz bieten zu können.
<b>CRYPTOMATOR COMMUNITY</b>
Tritt der Cryptomator Community bei und tausche dich mit Cryptomator-Nutzern aus: <a href="https://community.cryptomator.org">https://community.cryptomator.org</a>
• Folge uns auf Twitter <a href="https://twitter.com/Cryptomator">@Cryptomator</a>
• Like uns auf Facebook <a href="https://facebook.com/Cryptomator">/Cryptomator</a>

View File

@ -0,0 +1 @@
Nimm die Sicherung deiner Cloud-Daten selbst in die Hand

View File

@ -0,0 +1 @@
Cryptomator

View File

@ -0,0 +1,4 @@
- Added possibility to sort vault list
- Added logging to Google drive cloud
- Enhanced behavior when OneDrive password changed
- Polished CryptoBot icons

View File

@ -0,0 +1,39 @@
With Cryptomator, the key to your data is in your hands. Cryptomator encrypts your data quickly and easily. Afterwards you upload them protected to your favorite cloud service.
<b>EASY-TO-USE</b>
Cryptomator is a simple tool for digital self-defense. It allows you to protect your cloud data by yourself and independently.
• Simply create a vault and assign a password
• No additional account or configuration needed
• Unlock vaults with your fingerprint*
* from Android 6.0 and smartphones with fingerprint sensor
<b>COMPATIBLE</b>
Cryptomator is compatible with the most commonly used cloud storages and available for all major operating systems.
• Compatible with Dropbox, Google Drive, OneDrive, and WebDAV-based cloud storage services
• Create vaults in Androids local storage (e.g., works with third-party sync apps)
• Access your vaults on all your mobile devices and computers
<b>SECURE</b>
You don't have to trust Cryptomator blindly, because <a href="https://github.com/cryptomator/android">it is open source software</a>. For you as a user, this means that everyone can see the code.
• File content and filename encryption with AES and 256 bit key length
• Vault password is secured with scrypt for enhanced brute-force resistance
• Vaults are automatically locked after sending app to background
• Crypto implementation is publicly documented
<b>AWARD-WINNING</b>
Cryptomator received the <b>CeBIT Innovation Award 2016 for Usable Security and Privacy</b>. We're proud to provide security and privacy for hundreds of thousands of Cryptomator users.
<b>CRYPTOMATOR COMMUNITY</b>
Join the <a href="https://community.cryptomator.org">Cryptomator Community</a> and participate in the conversations with other Cryptomator users.
• Follow us on Twitter <a href="https://twitter.com/Cryptomator">@Cryptomator</a>
• Like us on Facebook <a href="https://facebook.com/Cryptomator">/Cryptomator</a>

View File

@ -0,0 +1 @@
Put a lock on your cloud: Take the security of your data into your own hands

View File

@ -0,0 +1 @@
Cryptomator

View File

@ -0,0 +1,39 @@
Cryptomator rend votre stockage dans le cloud beaucoup plus sûr. L'application chiffre les fichiers sur votre appareil mobile avant qu'ils ne soient envoyés dans votre cloud. Même si une tierce partie obtient un accès non autorisé à vos fichiers (par exemple, une attaque de pirates informatiques), vos fichiers sont à l'abri des regards indiscrets.
<b>SIMPLICITÉ</b>
Cryptomator a été développé en mettant l'accent sur la convivialité.
• Il suffit de créer un coffre-fort et d'y attribuer un mot de passe
• Aucun compte ou configuration supplémentaire n'est nécessaire
• Déverrouillez les coffres-forts avec votre empreinte digitale*.
* à partir d'Android 6.0 et sur smartphones avec capteur biométrique d'empreintes digitales
<b>COMPATIBILITÉ</b>
Cryptomator est compatible avec les systèmes de stockage dans le cloud les plus couramment utilisés et disponible pour tous les principaux systèmes d'exploitation.
• Compatible avec Dropbox, Google Drive, OneDrive et les services de stockage dans le nuage basés sur WebDAV
• Créer des coffres-forts dans le stockage local d'Android (par exemple, fonctionne avec des applications de synchronisation tierces)
• Accédez à vos coffres-forts sur tous vos appareils mobiles et ordinateurs
<b>SÉCURITÉ</b>
Cryptomator pour Android est basé sur le projet open-source de Cryptomator pour Ordinateur de bureau.
• Chiffrement du contenu et des noms de fichiers via AES et une longueur de clé de 256 bits
• Le mot de passe du coffre-fort est sécurisé par cryptage pour une meilleure résistance aux attaque par force brut
• Les coffre-forts sont automatiquement verrouillées après la mise en arrière-plan de l'application
• La mise en œuvre de Crypto est basée sur la bibliothèque open-source CryptoLib et est documentée publiquement
<b>UNE GÉNIALITUDE GÉNÉRAL</b>
Cryptomator a reçu le prix de l'innovation CeBIT 2016 pour la sécurité pratique et la confidentialité. Nous sommes fiers d'assurer la sécurité et la confidentialité des centaines de milliers d'utilisateurs du Cryptomator.
<b>LA COMMUNAUTÉ CRYPTOMATOR</b>
Rejoignez la communauté de Cryptomator et participez aux conversations avec les autres utilisateurs de Cryptomator: https://community.cryptomator.org
- Suivez-nous sur Twitter @Cryptomator
- Comme nous sur Facebook/Cryptomator

View File

@ -0,0 +1 @@
Verrouillé votre cloud: Prenez en mains la sécurité de vos données

View File

@ -0,0 +1 @@
Cryptomator

View File

@ -0,0 +1,6 @@
<ul>
<li>Added possibility to sort vault list</li>
<li>Added logging to Google drive cloud</li>
<li>Enhanced behavior when OneDrive password changed</li>
<li>Polished CryptoBot icons</li>
</ul>

4
fastlane/release/.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
# Ignore everything in this directory
*
# Except this file
!.gitignore

View File

@ -0,0 +1,78 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<group android:scaleX="0.77"
android:scaleY="0.77"
android:translateX="12.42"
android:translateY="12.42">
<path android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z"/>
<path android:fillColor="#00000000" android:pathData="M9,0L9,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,0L19,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M29,0L29,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M39,0L39,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M49,0L49,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M59,0L59,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M69,0L69,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M79,0L79,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M89,0L89,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M99,0L99,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,9L108,9"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,19L108,19"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,29L108,29"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,39L108,39"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,49L108,49"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,59L108,59"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,69L108,69"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,79L108,79"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,89L108,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,99L108,99"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,29L89,29"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,39L89,39"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,49L89,49"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,59L89,59"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,69L89,69"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,79L89,79"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M29,19L29,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M39,19L39,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M49,19L49,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M59,19L59,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M69,19L69,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M79,19L79,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
</group>
</vector>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#F1C40F</color>
</resources>

View File

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name" translatable="false">Debug Cryptomator</string>
</resources>

View File

@ -112,7 +112,9 @@ class AuthenticateCloudPresenter @Inject constructor( //
} }
private fun failAuthentication(cloudName: Int) { private fun failAuthentication(cloudName: Int) {
activity().runOnUiThread {
view?.showMessage(String.format(getString(R.string.screen_authenticate_auth_authentication_failed), getString(cloudName))) view?.showMessage(String.format(getString(R.string.screen_authenticate_auth_authentication_failed), getString(cloudName)))
}
finish() finish()
} }

View File

@ -1,6 +1,5 @@
package org.cryptomator.presentation.model package org.cryptomator.presentation.model
import android.os.Build
import org.cryptomator.domain.CloudType import org.cryptomator.domain.CloudType
import org.cryptomator.presentation.R import org.cryptomator.presentation.R
@ -23,13 +22,13 @@ enum class CloudTypeModel(builder: Builder) {
LOCAL(Builder("LOCAL", R.string.cloud_names_local_storage) // LOCAL(Builder("LOCAL", R.string.cloud_names_local_storage) //
.withCloudImageResource(R.drawable.storage_type_local) // .withCloudImageResource(R.drawable.storage_type_local) //
.withCloudImageLargeResource(R.drawable.storage_type_local_large) // .withCloudImageLargeResource(R.drawable.storage_type_local_large) //
.withMultiInstancesIfLollipopOrLater()); .withMultiInstances());
val cloudName: String val cloudName: String = builder.cloudName
val displayNameResource: Int val displayNameResource: Int = builder.displayNameResource
val cloudImageResource: Int val cloudImageResource: Int = builder.cloudImageResource
val cloudImageLargeResource: Int val cloudImageLargeResource: Int = builder.cloudImageLargeResource
val isMultiInstance: Boolean val isMultiInstance: Boolean = builder.multiInstances
private class Builder(val cloudName: String, val displayNameResource: Int) { private class Builder(val cloudName: String, val displayNameResource: Int) {
var cloudImageResource = 0 var cloudImageResource = 0
@ -50,11 +49,6 @@ enum class CloudTypeModel(builder: Builder) {
multiInstances = true multiInstances = true
return this return this
} }
fun withMultiInstancesIfLollipopOrLater(): Builder {
multiInstances = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
return this
}
} }
companion object { companion object {
@ -66,12 +60,4 @@ enum class CloudTypeModel(builder: Builder) {
return CloudType.valueOf(type.name) return CloudType.valueOf(type.name)
} }
} }
init {
cloudName = builder.cloudName
displayNameResource = builder.displayNameResource
cloudImageResource = builder.cloudImageResource
cloudImageLargeResource = builder.cloudImageLargeResource
isMultiInstance = builder.multiInstances
}
} }

View File

@ -13,6 +13,8 @@ class VaultModel(private val vault: Vault) : Serializable {
get() = vault.path get() = vault.path
val isLocked: Boolean val isLocked: Boolean
get() = !vault.isUnlocked get() = !vault.isUnlocked
val position: Int
get() = vault.position
fun toVault(): Vault { fun toVault(): Vault {
return vault return vault

View File

@ -0,0 +1,10 @@
package org.cryptomator.presentation.model.comparator
import org.cryptomator.presentation.model.VaultModel
class VaultPositionComparator : Comparator<VaultModel> {
override fun compare(v1: VaultModel, v2: VaultModel): Int {
return v1.position - v2.position
}
}

View File

@ -673,23 +673,12 @@ class BrowseFilesPresenter @Inject constructor( //
} }
fun onExportFileClicked(cloudFile: CloudFileModel, trigger: ExportOperation) { fun onExportFileClicked(cloudFile: CloudFileModel, trigger: ExportOperation) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
exportFileToDownloadDirectory(cloudFile, trigger)
} else {
exportFileToUserSelectedLocation(cloudFile, trigger) exportFileToUserSelectedLocation(cloudFile, trigger)
} }
}
fun onExportNodesClicked(selectedCloudFiles: ArrayList<CloudNodeModel<*>>, trigger: ExportOperation) { fun onExportNodesClicked(selectedCloudFiles: ArrayList<CloudNodeModel<*>>, trigger: ExportOperation) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
exportNodesToUserSelectedLocation(selectedCloudFiles, trigger) exportNodesToUserSelectedLocation(selectedCloudFiles, trigger)
} }
}
private fun exportFileToDownloadDirectory(fileToExport: CloudFileModel, exportOperation: ExportOperation) {
requestPermissions(PermissionsResultCallbacks.exportFileToDownloadDirectory(fileToExport, exportOperation), //
R.string.permission_message_export_file, Manifest.permission.WRITE_EXTERNAL_STORAGE)
}
@Callback @Callback
fun exportFileToDownloadDirectory(result: PermissionsResult, fileToExport: CloudFileModel, exportOperation: ExportOperation) { fun exportFileToDownloadDirectory(result: PermissionsResult, fileToExport: CloudFileModel, exportOperation: ExportOperation) {

View File

@ -103,7 +103,7 @@ class CloudConnectionListPresenter @Inject constructor( //
} }
private fun releaseUriPermissionForLocalStorageCloud(cloudModel: LocalStorageModel) { private fun releaseUriPermissionForLocalStorageCloud(cloudModel: LocalStorageModel) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && (cloudModel.toCloud() as LocalStorageCloud).rootUri() != null) { if ((cloudModel.toCloud() as LocalStorageCloud).rootUri() != null) {
releaseUriPermission(cloudModel.uri()) releaseUriPermission(cloudModel.uri())
} }
} }

View File

@ -53,6 +53,7 @@ class VaultListPresenter @Inject constructor( //
private val addExistingVaultWorkflow: AddExistingVaultWorkflow, // private val addExistingVaultWorkflow: AddExistingVaultWorkflow, //
private val createNewVaultWorkflow: CreateNewVaultWorkflow, // private val createNewVaultWorkflow: CreateNewVaultWorkflow, //
private val saveVaultUseCase: SaveVaultUseCase, // private val saveVaultUseCase: SaveVaultUseCase, //
private val moveVaultPositionUseCase: MoveVaultPositionUseCase, //
private val changePasswordUseCase: ChangePasswordUseCase, // private val changePasswordUseCase: ChangePasswordUseCase, //
private val removeStoredVaultPasswordsUseCase: RemoveStoredVaultPasswordsUseCase, // private val removeStoredVaultPasswordsUseCase: RemoveStoredVaultPasswordsUseCase, //
private val licenseCheckUseCase: DoLicenseCheckUseCase, // private val licenseCheckUseCase: DoLicenseCheckUseCase, //
@ -603,6 +604,25 @@ class VaultListPresenter @Inject constructor( //
view?.showDialog(AppIsObscuredInfoDialog.newInstance()) view?.showDialog(AppIsObscuredInfoDialog.newInstance())
} }
fun onRowMoved(fromPosition: Int, toPosition: Int) {
view?.rowMoved(fromPosition, toPosition)
}
fun onVaultMoved(fromPosition: Int, toPosition: Int) {
moveVaultPositionUseCase
.withFromPosition(fromPosition) //
.andToPosition(toPosition) //
.run(object : DefaultResultHandler<List<Vault>>() {
override fun onSuccess(vaults: List<Vault>) {
view?.vaultMoved(vaults.mapTo(ArrayList()) { VaultModel(it) })
}
override fun onError(e: Throwable) {
Timber.tag("VaultListPresenter").e(e, "Failed to execute MoveVaultUseCase")
}
})
}
fun onBiometricAuthenticationSucceeded(vaultModel: VaultModel) { fun onBiometricAuthenticationSucceeded(vaultModel: VaultModel) {
if (changedVaultPassword) { if (changedVaultPassword) {
changedVaultPassword = false changedVaultPassword = false
@ -690,6 +710,7 @@ class VaultListPresenter @Inject constructor( //
lockVaultUseCase, // lockVaultUseCase, //
getVaultListUseCase, // getVaultListUseCase, //
saveVaultUseCase, // saveVaultUseCase, //
moveVaultPositionUseCase, //
removeStoredVaultPasswordsUseCase, // removeStoredVaultPasswordsUseCase, //
unlockVaultUseCase, // unlockVaultUseCase, //
prepareUnlockUseCase, // prepareUnlockUseCase, //

View File

@ -34,7 +34,7 @@ class AutoUploadNotification(private val context: Context, private val amountOfP
this.builder = NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID) // this.builder = NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID) //
.setContentTitle(context.getString(R.string.notification_auto_upload_title)) // .setContentTitle(context.getString(R.string.notification_auto_upload_title)) //
.setSmallIcon(R.mipmap.ic_launcher) // .setSmallIcon(R.drawable.background_splash_cryptomator) //
.setColor(getColor(R.color.colorPrimary)) // .setColor(getColor(R.color.colorPrimary)) //
.addAction(cancelNowAction()) .addAction(cancelNowAction())
.setGroup(NOTIFICATION_GROUP_KEY) .setGroup(NOTIFICATION_GROUP_KEY)

View File

@ -35,7 +35,7 @@ class OpenWritableFileNotification(private val context: Context, private val uri
this.builder = NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID) // this.builder = NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID) //
.setContentTitle(context.getString(R.string.notification_open_writable_file_title)) // .setContentTitle(context.getString(R.string.notification_open_writable_file_title)) //
.setContentText(context.getString(R.string.notification_open_writable_file_message)) // .setContentText(context.getString(R.string.notification_open_writable_file_message)) //
.setSmallIcon(R.mipmap.ic_launcher) // .setSmallIcon(R.drawable.background_splash_cryptomator) //
.setColor(getColor(R.color.colorPrimary)) // .setColor(getColor(R.color.colorPrimary)) //
.setGroup(NOTIFICATION_GROUP_KEY) .setGroup(NOTIFICATION_GROUP_KEY)
.setOngoing(true) .setOngoing(true)

View File

@ -7,12 +7,13 @@ import android.app.job.JobScheduler
import android.app.job.JobService import android.app.job.JobService
import android.content.ComponentName import android.content.ComponentName
import android.content.Context import android.content.Context
import android.database.Cursor import android.database.MergeCursor
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Handler import android.os.Handler
import android.provider.MediaStore import android.provider.MediaStore
import android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI import android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI
import android.provider.MediaStore.Images.Media.INTERNAL_CONTENT_URI
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import org.cryptomator.domain.exception.FatalBackendException import org.cryptomator.domain.exception.FatalBackendException
import org.cryptomator.presentation.R import org.cryptomator.presentation.R
@ -21,72 +22,69 @@ import org.cryptomator.presentation.util.ResourceHelper
import org.cryptomator.util.file.MimeTypeMap_Factory import org.cryptomator.util.file.MimeTypeMap_Factory
import org.cryptomator.util.file.MimeTypes import org.cryptomator.util.file.MimeTypes
import timber.log.Timber import timber.log.Timber
import java.lang.String.format
import java.util.*
@RequiresApi(api = Build.VERSION_CODES.N) @RequiresApi(api = Build.VERSION_CODES.N)
class PhotoContentJob : JobService() { class PhotoContentJob : JobService() {
private val mHandler = Handler() private val handler = Handler()
private val mWorker: Runnable = Runnable { private val worker: Runnable = Runnable {
scheduleJob(applicationContext) scheduleJob(applicationContext)
jobFinished(mRunningParams, false) jobFinished(runningParams, false)
} }
private lateinit var mRunningParams: JobParameters private lateinit var runningParams: JobParameters
override fun onStartJob(params: JobParameters): Boolean { override fun onStartJob(params: JobParameters): Boolean {
Timber.tag("PhotoContentJob").i("Job started!") Timber.tag("PhotoContentJob").i("Job started!")
val fileUtil = FileUtil(baseContext, MimeTypes(MimeTypeMap_Factory.newInstance())) val fileUtil = FileUtil(baseContext, MimeTypes(MimeTypeMap_Factory.newInstance()))
mRunningParams = params runningParams = params
if (params.triggeredContentAuthorities != null) {
params.triggeredContentAuthorities?.let {
if (params.triggeredContentUris != null) { if (params.triggeredContentUris != null) {
val ids = getIds(params) val ids = getIds(params)
if (ids != null && ids.size > 0) { if (ids != null && ids.isNotEmpty()) {
val selection = buildSelection(ids) val selection = buildSelection(ids)
var cursor: Cursor? = null contentResolver.query(EXTERNAL_CONTENT_URI, PROJECTION, selection, null, null).use { externalCursor ->
try { contentResolver.query(INTERNAL_CONTENT_URI, PROJECTION, selection, null, null).use { internalCursor ->
cursor = contentResolver.query(EXTERNAL_CONTENT_URI, PROJECTION, selection, null, null) MergeCursor(arrayOf(externalCursor, internalCursor)).use { cursor ->
cursor?.let {
while (cursor.moveToNext()) { while (cursor.moveToNext()) {
val dir = cursor.getString(PROJECTION_DATA)
try { try {
val dir = cursor.getString(PROJECTION_DATA)
fileUtil.addImageToAutoUploads(dir) fileUtil.addImageToAutoUploads(dir)
Timber.tag("PhotoContentJob").i("Added file to UploadList") Timber.tag("PhotoContentJob").i("Added file to UploadList")
Timber.tag("PhotoContentJob").d(format("Added file to UploadList %s", dir)) Timber.tag("PhotoContentJob").d(String.format("Added file to UploadList %s", dir))
} catch (e: FatalBackendException) { } catch (e: FatalBackendException) {
Timber.tag("PhotoContentJob").e(e, "Failed to add image to auto upload list") Timber.tag("PhotoContentJob").e(e, "Failed to add image to auto upload list")
}
}
} ?: Timber.tag("PhotoContentJob").e("Error: no access to media!")
} catch (e: SecurityException) { } catch (e: SecurityException) {
Timber.tag("PhotoContentJob").e("Error: no access to media!") Timber.tag("PhotoContentJob").e(e, "No access to storage")
} finally {
cursor?.close()
} }
} }
}
}
}
} else {
Timber.tag("PhotoContentJob").d("ids are null or 0: %s", ids)
}
} else { } else {
Timber.tag("PhotoContentJob").w("Photos rescan needed!") Timber.tag("PhotoContentJob").w("Photos rescan needed!")
return true return true
} }
} else { } ?: Timber.tag("PhotoContentJob").w("No photos content")
Timber.tag("PhotoContentJob").w("No photos content")
}
mHandler.post(mWorker) handler.post(worker)
return false return false
} }
private fun getIds(params: JobParameters): ArrayList<String>? { private fun getIds(params: JobParameters): Set<String>? {
return params.triggeredContentUris return params.triggeredContentUris
?.map { it.pathSegments } ?.map { it.pathSegments }
?.filter { it != null && it.size == EXTERNAL_PATH_SEGMENTS.size + 1 } ?.filter { it != null && (it.size == EXTERNAL_CONTENT_URI.pathSegments.size + 1 || it.size == INTERNAL_CONTENT_URI.pathSegments.size + 1) }
?.mapTo(ArrayList()) { it[it.size - 1] } ?.mapTo(HashSet()) { it[it.size - 1] }
} }
private fun buildSelection(ids: ArrayList<String>): String { private fun buildSelection(ids: Set<String>): String {
val selection = StringBuilder() val selection = StringBuilder()
ids.indices.forEach { i -> ids.indices.forEach { i ->
if (selection.isNotEmpty()) { if (selection.isNotEmpty()) {
@ -94,7 +92,7 @@ class PhotoContentJob : JobService() {
} }
selection.append(MediaStore.Images.ImageColumns._ID) selection.append(MediaStore.Images.ImageColumns._ID)
selection.append("='") selection.append("='")
selection.append(ids[i]) selection.append(ids.elementAt(i))
selection.append("'") selection.append("'")
} }
return selection.toString() return selection.toString()
@ -102,7 +100,7 @@ class PhotoContentJob : JobService() {
override fun onStopJob(params: JobParameters): Boolean { override fun onStopJob(params: JobParameters): Boolean {
Timber.tag("PhotoContentJob").i("onStopJob called, must stop, reschedule later") Timber.tag("PhotoContentJob").i("onStopJob called, must stop, reschedule later")
mHandler.removeCallbacks(mWorker) handler.removeCallbacks(worker)
return true return true
} }
@ -114,7 +112,6 @@ class PhotoContentJob : JobService() {
companion object { companion object {
private val MEDIA_URI = Uri.parse("content://" + MediaStore.AUTHORITY + "/") private val MEDIA_URI = Uri.parse("content://" + MediaStore.AUTHORITY + "/")
internal val EXTERNAL_PATH_SEGMENTS = EXTERNAL_CONTENT_URI.pathSegments
internal val PROJECTION = arrayOf(MediaStore.Images.ImageColumns._ID, MediaStore.Images.ImageColumns.DATA) internal val PROJECTION = arrayOf(MediaStore.Images.ImageColumns._ID, MediaStore.Images.ImageColumns.DATA)
internal const val PROJECTION_DATA = 1 internal const val PROJECTION_DATA = 1
@ -125,6 +122,7 @@ class PhotoContentJob : JobService() {
init { init {
val builder = JobInfo.Builder(PHOTOS_CONTENT_JOB, ComponentName(ResourceHelper.getString(R.string.app_id), PhotoContentJob::class.java.name)) val builder = JobInfo.Builder(PHOTOS_CONTENT_JOB, ComponentName(ResourceHelper.getString(R.string.app_id), PhotoContentJob::class.java.name))
builder.addTriggerContentUri(JobInfo.TriggerContentUri(EXTERNAL_CONTENT_URI, FLAG_NOTIFY_FOR_DESCENDANTS)) builder.addTriggerContentUri(JobInfo.TriggerContentUri(EXTERNAL_CONTENT_URI, FLAG_NOTIFY_FOR_DESCENDANTS))
builder.addTriggerContentUri(JobInfo.TriggerContentUri(INTERNAL_CONTENT_URI, FLAG_NOTIFY_FOR_DESCENDANTS))
builder.addTriggerContentUri(JobInfo.TriggerContentUri(MEDIA_URI, FLAG_NOTIFY_FOR_DESCENDANTS)) builder.addTriggerContentUri(JobInfo.TriggerContentUri(MEDIA_URI, FLAG_NOTIFY_FOR_DESCENDANTS))
jobInfo = builder.build() jobInfo = builder.build()
} }

View File

@ -54,7 +54,7 @@ class UnlockedNotification {
} }
this.builder = new NotificationCompat.Builder(service, NOTIFICATION_CHANNEL_ID) // this.builder = new NotificationCompat.Builder(service, NOTIFICATION_CHANNEL_ID) //
.setSmallIcon(R.mipmap.ic_launcher) // .setSmallIcon(R.drawable.background_splash_cryptomator) //
.setColor(ResourceHelper.Companion.getColor(R.color.colorPrimary)) // .setColor(ResourceHelper.Companion.getColor(R.color.colorPrimary)) //
.addAction(lockNowAction()) // .addAction(lockNowAction()) //
.setGroup(NOTIFICATION_GROUP_KEY) // .setGroup(NOTIFICATION_GROUP_KEY) //

View File

@ -1,7 +1,6 @@
package org.cryptomator.presentation.ui.activity package org.cryptomator.presentation.ui.activity
import android.net.Uri import android.net.Uri
import android.os.Build
import android.view.View.* import android.view.View.*
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
@ -91,10 +90,8 @@ class ImagePreviewActivity : BaseActivity(), ImagePreviewView, ConfirmDeleteClou
} }
private fun setupStatusBar() { private fun setupStatusBar() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
window.statusBarColor = ContextCompat.getColor(this, R.color.colorBlack) window.statusBarColor = ContextCompat.getColor(this, R.color.colorBlack)
} }
}
private fun setupToolbar(index: Int) { private fun setupToolbar(index: Int) {
updateTitle(index) updateTitle(index)
@ -131,10 +128,7 @@ class ImagePreviewActivity : BaseActivity(), ImagePreviewView, ConfirmDeleteClou
var newUiOptions = window.decorView.systemUiVisibility var newUiOptions = window.decorView.systemUiVisibility
newUiOptions = newUiOptions or SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION newUiOptions = newUiOptions or SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
newUiOptions = newUiOptions xor SYSTEM_UI_FLAG_FULLSCREEN newUiOptions = newUiOptions xor SYSTEM_UI_FLAG_FULLSCREEN
if (Build.VERSION.SDK_INT >= 19) {
newUiOptions = newUiOptions xor SYSTEM_UI_FLAG_IMMERSIVE_STICKY newUiOptions = newUiOptions xor SYSTEM_UI_FLAG_IMMERSIVE_STICKY
}
window.decorView.systemUiVisibility = newUiOptions window.decorView.systemUiVisibility = newUiOptions
} }

View File

@ -150,6 +150,14 @@ class VaultListActivity : BaseActivity(), //
return biometricAuthentication?.stoppedBiometricAuthDuringCloudAuthentication() == true return biometricAuthentication?.stoppedBiometricAuthDuringCloudAuthentication() == true
} }
override fun rowMoved(fromPosition: Int, toPosition: Int) {
vaultListFragment().rowMoved(fromPosition, toPosition)
}
override fun vaultMoved(vaults: List<VaultModel>) {
vaultListFragment().vaultMoved(vaults)
}
override fun showVaultSettingsDialog(vaultModel: VaultModel) { override fun showVaultSettingsDialog(vaultModel: VaultModel) {
val vaultSettingDialog = // val vaultSettingDialog = //
SettingsVaultBottomSheet.newInstance(vaultModel) SettingsVaultBottomSheet.newInstance(vaultModel)

View File

@ -23,5 +23,7 @@ interface VaultListView : View {
fun isVaultLocked(vaultModel: VaultModel): Boolean fun isVaultLocked(vaultModel: VaultModel): Boolean
fun cancelBasicAuthIfRunning() fun cancelBasicAuthIfRunning()
fun stoppedBiometricAuthDuringCloudAuthentication(): Boolean fun stoppedBiometricAuthDuringCloudAuthentication(): Boolean
fun rowMoved(fromPosition: Int, toPosition: Int)
fun vaultMoved(vaults: List<VaultModel>)
} }

View File

@ -2,15 +2,18 @@ package org.cryptomator.presentation.ui.adapter
import android.view.View import android.view.View
import com.google.android.material.switchmaterial.SwitchMaterial import com.google.android.material.switchmaterial.SwitchMaterial
import kotlinx.android.synthetic.main.item_biometric_auth_vault.view.*
import org.cryptomator.presentation.R import org.cryptomator.presentation.R
import org.cryptomator.presentation.model.VaultModel import org.cryptomator.presentation.model.VaultModel
import org.cryptomator.presentation.model.comparator.VaultPositionComparator
import org.cryptomator.presentation.ui.adapter.BiometricAuthSettingsAdapter.BiometricAuthSettingsViewHolder import org.cryptomator.presentation.ui.adapter.BiometricAuthSettingsAdapter.BiometricAuthSettingsViewHolder
import javax.inject.Inject import javax.inject.Inject
import kotlinx.android.synthetic.main.item_biometric_auth_vault.view.cloud
import kotlinx.android.synthetic.main.item_biometric_auth_vault.view.toggleBiometricAuth
import kotlinx.android.synthetic.main.item_biometric_auth_vault.view.vaultName
class BiometricAuthSettingsAdapter // class BiometricAuthSettingsAdapter //
@Inject @Inject
constructor() : RecyclerViewBaseAdapter<VaultModel, BiometricAuthSettingsAdapter.OnVaultBiometricAuthSettingsChanged, BiometricAuthSettingsViewHolder>() { constructor() : RecyclerViewBaseAdapter<VaultModel, BiometricAuthSettingsAdapter.OnVaultBiometricAuthSettingsChanged, BiometricAuthSettingsViewHolder>(VaultPositionComparator()) {
private var onVaultBiometricAuthSettingsChanged: OnVaultBiometricAuthSettingsChanged? = null private var onVaultBiometricAuthSettingsChanged: OnVaultBiometricAuthSettingsChanged? = null

View File

@ -1,14 +1,19 @@
package org.cryptomator.presentation.ui.adapter package org.cryptomator.presentation.ui.adapter
import android.view.View import android.view.View
import kotlinx.android.synthetic.main.item_shareable_location.view.*
import org.cryptomator.presentation.R import org.cryptomator.presentation.R
import org.cryptomator.presentation.model.VaultModel import org.cryptomator.presentation.model.VaultModel
import org.cryptomator.presentation.model.comparator.VaultPositionComparator
import org.cryptomator.presentation.ui.adapter.SharedLocationsAdapter.VaultViewHolder import org.cryptomator.presentation.ui.adapter.SharedLocationsAdapter.VaultViewHolder
import javax.inject.Inject import javax.inject.Inject
import kotlinx.android.synthetic.main.item_shareable_location.view.chooseFolderLocation
import kotlinx.android.synthetic.main.item_shareable_location.view.chosenLocation
import kotlinx.android.synthetic.main.item_shareable_location.view.cloudImage
import kotlinx.android.synthetic.main.item_shareable_location.view.selectedVault
import kotlinx.android.synthetic.main.item_shareable_location.view.vaultName
class SharedLocationsAdapter @Inject class SharedLocationsAdapter @Inject
constructor() : RecyclerViewBaseAdapter<VaultModel, SharedLocationsAdapter.Callback, VaultViewHolder>() { constructor() : RecyclerViewBaseAdapter<VaultModel, SharedLocationsAdapter.Callback, VaultViewHolder>(VaultPositionComparator()) {
private var selectedVault: VaultModel? = null private var selectedVault: VaultModel? = null
private var selectedLocation: String? = null private var selectedLocation: String? = null

View File

@ -1,21 +1,31 @@
package org.cryptomator.presentation.ui.adapter package org.cryptomator.presentation.ui.adapter
import android.view.View import android.view.View
import kotlinx.android.synthetic.main.item_vault.view.*
import org.cryptomator.presentation.R import org.cryptomator.presentation.R
import org.cryptomator.presentation.model.VaultModel import org.cryptomator.presentation.model.VaultModel
import org.cryptomator.presentation.model.comparator.VaultPositionComparator
import org.cryptomator.presentation.ui.adapter.VaultsAdapter.VaultViewHolder import org.cryptomator.presentation.ui.adapter.VaultsAdapter.VaultViewHolder
import javax.inject.Inject import javax.inject.Inject
import kotlinx.android.synthetic.main.item_vault.view.cloudImage
import kotlinx.android.synthetic.main.item_vault.view.settings
import kotlinx.android.synthetic.main.item_vault.view.unlockedImage
import kotlinx.android.synthetic.main.item_vault.view.vaultName
import kotlinx.android.synthetic.main.item_vault.view.vaultPath
class VaultsAdapter @Inject class VaultsAdapter @Inject
internal constructor() : RecyclerViewBaseAdapter<VaultModel, VaultsAdapter.OnItemClickListener, VaultViewHolder>() { internal constructor() : RecyclerViewBaseAdapter<VaultModel, VaultsAdapter.OnItemInteractionListener, VaultViewHolder>(VaultPositionComparator()), VaultsMoveListener.Listener {
interface OnItemInteractionListener {
interface OnItemClickListener {
fun onVaultClicked(vaultModel: VaultModel) fun onVaultClicked(vaultModel: VaultModel)
fun onVaultSettingsClicked(vaultModel: VaultModel) fun onVaultSettingsClicked(vaultModel: VaultModel)
fun onVaultLockClicked(vaultModel: VaultModel) fun onVaultLockClicked(vaultModel: VaultModel)
fun onRowMoved(fromPosition: Int, toPosition: Int)
fun onVaultMoved(fromPosition: Int, toPosition: Int)
} }
override fun getItemLayout(viewType: Int): Int { override fun getItemLayout(viewType: Int): Int {
@ -65,4 +75,12 @@ internal constructor() : RecyclerViewBaseAdapter<VaultModel, VaultsAdapter.OnIte
itemView.settings.setOnClickListener { callback.onVaultSettingsClicked(vaultModel) } itemView.settings.setOnClickListener { callback.onVaultSettingsClicked(vaultModel) }
} }
} }
override fun onVaultMoved(fromPosition: Int, toPosition: Int) {
callback.onVaultMoved(fromPosition, toPosition)
}
override fun onRowMoved(fromPosition: Int, toPosition: Int) {
callback.onRowMoved(fromPosition, toPosition)
}
} }

View File

@ -0,0 +1,57 @@
package org.cryptomator.presentation.ui.adapter
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
class VaultsMoveListener(val adapter: VaultsAdapter) : ItemTouchHelper.Callback() {
var dragFrom = -1
var dragTo = -1
override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int {
val dragFlags = ItemTouchHelper.UP or ItemTouchHelper.DOWN
return makeMovementFlags(dragFlags, 0)
}
override fun isItemViewSwipeEnabled(): Boolean {
return false
}
override fun isLongPressDragEnabled(): Boolean {
return true
}
override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean {
val fromPosition = viewHolder.adapterPosition
val toPosition = target.adapterPosition
if (dragFrom == -1) {
dragFrom = fromPosition;
}
dragTo = toPosition;
adapter.onRowMoved(viewHolder.adapterPosition, target.adapterPosition)
return true
}
override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {
super.clearView(recyclerView, viewHolder)
if (dragFrom != -1 && dragTo != -1 && dragFrom != dragTo) {
adapter.onVaultMoved(dragFrom, dragTo)
}
dragTo = -1
dragFrom = -1
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
}
interface Listener {
fun onRowMoved(fromPosition: Int, toPosition: Int)
fun onVaultMoved(fromPosition: Int, toPosition: Int)
}
}

View File

@ -179,11 +179,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
val strDate: String = dateFormatUser.format(lastUpdateCheck) val strDate: String = dateFormatUser.format(lastUpdateCheck)
format(getString(R.string.screen_settings_last_check_updates), strDate) format(getString(R.string.screen_settings_last_check_updates), strDate)
} else { } else {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
getString(R.string.screen_settings_last_check_updates_never) getString(R.string.screen_settings_last_check_updates_never)
} else {
getString(R.string.screen_settings_last_check_updates_never_pre_marshmallow)
}
} }
val date = SpannableString(readableDate) val date = SpannableString(readableDate)

View File

@ -8,6 +8,7 @@ import org.cryptomator.presentation.R
import org.cryptomator.presentation.model.CloudFolderModel import org.cryptomator.presentation.model.CloudFolderModel
import org.cryptomator.presentation.model.SharedFileModel import org.cryptomator.presentation.model.SharedFileModel
import org.cryptomator.presentation.model.VaultModel import org.cryptomator.presentation.model.VaultModel
import org.cryptomator.presentation.model.comparator.VaultPositionComparator
import org.cryptomator.presentation.presenter.SharedFilesPresenter import org.cryptomator.presentation.presenter.SharedFilesPresenter
import org.cryptomator.presentation.ui.adapter.SharedFilesAdapter import org.cryptomator.presentation.ui.adapter.SharedFilesAdapter
import org.cryptomator.presentation.ui.adapter.SharedFilesAdapter.Callback import org.cryptomator.presentation.ui.adapter.SharedFilesAdapter.Callback
@ -69,9 +70,10 @@ class SharedFilesFragment : BaseFragment() {
} }
fun displayVaults(vaults: List<VaultModel>?) { fun displayVaults(vaults: List<VaultModel>?) {
if (vaults?.isNotEmpty() == true) { val sortedVaults = vaults?.sortedWith(VaultPositionComparator())
presenter.selectedVault?.let { presenter.selectedVault = vaults[vaults.indexOf(it)] } if (sortedVaults?.isNotEmpty() == true) {
val preselectedVault = presenter.selectedVault ?: vaults[0] presenter.selectedVault?.let { presenter.selectedVault = sortedVaults[sortedVaults.indexOf(it)] }
val preselectedVault = presenter.selectedVault ?: sortedVaults[0]
locationsAdapter.setPreselectedVault(preselectedVault) locationsAdapter.setPreselectedVault(preselectedVault)
presenter.onVaultSelected(preselectedVault) presenter.onVaultSelected(preselectedVault)
} }

View File

@ -2,6 +2,7 @@ package org.cryptomator.presentation.ui.fragment
import android.util.TypedValue import android.util.TypedValue
import android.view.View import android.view.View
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.android.synthetic.main.fragment_vault_list.* import kotlinx.android.synthetic.main.fragment_vault_list.*
import kotlinx.android.synthetic.main.recycler_view_layout.* import kotlinx.android.synthetic.main.recycler_view_layout.*
@ -11,6 +12,7 @@ import org.cryptomator.presentation.R
import org.cryptomator.presentation.model.VaultModel import org.cryptomator.presentation.model.VaultModel
import org.cryptomator.presentation.presenter.VaultListPresenter import org.cryptomator.presentation.presenter.VaultListPresenter
import org.cryptomator.presentation.ui.adapter.VaultsAdapter import org.cryptomator.presentation.ui.adapter.VaultsAdapter
import org.cryptomator.presentation.ui.adapter.VaultsMoveListener
import javax.inject.Inject import javax.inject.Inject
@Fragment(R.layout.fragment_vault_list) @Fragment(R.layout.fragment_vault_list)
@ -22,7 +24,9 @@ class VaultListFragment : BaseFragment() {
@Inject @Inject
lateinit var vaultsAdapter: VaultsAdapter lateinit var vaultsAdapter: VaultsAdapter
private val onItemClickListener = object : VaultsAdapter.OnItemClickListener { lateinit var touchHelper: ItemTouchHelper
private val onItemClickListener = object : VaultsAdapter.OnItemInteractionListener {
override fun onVaultClicked(vaultModel: VaultModel) { override fun onVaultClicked(vaultModel: VaultModel) {
vaultListPresenter.onVaultClicked(vaultModel) vaultListPresenter.onVaultClicked(vaultModel)
} }
@ -34,8 +38,17 @@ class VaultListFragment : BaseFragment() {
override fun onVaultLockClicked(vaultModel: VaultModel) { override fun onVaultLockClicked(vaultModel: VaultModel) {
vaultListPresenter.onVaultLockClicked(vaultModel) vaultListPresenter.onVaultLockClicked(vaultModel)
} }
override fun onRowMoved(fromPosition: Int, toPosition: Int) {
vaultListPresenter.onRowMoved(fromPosition, toPosition)
} }
override fun onVaultMoved(fromPosition: Int, toPosition: Int) {
vaultListPresenter.onVaultMoved(fromPosition, toPosition)
}
}
override fun setupView() { override fun setupView() {
setupRecyclerView() setupRecyclerView()
fab_vault.setOnClickListener { vaultListPresenter.onCreateVaultClicked() } fab_vault.setOnClickListener { vaultListPresenter.onCreateVaultClicked() }
@ -48,6 +61,9 @@ class VaultListFragment : BaseFragment() {
private fun setupRecyclerView() { private fun setupRecyclerView() {
vaultsAdapter.setCallback(onItemClickListener) vaultsAdapter.setCallback(onItemClickListener)
touchHelper = ItemTouchHelper(VaultsMoveListener(vaultsAdapter))
touchHelper.attachToRecyclerView(recyclerView)
recyclerView.layoutManager = LinearLayoutManager(context()) recyclerView.layoutManager = LinearLayoutManager(context())
recyclerView.adapter = vaultsAdapter recyclerView.adapter = vaultsAdapter
recyclerView.setHasFixedSize(true) // smoother scrolling recyclerView.setHasFixedSize(true) // smoother scrolling
@ -83,5 +99,14 @@ class VaultListFragment : BaseFragment() {
vaultsAdapter.addOrUpdateVault(vaultModel) vaultsAdapter.addOrUpdateVault(vaultModel)
} }
fun vaultMoved(vaults: List<VaultModel>) {
vaultsAdapter.clear()
vaultsAdapter.addAll(vaults)
}
fun rowMoved(fromPosition: Int, toPosition: Int) {
vaultsAdapter.notifyItemMoved(fromPosition, toPosition)
}
fun rootView(): View = coordinatorLayout fun rootView(): View = coordinatorLayout
} }

View File

@ -7,6 +7,7 @@ import org.cryptomator.domain.CloudFolder;
import org.cryptomator.domain.Vault; import org.cryptomator.domain.Vault;
import org.cryptomator.domain.di.PerView; import org.cryptomator.domain.di.PerView;
import org.cryptomator.domain.usecases.cloud.GetRootFolderUseCase; import org.cryptomator.domain.usecases.cloud.GetRootFolderUseCase;
import org.cryptomator.domain.usecases.vault.GetVaultListUseCase;
import org.cryptomator.domain.usecases.vault.SaveVaultUseCase; import org.cryptomator.domain.usecases.vault.SaveVaultUseCase;
import org.cryptomator.generator.Callback; import org.cryptomator.generator.Callback;
import org.cryptomator.presentation.R; import org.cryptomator.presentation.R;
@ -18,6 +19,7 @@ import org.cryptomator.presentation.model.mappers.CloudModelMapper;
import org.cryptomator.presentation.presenter.VaultListPresenter; import org.cryptomator.presentation.presenter.VaultListPresenter;
import java.io.Serializable; import java.io.Serializable;
import java.util.List;
import javax.inject.Inject; import javax.inject.Inject;
@ -30,6 +32,7 @@ import static org.cryptomator.presentation.intent.Intents.chooseCloudServiceInte
public class AddExistingVaultWorkflow extends Workflow<AddExistingVaultWorkflow.State> { public class AddExistingVaultWorkflow extends Workflow<AddExistingVaultWorkflow.State> {
private final SaveVaultUseCase saveVaultUseCase; private final SaveVaultUseCase saveVaultUseCase;
private final GetVaultListUseCase getVaultListUseCase;
private final GetRootFolderUseCase getRootFolderUseCase; private final GetRootFolderUseCase getRootFolderUseCase;
private final CloudModelMapper cloudModelMapper; private final CloudModelMapper cloudModelMapper;
private final AuthenticationExceptionHandler authenticationExceptionHandler; private final AuthenticationExceptionHandler authenticationExceptionHandler;
@ -39,12 +42,14 @@ public class AddExistingVaultWorkflow extends Workflow<AddExistingVaultWorkflow.
public AddExistingVaultWorkflow( // public AddExistingVaultWorkflow( //
Context context, // Context context, //
SaveVaultUseCase saveVaultUseCase, // SaveVaultUseCase saveVaultUseCase, //
GetVaultListUseCase getVaultListUseCase, //
GetRootFolderUseCase getRootFolderUseCase, // GetRootFolderUseCase getRootFolderUseCase, //
CloudModelMapper cloudModelMapper, // CloudModelMapper cloudModelMapper, //
AuthenticationExceptionHandler authenticationExceptionHandler) { AuthenticationExceptionHandler authenticationExceptionHandler) {
super(new State()); super(new State());
this.context = context; this.context = context;
this.saveVaultUseCase = saveVaultUseCase; this.saveVaultUseCase = saveVaultUseCase;
this.getVaultListUseCase = getVaultListUseCase;
this.getRootFolderUseCase = getRootFolderUseCase; this.getRootFolderUseCase = getRootFolderUseCase;
this.cloudModelMapper = cloudModelMapper; this.cloudModelMapper = cloudModelMapper;
this.authenticationExceptionHandler = authenticationExceptionHandler; this.authenticationExceptionHandler = authenticationExceptionHandler;
@ -117,9 +122,13 @@ public class AddExistingVaultWorkflow extends Workflow<AddExistingVaultWorkflow.
@Override @Override
void completed() { void completed() {
presenter().getView().showProgress(ProgressModel.GENERIC); presenter().getView().showProgress(ProgressModel.GENERIC);
getVaultListUseCase.run(presenter().new ProgressCompletingResultHandler<List<Vault>>() {
@Override
public void onSuccess(List<Vault> vaults) {
saveVaultUseCase// saveVaultUseCase//
.withVault(aVault() // .withVault(aVault() //
.withNamePathAndCloudFrom(state().masterkeyFile.getParent()) // .withNamePathAndCloudFrom(state().masterkeyFile.getParent()) //
.withPosition(vaults.size()) //
.thatIsNew() // .thatIsNew() //
.build()) // .build()) //
.run(presenter().new ProgressCompletingResultHandler<Vault>() { .run(presenter().new ProgressCompletingResultHandler<Vault>() {
@ -129,6 +138,8 @@ public class AddExistingVaultWorkflow extends Workflow<AddExistingVaultWorkflow.
} }
}); });
} }
});
}
public static class State implements Serializable { public static class State implements Serializable {

View File

@ -0,0 +1,78 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<group android:scaleX="0.77"
android:scaleY="0.77"
android:translateX="12.42"
android:translateY="12.42">
<path android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z"/>
<path android:fillColor="#00000000" android:pathData="M9,0L9,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,0L19,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M29,0L29,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M39,0L39,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M49,0L49,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M59,0L59,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M69,0L69,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M79,0L79,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M89,0L89,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M99,0L99,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,9L108,9"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,19L108,19"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,29L108,29"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,39L108,39"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,49L108,49"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,59L108,59"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,69L108,69"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,79L108,79"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,89L108,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,99L108,99"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,29L89,29"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,39L89,39"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,49L89,49"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,59L89,59"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,69L89,69"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,79L89,79"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M29,19L29,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M39,19L39,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M49,19L49,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M59,19L59,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M69,19L69,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M79,19L79,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
</group>
</vector>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

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