diff --git a/.github/ISSUE_TEMPLATE/bug.md b/.github/ISSUE_TEMPLATE/bug.md index 4775a0aa2..528dd76ad 100644 --- a/.github/ISSUE_TEMPLATE/bug.md +++ b/.github/ISSUE_TEMPLATE/bug.md @@ -5,8 +5,11 @@ labels: type:bug --- diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000..fd88223e4 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: Cryptomator Community + url: https://community.cryptomator.org/ + about: Please ask and answer questions here + - name: Documentation + url: https://docs.cryptomator.org/ + about: Get instructions on how to use Cryptomator diff --git a/main/buildkit/pom.xml b/main/buildkit/pom.xml index 5fb81e1b7..7249ec1da 100644 --- a/main/buildkit/pom.xml +++ b/main/buildkit/pom.xml @@ -4,7 +4,7 @@ org.cryptomator main - 1.5.10 + 1.5.11 buildkit pom diff --git a/main/commons/pom.xml b/main/commons/pom.xml index d973da126..8391a20ac 100644 --- a/main/commons/pom.xml +++ b/main/commons/pom.xml @@ -4,7 +4,7 @@ org.cryptomator main - 1.5.10 + 1.5.11 commons Cryptomator Commons diff --git a/main/commons/src/main/java/org/cryptomator/common/LazyInitializer.java b/main/commons/src/main/java/org/cryptomator/common/LazyInitializer.java deleted file mode 100644 index 7bca69457..000000000 --- a/main/commons/src/main/java/org/cryptomator/common/LazyInitializer.java +++ /dev/null @@ -1,79 +0,0 @@ -/******************************************************************************* - * Copyright (c) 2017 Skymatic UG (haftungsbeschränkt). - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the accompanying LICENSE file. - *******************************************************************************/ -package org.cryptomator.common; - -import com.google.common.base.Throwables; - -import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Supplier; -import java.util.function.UnaryOperator; - -public final class LazyInitializer { - - private LazyInitializer() { - } - - /** - * Same as {@link #initializeLazily(AtomicReference, SupplierThrowingException, Class)} except that no checked exception may be thrown by the factory function. - * - * @param Type of the value - * @param reference A reference to a maybe not yet initialized value. - * @param factory A factory providing a value for the reference, if it doesn't exist yet. The factory may be invoked multiple times, but only one result will survive. - * @return The initialized value - */ - public static T initializeLazily(AtomicReference reference, Supplier factory) { - SupplierThrowingException factoryThrowingRuntimeExceptions = () -> factory.get(); - return initializeLazily(reference, factoryThrowingRuntimeExceptions, RuntimeException.class); - } - - /** - * Threadsafe lazy initialization pattern as proposed on http://stackoverflow.com/a/30247202/4014509 - * - * @param Type of the value - * @param Type of the any expected exception that may occur during initialization - * @param reference A reference to a maybe not yet initialized value. - * @param factory A factory providing a value for the reference, if it doesn't exist yet. The factory may be invoked multiple times, but only one result will survive. - * @param exceptionType Expected exception type. - * @return The initialized value - * @throws E Exception thrown by the factory function. - */ - public static T initializeLazily(AtomicReference reference, SupplierThrowingException factory, Class exceptionType) throws E { - final T existing = reference.get(); - if (existing != null) { - return existing; - } else { - try { - return reference.updateAndGet(invokeFactoryIfNull(factory)); - } catch (InitializationException e) { - Throwables.throwIfUnchecked(e.getCause()); - Throwables.throwIfInstanceOf(e.getCause(), exceptionType); - throw e; - } - } - } - - private static UnaryOperator invokeFactoryIfNull(SupplierThrowingException factory) throws InitializationException { - return currentValue -> { - if (currentValue == null) { - try { - return factory.get(); - } catch (Exception e) { - throw new InitializationException(e); - } - } else { - return currentValue; - } - }; - } - - private static class InitializationException extends RuntimeException { - - public InitializationException(Throwable cause) { - super(cause); - } - - } -} diff --git a/main/commons/src/main/java/org/cryptomator/common/mountpoint/MacVolumeMountChooser.java b/main/commons/src/main/java/org/cryptomator/common/mountpoint/MacVolumeMountChooser.java index d6ebb4da5..334746860 100644 --- a/main/commons/src/main/java/org/cryptomator/common/mountpoint/MacVolumeMountChooser.java +++ b/main/commons/src/main/java/org/cryptomator/common/mountpoint/MacVolumeMountChooser.java @@ -3,25 +3,22 @@ package org.cryptomator.common.mountpoint; import org.apache.commons.lang3.SystemUtils; import org.cryptomator.common.settings.VaultSettings; import org.cryptomator.common.vaults.Volume; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import javax.inject.Inject; -import java.nio.file.Files; import java.nio.file.Path; import java.util.Optional; class MacVolumeMountChooser implements MountPointChooser { - private static final Logger LOG = LoggerFactory.getLogger(MacVolumeMountChooser.class); - private static final int MAX_MOUNTPOINT_CREATION_RETRIES = 10; private static final Path VOLUME_PATH = Path.of("/Volumes"); private final VaultSettings vaultSettings; + private final MountPointHelper helper; @Inject - public MacVolumeMountChooser(VaultSettings vaultSettings) { + public MacVolumeMountChooser(VaultSettings vaultSettings, MountPointHelper helper) { this.vaultSettings = vaultSettings; + this.helper = helper; } @Override @@ -31,26 +28,7 @@ class MacVolumeMountChooser implements MountPointChooser { @Override public Optional chooseMountPoint(Volume caller) { - String basename = this.vaultSettings.mountName().get(); - // regular - Path mountPoint = VOLUME_PATH.resolve(basename); - if (Files.notExists(mountPoint)) { - return Optional.of(mountPoint); - } - // with id - mountPoint = VOLUME_PATH.resolve(basename + " (" + vaultSettings.getId() + ")"); - if (Files.notExists(mountPoint)) { - return Optional.of(mountPoint); - } - // with id and count - for (int i = 1; i < MAX_MOUNTPOINT_CREATION_RETRIES; i++) { - mountPoint = VOLUME_PATH.resolve(basename + "_(" + vaultSettings.getId() + ")_" + i); - if (Files.notExists(mountPoint)) { - return Optional.of(mountPoint); - } - } - LOG.error("Failed to find feasible mountpoint at /Volumes/{}_x. Giving up after {} attempts.", basename, MAX_MOUNTPOINT_CREATION_RETRIES); - return Optional.empty(); + return Optional.of(helper.chooseTemporaryMountPoint(vaultSettings, VOLUME_PATH)); } @Override diff --git a/main/commons/src/main/java/org/cryptomator/common/mountpoint/IrregularUnmountCleaner.java b/main/commons/src/main/java/org/cryptomator/common/mountpoint/MountPointHelper.java similarity index 60% rename from main/commons/src/main/java/org/cryptomator/common/mountpoint/IrregularUnmountCleaner.java rename to main/commons/src/main/java/org/cryptomator/common/mountpoint/MountPointHelper.java index 6bcb36f9a..704f2f62d 100644 --- a/main/commons/src/main/java/org/cryptomator/common/mountpoint/IrregularUnmountCleaner.java +++ b/main/commons/src/main/java/org/cryptomator/common/mountpoint/MountPointHelper.java @@ -1,11 +1,13 @@ package org.cryptomator.common.mountpoint; import org.cryptomator.common.Environment; +import org.cryptomator.common.settings.VaultSettings; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.inject.Inject; import javax.inject.Singleton; +import java.io.File; import java.io.IOException; import java.nio.file.DirectoryNotEmptyException; import java.nio.file.Files; @@ -15,27 +17,50 @@ import java.nio.file.attribute.BasicFileAttributes; import java.util.Optional; @Singleton -class IrregularUnmountCleaner { +class MountPointHelper { - public static Logger LOG = LoggerFactory.getLogger(IrregularUnmountCleaner.class); + public static Logger LOG = LoggerFactory.getLogger(MountPointHelper.class); + private static final int MAX_TMPMOUNTPOINT_CREATION_RETRIES = 10; private final Optional tmpMountPointDir; - private volatile boolean alreadyChecked = false; + private volatile boolean unmountDebrisCleared = false; @Inject - public IrregularUnmountCleaner(Environment env) { + public MountPointHelper(Environment env) { this.tmpMountPointDir = env.getMountPointsDir(); } + public Path chooseTemporaryMountPoint(VaultSettings vaultSettings, Path parentDir) { + String basename = vaultSettings.mountName().get(); + //regular + Path mountPoint = parentDir.resolve(basename); + if (Files.notExists(mountPoint)) { + return mountPoint; + } + //with id + mountPoint = parentDir.resolve(basename + " (" + vaultSettings.getId() + ")"); + if (Files.notExists(mountPoint)) { + return mountPoint; + } + //with id and count + for (int i = 1; i < MAX_TMPMOUNTPOINT_CREATION_RETRIES; i++) { + mountPoint = parentDir.resolve(basename + "_(" + vaultSettings.getId() + ")_" + i); + if (Files.notExists(mountPoint)) { + return mountPoint; + } + } + LOG.error("Failed to find feasible mountpoint at {}{}{}_x. Giving up after {} attempts.", parentDir, File.separator, basename, MAX_TMPMOUNTPOINT_CREATION_RETRIES); + return null; + } public synchronized void clearIrregularUnmountDebrisIfNeeded() { - if (alreadyChecked || tmpMountPointDir.isEmpty()) { - return; //nuthin to do + if (unmountDebrisCleared || tmpMountPointDir.isEmpty()) { + return; // nothing to do } if (Files.exists(tmpMountPointDir.get(), LinkOption.NOFOLLOW_LINKS)) { clearIrregularUnmountDebris(tmpMountPointDir.get()); } - alreadyChecked = true; + unmountDebrisCleared = true; } private void clearIrregularUnmountDebris(Path dirContainingMountPoints) { @@ -66,13 +91,14 @@ class IrregularUnmountCleaner { } catch (IOException e) { LOG.warn("Unable to perform cleanup of mountpoint dir {}.", dirContainingMountPoints, e); } finally { - alreadyChecked = true; + unmountDebrisCleared = true; } } private void deleteEmptyDir(Path dir) throws IOException { assert Files.isDirectory(dir, LinkOption.NOFOLLOW_LINKS); try { + ensureIsEmpty(dir); Files.delete(dir); // attempt to delete dir non-recursively (will fail, if there are contents) } catch (DirectoryNotEmptyException e) { LOG.info("Found non-empty directory in mountpoint dir: {}", dir); @@ -86,4 +112,9 @@ class IrregularUnmountCleaner { } } + private void ensureIsEmpty(Path dir) throws IOException { + if (Files.newDirectoryStream(dir).iterator().hasNext()) { + throw new DirectoryNotEmptyException(dir.toString()); + } + } } diff --git a/main/commons/src/main/java/org/cryptomator/common/mountpoint/TemporaryMountPointChooser.java b/main/commons/src/main/java/org/cryptomator/common/mountpoint/TemporaryMountPointChooser.java index 071ed7d1a..eb1d8d0b1 100644 --- a/main/commons/src/main/java/org/cryptomator/common/mountpoint/TemporaryMountPointChooser.java +++ b/main/commons/src/main/java/org/cryptomator/common/mountpoint/TemporaryMountPointChooser.java @@ -7,7 +7,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.inject.Inject; -import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; @@ -16,18 +15,16 @@ import java.util.Optional; class TemporaryMountPointChooser implements MountPointChooser { private static final Logger LOG = LoggerFactory.getLogger(TemporaryMountPointChooser.class); - private static final int MAX_TMPMOUNTPOINT_CREATION_RETRIES = 10; private final VaultSettings vaultSettings; private final Environment environment; - private final IrregularUnmountCleaner cleaner; - private volatile boolean clearedDebris; + private final MountPointHelper helper; @Inject - public TemporaryMountPointChooser(VaultSettings vaultSettings, Environment environment, IrregularUnmountCleaner cleaner) { + public TemporaryMountPointChooser(VaultSettings vaultSettings, Environment environment, MountPointHelper helper) { this.vaultSettings = vaultSettings; this.environment = environment; - this.cleaner = cleaner; + this.helper = helper; } @Override @@ -44,32 +41,8 @@ class TemporaryMountPointChooser implements MountPointChooser { assert environment.getMountPointsDir().isPresent(); //clean leftovers of not-regularly unmounted vaults //see https://github.com/cryptomator/cryptomator/issues/1013 and https://github.com/cryptomator/cryptomator/issues/1061 - cleaner.clearIrregularUnmountDebrisIfNeeded(); - return this.environment.getMountPointsDir().map(this::choose); - } - - - private Path choose(Path parent) { - String basename = this.vaultSettings.mountName().get(); - //regular - Path mountPoint = parent.resolve(basename); - if (Files.notExists(mountPoint)) { - return mountPoint; - } - //with id - mountPoint = parent.resolve(basename + " (" + vaultSettings.getId() + ")"); - if (Files.notExists(mountPoint)) { - return mountPoint; - } - //with id and count - for (int i = 1; i < MAX_TMPMOUNTPOINT_CREATION_RETRIES; i++) { - mountPoint = parent.resolve(basename + "_(" + vaultSettings.getId() + ")_" + i); - if (Files.notExists(mountPoint)) { - return mountPoint; - } - } - LOG.error("Failed to find feasible mountpoint at {}{}{}_x. Giving up after {} attempts.", parent, File.separator, basename, MAX_TMPMOUNTPOINT_CREATION_RETRIES); - return null; + helper.clearIrregularUnmountDebrisIfNeeded(); + return this.environment.getMountPointsDir().map(dir -> this.helper.chooseTemporaryMountPoint(this.vaultSettings, dir)); } @Override diff --git a/main/commons/src/main/java/org/cryptomator/common/settings/SettingsProvider.java b/main/commons/src/main/java/org/cryptomator/common/settings/SettingsProvider.java index 390e40f5a..85bc436a2 100644 --- a/main/commons/src/main/java/org/cryptomator/common/settings/SettingsProvider.java +++ b/main/commons/src/main/java/org/cryptomator/common/settings/SettingsProvider.java @@ -8,13 +8,13 @@ *******************************************************************************/ package org.cryptomator.common.settings; +import com.google.common.base.Suppliers; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonElement; import com.google.gson.JsonParseException; import com.google.gson.JsonParser; import org.cryptomator.common.Environment; -import org.cryptomator.common.LazyInitializer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -48,7 +48,7 @@ public class SettingsProvider implements Supplier { private static final long SAVE_DELAY_MS = 1000; private final AtomicReference> scheduledSaveCmd = new AtomicReference<>(); - private final AtomicReference settings = new AtomicReference<>(); + private final Supplier settings = Suppliers.memoize(this::load); private final SettingsJsonAdapter settingsJsonAdapter = new SettingsJsonAdapter(); private final Environment env; private final ScheduledExecutorService scheduler; @@ -66,7 +66,7 @@ public class SettingsProvider implements Supplier { @Override public Settings get() { - return LazyInitializer.initializeLazily(settings, this::load); + return settings.get(); } private Settings load() { diff --git a/main/commons/src/main/java/org/cryptomator/common/vaults/AbstractVolume.java b/main/commons/src/main/java/org/cryptomator/common/vaults/AbstractVolume.java index 6a09683ca..d66ad19d8 100644 --- a/main/commons/src/main/java/org/cryptomator/common/vaults/AbstractVolume.java +++ b/main/commons/src/main/java/org/cryptomator/common/vaults/AbstractVolume.java @@ -20,7 +20,8 @@ public abstract class AbstractVolume implements Volume { } protected Path determineMountPoint() throws InvalidMountPointException { - for (var chooser : Iterables.filter(choosers, c -> c.isApplicable(this))) { + var applicableChoosers = Iterables.filter(choosers, c -> c.isApplicable(this)); + for (var chooser : applicableChoosers) { Optional chosenPath = chooser.chooseMountPoint(this); if (chosenPath.isEmpty()) { // chooser couldn't find a feasible mountpoint continue; @@ -29,7 +30,7 @@ public abstract class AbstractVolume implements Volume { this.usedChooser = chooser; return chosenPath.get(); } - throw new InvalidMountPointException("No feasible MountPoint found!"); + throw new InvalidMountPointException(String.format("No feasible MountPoint found by choosers: %s", applicableChoosers)); } protected void cleanupMountPoint() { diff --git a/main/commons/src/main/java/org/cryptomator/common/vaults/DokanyVolume.java b/main/commons/src/main/java/org/cryptomator/common/vaults/DokanyVolume.java index 89fd73203..e0379e9db 100644 --- a/main/commons/src/main/java/org/cryptomator/common/vaults/DokanyVolume.java +++ b/main/commons/src/main/java/org/cryptomator/common/vaults/DokanyVolume.java @@ -13,7 +13,6 @@ import org.slf4j.LoggerFactory; import javax.inject.Inject; import javax.inject.Named; -import java.util.SortedSet; import java.util.concurrent.ExecutorService; public class DokanyVolume extends AbstractVolume { @@ -42,7 +41,6 @@ public class DokanyVolume extends AbstractVolume { @Override public void mount(CryptoFileSystem fs, String mountFlags) throws InvalidMountPointException, VolumeException { this.mountPoint = determineMountPoint(); - String mountName = vaultSettings.mountName().get(); try { this.mount = mountFactory.mount(fs.getPath("/"), mountPoint, vaultSettings.mountName().get(), FS_TYPE_NAME, mountFlags.strip()); } catch (MountFailedException e) { @@ -62,11 +60,25 @@ public class DokanyVolume extends AbstractVolume { } @Override - public void unmount() { - mount.close(); + public void unmount() throws VolumeException { + try { + mount.unmount(); + } catch (IllegalStateException e) { + throw new VolumeException("Unmount Failed.", e); + } cleanupMountPoint(); } + @Override + public void unmountForced() { + mount.unmountForced(); + cleanupMountPoint(); + } + + @Override + public boolean supportsForcedUnmount() { + return true; + } @Override public boolean isSupported() { return DokanyVolume.isSupportedStatic(); diff --git a/main/commons/src/main/java/org/cryptomator/common/vaults/Vault.java b/main/commons/src/main/java/org/cryptomator/common/vaults/Vault.java index f2ab973f6..730f0fb6d 100644 --- a/main/commons/src/main/java/org/cryptomator/common/vaults/Vault.java +++ b/main/commons/src/main/java/org/cryptomator/common/vaults/Vault.java @@ -10,7 +10,6 @@ package org.cryptomator.common.vaults; import com.google.common.base.Strings; import org.apache.commons.lang3.SystemUtils; -import org.cryptomator.common.LazyInitializer; import org.cryptomator.common.mountpoint.InvalidMountPointException; import org.cryptomator.common.settings.VaultSettings; import org.cryptomator.common.vaults.Volume.VolumeException; @@ -100,11 +99,7 @@ public class Vault { // Commands // ********************************************************************************/ - private CryptoFileSystem getCryptoFileSystem(CharSequence passphrase) throws NoSuchFileException, IOException, InvalidPassphraseException, CryptoException { - return LazyInitializer.initializeLazily(cryptoFileSystem, () -> unlockCryptoFileSystem(passphrase), IOException.class); - } - - private CryptoFileSystem unlockCryptoFileSystem(CharSequence passphrase) throws NoSuchFileException, IOException, InvalidPassphraseException, CryptoException { + private CryptoFileSystem createCryptoFileSystem(CharSequence passphrase) throws NoSuchFileException, IOException, InvalidPassphraseException, CryptoException { Set flags = EnumSet.noneOf(FileSystemFlags.class); if (vaultSettings.usesReadOnlyMode().get()) { flags.add(FileSystemFlags.READONLY); @@ -127,9 +122,14 @@ public class Vault { } public synchronized void unlock(CharSequence passphrase) throws CryptoException, IOException, VolumeException, InvalidMountPointException { - CryptoFileSystem fs = getCryptoFileSystem(passphrase); - volume = volumeProvider.get(); - volume.mount(fs, getEffectiveMountFlags()); + if (cryptoFileSystem.get() == null) { + CryptoFileSystem fs = createCryptoFileSystem(passphrase); + cryptoFileSystem.set(fs); + volume = volumeProvider.get(); + volume.mount(fs, getEffectiveMountFlags()); + } else { + throw new IllegalStateException("Already unlocked."); + } } public synchronized void lock(boolean forced) throws VolumeException { diff --git a/main/commons/src/main/java/org/cryptomator/common/vaults/VaultModule.java b/main/commons/src/main/java/org/cryptomator/common/vaults/VaultModule.java index e9fe26957..56a2814cf 100644 --- a/main/commons/src/main/java/org/cryptomator/common/vaults/VaultModule.java +++ b/main/commons/src/main/java/org/cryptomator/common/vaults/VaultModule.java @@ -156,7 +156,7 @@ public class VaultModule { //See: https://github.com/billziss-gh/winfsp/issues/319 if (!readOnly.get()) { flags.append(" -ouid=-1"); - flags.append(" -ogid=-1"); + flags.append(" -ogid=11"); } flags.append(" -ovolname=").append('"').append(mountName.get()).append('"'); //Dokany requires this option to be set, WinFSP doesn't seem to share this peculiarity, diff --git a/main/commons/src/test/java/org/cryptomator/common/vaults/VaultModuleTest.java b/main/commons/src/test/java/org/cryptomator/common/vaults/VaultModuleTest.java index 9b25ebb08..835c05e16 100644 --- a/main/commons/src/test/java/org/cryptomator/common/vaults/VaultModuleTest.java +++ b/main/commons/src/test/java/org/cryptomator/common/vaults/VaultModuleTest.java @@ -43,7 +43,7 @@ public class VaultModuleTest { StringBinding result = module.provideDefaultMountFlags(settings, vaultSettings); - MatcherAssert.assertThat(result.get(), CoreMatchers.containsString("-ovolname=TEST")); + MatcherAssert.assertThat(result.get(), CoreMatchers.containsString("-ovolname=\"TEST\"")); MatcherAssert.assertThat(result.get(), CoreMatchers.containsString("-ordonly")); } diff --git a/main/launcher/pom.xml b/main/launcher/pom.xml index b21876369..0c6192086 100644 --- a/main/launcher/pom.xml +++ b/main/launcher/pom.xml @@ -4,7 +4,7 @@ org.cryptomator main - 1.5.10 + 1.5.11 launcher Cryptomator Launcher diff --git a/main/pom.xml b/main/pom.xml index a558f8dea..6e8157d1a 100644 --- a/main/pom.xml +++ b/main/pom.xml @@ -3,7 +3,7 @@ 4.0.0 org.cryptomator main - 1.5.10 + 1.5.11 pom Cryptomator @@ -26,12 +26,12 @@ 1.9.13 0.1.6 - 0.1.0-beta1 + 0.2.0 0.1.0-beta3 0.1.0-beta2 - 1.2.5 - 1.2.0 - 1.0.13 + 1.2.6 + 1.2.1 + 1.0.14 15 @@ -175,6 +175,13 @@ java-jwt ${jwt.version} + + + com.fasterxml.jackson.core + jackson-databind + 2.10.5.1 + + @@ -223,6 +230,13 @@ ${javafx.version} test + + + + com.fasterxml.jackson.core + jackson-databind + 2.10.5.1 + @@ -325,6 +339,32 @@ + + dependency-check + + + + org.owasp + dependency-check-maven + 6.0.3 + + 24 + 0 + true + true + suppression.xml + + + + + check + + + + + + + diff --git a/main/suppression.xml b/main/suppression.xml new file mode 100644 index 000000000..6fe12f417 --- /dev/null +++ b/main/suppression.xml @@ -0,0 +1,19 @@ + + + + + + com.fasterxml.jackson.core:jackson-databind:2.10.5.1 + CVE-2020-25649 + + + + ^org\.cryptomator:fuse-nio-adapter:.*$ + 9 + + + + ^com\.github\.serceman:jnr-fuse:.*$ + 9 + + \ No newline at end of file diff --git a/main/ui/pom.xml b/main/ui/pom.xml index 08251656b..00a3d46d9 100644 --- a/main/ui/pom.xml +++ b/main/ui/pom.xml @@ -4,7 +4,7 @@ org.cryptomator main - 1.5.10 + 1.5.11 ui Cryptomator GUI diff --git a/main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/CreateNewVaultLocationController.java b/main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/CreateNewVaultLocationController.java index 1efd5ba50..4eeea5c3d 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/CreateNewVaultLocationController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/CreateNewVaultLocationController.java @@ -1,6 +1,5 @@ package org.cryptomator.ui.addvaultwizard; -import com.tobiasdiez.easybind.EasyBind; import dagger.Lazy; import org.cryptomator.ui.common.ErrorComponent; import org.cryptomator.ui.common.FxController; @@ -84,11 +83,11 @@ public class CreateNewVaultLocationController implements FxController { public void initialize() { predefinedLocationToggler.selectedToggleProperty().addListener(this::togglePredefinedLocation); usePresetPath.bind(predefinedLocationToggler.selectedToggleProperty().isNotEqualTo(customRadioButton)); - EasyBind.subscribe(vaultPath, this::vaultPathDidChange); + vaultPath.addListener(this::vaultPathDidChange); } - private void vaultPathDidChange(Path newValue) { - if (newValue != null && !Files.notExists(newValue)) { + private void vaultPathDidChange(@SuppressWarnings("unused") ObservableValue observable, @SuppressWarnings("unused") Path oldValue, Path newValue) { + if (!Files.notExists(newValue)) { warningText.set(resourceBundle.getString("addvaultwizard.new.fileAlreadyExists")); } else { warningText.set(null); diff --git a/main/ui/src/main/java/org/cryptomator/ui/common/FxmlFile.java b/main/ui/src/main/java/org/cryptomator/ui/common/FxmlFile.java index 43074a605..310e11747 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/common/FxmlFile.java +++ b/main/ui/src/main/java/org/cryptomator/ui/common/FxmlFile.java @@ -11,6 +11,8 @@ public enum FxmlFile { CHANGEPASSWORD("/fxml/changepassword.fxml"), // ERROR("/fxml/error.fxml"), // FORGET_PASSWORD("/fxml/forget_password.fxml"), // + LOCK_FORCED("/fxml/lock_forced.fxml"), // + LOCK_FAILED("/fxml/lock_failed.fxml"), // MAIN_WINDOW("/fxml/main_window.fxml"), // MIGRATION_CAPABILITY_ERROR("/fxml/migration_capability_error.fxml"), // MIGRATION_IMPOSSIBLE("/fxml/migration_impossible.fxml"), diff --git a/main/ui/src/main/java/org/cryptomator/ui/common/VaultService.java b/main/ui/src/main/java/org/cryptomator/ui/common/VaultService.java index 35ed49870..1f4ffc3fd 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/common/VaultService.java +++ b/main/ui/src/main/java/org/cryptomator/ui/common/VaultService.java @@ -65,6 +65,7 @@ public class VaultService { public Task createLockTask(Vault vault, boolean forced) { Task task = new LockVaultTask(vault, forced); task.setOnSucceeded(evt -> LOG.info("Locked {}", vault.getDisplayName())); + task.setOnFailed(evt -> LOG.info("Failed to lock {}.", vault.getDisplayName(), evt.getSource().getException())); return task; } diff --git a/main/ui/src/main/java/org/cryptomator/ui/fxapp/FxApplication.java b/main/ui/src/main/java/org/cryptomator/ui/fxapp/FxApplication.java index 650254649..1a4cb84de 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/fxapp/FxApplication.java +++ b/main/ui/src/main/java/org/cryptomator/ui/fxapp/FxApplication.java @@ -1,6 +1,5 @@ package org.cryptomator.ui.fxapp; -import com.tobiasdiez.easybind.EasyBind; import dagger.Lazy; import org.cryptomator.common.LicenseHolder; import org.cryptomator.common.settings.Settings; @@ -12,6 +11,7 @@ import org.cryptomator.integrations.uiappearance.UiAppearanceException; import org.cryptomator.integrations.uiappearance.UiAppearanceListener; import org.cryptomator.integrations.uiappearance.UiAppearanceProvider; import org.cryptomator.ui.common.VaultService; +import org.cryptomator.ui.lock.LockComponent; import org.cryptomator.ui.mainwindow.MainWindowComponent; import org.cryptomator.ui.preferences.PreferencesComponent; import org.cryptomator.ui.preferences.SelectedPreferencesTab; @@ -27,8 +27,9 @@ import javafx.application.Platform; import javafx.beans.binding.Bindings; import javafx.beans.binding.BooleanBinding; import javafx.beans.value.ObservableValue; -import javafx.collections.ObservableSet; +import javafx.collections.ObservableList; import javafx.stage.Stage; +import javafx.stage.Window; import java.awt.desktop.QuitResponse; import java.util.Optional; @@ -40,34 +41,38 @@ public class FxApplication extends Application { private final Settings settings; private final Lazy mainWindow; private final Lazy preferencesWindow; + private final Lazy quitWindow; private final Provider unlockWindowBuilderProvider; - private final Provider quitWindowBuilderProvider; + private final Provider lockWindowBuilderProvider; private final Optional trayIntegration; private final Optional appearanceProvider; private final VaultService vaultService; private final LicenseHolder licenseHolder; - private final BooleanBinding hasVisibleStages; + private final ObservableList visibleWindows; + private final BooleanBinding hasVisibleWindows; private final UiAppearanceListener systemInterfaceThemeListener = this::systemInterfaceThemeChanged; @Inject - FxApplication(Settings settings, Lazy mainWindow, Lazy preferencesWindow, Provider unlockWindowBuilderProvider, Provider quitWindowBuilderProvider, Optional trayIntegration, Optional appearanceProvider, VaultService vaultService, LicenseHolder licenseHolder, ObservableSet visibleStages) { + FxApplication(Settings settings, Lazy mainWindow, Lazy preferencesWindow, Provider unlockWindowBuilderProvider, Provider lockWindowBuilderProvider, Lazy quitWindow, Optional trayIntegration, Optional appearanceProvider, VaultService vaultService, LicenseHolder licenseHolder) { this.settings = settings; this.mainWindow = mainWindow; this.preferencesWindow = preferencesWindow; this.unlockWindowBuilderProvider = unlockWindowBuilderProvider; - this.quitWindowBuilderProvider = quitWindowBuilderProvider; + this.lockWindowBuilderProvider = lockWindowBuilderProvider; + this.quitWindow = quitWindow; this.trayIntegration = trayIntegration; this.appearanceProvider = appearanceProvider; this.vaultService = vaultService; this.licenseHolder = licenseHolder; - this.hasVisibleStages = Bindings.isNotEmpty(visibleStages); + this.visibleWindows = Stage.getWindows().filtered(Window::isShowing); + this.hasVisibleWindows = Bindings.isNotEmpty(visibleWindows); } public void start() { LOG.trace("FxApplication.start()"); Platform.setImplicitExit(false); - EasyBind.subscribe(hasVisibleStages, this::hasVisibleStagesChanged); + hasVisibleWindows.addListener(this::hasVisibleStagesChanged); settings.theme().addListener(this::appThemeChanged); loadSelectedStyleSheet(settings.theme().get()); @@ -78,7 +83,8 @@ public class FxApplication extends Application { throw new UnsupportedOperationException("Use start() instead."); } - private void hasVisibleStagesChanged(boolean newValue) { + private void hasVisibleStagesChanged(@SuppressWarnings("unused") ObservableValue observableValue, @SuppressWarnings("unused") boolean oldValue, boolean newValue) { + LOG.warn("has visible stages: {}", newValue); if (newValue) { trayIntegration.ifPresent(TrayIntegrationProvider::restoredFromTray); } else { @@ -107,9 +113,16 @@ public class FxApplication extends Application { }); } + public void startLockWorkflow(Vault vault, Optional owner) { + Platform.runLater(() -> { + lockWindowBuilderProvider.get().vault(vault).owner(owner).build().startLockWorkflow(); + LOG.debug("Start lock workflow for {}", vault.getDisplayName()); + }); + } + public void showQuitWindow(QuitResponse response) { Platform.runLater(() -> { - quitWindowBuilderProvider.get().quitResponse(response).build().showQuitWindow(); + quitWindow.get().showQuitWindow(response); LOG.debug("Showing QuitWindow"); }); } diff --git a/main/ui/src/main/java/org/cryptomator/ui/fxapp/FxApplicationModule.java b/main/ui/src/main/java/org/cryptomator/ui/fxapp/FxApplicationModule.java index a6297ce4a..74c201372 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/fxapp/FxApplicationModule.java +++ b/main/ui/src/main/java/org/cryptomator/ui/fxapp/FxApplicationModule.java @@ -11,6 +11,7 @@ import dagger.Provides; import org.apache.commons.lang3.SystemUtils; import org.cryptomator.ui.common.ErrorComponent; import org.cryptomator.ui.common.StageFactory; +import org.cryptomator.ui.lock.LockComponent; import org.cryptomator.ui.mainwindow.MainWindowComponent; import org.cryptomator.ui.preferences.PreferencesComponent; import org.cryptomator.ui.quit.QuitComponent; @@ -18,7 +19,6 @@ import org.cryptomator.ui.unlock.UnlockComponent; import javax.inject.Named; import javafx.application.Application; -import javafx.collections.FXCollections; import javafx.collections.ObservableSet; import javafx.scene.image.Image; import javafx.stage.Stage; @@ -28,15 +28,9 @@ import java.io.UncheckedIOException; import java.util.Collections; import java.util.List; -@Module(includes = {UpdateCheckerModule.class}, subcomponents = {MainWindowComponent.class, PreferencesComponent.class, UnlockComponent.class, QuitComponent.class, ErrorComponent.class}) +@Module(includes = {UpdateCheckerModule.class}, subcomponents = {MainWindowComponent.class, PreferencesComponent.class, UnlockComponent.class, LockComponent.class, QuitComponent.class, ErrorComponent.class}) abstract class FxApplicationModule { - @Provides - @FxApplicationScoped - static ObservableSet provideVisibleStages() { - return FXCollections.observableSet(); - } - @Provides @Named("windowIcons") @FxApplicationScoped @@ -56,16 +50,9 @@ abstract class FxApplicationModule { @Provides @FxApplicationScoped - static StageFactory provideStageFactory(@Named("windowIcons") List windowIcons, ObservableSet visibleStages) { + static StageFactory provideStageFactory(@Named("windowIcons") List windowIcons) { return new StageFactory(stage -> { stage.getIcons().addAll(windowIcons); - stage.showingProperty().addListener((observableValue, wasShowing, isShowing) -> { - if (isShowing) { - visibleStages.add(stage); - } else { - visibleStages.remove(stage); - } - }); }); } @@ -88,4 +75,8 @@ abstract class FxApplicationModule { return builder.build(); } + @Provides + static QuitComponent provideQuitComponent(QuitComponent.Builder builder) { + return builder.build(); + } } diff --git a/main/ui/src/main/java/org/cryptomator/ui/lock/LockComponent.java b/main/ui/src/main/java/org/cryptomator/ui/lock/LockComponent.java new file mode 100644 index 000000000..9796c88c7 --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/lock/LockComponent.java @@ -0,0 +1,39 @@ +package org.cryptomator.ui.lock; + +import dagger.BindsInstance; +import dagger.Subcomponent; +import org.cryptomator.common.vaults.Vault; + +import javax.inject.Named; +import javafx.stage.Stage; +import java.util.Optional; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; + + +@LockScoped +@Subcomponent(modules = {LockModule.class}) +public interface LockComponent { + + ExecutorService defaultExecutorService(); + + LockWorkflow lockWorkflow(); + + default Future startLockWorkflow() { + LockWorkflow workflow = lockWorkflow(); + defaultExecutorService().submit(workflow); + return workflow; + } + + @Subcomponent.Builder + interface Builder { + + @BindsInstance + LockComponent.Builder vault(@LockWindow Vault vault); + + @BindsInstance + LockComponent.Builder owner(@Named("lockWindowOwner") Optional owner); + + LockComponent build(); + } +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/lock/LockFailedController.java b/main/ui/src/main/java/org/cryptomator/ui/lock/LockFailedController.java new file mode 100644 index 000000000..e46d1d868 --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/lock/LockFailedController.java @@ -0,0 +1,31 @@ +package org.cryptomator.ui.lock; + +import org.cryptomator.common.vaults.Vault; +import org.cryptomator.ui.common.FxController; + +import javax.inject.Inject; +import javafx.fxml.FXML; +import javafx.stage.Stage; + +@LockScoped +public class LockFailedController implements FxController { + + private final Stage window; + private final Vault vault; + + @Inject + public LockFailedController(@LockWindow Stage window, @LockWindow Vault vault) { + this.window = window; + this.vault = vault; + } + + @FXML + public void close() { + window.close(); + } + + // ----- Getter & Setter ----- + public String getVaultName() { + return vault.getDisplayName(); + } +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/lock/LockForcedController.java b/main/ui/src/main/java/org/cryptomator/ui/lock/LockForcedController.java new file mode 100644 index 000000000..8d4ce32d3 --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/lock/LockForcedController.java @@ -0,0 +1,57 @@ +package org.cryptomator.ui.lock; + +import org.cryptomator.common.vaults.Vault; +import org.cryptomator.ui.common.FxController; +import org.cryptomator.ui.common.UserInteractionLock; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.inject.Inject; +import javafx.fxml.FXML; +import javafx.stage.Stage; +import javafx.stage.WindowEvent; + +@LockScoped +public class LockForcedController implements FxController { + + private static final Logger LOG = LoggerFactory.getLogger(LockForcedController.class); + + private final Stage window; + private final Vault vault; + private final UserInteractionLock forceLockDecisionLock; + + @Inject + public LockForcedController(@LockWindow Stage window, @LockWindow Vault vault, UserInteractionLock forceLockDecisionLock) { + this.window = window; + this.vault = vault; + this.forceLockDecisionLock = forceLockDecisionLock; + this.window.setOnHiding(this::windowClosed); + } + + @FXML + public void cancel() { + forceLockDecisionLock.interacted(LockModule.ForceLockDecision.CANCEL); + window.close(); + } + + @FXML + public void confirmForcedLock() { + forceLockDecisionLock.interacted(LockModule.ForceLockDecision.FORCE); + window.close(); + } + + private void windowClosed(WindowEvent windowEvent) { + // if not already interacted, set the decision to CANCEL + if (forceLockDecisionLock.awaitingInteraction().get()) { + LOG.debug("Lock canceled in force-lock-phase by user."); + forceLockDecisionLock.interacted(LockModule.ForceLockDecision.CANCEL); + } + } + + // ----- Getter & Setter ----- + + public String getVaultName() { + return vault.getDisplayName(); + } + +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/lock/LockModule.java b/main/ui/src/main/java/org/cryptomator/ui/lock/LockModule.java new file mode 100644 index 000000000..bbc8c2209 --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/lock/LockModule.java @@ -0,0 +1,89 @@ +package org.cryptomator.ui.lock; + +import dagger.Binds; +import dagger.Module; +import dagger.Provides; +import dagger.multibindings.IntoMap; +import org.cryptomator.common.vaults.Vault; +import org.cryptomator.ui.common.DefaultSceneFactory; +import org.cryptomator.ui.common.FXMLLoaderFactory; +import org.cryptomator.ui.common.FxController; +import org.cryptomator.ui.common.FxControllerKey; +import org.cryptomator.ui.common.FxmlFile; +import org.cryptomator.ui.common.FxmlScene; +import org.cryptomator.ui.common.StageFactory; +import org.cryptomator.ui.common.UserInteractionLock; + +import javax.inject.Named; +import javax.inject.Provider; +import javafx.scene.Scene; +import javafx.stage.Modality; +import javafx.stage.Stage; +import java.util.Map; +import java.util.Optional; +import java.util.ResourceBundle; + +@Module +abstract class LockModule { + + enum ForceLockDecision { + CANCEL, + FORCE; + } + + @Provides + @LockScoped + static UserInteractionLock provideForceLockDecisionLock() { + return new UserInteractionLock<>(null); + } + + @Provides + @LockWindow + @LockScoped + static FXMLLoaderFactory provideFxmlLoaderFactory(Map, Provider> factories, DefaultSceneFactory sceneFactory, ResourceBundle resourceBundle) { + return new FXMLLoaderFactory(factories, sceneFactory, resourceBundle); + } + + @Provides + @LockWindow + @LockScoped + static Stage provideWindow(StageFactory factory, @LockWindow Vault vault, @Named("lockWindowOwner") Optional owner) { + Stage stage = factory.create(); + stage.setTitle(vault.getDisplayName()); + stage.setResizable(false); + if (owner.isPresent()) { + stage.initOwner(owner.get()); + stage.initModality(Modality.WINDOW_MODAL); + } else { + stage.initModality(Modality.APPLICATION_MODAL); + } + return stage; + } + + @Provides + @FxmlScene(FxmlFile.LOCK_FORCED) + @LockScoped + static Scene provideForceLockScene(@LockWindow FXMLLoaderFactory fxmlLoaders) { + return fxmlLoaders.createScene("/fxml/lock_forced.fxml"); + } + + @Provides + @FxmlScene(FxmlFile.LOCK_FAILED) + @LockScoped + static Scene provideLockFailedScene(@LockWindow FXMLLoaderFactory fxmlLoaders) { + return fxmlLoaders.createScene("/fxml/lock_failed.fxml"); + } + + // ------------------ + + @Binds + @IntoMap + @FxControllerKey(LockForcedController.class) + abstract FxController bindLockForcedController(LockForcedController controller); + + @Binds + @IntoMap + @FxControllerKey(LockFailedController.class) + abstract FxController bindLockFailedController(LockFailedController controller); + +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/lock/LockScoped.java b/main/ui/src/main/java/org/cryptomator/ui/lock/LockScoped.java new file mode 100644 index 000000000..68d05f6e0 --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/lock/LockScoped.java @@ -0,0 +1,13 @@ +package org.cryptomator.ui.lock; + +import javax.inject.Scope; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Scope +@Documented +@Retention(RetentionPolicy.RUNTIME) +@interface LockScoped { + +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/lock/LockWindow.java b/main/ui/src/main/java/org/cryptomator/ui/lock/LockWindow.java new file mode 100644 index 000000000..10d6445ab --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/lock/LockWindow.java @@ -0,0 +1,14 @@ +package org.cryptomator.ui.lock; + +import javax.inject.Qualifier; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Qualifier +@Documented +@Retention(RUNTIME) +@interface LockWindow { + +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/lock/LockWorkflow.java b/main/ui/src/main/java/org/cryptomator/ui/lock/LockWorkflow.java new file mode 100644 index 000000000..acb6d1355 --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/lock/LockWorkflow.java @@ -0,0 +1,105 @@ +package org.cryptomator.ui.lock; + +import dagger.Lazy; +import org.cryptomator.common.vaults.Vault; +import org.cryptomator.common.vaults.VaultState; +import org.cryptomator.common.vaults.Volume; +import org.cryptomator.ui.common.FxmlFile; +import org.cryptomator.ui.common.FxmlScene; +import org.cryptomator.ui.common.UserInteractionLock; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.inject.Inject; +import javafx.application.Platform; +import javafx.concurrent.Task; +import javafx.scene.Scene; +import javafx.stage.Stage; +import javafx.stage.Window; + +/** + * The sequence of actions performed and checked during lock of a vault. + *

+ * This class implements the Task interface, sucht that it can run in the background with some possible forground operations/requests to the ui, without blocking the main app. + * If the task state is + *

  • succeeded, the vault was successfully locked;
  • + *
  • canceled, the lock was canceled;
  • + *
  • failed, the lock failed due to an exception.
  • + */ +public class LockWorkflow extends Task { + + private static final Logger LOG = LoggerFactory.getLogger(LockWorkflow.class); + + private final Stage lockWindow; + private final Vault vault; + private final UserInteractionLock forceLockDecisionLock; + private final Lazy lockForcedScene; + private final Lazy lockFailedScene; + + @Inject + public LockWorkflow(@LockWindow Stage lockWindow, @LockWindow Vault vault, UserInteractionLock forceLockDecisionLock, @FxmlScene(FxmlFile.LOCK_FORCED) Lazy lockForcedScene, @FxmlScene(FxmlFile.LOCK_FAILED) Lazy lockFailedScene) { + this.lockWindow = lockWindow; + this.vault = vault; + this.forceLockDecisionLock = forceLockDecisionLock; + this.lockForcedScene = lockForcedScene; + this.lockFailedScene = lockFailedScene; + } + + @Override + protected Void call() throws Volume.VolumeException, InterruptedException { + try { + vault.lock(false); + } catch (Volume.VolumeException e) { + LOG.debug("Regular lock of {} failed.", vault.getDisplayName(), e); + var decision = askUserForAction(); + switch (decision) { + case FORCE -> vault.lock(true); + case CANCEL -> cancel(false); + } + } + return null; + } + + private LockModule.ForceLockDecision askUserForAction() throws InterruptedException { + // show forcedLock dialogue ... + Platform.runLater(() -> { + lockWindow.setScene(lockForcedScene.get()); + lockWindow.show(); + Window owner = lockWindow.getOwner(); + if (owner != null) { + lockWindow.setX(owner.getX() + (owner.getWidth() - lockWindow.getWidth()) / 2); + lockWindow.setY(owner.getY() + (owner.getHeight() - lockWindow.getHeight()) / 2); + } else { + lockWindow.centerOnScreen(); + } + }); + // ... and wait for answer + return forceLockDecisionLock.awaitInteraction(); + } + + @Override + protected void scheduled() { + vault.setState(VaultState.PROCESSING); + } + + @Override + protected void succeeded() { + LOG.info("Lock of {} succeeded.", vault.getDisplayName()); + vault.setState(VaultState.LOCKED); + } + + @Override + protected void failed() { + LOG.warn("Failed to lock {}.", vault.getDisplayName()); + vault.setState(VaultState.UNLOCKED); + lockWindow.setScene(lockFailedScene.get()); + lockWindow.show(); + } + + @Override + protected void cancelled() { + LOG.debug("Lock of {} canceled.", vault.getDisplayName()); + vault.setState(VaultState.UNLOCKED); + } + +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailUnlockedController.java b/main/ui/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailUnlockedController.java index 1806d9e5e..0af909bbc 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailUnlockedController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailUnlockedController.java @@ -6,25 +6,32 @@ import com.google.common.cache.LoadingCache; import org.cryptomator.common.vaults.Vault; import org.cryptomator.ui.common.FxController; import org.cryptomator.ui.common.VaultService; +import org.cryptomator.ui.fxapp.FxApplication; import org.cryptomator.ui.stats.VaultStatisticsComponent; import javax.inject.Inject; import javafx.beans.property.ObjectProperty; import javafx.beans.property.ReadOnlyObjectProperty; import javafx.fxml.FXML; +import javafx.stage.Stage; +import java.util.Optional; @MainWindowScoped public class VaultDetailUnlockedController implements FxController { private final ReadOnlyObjectProperty vault; + private final FxApplication application; private final VaultService vaultService; + private final Stage mainWindow; private final LoadingCache vaultStats; private final VaultStatisticsComponent.Builder vaultStatsBuilder; @Inject - public VaultDetailUnlockedController(ObjectProperty vault, VaultService vaultService, VaultStatisticsComponent.Builder vaultStatsBuilder) { + public VaultDetailUnlockedController(ObjectProperty vault, FxApplication application, VaultService vaultService, VaultStatisticsComponent.Builder vaultStatsBuilder, @MainWindow Stage mainWindow) { this.vault = vault; + this.application = application; this.vaultService = vaultService; + this.mainWindow = mainWindow; this.vaultStats = CacheBuilder.newBuilder().weakValues().build(CacheLoader.from(this::buildVaultStats)); this.vaultStatsBuilder = vaultStatsBuilder; } @@ -40,8 +47,7 @@ public class VaultDetailUnlockedController implements FxController { @FXML public void lock() { - vaultService.lock(vault.get(), false); - // TODO count lock attempts, and allow forced lock + application.startLockWorkflow(vault.get(), Optional.of(mainWindow)); } @FXML diff --git a/main/ui/src/main/java/org/cryptomator/ui/mainwindow/VaultListController.java b/main/ui/src/main/java/org/cryptomator/ui/mainwindow/VaultListController.java index 0533cf50f..fc751d862 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/mainwindow/VaultListController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/mainwindow/VaultListController.java @@ -1,6 +1,5 @@ package org.cryptomator.ui.mainwindow; -import com.tobiasdiez.easybind.EasyBind; import org.cryptomator.common.vaults.Vault; import org.cryptomator.common.vaults.VaultListManager; import org.cryptomator.ui.addvaultwizard.AddVaultWizardComponent; @@ -13,6 +12,7 @@ import javax.inject.Inject; import javafx.beans.binding.Bindings; import javafx.beans.binding.BooleanBinding; import javafx.beans.property.ObjectProperty; +import javafx.beans.value.ObservableValue; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import javafx.fxml.FXML; @@ -41,7 +41,7 @@ public class VaultListController implements FxController { this.removeVault = removeVault; this.noVaultSelected = selectedVault.isNull(); this.emptyVaultList = Bindings.isEmpty(vaults); - EasyBind.subscribe(selectedVault, this::selectedVaultDidChange); + selectedVault.addListener(this::selectedVaultDidChange); } public void initialize() { @@ -58,10 +58,11 @@ public class VaultListController implements FxController { }); } - private void selectedVaultDidChange(Vault newValue) { - if (newValue != null) { - VaultListManager.redetermineVaultState(newValue); + private void selectedVaultDidChange(@SuppressWarnings("unused") ObservableValue observableValue, @SuppressWarnings("unused") Vault oldValue, Vault newValue) { + if (newValue == null) { + return; } + VaultListManager.redetermineVaultState(newValue); } @FXML diff --git a/main/ui/src/main/java/org/cryptomator/ui/preferences/AutoStartMacStrategy.java b/main/ui/src/main/java/org/cryptomator/ui/preferences/AutoStartMacStrategy.java deleted file mode 100644 index 26811cdf2..000000000 --- a/main/ui/src/main/java/org/cryptomator/ui/preferences/AutoStartMacStrategy.java +++ /dev/null @@ -1,46 +0,0 @@ -package org.cryptomator.ui.preferences; - -import org.cryptomator.integrations.autostart.AutoStartProvider; -import org.cryptomator.integrations.autostart.ToggleAutoStartFailedException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionStage; - -@Deprecated -class AutoStartMacStrategy implements AutoStartStrategy { - - private static final Logger LOG = LoggerFactory.getLogger(AutoStartMacStrategy.class); - - private final AutoStartProvider autoStartProvider; - - public AutoStartMacStrategy(AutoStartProvider autoStartProvider) { - this.autoStartProvider = autoStartProvider; - } - - @Override - public CompletionStage isAutoStartEnabled() { - return CompletableFuture.completedFuture(autoStartProvider.isEnabled()); - } - - @Override - public void enableAutoStart() throws TogglingAutoStartFailedException { - try { - autoStartProvider.enable(); - LOG.debug("Added login item."); - } catch (ToggleAutoStartFailedException e) { - throw new TogglingAutoStartFailedException("Failed to add login item."); - } - } - - @Override - public void disableAutoStart() throws TogglingAutoStartFailedException { - try { - autoStartProvider.disable(); - LOG.debug("Removed login item."); - } catch (ToggleAutoStartFailedException e) { - throw new TogglingAutoStartFailedException("Failed to remove login item."); - } - } -} diff --git a/main/ui/src/main/java/org/cryptomator/ui/preferences/AutoStartModule.java b/main/ui/src/main/java/org/cryptomator/ui/preferences/AutoStartModule.java deleted file mode 100644 index 7f6406f22..000000000 --- a/main/ui/src/main/java/org/cryptomator/ui/preferences/AutoStartModule.java +++ /dev/null @@ -1,27 +0,0 @@ -package org.cryptomator.ui.preferences; - -import dagger.Module; -import dagger.Provides; -import org.apache.commons.lang3.SystemUtils; -import org.cryptomator.integrations.autostart.AutoStartProvider; - -import java.util.Optional; - -@Deprecated -@Module -abstract class AutoStartModule { - - @Provides - @PreferencesScoped - public static Optional provideAutoStartStrategy(Optional autoStartProvider) { - if (SystemUtils.IS_OS_MAC_OSX && autoStartProvider.isPresent()) { - return Optional.of(new AutoStartMacStrategy(autoStartProvider.get())); - } else if (SystemUtils.IS_OS_WINDOWS) { - Optional exeName = ProcessHandle.current().info().command(); - return exeName.map(AutoStartWinStrategy::new); - } else { - return Optional.empty(); - } - } - -} diff --git a/main/ui/src/main/java/org/cryptomator/ui/preferences/AutoStartStrategy.java b/main/ui/src/main/java/org/cryptomator/ui/preferences/AutoStartStrategy.java deleted file mode 100644 index 9848e58f8..000000000 --- a/main/ui/src/main/java/org/cryptomator/ui/preferences/AutoStartStrategy.java +++ /dev/null @@ -1,25 +0,0 @@ -package org.cryptomator.ui.preferences; - -import java.util.concurrent.CompletionStage; - -@Deprecated -public interface AutoStartStrategy { - - CompletionStage isAutoStartEnabled(); - - void enableAutoStart() throws TogglingAutoStartFailedException; - - void disableAutoStart() throws TogglingAutoStartFailedException; - - class TogglingAutoStartFailedException extends Exception { - - public TogglingAutoStartFailedException(String message) { - super(message); - } - - public TogglingAutoStartFailedException(String message, Throwable cause) { - super(message, cause); - } - - } -} diff --git a/main/ui/src/main/java/org/cryptomator/ui/preferences/AutoStartWinStrategy.java b/main/ui/src/main/java/org/cryptomator/ui/preferences/AutoStartWinStrategy.java deleted file mode 100644 index b3c0a4674..000000000 --- a/main/ui/src/main/java/org/cryptomator/ui/preferences/AutoStartWinStrategy.java +++ /dev/null @@ -1,209 +0,0 @@ -package org.cryptomator.ui.preferences; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.NoSuchFileException; -import java.nio.file.Path; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionStage; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; - -/** - * OS specific class to check, en- and disable the auto start on Windows. - *

    - * Two strategies are implemented for this feature, the first uses the registry and the second one the autostart folder. - *

    - * The registry strategy checks/add/removes at the registry key {@value HKCU_AUTOSTART_KEY} an entry for Cryptomator. - * The folder strategy checks/add/removes at the location {@value WINDOWS_START_MENU_ENTRY}. - *

    - * To check if the feature is active, both strategies are applied. - * To enable the feature, first the registry is tried and only on failure the autostart folder is used. - * To disable it, first it is determined by an internal state, which strategies must be used and in the second step those are executed. - * - * @apiNote This class is not thread safe, hence it should be avoided to call its methods simultaniously by different threads. - * @deprecated To be moved to integration-win project - */ -@Deprecated -class AutoStartWinStrategy implements AutoStartStrategy { - - private static final Logger LOG = LoggerFactory.getLogger(AutoStartWinStrategy.class); - private static final String HKCU_AUTOSTART_KEY = "\"HKCU\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run\""; - private static final String AUTOSTART_VALUE = "Cryptomator"; - private static final String WINDOWS_START_MENU_ENTRY = "\\AppData\\Roaming\\Microsoft\\Windows\\Start Menu\\Programs\\Cryptomator.lnk"; - - private final String exePath; - - private boolean activatedUsingFolder; - private boolean activatedUsingRegistry; - - public AutoStartWinStrategy(String exePath) { - this.exePath = exePath; - this.activatedUsingFolder = false; - this.activatedUsingRegistry = false; - } - - @Override - public CompletionStage isAutoStartEnabled() { - return isAutoStartEnabledUsingRegistry().thenCombine(isAutoStartEnabledUsingFolder(), (bReg, bFolder) -> bReg || bFolder); - } - - private CompletableFuture isAutoStartEnabledUsingFolder() { - Path autoStartEntry = Path.of(System.getProperty("user.home") + WINDOWS_START_MENU_ENTRY); - this.activatedUsingFolder = Files.exists(autoStartEntry); - return CompletableFuture.completedFuture(activatedUsingFolder); - } - - private CompletableFuture isAutoStartEnabledUsingRegistry() { - ProcessBuilder regQuery = new ProcessBuilder("reg", "query", HKCU_AUTOSTART_KEY, // - "/v", AUTOSTART_VALUE); - try { - Process proc = regQuery.start(); - return proc.onExit().thenApply(p -> { - this.activatedUsingRegistry = p.exitValue() == 0; - return activatedUsingRegistry; - }); - } catch (IOException e) { - LOG.debug("Failed to query {} from registry key {}", AUTOSTART_VALUE, HKCU_AUTOSTART_KEY); - return CompletableFuture.completedFuture(false); - } - } - - @Override - public void enableAutoStart() throws TogglingAutoStartFailedException { - try { - enableAutoStartUsingRegistry().thenAccept((Void v) -> this.activatedUsingRegistry = true).exceptionallyCompose(e -> { - LOG.debug("Falling back to using autostart folder."); - return this.enableAutoStartUsingFolder(); - }).thenAccept((Void v) -> this.activatedUsingFolder = true).get(); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - throw new TogglingAutoStartFailedException("Execution of enabling auto start setting was interrupted."); - } catch (ExecutionException e) { - throw new TogglingAutoStartFailedException("Enabling auto start failed both using registry and auto start folder."); - } - } - - private CompletableFuture enableAutoStartUsingRegistry() { - ProcessBuilder regAdd = new ProcessBuilder("reg", "add", HKCU_AUTOSTART_KEY, // - "/v", AUTOSTART_VALUE, // - "/t", "REG_SZ", // - "/d", "\"" + exePath + "\"", // - "/f"); - try { - Process proc = regAdd.start(); - boolean finishedInTime = waitForProcessOrCancel(proc, 5, TimeUnit.SECONDS); - if (finishedInTime && proc.exitValue() == 0) { - LOG.debug("Added {} to registry key {}.", AUTOSTART_VALUE, HKCU_AUTOSTART_KEY); - return CompletableFuture.completedFuture(null); - } else { - throw new IOException("Process exited with error code " + proc.exitValue()); - } - } catch (IOException e) { - LOG.debug("Registry could not be edited to set auto start.", e); - return CompletableFuture.failedFuture(new SystemCommandException("Adding registry value failed.")); - } - } - - private CompletableFuture enableAutoStartUsingFolder() { - String autoStartFolderEntry = System.getProperty("user.home") + WINDOWS_START_MENU_ENTRY; - String createShortcutCommand = "$s=(New-Object -COM WScript.Shell).CreateShortcut('" + autoStartFolderEntry + "');$s.TargetPath='" + exePath + "';$s.Save();"; - ProcessBuilder shortcutAdd = new ProcessBuilder("cmd", "/c", "Start powershell " + createShortcutCommand); - try { - Process proc = shortcutAdd.start(); - boolean finishedInTime = waitForProcessOrCancel(proc, 5, TimeUnit.SECONDS); - if (finishedInTime && proc.exitValue() == 0) { - LOG.debug("Created file {} for auto start.", autoStartFolderEntry); - return CompletableFuture.completedFuture(null); - } else { - throw new IOException("Process exited with error code " + proc.exitValue()); - } - } catch (IOException e) { - LOG.debug("Adding entry to auto start folder failed.", e); - return CompletableFuture.failedFuture(new SystemCommandException("Adding entry to auto start folder failed.")); - } - } - - - @Override - public void disableAutoStart() throws TogglingAutoStartFailedException { - if (activatedUsingRegistry) { - disableAutoStartUsingRegistry().whenComplete((voit, ex) -> { - if (ex == null) { - this.activatedUsingRegistry = false; - } - }); - } - - if (activatedUsingFolder) { - disableAutoStartUsingFolder().whenComplete((voit, ex) -> { - if (ex == null) { - this.activatedUsingFolder = false; - } - }); - } - - if (activatedUsingRegistry || activatedUsingFolder) { - throw new TogglingAutoStartFailedException("Disabling auto start failed using registry and/or auto start folder."); - } - } - - public CompletableFuture disableAutoStartUsingRegistry() { - ProcessBuilder regRemove = new ProcessBuilder("reg", "delete", HKCU_AUTOSTART_KEY, // - "/v", AUTOSTART_VALUE, // - "/f"); - try { - Process proc = regRemove.start(); - boolean finishedInTime = waitForProcessOrCancel(proc, 5, TimeUnit.SECONDS); - if (finishedInTime && proc.exitValue() == 0) { - LOG.debug("Removed {} from registry key {}.", AUTOSTART_VALUE, HKCU_AUTOSTART_KEY); - return CompletableFuture.completedFuture(null); - } else { - throw new IOException("Process exited with error code " + proc.exitValue()); - } - } catch (IOException e) { - LOG.debug("Registry could not be edited to remove auto start.", e); - return CompletableFuture.failedFuture(new SystemCommandException("Removing registry value failed.")); - } - } - - private CompletableFuture disableAutoStartUsingFolder() { - try { - Files.delete(Path.of(WINDOWS_START_MENU_ENTRY)); - LOG.debug("Successfully deleted {}.", WINDOWS_START_MENU_ENTRY); - return CompletableFuture.completedFuture(null); - } catch (NoSuchFileException e) { - //that is also okay - return CompletableFuture.completedFuture(null); - } catch (IOException e) { - LOG.debug("Failed to delete entry from auto start folder.", e); - return CompletableFuture.failedFuture(e); - } - } - - private static boolean waitForProcessOrCancel(Process proc, int timeout, TimeUnit timeUnit) { - boolean finishedInTime = false; - try { - finishedInTime = proc.waitFor(timeout, timeUnit); - } catch (InterruptedException e) { - LOG.error("Timeout while reading registry", e); - Thread.currentThread().interrupt(); - } finally { - if (!finishedInTime) { - proc.destroyForcibly(); - } - } - return finishedInTime; - } - - private class SystemCommandException extends RuntimeException { - - public SystemCommandException(String msg) { - super(msg); - } - } - -} diff --git a/main/ui/src/main/java/org/cryptomator/ui/preferences/DonationKeyPreferencesController.java b/main/ui/src/main/java/org/cryptomator/ui/preferences/DonationKeyPreferencesController.java index e7708af81..a4814ec82 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/preferences/DonationKeyPreferencesController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/preferences/DonationKeyPreferencesController.java @@ -1,5 +1,6 @@ package org.cryptomator.ui.preferences; +import com.google.common.base.CharMatcher; import org.cryptomator.common.LicenseHolder; import org.cryptomator.common.settings.Settings; import org.cryptomator.common.settings.UiTheme; @@ -10,6 +11,7 @@ import javafx.application.Application; import javafx.beans.value.ObservableValue; import javafx.fxml.FXML; import javafx.scene.control.TextArea; +import javafx.scene.control.TextFormatter; @PreferencesScoped public class DonationKeyPreferencesController implements FxController { @@ -32,6 +34,15 @@ public class DonationKeyPreferencesController implements FxController { public void initialize() { donationKeyField.setText(licenseHolder.getLicenseKey().orElse(null)); donationKeyField.textProperty().addListener(this::registrationKeyChanged); + donationKeyField.setTextFormatter(new TextFormatter<>(this::checkVaultNameLength)); + } + + private TextFormatter.Change checkVaultNameLength(TextFormatter.Change change) { + if (change.isContentChange()) { + var strippedText = CharMatcher.whitespace().removeFrom(change.getText()); + change.setText(strippedText); + } + return change; } private void registrationKeyChanged(@SuppressWarnings("unused") ObservableValue observable, @SuppressWarnings("unused") String oldValue, String newValue) { diff --git a/main/ui/src/main/java/org/cryptomator/ui/preferences/GeneralPreferencesController.java b/main/ui/src/main/java/org/cryptomator/ui/preferences/GeneralPreferencesController.java index 496ada2ce..6af9a156e 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/preferences/GeneralPreferencesController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/preferences/GeneralPreferencesController.java @@ -5,6 +5,8 @@ import org.cryptomator.common.LicenseHolder; import org.cryptomator.common.settings.KeychainBackend; import org.cryptomator.common.settings.Settings; import org.cryptomator.common.settings.UiTheme; +import org.cryptomator.integrations.autostart.AutoStartProvider; +import org.cryptomator.integrations.autostart.ToggleAutoStartFailedException; import org.cryptomator.integrations.keychain.KeychainAccessProvider; import org.cryptomator.ui.common.ErrorComponent; import org.cryptomator.ui.common.FxController; @@ -14,10 +16,8 @@ import org.slf4j.LoggerFactory; import javax.inject.Inject; import javax.inject.Named; import javafx.application.Application; -import javafx.application.Platform; import javafx.beans.property.ObjectProperty; import javafx.beans.value.ObservableValue; -import javafx.concurrent.Task; import javafx.fxml.FXML; import javafx.geometry.NodeOrientation; import javafx.scene.control.CheckBox; @@ -42,7 +42,7 @@ public class GeneralPreferencesController implements FxController { private final Stage window; private final Settings settings; private final boolean trayMenuSupported; - private final Optional autoStartStrategy; + private final Optional autoStartProvider; private final ObjectProperty selectedTabProperty; private final LicenseHolder licenseHolder; private final ExecutorService executor; @@ -61,11 +61,11 @@ public class GeneralPreferencesController implements FxController { public RadioButton nodeOrientationRtl; @Inject - GeneralPreferencesController(@PreferencesWindow Stage window, Settings settings, @Named("trayMenuSupported") boolean trayMenuSupported, Optional autoStartStrategy, Set keychainAccessProviders, ObjectProperty selectedTabProperty, LicenseHolder licenseHolder, ExecutorService executor, ResourceBundle resourceBundle, Application application, Environment environment, ErrorComponent.Builder errorComponent) { + GeneralPreferencesController(@PreferencesWindow Stage window, Settings settings, @Named("trayMenuSupported") boolean trayMenuSupported, Optional autoStartProvider, Set keychainAccessProviders, ObjectProperty selectedTabProperty, LicenseHolder licenseHolder, ExecutorService executor, ResourceBundle resourceBundle, Application application, Environment environment, ErrorComponent.Builder errorComponent) { this.window = window; this.settings = settings; this.trayMenuSupported = trayMenuSupported; - this.autoStartStrategy = autoStartStrategy; + this.autoStartProvider = autoStartProvider; this.keychainAccessProviders = keychainAccessProviders; this.selectedTabProperty = selectedTabProperty; this.licenseHolder = licenseHolder; @@ -89,11 +89,7 @@ public class GeneralPreferencesController implements FxController { debugModeCheckbox.selectedProperty().bindBidirectional(settings.debugMode()); - autoStartStrategy.ifPresent(autoStart -> { - autoStart.isAutoStartEnabled().thenAccept(enabled -> { - Platform.runLater(() -> autoStartCheckbox.setSelected(enabled)); - }); - }); + autoStartProvider.ifPresent(autoStart -> autoStartCheckbox.setSelected(autoStart.isEnabled())); nodeOrientationLtr.setSelected(settings.userInterfaceOrientation().get() == NodeOrientation.LEFT_TO_RIGHT); nodeOrientationRtl.setSelected(settings.userInterfaceOrientation().get() == NodeOrientation.RIGHT_TO_LEFT); @@ -114,7 +110,7 @@ public class GeneralPreferencesController implements FxController { } public boolean isAutoStartSupported() { - return autoStartStrategy.isPresent(); + return autoStartProvider.isPresent(); } private void toggleNodeOrientation(@SuppressWarnings("unused") ObservableValue observable, @SuppressWarnings("unused") Toggle oldValue, Toggle newValue) { @@ -129,15 +125,19 @@ public class GeneralPreferencesController implements FxController { @FXML public void toggleAutoStart() { - autoStartStrategy.ifPresent(autoStart -> { + autoStartProvider.ifPresent(autoStart -> { boolean enableAutoStart = autoStartCheckbox.isSelected(); - Task toggleTask = new ToggleAutoStartTask(autoStart, enableAutoStart); - toggleTask.setOnFailed(event -> { + try { + if (enableAutoStart) { + autoStart.enable(); + } else { + autoStart.disable(); + } + } catch (ToggleAutoStartFailedException e) { autoStartCheckbox.setSelected(!enableAutoStart); // restore previous state - LOG.error("Failed to toggle autostart.", event.getSource().getException()); - errorComponent.cause(event.getSource().getException()).window(window).returnToScene(window.getScene()).build().showErrorScene(); - }); - executor.execute(toggleTask); + LOG.error("Failed to toggle autostart.", e); + errorComponent.cause(e).window(window).returnToScene(window.getScene()).build().showErrorScene(); + } }); } @@ -196,27 +196,4 @@ public class GeneralPreferencesController implements FxController { } } - private static class ToggleAutoStartTask extends Task { - - private final AutoStartStrategy autoStart; - private final boolean enable; - - public ToggleAutoStartTask(AutoStartStrategy autoStart, boolean enable) { - this.autoStart = autoStart; - this.enable = enable; - - setOnFailed(event -> LOG.error("Failed to toggle Autostart", getException())); - } - - @Override - protected Void call() throws Exception { - if (enable) { - autoStart.enableAutoStart(); - } else { - autoStart.disableAutoStart(); - } - return null; - } - } - } diff --git a/main/ui/src/main/java/org/cryptomator/ui/preferences/PreferencesModule.java b/main/ui/src/main/java/org/cryptomator/ui/preferences/PreferencesModule.java index 2558582fd..3cc012b03 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/preferences/PreferencesModule.java +++ b/main/ui/src/main/java/org/cryptomator/ui/preferences/PreferencesModule.java @@ -20,7 +20,7 @@ import javafx.stage.Stage; import java.util.Map; import java.util.ResourceBundle; -@Module(includes = {AutoStartModule.class}) +@Module abstract class PreferencesModule { @Provides diff --git a/main/ui/src/main/java/org/cryptomator/ui/quit/QuitComponent.java b/main/ui/src/main/java/org/cryptomator/ui/quit/QuitComponent.java index 23ddf311a..e100c52e9 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/quit/QuitComponent.java +++ b/main/ui/src/main/java/org/cryptomator/ui/quit/QuitComponent.java @@ -5,7 +5,6 @@ *******************************************************************************/ package org.cryptomator.ui.quit; -import dagger.BindsInstance; import dagger.Lazy; import dagger.Subcomponent; import org.cryptomator.ui.common.FxmlFile; @@ -25,7 +24,10 @@ public interface QuitComponent { @FxmlScene(FxmlFile.QUIT) Lazy scene(); - default Stage showQuitWindow() { + QuitController controller(); + + default Stage showQuitWindow(QuitResponse response) { + controller().updateQuitRequest(response); Stage stage = window(); stage.setScene(scene().get()); stage.show(); @@ -36,9 +38,6 @@ public interface QuitComponent { @Subcomponent.Builder interface Builder { - @BindsInstance - Builder quitResponse(QuitResponse response); - QuitComponent build(); } diff --git a/main/ui/src/main/java/org/cryptomator/ui/quit/QuitController.java b/main/ui/src/main/java/org/cryptomator/ui/quit/QuitController.java index 130556ef2..12ddbbca3 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/quit/QuitController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/quit/QuitController.java @@ -16,6 +16,8 @@ import javafx.stage.Stage; import java.awt.desktop.QuitResponse; import java.util.Collection; import java.util.concurrent.ExecutorService; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; import java.util.stream.Collectors; @QuitScoped @@ -24,26 +26,40 @@ public class QuitController implements FxController { private static final Logger LOG = LoggerFactory.getLogger(QuitController.class); private final Stage window; - private final QuitResponse response; private final ObservableList unlockedVaults; private final ExecutorService executorService; private final VaultService vaultService; + private final AtomicReference quitResponse = new AtomicReference<>(); public Button lockAndQuitButton; @Inject - QuitController(@QuitWindow Stage window, QuitResponse response, ObservableList vaults, ExecutorService executorService, VaultService vaultService) { + QuitController(@QuitWindow Stage window, ObservableList vaults, ExecutorService executorService, VaultService vaultService) { this.window = window; - this.response = response; this.unlockedVaults = vaults.filtered(Vault::isUnlocked); this.executorService = executorService; this.vaultService = vaultService; + window.setOnCloseRequest(windowEvent -> cancel()); + } + + public void updateQuitRequest(QuitResponse newResponse) { + var oldResponse = quitResponse.getAndSet(newResponse); + if (oldResponse != null) { + oldResponse.cancelQuit(); + } + } + + private void respondToQuitRequest(Consumer action) { + var response = quitResponse.getAndSet(null); + if (response != null) { + action.accept(response); + } } @FXML public void cancel() { LOG.info("Quitting application canceled by user."); window.close(); - response.cancelQuit(); + respondToQuitRequest(QuitResponse::cancelQuit); } @FXML @@ -56,16 +72,16 @@ public class QuitController implements FxController { LOG.info("Locked {}", lockAllTask.getValue().stream().map(Vault::getDisplayName).collect(Collectors.joining(", "))); if (unlockedVaults.isEmpty()) { window.close(); - response.performQuit(); + respondToQuitRequest(QuitResponse::performQuit); } }); lockAllTask.setOnFailed(evt -> { LOG.warn("Locking failed", lockAllTask.getException()); lockAndQuitButton.setDisable(false); lockAndQuitButton.setContentDisplay(ContentDisplay.TEXT_ONLY); - // TODO: show force lock or force quit scene (and DO NOT cancelQuit() here!) - // see https://github.com/cryptomator/cryptomator/blob/1.4.16/main/ui/src/main/java/org/cryptomator/ui/model/Vault.java#L151-L163 - response.cancelQuit(); + // TODO: show force lock or force quit scene (and DO NOT cancelQuit() here!) (see https://github.com/cryptomator/cryptomator/pull/1416) + window.close(); + respondToQuitRequest(QuitResponse::cancelQuit); }); executorService.execute(lockAllTask); } diff --git a/main/ui/src/main/java/org/cryptomator/ui/quit/QuitModule.java b/main/ui/src/main/java/org/cryptomator/ui/quit/QuitModule.java index e73fa9d46..9c779f434 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/quit/QuitModule.java +++ b/main/ui/src/main/java/org/cryptomator/ui/quit/QuitModule.java @@ -43,7 +43,7 @@ abstract class QuitModule { @Provides @FxmlScene(FxmlFile.QUIT) @QuitScoped - static Scene provideUnlockScene(@QuitWindow FXMLLoaderFactory fxmlLoaders) { + static Scene provideQuitScene(@QuitWindow FXMLLoaderFactory fxmlLoaders) { return fxmlLoaders.createScene("/fxml/quit.fxml"); } diff --git a/main/ui/src/main/java/org/cryptomator/ui/traymenu/TrayMenuController.java b/main/ui/src/main/java/org/cryptomator/ui/traymenu/TrayMenuController.java index 51f7c2225..a65f1e493 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/traymenu/TrayMenuController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/traymenu/TrayMenuController.java @@ -108,7 +108,7 @@ class TrayMenuController { } private void lockVault(Vault vault) { - fxApplicationStarter.get(true).thenAccept(app -> app.getVaultService().lock(vault, false)); + fxApplicationStarter.get(true).thenAccept(app -> app.startLockWorkflow(vault, Optional.empty())); } private void lockAllVaults(ActionEvent actionEvent) { diff --git a/main/ui/src/main/java/org/cryptomator/ui/vaultoptions/GeneralVaultOptionsController.java b/main/ui/src/main/java/org/cryptomator/ui/vaultoptions/GeneralVaultOptionsController.java index b760ebf9a..e860ee811 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/vaultoptions/GeneralVaultOptionsController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/vaultoptions/GeneralVaultOptionsController.java @@ -39,7 +39,7 @@ public class GeneralVaultOptionsController implements FxController { public void initialize() { vaultName.textProperty().set(vault.getVaultSettings().displayName().get()); vaultName.focusedProperty().addListener(this::trimVaultNameOnFocusLoss); - vaultName.setTextFormatter(new TextFormatter<>(this::checkVaultNameLength)); + vaultName.setTextFormatter(new TextFormatter<>(this::removeWhitespaces)); unlockOnStartupCheckbox.selectedProperty().bindBidirectional(vault.getVaultSettings().unlockAfterStartup()); actionAfterUnlockChoiceBox.getItems().addAll(WhenUnlocked.values()); actionAfterUnlockChoiceBox.valueProperty().bindBidirectional(vault.getVaultSettings().actionAfterUnlock()); @@ -53,7 +53,7 @@ public class GeneralVaultOptionsController implements FxController { } } - private TextFormatter.Change checkVaultNameLength(TextFormatter.Change change) { + private TextFormatter.Change removeWhitespaces(TextFormatter.Change change) { if (change.isContentChange() && change.getControlNewText().length() > VAULTNAME_TRUNCATE_THRESHOLD) { return null; // reject any change that would lead to a text exceeding threshold } else { diff --git a/main/ui/src/main/resources/fxml/lock_failed.fxml b/main/ui/src/main/resources/fxml/lock_failed.fxml new file mode 100644 index 000000000..f599b0fd8 --- /dev/null +++ b/main/ui/src/main/resources/fxml/lock_failed.fxml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + +