From d2a2e2304dcca0fa7b7b546d2174f5914e1d6e65 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Thu, 4 May 2017 12:47:15 +0200 Subject: [PATCH] Implemented #40, tested on macOS --- .../cryptomator/common/LazyInitializer.java | 1 + .../cryptomator/common/settings/Settings.java | 11 ++- .../common/settings/SettingsJsonAdapter.java | 14 +-- .../common/settings/SettingsProvider.java | 5 +- .../common/settings/VaultSettings.java | 25 +++--- .../settings/VaultSettingsJsonAdapter.java | 14 ++- .../settings/SettingsJsonAdapterTest.java | 5 +- .../common/settings/SettingsTest.java | 33 +++++++ .../VaultSettingsJsonAdapterTest.java | 4 +- .../ui/controllers/MainController.java | 21 ++--- .../ui/controllers/UnlockController.java | 37 +++++--- .../cryptomator/ui/model/AutoUnlocker.java | 85 +++++++++++++++++++ .../java/org/cryptomator/ui/model/Vault.java | 21 ++--- .../cryptomator/ui/model/VaultFactory.java | 3 +- .../org/cryptomator/ui/model/VaultList.java | 42 +++++---- main/ui/src/main/resources/fxml/settings.fxml | 2 +- main/ui/src/main/resources/fxml/unlock.fxml | 20 +++-- .../ui/src/main/resources/localization/en.txt | 1 + 18 files changed, 245 insertions(+), 99 deletions(-) create mode 100644 main/commons/src/test/java/org/cryptomator/common/settings/SettingsTest.java create mode 100644 main/ui/src/main/java/org/cryptomator/ui/model/AutoUnlocker.java diff --git a/main/commons/src/main/java/org/cryptomator/common/LazyInitializer.java b/main/commons/src/main/java/org/cryptomator/common/LazyInitializer.java index 0d14ce339..de5d8abfd 100644 --- a/main/commons/src/main/java/org/cryptomator/common/LazyInitializer.java +++ b/main/commons/src/main/java/org/cryptomator/common/LazyInitializer.java @@ -43,6 +43,7 @@ public final class LazyInitializer { try { return reference.updateAndGet(invokeFactoryIfNull(factory)); } catch (InitializationException e) { + Throwables.throwIfUnchecked(e); Throwables.throwIfInstanceOf(e.getCause(), exceptionType); throw e; } diff --git a/main/commons/src/main/java/org/cryptomator/common/settings/Settings.java b/main/commons/src/main/java/org/cryptomator/common/settings/Settings.java index a43c8cf83..cccd9808d 100644 --- a/main/commons/src/main/java/org/cryptomator/common/settings/Settings.java +++ b/main/commons/src/main/java/org/cryptomator/common/settings/Settings.java @@ -34,20 +34,19 @@ public class Settings { public static final String DEFAULT_GVFS_SCHEME = "dav"; public static final boolean DEFAULT_DEBUG_MODE = false; - private final Consumer saveCmd; - private final ObservableList directories = FXCollections.observableArrayList(); + private final ObservableList directories = FXCollections.observableArrayList(VaultSettings::observables); private final BooleanProperty checkForUpdates = new SimpleBooleanProperty(DEFAULT_CHECK_FOR_UDPATES); private final IntegerProperty port = new SimpleIntegerProperty(DEFAULT_PORT); private final BooleanProperty useIpv6 = new SimpleBooleanProperty(DEFAULT_USE_IPV6); private final IntegerProperty numTrayNotifications = new SimpleIntegerProperty(DEFAULT_NUM_TRAY_NOTIFICATIONS); private final StringProperty preferredGvfsScheme = new SimpleStringProperty(DEFAULT_GVFS_SCHEME); private final BooleanProperty debugMode = new SimpleBooleanProperty(DEFAULT_DEBUG_MODE); + private Consumer saveCmd; /** * Package-private constructor; use {@link SettingsProvider}. */ - Settings(Consumer saveCmd) { - this.saveCmd = saveCmd; + Settings() { directories.addListener((ListChangeListener.Change change) -> this.save()); checkForUpdates.addListener(this::somethingChanged); port.addListener(this::somethingChanged); @@ -57,6 +56,10 @@ public class Settings { debugMode.addListener(this::somethingChanged); } + void setSaveCmd(Consumer saveCmd) { + this.saveCmd = saveCmd; + } + private void somethingChanged(ObservableValue observable, Object oldValue, Object newValue) { this.save(); } diff --git a/main/commons/src/main/java/org/cryptomator/common/settings/SettingsJsonAdapter.java b/main/commons/src/main/java/org/cryptomator/common/settings/SettingsJsonAdapter.java index 1bb4ddbc8..cc1bc3792 100644 --- a/main/commons/src/main/java/org/cryptomator/common/settings/SettingsJsonAdapter.java +++ b/main/commons/src/main/java/org/cryptomator/common/settings/SettingsJsonAdapter.java @@ -8,7 +8,6 @@ package org.cryptomator.common.settings; import java.io.IOException; import java.util.ArrayList; import java.util.List; -import java.util.function.Consumer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -22,13 +21,8 @@ public class SettingsJsonAdapter extends TypeAdapter { private static final Logger LOG = LoggerFactory.getLogger(SettingsJsonAdapter.class); - private final Consumer saveCmd; private final VaultSettingsJsonAdapter vaultSettingsJsonAdapter = new VaultSettingsJsonAdapter(); - public SettingsJsonAdapter(Consumer saveCmd) { - this.saveCmd = saveCmd; - } - @Override public void write(JsonWriter out, Settings value) throws IOException { out.beginObject(); @@ -53,14 +47,14 @@ public class SettingsJsonAdapter extends TypeAdapter { @Override public Settings read(JsonReader in) throws IOException { - Settings settings = new Settings(saveCmd); + Settings settings = new Settings(); in.beginObject(); while (in.hasNext()) { String name = in.nextName(); switch (name) { case "directories": - settings.getDirectories().addAll(readVaultSettingsArray(in, settings)); + settings.getDirectories().addAll(readVaultSettingsArray(in)); break; case "checkForUpdatesEnabled": settings.checkForUpdates().set(in.nextBoolean()); @@ -93,11 +87,11 @@ public class SettingsJsonAdapter extends TypeAdapter { return settings; } - private List readVaultSettingsArray(JsonReader in, Settings settings) throws IOException { + private List readVaultSettingsArray(JsonReader in) throws IOException { List result = new ArrayList<>(); in.beginArray(); while (!JsonToken.END_ARRAY.equals(in.peek())) { - result.add(vaultSettingsJsonAdapter.read(in, settings)); + result.add(vaultSettingsJsonAdapter.read(in)); } in.endArray(); return result; 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 b413a7e23..2974252db 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 @@ -62,7 +62,7 @@ public class SettingsProvider implements Provider { private final ScheduledExecutorService saveScheduler = Executors.newSingleThreadScheduledExecutor(); private final AtomicReference> scheduledSaveCmd = new AtomicReference<>(); private final AtomicReference settings = new AtomicReference<>(); - private final SettingsJsonAdapter settingsJsonAdapter = new SettingsJsonAdapter(this::scheduleSave); + private final SettingsJsonAdapter settingsJsonAdapter = new SettingsJsonAdapter(); private final Gson gson; @Inject @@ -100,8 +100,9 @@ public class SettingsProvider implements Provider { LOG.info("Settings loaded from " + settingsPath); } catch (IOException e) { LOG.info("Failed to load settings, creating new one."); - settings = new Settings(this::scheduleSave); + settings = new Settings(); } + settings.setSaveCmd(this::scheduleSave); return settings; } diff --git a/main/commons/src/main/java/org/cryptomator/common/settings/VaultSettings.java b/main/commons/src/main/java/org/cryptomator/common/settings/VaultSettings.java index 34e281aac..8ee82146f 100644 --- a/main/commons/src/main/java/org/cryptomator/common/settings/VaultSettings.java +++ b/main/commons/src/main/java/org/cryptomator/common/settings/VaultSettings.java @@ -15,41 +15,36 @@ import java.util.UUID; import org.apache.commons.lang3.StringUtils; import org.fxmisc.easybind.EasyBind; +import javafx.beans.Observable; import javafx.beans.property.BooleanProperty; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; -import javafx.beans.value.ObservableValue; public class VaultSettings { + public static final boolean DEFAULT_UNLOCK_AFTER_STARTUP = false; public static final boolean DEFAULT_MOUNT_AFTER_UNLOCK = true; public static final boolean DEFAULT_REAVEAL_AFTER_MOUNT = true; - private final Settings settings; private final String id; private final ObjectProperty path = new SimpleObjectProperty<>(); private final StringProperty mountName = new SimpleStringProperty(); private final StringProperty winDriveLetter = new SimpleStringProperty(); + private final BooleanProperty unlockAfterStartup = new SimpleBooleanProperty(DEFAULT_UNLOCK_AFTER_STARTUP); private final BooleanProperty mountAfterUnlock = new SimpleBooleanProperty(DEFAULT_MOUNT_AFTER_UNLOCK); private final BooleanProperty revealAfterMount = new SimpleBooleanProperty(DEFAULT_REAVEAL_AFTER_MOUNT); - public VaultSettings(Settings settings, String id) { - this.settings = settings; + public VaultSettings(String id) { this.id = Objects.requireNonNull(id); EasyBind.subscribe(path, this::deriveMountNameFromPath); - path.addListener(this::somethingChanged); - mountName.addListener(this::somethingChanged); - winDriveLetter.addListener(this::somethingChanged); - mountAfterUnlock.addListener(this::somethingChanged); - revealAfterMount.addListener(this::somethingChanged); } - private void somethingChanged(ObservableValue observable, Object oldValue, Object newValue) { - settings.save(); + Observable[] observables() { + return new Observable[] {path, mountName, winDriveLetter, unlockAfterStartup, mountAfterUnlock, revealAfterMount}; } private void deriveMountNameFromPath(Path path) { @@ -58,8 +53,8 @@ public class VaultSettings { } } - public static VaultSettings withRandomId(Settings settings) { - return new VaultSettings(settings, generateId()); + public static VaultSettings withRandomId() { + return new VaultSettings(generateId()); } private static String generateId() { @@ -116,6 +111,10 @@ public class VaultSettings { return winDriveLetter; } + public BooleanProperty unlockAfterStartup() { + return unlockAfterStartup; + } + public BooleanProperty mountAfterUnlock() { return mountAfterUnlock; } diff --git a/main/commons/src/main/java/org/cryptomator/common/settings/VaultSettingsJsonAdapter.java b/main/commons/src/main/java/org/cryptomator/common/settings/VaultSettingsJsonAdapter.java index 7fd9bc407..d1de6231e 100644 --- a/main/commons/src/main/java/org/cryptomator/common/settings/VaultSettingsJsonAdapter.java +++ b/main/commons/src/main/java/org/cryptomator/common/settings/VaultSettingsJsonAdapter.java @@ -24,18 +24,20 @@ class VaultSettingsJsonAdapter { out.name("path").value(value.path().get().toString()); out.name("mountName").value(value.mountName().get()); out.name("winDriveLetter").value(value.winDriveLetter().get()); + out.name("unlockAfterStartup").value(value.unlockAfterStartup().get()); out.name("mountAfterUnlock").value(value.mountAfterUnlock().get()); out.name("revealAfterMount").value(value.revealAfterMount().get()); out.endObject(); } - public VaultSettings read(JsonReader in, Settings settings) throws IOException { + public VaultSettings read(JsonReader in) throws IOException { String id = null; String path = null; String mountName = null; String winDriveLetter = null; - boolean mountAfterUnlock = true; - boolean revealAfterMount = true; + boolean unlockAfterStartup = VaultSettings.DEFAULT_UNLOCK_AFTER_STARTUP; + boolean mountAfterUnlock = VaultSettings.DEFAULT_MOUNT_AFTER_UNLOCK; + boolean revealAfterMount = VaultSettings.DEFAULT_REAVEAL_AFTER_MOUNT; in.beginObject(); while (in.hasNext()) { @@ -53,6 +55,9 @@ class VaultSettingsJsonAdapter { case "winDriveLetter": winDriveLetter = in.nextString(); break; + case "unlockAfterStartup": + unlockAfterStartup = in.nextBoolean(); + break; case "mountAfterUnlock": mountAfterUnlock = in.nextBoolean(); break; @@ -66,10 +71,11 @@ class VaultSettingsJsonAdapter { } in.endObject(); - VaultSettings vaultSettings = (id == null) ? VaultSettings.withRandomId(settings) : new VaultSettings(settings, id); + VaultSettings vaultSettings = (id == null) ? VaultSettings.withRandomId() : new VaultSettings(id); vaultSettings.mountName().set(mountName); vaultSettings.path().set(Paths.get(path)); vaultSettings.winDriveLetter().set(winDriveLetter); + vaultSettings.unlockAfterStartup().set(unlockAfterStartup); vaultSettings.mountAfterUnlock().set(mountAfterUnlock); vaultSettings.revealAfterMount().set(revealAfterMount); return vaultSettings; diff --git a/main/commons/src/test/java/org/cryptomator/common/settings/SettingsJsonAdapterTest.java b/main/commons/src/test/java/org/cryptomator/common/settings/SettingsJsonAdapterTest.java index 6b32a06b3..23ddaab1d 100644 --- a/main/commons/src/test/java/org/cryptomator/common/settings/SettingsJsonAdapterTest.java +++ b/main/commons/src/test/java/org/cryptomator/common/settings/SettingsJsonAdapterTest.java @@ -12,10 +12,7 @@ import org.junit.Test; public class SettingsJsonAdapterTest { - private final SettingsJsonAdapter adapter = new SettingsJsonAdapter(this::noop); - - private void noop(Settings settings) { - } + private final SettingsJsonAdapter adapter = new SettingsJsonAdapter(); @Test public void testDeserialize() throws IOException { diff --git a/main/commons/src/test/java/org/cryptomator/common/settings/SettingsTest.java b/main/commons/src/test/java/org/cryptomator/common/settings/SettingsTest.java new file mode 100644 index 000000000..fcce53253 --- /dev/null +++ b/main/commons/src/test/java/org/cryptomator/common/settings/SettingsTest.java @@ -0,0 +1,33 @@ +package org.cryptomator.common.settings; + +import java.io.IOException; +import java.util.function.Consumer; + +import org.junit.Test; +import org.mockito.Mockito; + +public class SettingsTest { + + @Test + public void testAutoSave() throws IOException { + @SuppressWarnings("unchecked") + Consumer changeListener = Mockito.mock(Consumer.class); + Settings settings = new Settings(); + settings.setSaveCmd(changeListener); + VaultSettings vaultSettings = VaultSettings.withRandomId(); + Mockito.verify(changeListener, Mockito.times(0)).accept(settings); + + // first change (to property): + settings.preferredGvfsScheme().set("asd"); + Mockito.verify(changeListener, Mockito.times(1)).accept(settings); + + // second change (to list): + settings.getDirectories().add(vaultSettings); + Mockito.verify(changeListener, Mockito.times(2)).accept(settings); + + // third change (to property of list item): + vaultSettings.mountName().set("asd"); + Mockito.verify(changeListener, Mockito.times(3)).accept(settings); + } + +} diff --git a/main/commons/src/test/java/org/cryptomator/common/settings/VaultSettingsJsonAdapterTest.java b/main/commons/src/test/java/org/cryptomator/common/settings/VaultSettingsJsonAdapterTest.java index 0c2604e10..7320b6768 100644 --- a/main/commons/src/test/java/org/cryptomator/common/settings/VaultSettingsJsonAdapterTest.java +++ b/main/commons/src/test/java/org/cryptomator/common/settings/VaultSettingsJsonAdapterTest.java @@ -11,7 +11,6 @@ import java.nio.file.Paths; import org.junit.Assert; import org.junit.Test; -import org.mockito.Mockito; import com.google.gson.stream.JsonReader; @@ -23,9 +22,8 @@ public class VaultSettingsJsonAdapterTest { public void testDeserialize() throws IOException { String json = "{\"id\": \"foo\", \"path\": \"/foo/bar\", \"mountName\": \"test\", \"winDriveLetter\": \"X\", \"shouldBeIgnored\": true}"; JsonReader jsonReader = new JsonReader(new StringReader(json)); - Settings settings = Mockito.mock(Settings.class); - VaultSettings vaultSettings = adapter.read(jsonReader, settings); + VaultSettings vaultSettings = adapter.read(jsonReader); Assert.assertEquals("foo", vaultSettings.getId()); Assert.assertEquals(Paths.get("/foo/bar"), vaultSettings.path().get()); Assert.assertEquals("test", vaultSettings.mountName().get()); diff --git a/main/ui/src/main/java/org/cryptomator/ui/controllers/MainController.java b/main/ui/src/main/java/org/cryptomator/ui/controllers/MainController.java index 80223b92f..96eaaeec6 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/controllers/MainController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/controllers/MainController.java @@ -26,10 +26,10 @@ import javax.inject.Named; import javax.inject.Singleton; import org.apache.commons.lang3.SystemUtils; -import org.cryptomator.common.settings.Settings; import org.cryptomator.common.settings.VaultSettings; import org.cryptomator.ui.ExitUtil; import org.cryptomator.ui.controls.DirectoryListCell; +import org.cryptomator.ui.model.AutoUnlocker; import org.cryptomator.ui.model.UpgradeStrategies; import org.cryptomator.ui.model.UpgradeStrategy; import org.cryptomator.ui.model.Vault; @@ -50,7 +50,9 @@ import javafx.beans.binding.Bindings; import javafx.beans.binding.BooleanBinding; import javafx.beans.binding.BooleanExpression; import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleObjectProperty; +import javafx.collections.ObservableList; import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.geometry.Side; @@ -83,11 +85,11 @@ public class MainController implements ViewController { private final Localization localization; private final ExecutorService executorService; private final BlockingQueue fileOpenRequests; - private final Settings settings; private final VaultFactory vaultFactoy; private final ViewControllerLoader viewControllerLoader; private final ObjectProperty activeController = new SimpleObjectProperty<>(); - private final VaultList vaults; + private final ObservableList vaults; + private final BooleanBinding areAllVaultsLocked; private final ObjectProperty selectedVault = new SimpleObjectProperty<>(); private final BooleanExpression isSelectedVaultUnlocked = BooleanExpression.booleanExpression(EasyBind.select(selectedVault).selectObject(Vault::unlockedProperty).orElse(false)); private final BooleanExpression isSelectedVaultValid = BooleanExpression.booleanExpression(EasyBind.monadic(selectedVault).map(Vault::isValidVaultDirectory).orElse(false)); @@ -100,13 +102,12 @@ public class MainController implements ViewController { @Inject public MainController(@Named("mainWindow") Stage mainWindow, ExecutorService executorService, @Named("fileOpenRequests") BlockingQueue fileOpenRequests, ExitUtil exitUtil, Localization localization, - Settings settings, VaultFactory vaultFactoy, ViewControllerLoader viewControllerLoader, UpgradeStrategies upgradeStrategies, VaultList vaults) { + VaultFactory vaultFactoy, ViewControllerLoader viewControllerLoader, UpgradeStrategies upgradeStrategies, VaultList vaults, AutoUnlocker autoUnlocker) { this.mainWindow = mainWindow; this.executorService = executorService; this.fileOpenRequests = fileOpenRequests; this.exitUtil = exitUtil; this.localization = localization; - this.settings = settings; this.vaultFactoy = vaultFactoy; this.viewControllerLoader = viewControllerLoader; this.vaults = vaults; @@ -114,6 +115,10 @@ public class MainController implements ViewController { // derived bindings: this.isShowingSettings = Bindings.equal(SettingsController.class, EasyBind.monadic(activeController).map(ViewController::getClass)); this.upgradeStrategyForSelectedVault = EasyBind.monadic(selectedVault).map(upgradeStrategies::getUpgradeStrategy); + this.areAllVaultsLocked = new SimpleBooleanProperty(false).not(); // = Bindings.isEmpty(FXCollections.observableList(vaults, Vault::observables).filtered(Vault::isUnlocked)); + + EasyBind.subscribe(areAllVaultsLocked, Platform::setImplicitExit); + autoUnlocker.unlockAllSilently(); } @FXML @@ -281,7 +286,7 @@ public class MainController implements ViewController { } final Vault vault = vaults.stream().filter(v -> v.getPath().equals(vaultPath)).findAny().orElseGet(() -> { - VaultSettings vaultSettings = VaultSettings.withRandomId(settings); + VaultSettings vaultSettings = VaultSettings.withRandomId(); vaultSettings.path().set(vaultPath); return vaultFactoy.get(vaultSettings); }); @@ -399,7 +404,6 @@ public class MainController implements ViewController { } public void didUnlock(Vault vault) { - Platform.setImplicitExit(false); if (vault.equals(selectedVault.getValue())) { this.showUnlockedView(vault); } @@ -417,9 +421,6 @@ public class MainController implements ViewController { public void didLock(UnlockedController ctrl) { unlockedVaults.remove(ctrl.getVault()); showUnlockView(); - if (!vaults.stream().anyMatch(Vault::isUnlocked)) { - Platform.setImplicitExit(true); - } } private void showChangePasswordView() { diff --git a/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockController.java b/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockController.java index 0c8f79434..ffaad54cc 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockController.java @@ -17,6 +17,7 @@ import javax.inject.Inject; import org.apache.commons.lang3.CharUtils; import org.apache.commons.lang3.SystemUtils; +import org.cryptomator.common.settings.VaultSettings; import org.cryptomator.cryptolib.api.InvalidPassphraseException; import org.cryptomator.cryptolib.api.UnsupportedVaultFormatException; import org.cryptomator.frontend.webdav.ServerLifecycleException; @@ -28,6 +29,8 @@ import org.cryptomator.ui.model.WindowsDriveLetters; import org.cryptomator.ui.settings.Localization; import org.cryptomator.ui.util.AsyncTaskService; import org.cryptomator.ui.util.DialogBuilderUtil; +import org.fxmisc.easybind.EasyBind; +import org.fxmisc.easybind.Subscription; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -72,6 +75,7 @@ public class UnlockController implements ViewController { private final Optional keychainAccess; private Vault vault; private Optional listener = Optional.empty(); + private Subscription vaultSubs = Subscription.EMPTY; @Inject public UnlockController(Application app, Localization localization, AsyncTaskService asyncTaskService, WindowsDriveLetters driveLetters, Optional keychainAccess) { @@ -124,6 +128,9 @@ public class UnlockController implements ViewController { @FXML private GridPane root; + @FXML + private CheckBox unlockAfterStartup; + @Override public void initialize() { advancedOptions.managedProperty().bind(advancedOptions.visibleProperty()); @@ -133,6 +140,7 @@ public class UnlockController implements ViewController { mountName.addEventFilter(KeyEvent.KEY_TYPED, this::filterAlphanumericKeyEvents); mountName.textProperty().addListener(this::mountNameDidChange); savePassword.setDisable(!keychainAccess.isPresent()); + unlockAfterStartup.disableProperty().bind(savePassword.disabledProperty().or(savePassword.selectedProperty().not())); if (SystemUtils.IS_OS_WINDOWS) { winDriveLetter.setConverter(new WinDriveLetterLabelConverter()); } else { @@ -149,18 +157,17 @@ public class UnlockController implements ViewController { } void setVault(Vault vault) { - // TODO overheadhunter refactor - if (this.vault != null) { - this.vault.getVaultSettings().mountAfterUnlock().unbind(); - this.vault.getVaultSettings().revealAfterMount().unbind(); - } + vaultSubs.unsubscribe(); + vaultSubs = Subscription.EMPTY; + // trigger "default" change to refresh key bindings: unlockButton.setDefaultButton(false); unlockButton.setDefaultButton(true); - if (vault.equals(this.vault)) { + if (Objects.equals(this.vault, Objects.requireNonNull(vault))) { return; } - this.vault = Objects.requireNonNull(vault); + assert vault != null; + this.vault = vault; passwordField.swipe(); advancedOptions.setVisible(false); advancedOptionsButton.setText(localization.getString("unlock.button.advancedOptions.show")); @@ -190,10 +197,18 @@ public class UnlockController implements ViewController { Arrays.fill(storedPw, ' '); } } - mountAfterUnlock.setSelected(this.vault.getVaultSettings().mountAfterUnlock().get()); - revealAfterMount.setSelected(this.vault.getVaultSettings().revealAfterMount().get()); - this.vault.getVaultSettings().mountAfterUnlock().bind(mountAfterUnlock.selectedProperty()); - this.vault.getVaultSettings().revealAfterMount().bind(revealAfterMount.selectedProperty()); + VaultSettings settings = vault.getVaultSettings(); + unlockAfterStartup.setSelected(savePassword.isSelected() && settings.unlockAfterStartup().get()); + mountAfterUnlock.setSelected(settings.mountAfterUnlock().get()); + revealAfterMount.setSelected(settings.revealAfterMount().get()); + + // settings.unlockAfterStartup().bind(unlockAfterStartup.selectedProperty()); + // settings.mountAfterUnlock().bind(mountAfterUnlock.selectedProperty()); + // settings.revealAfterMount().bind(revealAfterMount.selectedProperty()); + + vaultSubs = vaultSubs.and(EasyBind.subscribe(unlockAfterStartup.selectedProperty(), settings.unlockAfterStartup()::set)); + vaultSubs = vaultSubs.and(EasyBind.subscribe(mountAfterUnlock.selectedProperty(), settings.mountAfterUnlock()::set)); + vaultSubs = vaultSubs.and(EasyBind.subscribe(revealAfterMount.selectedProperty(), settings.revealAfterMount()::set)); } // **************************************** diff --git a/main/ui/src/main/java/org/cryptomator/ui/model/AutoUnlocker.java b/main/ui/src/main/java/org/cryptomator/ui/model/AutoUnlocker.java new file mode 100644 index 000000000..c947aa53d --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/model/AutoUnlocker.java @@ -0,0 +1,85 @@ +package org.cryptomator.ui.model; + +import java.nio.CharBuffer; +import java.util.Arrays; +import java.util.Optional; +import java.util.concurrent.ExecutorService; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import org.cryptomator.cryptolib.api.CryptoException; +import org.cryptomator.frontend.webdav.mount.Mounter.CommandFailedException; +import org.cryptomator.keychain.KeychainAccess; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@Singleton +public class AutoUnlocker { + + private static final Logger LOG = LoggerFactory.getLogger(AutoUnlocker.class); + + private final Optional keychainAccess; + private final VaultList vaults; + private final ExecutorService executor; + + @Inject + public AutoUnlocker(Optional keychainAccess, VaultList vaults, ExecutorService executor) { + this.keychainAccess = keychainAccess; + this.vaults = vaults; + this.executor = executor; + } + + public void unlockAllSilently() { + if (keychainAccess.isPresent()) { + vaults.stream().filter(this::shouldUnlockAfterStartup).map(this::createUnlockTask).forEach(executor::submit); + } + } + + private boolean shouldUnlockAfterStartup(Vault vault) { + return vault.getVaultSettings().unlockAfterStartup().get(); + } + + private Runnable createUnlockTask(Vault vault) { + return () -> unlockSilently(vault); + } + + private void unlockSilently(Vault vault) { + char[] storedPw = keychainAccess.get().loadPassphrase(vault.getId()); + if (storedPw == null) { + LOG.warn("No passphrase stored in keychain for vault registered for auto unlocking: {}", vault.getPath()); + } + try { + vault.unlock(CharBuffer.wrap(storedPw)); + mountSilently(vault); + } catch (CryptoException e) { + LOG.error("Auto unlock failed.", e); + } finally { + Arrays.fill(storedPw, ' '); + } + } + + private void mountSilently(Vault unlockedVault) { + if (!unlockedVault.getVaultSettings().mountAfterUnlock().get()) { + return; + } + try { + unlockedVault.mount(); + revealSilently(unlockedVault); + } catch (CommandFailedException e) { + LOG.error("Auto unlock succeded, but mounting the drive failed.", e); + } + } + + private void revealSilently(Vault mountedVault) { + if (!mountedVault.getVaultSettings().revealAfterMount().get()) { + return; + } + try { + mountedVault.reveal(); + } catch (CommandFailedException e) { + LOG.error("Auto unlock succeded, but revealing the drive failed.", e); + } + } + +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/model/Vault.java b/main/ui/src/main/java/org/cryptomator/ui/model/Vault.java index d972c3ab6..a9be7c3d3 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/model/Vault.java +++ b/main/ui/src/main/java/org/cryptomator/ui/model/Vault.java @@ -44,6 +44,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javafx.application.Platform; +import javafx.beans.Observable; import javafx.beans.binding.Binding; import javafx.beans.property.BooleanProperty; import javafx.beans.property.SimpleBooleanProperty; @@ -77,7 +78,7 @@ public class Vault { // Commands // ********************************************************************************/ - private CryptoFileSystem getCryptoFileSystem(CharSequence passphrase) throws IOException { + private CryptoFileSystem getCryptoFileSystem(CharSequence passphrase) throws IOException, CryptoException { return LazyInitializer.initializeLazily(cryptoFileSystem, () -> createCryptoFileSystem(passphrase), IOException.class); } @@ -126,7 +127,7 @@ public class Vault { } } - public synchronized void mount() { + public synchronized void mount() throws CommandFailedException { if (servlet == null) { throw new IllegalStateException("Mounting requires unlocked WebDAV servlet."); } @@ -136,14 +137,10 @@ public class Vault { .withPreferredGvfsScheme(settings.preferredGvfsScheme().get()) // .build(); - try { - mount = servlet.mount(mountParams); - Platform.runLater(() -> { - mounted.set(true); - }); - } catch (CommandFailedException e) { - LOG.error("Unable to mount filesystem", e); - } + mount = servlet.mount(mountParams); + Platform.runLater(() -> { + mounted.set(true); + }); } public synchronized void unmount() throws Exception { @@ -190,6 +187,10 @@ public class Vault { // Getter/Setter // *******************************************************************************/ + public Observable[] observables() { + return new Observable[] {unlockedProperty(), mountedProperty()}; + } + public VaultSettings getVaultSettings() { return vaultSettings; } diff --git a/main/ui/src/main/java/org/cryptomator/ui/model/VaultFactory.java b/main/ui/src/main/java/org/cryptomator/ui/model/VaultFactory.java index 7b4b553e2..824b3cc1d 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/model/VaultFactory.java +++ b/main/ui/src/main/java/org/cryptomator/ui/model/VaultFactory.java @@ -15,12 +15,11 @@ import javax.inject.Inject; import javax.inject.Singleton; import org.cryptomator.common.settings.VaultSettings; -import org.cryptomator.ui.model.VaultComponent.Builder; @Singleton public class VaultFactory { - private final Builder vaultComponentBuilder; + private final VaultComponent.Builder vaultComponentBuilder; private final ConcurrentMap vaults = new ConcurrentHashMap<>(); @Inject diff --git a/main/ui/src/main/java/org/cryptomator/ui/model/VaultList.java b/main/ui/src/main/java/org/cryptomator/ui/model/VaultList.java index 519fea90f..f2c9718fc 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/model/VaultList.java +++ b/main/ui/src/main/java/org/cryptomator/ui/model/VaultList.java @@ -5,8 +5,8 @@ *******************************************************************************/ package org.cryptomator.ui.model; -import java.util.ArrayList; import java.util.List; +import java.util.stream.IntStream; import javax.inject.Inject; import javax.inject.Singleton; @@ -14,7 +14,9 @@ import javax.inject.Singleton; import org.cryptomator.common.settings.Settings; import org.cryptomator.common.settings.VaultSettings; -import javafx.collections.ListChangeListener.Change; +import com.google.common.collect.Lists; + +import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import javafx.collections.transformation.TransformationList; @@ -59,15 +61,15 @@ public class VaultList extends TransformationList { } @Override - protected void sourceChanged(Change c) { + protected void sourceChanged(ListChangeListener.Change c) { this.fireChange(new VaultListChange(c)); } - private class VaultListChange extends Change { + private class VaultListChange extends ListChangeListener.Change { - private final Change delegate; + private final ListChangeListener.Change delegate; - public VaultListChange(Change delegate) { + public VaultListChange(ListChangeListener.Change delegate) { super(VaultList.this); this.delegate = delegate; } @@ -77,6 +79,11 @@ public class VaultList extends TransformationList { return delegate.next(); } + @Override + public boolean wasUpdated() { + return delegate.wasUpdated(); + } + @Override public void reset() { delegate.reset(); @@ -94,27 +101,28 @@ public class VaultList extends TransformationList { @Override public List getRemoved() { - List removed = new ArrayList<>(); - for (VaultSettings s : delegate.getRemoved()) { - removed.add(vaultFactory.get(s)); - } - return removed; + return Lists.transform(delegate.getRemoved(), vaultFactory::get); + } + + @Override + public boolean wasPermutated() { + return delegate.wasPermutated(); } @Override protected int[] getPermutation() { if (delegate.wasPermutated()) { - int len = getTo() - getFrom(); - int[] permutations = new int[len]; - for (int i = 0; i < len; i++) { - permutations[i] = getPermutation(i); - } - return permutations; + return IntStream.range(getFrom(), getTo()).map(delegate::getPermutation).toArray(); } else { return new int[0]; } } + @Override + public String toString() { + return delegate.toString(); + } + } } diff --git a/main/ui/src/main/resources/fxml/settings.fxml b/main/ui/src/main/resources/fxml/settings.fxml index 65199e284..4f7fc3bfe 100644 --- a/main/ui/src/main/resources/fxml/settings.fxml +++ b/main/ui/src/main/resources/fxml/settings.fxml @@ -53,7 +53,7 @@ -