Browse Source

Merge branch 'release/1.3.0-rc7'

Sebastian Stenzel 8 years ago
parent
commit
b6b660ec06

+ 1 - 1
main/ant-kit/pom.xml

@@ -8,7 +8,7 @@
 	<parent>
 		<groupId>org.cryptomator</groupId>
 		<artifactId>main</artifactId>
-		<version>1.3.0-rc6</version>
+		<version>1.3.0-rc7</version>
 	</parent>
 	<artifactId>ant-kit</artifactId>
 	<packaging>pom</packaging>

+ 1 - 0
main/ant-kit/src/main/resources/package/linux/postinst

@@ -22,6 +22,7 @@ case "$1" in
         echo Adding shortcut to the menu
 SECONDARY_LAUNCHERS_INSTALL
 APP_CDS_CACHE
+        mkdir -pm 644 /usr/share/desktop-directories
         xdg-desktop-menu install --novendor /opt/APPLICATION_FS_NAME/APPLICATION_LAUNCHER_FILENAME.desktop
 FILE_ASSOCIATION_INSTALL
 

+ 1 - 1
main/commons/pom.xml

@@ -10,7 +10,7 @@
 	<parent>
 		<groupId>org.cryptomator</groupId>
 		<artifactId>main</artifactId>
-		<version>1.3.0-rc6</version>
+		<version>1.3.0-rc7</version>
 	</parent>
 	<artifactId>commons</artifactId>
 	<name>Cryptomator Commons</name>

+ 1 - 1
main/jacoco-report/pom.xml

@@ -5,7 +5,7 @@
 	<parent>
 		<groupId>org.cryptomator</groupId>
 		<artifactId>main</artifactId>
-		<version>1.3.0-rc6</version>
+		<version>1.3.0-rc7</version>
 	</parent>
 	<artifactId>jacoco-report</artifactId>
 	<name>Cryptomator Code Coverage Report</name>

+ 1 - 1
main/keychain/pom.xml

@@ -3,7 +3,7 @@
 	<parent>
 		<groupId>org.cryptomator</groupId>
 		<artifactId>main</artifactId>
-		<version>1.3.0-rc6</version>
+		<version>1.3.0-rc7</version>
 	</parent>
 	<artifactId>keychain</artifactId>
 	<name>System Keychain Access</name>

+ 1 - 1
main/launcher/pom.xml

@@ -4,7 +4,7 @@
 	<parent>
 		<groupId>org.cryptomator</groupId>
 		<artifactId>main</artifactId>
-		<version>1.3.0-rc6</version>
+		<version>1.3.0-rc7</version>
 	</parent>
 	<artifactId>launcher</artifactId>
 	<name>Cryptomator Launcher</name>

+ 6 - 52
main/launcher/src/main/java/org/cryptomator/launcher/FileOpenRequestHandler.java

@@ -7,17 +7,13 @@
 package org.cryptomator.launcher;
 
 import java.io.File;
-import java.lang.reflect.InvocationHandler;
-import java.lang.reflect.Method;
-import java.lang.reflect.Proxy;
 import java.nio.file.FileSystem;
 import java.nio.file.FileSystems;
 import java.nio.file.InvalidPathException;
 import java.nio.file.Path;
-import java.util.List;
 import java.util.concurrent.BlockingQueue;
 
-import org.apache.commons.lang3.SystemUtils;
+import org.cryptomator.ui.util.EawtApplicationWrapper;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -28,9 +24,11 @@ class FileOpenRequestHandler {
 
 	public FileOpenRequestHandler(BlockingQueue<Path> fileOpenRequests) {
 		this.fileOpenRequests = fileOpenRequests;
-		if (SystemUtils.IS_OS_MAC_OSX) {
-			addOsxFileOpenHandler();
-		}
+		EawtApplicationWrapper.getApplication().ifPresent(app -> {
+			app.setOpenFileHandler(files -> {
+				files.stream().map(File::toPath).forEach(fileOpenRequests::add);
+			});
+		});
 	}
 
 	public void handleLaunchArgs(String[] args) {
@@ -55,48 +53,4 @@ class FileOpenRequestHandler {
 		}
 	}
 
-	/**
-	 * Event subscription code inspired by https://gitlab.com/axet/desktop/blob/master/java/src/main/java/com/github/axet/desktop/os/mac/AppleHandlers.java
-	 */
-	private void addOsxFileOpenHandler() {
-		try {
-			final Class<?> applicationClass = Class.forName("com.apple.eawt.Application");
-			final Class<?> openFilesHandlerClass = Class.forName("com.apple.eawt.OpenFilesHandler");
-			final Method getApplication = applicationClass.getMethod("getApplication");
-			final Object application = getApplication.invoke(null);
-			final Method setOpenFileHandler = applicationClass.getMethod("setOpenFileHandler", openFilesHandlerClass);
-			final ClassLoader openFilesHandlerClassLoader = openFilesHandlerClass.getClassLoader();
-			final OpenFilesEventInvocationHandler openFilesHandler = new OpenFilesEventInvocationHandler();
-			final Object openFilesHandlerObject = Proxy.newProxyInstance(openFilesHandlerClassLoader, new Class<?>[] {openFilesHandlerClass}, openFilesHandler);
-			setOpenFileHandler.invoke(application, openFilesHandlerObject);
-		} catch (ReflectiveOperationException | RuntimeException e) {
-			// Since we're trying to call OS-specific code, we'll just have to hope for the best.
-			LOG.error("Exception adding OS X file open handler", e);
-		}
-	}
-
-	/**
-	 * Handler class inspired by https://gitlab.com/axet/desktop/blob/master/java/src/main/java/com/github/axet/desktop/os/mac/AppleHandlers.java
-	 */
-	private class OpenFilesEventInvocationHandler implements InvocationHandler {
-		@Override
-		public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
-			if (method.getName().equals("openFiles")) {
-				final Class<?> openFilesEventClass = Class.forName("com.apple.eawt.AppEvent$OpenFilesEvent");
-				final Method getFiles = openFilesEventClass.getMethod("getFiles");
-				Object e = args[0];
-				try {
-					@SuppressWarnings("unchecked")
-					final List<File> ff = (List<File>) getFiles.invoke(e);
-					ff.stream().map(File::toPath).forEach(fileOpenRequests::add);
-				} catch (RuntimeException ee) {
-					throw ee;
-				} catch (Exception ee) {
-					throw new RuntimeException(ee);
-				}
-			}
-			return null;
-		}
-	}
-
 }

+ 5 - 5
main/pom.xml

@@ -6,7 +6,7 @@
 	<modelVersion>4.0.0</modelVersion>
 	<groupId>org.cryptomator</groupId>
 	<artifactId>main</artifactId>
-	<version>1.3.0-rc6</version>
+	<version>1.3.0-rc7</version>
 	<packaging>pom</packaging>
 	<name>Cryptomator</name>
 
@@ -27,8 +27,8 @@
 		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
 
 		<!-- dependency versions -->
-		<cryptomator.cryptolib.version>1.1.1</cryptomator.cryptolib.version>
-		<cryptomator.cryptofs.version>1.2.2</cryptomator.cryptofs.version>
+		<cryptomator.cryptolib.version>1.1.2</cryptomator.cryptolib.version>
+		<cryptomator.cryptofs.version>1.3.0</cryptomator.cryptofs.version>
 		<cryptomator.webdav.version>0.6.1</cryptomator.webdav.version>
 		<cryptomator.jni.version>1.0.2</cryptomator.jni.version>
 		<slf4j.version>1.7.25</slf4j.version>
@@ -42,9 +42,9 @@
 		<commons-codec.version>1.10</commons-codec.version>
 		<httpclient.version>4.5.3</httpclient.version>
 		<mockito.version>2.7.21</mockito.version>
-		<dagger.version>2.10</dagger.version>
+		<dagger.version>2.11</dagger.version>
 		<easybind.version>1.0.3</easybind.version>
-		<guava.version>21.0</guava.version>
+		<guava.version>22.0</guava.version>
 		<gson.version>2.8.0</gson.version>
 	</properties>
 

+ 1 - 1
main/uber-jar/pom.xml

@@ -5,7 +5,7 @@
 	<parent>
 		<groupId>org.cryptomator</groupId>
 		<artifactId>main</artifactId>
-		<version>1.3.0-rc6</version>
+		<version>1.3.0-rc7</version>
 	</parent>
 	<artifactId>uber-jar</artifactId>
 	<name>Single über jar with all dependencies</name>

+ 1 - 1
main/ui/pom.xml

@@ -12,7 +12,7 @@
 	<parent>
 		<groupId>org.cryptomator</groupId>
 		<artifactId>main</artifactId>
-		<version>1.3.0-rc6</version>
+		<version>1.3.0-rc7</version>
 	</parent>
 	<artifactId>ui</artifactId>
 	<name>Cryptomator GUI</name>

+ 21 - 2
main/ui/src/main/java/org/cryptomator/ui/controllers/MainController.java

@@ -37,6 +37,7 @@ import org.cryptomator.ui.model.Vault;
 import org.cryptomator.ui.model.VaultFactory;
 import org.cryptomator.ui.model.VaultList;
 import org.cryptomator.ui.util.DialogBuilderUtil;
+import org.cryptomator.ui.util.EawtApplicationWrapper;
 import org.fxmisc.easybind.EasyBind;
 import org.fxmisc.easybind.Subscription;
 import org.fxmisc.easybind.monadic.MonadicBinding;
@@ -119,6 +120,12 @@ public class MainController implements ViewController {
 
 		EasyBind.subscribe(areAllVaultsLocked, Platform::setImplicitExit);
 		autoUnlocker.unlockAllSilently();
+
+		EawtApplicationWrapper.getApplication().ifPresent(app -> {
+			app.setPreferencesHandler(() -> {
+				Platform.runLater(this::toggleShowSettings);
+			});
+		});
 	}
 
 	@FXML
@@ -328,10 +335,14 @@ public class MainController implements ViewController {
 
 	@FXML
 	private void didClickShowSettings(ActionEvent e) {
+		toggleShowSettings();
+	}
+
+	private void toggleShowSettings() {
 		if (isShowingSettings.get()) {
-			activeController.set(viewControllerLoader.load("/fxml/welcome.fxml"));
+			showWelcomeView();
 		} else {
-			activeController.set(viewControllerLoader.load("/fxml/settings.fxml"));
+			showPreferencesView();
 		}
 		vaultList.getSelectionModel().clearSelection();
 	}
@@ -375,6 +386,14 @@ public class MainController implements ViewController {
 	// Subcontroller for right panel
 	// ****************************************
 
+	private void showWelcomeView() {
+		activeController.set(viewControllerLoader.load("/fxml/welcome.fxml"));
+	}
+
+	private void showPreferencesView() {
+		activeController.set(viewControllerLoader.load("/fxml/settings.fxml"));
+	}
+
 	private void showNotFoundView() {
 		activeController.set(viewControllerLoader.load("/fxml/notfound.fxml"));
 	}

+ 30 - 49
main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockController.java

@@ -21,7 +21,6 @@ import org.cryptomator.common.settings.VaultSettings;
 import org.cryptomator.cryptolib.api.InvalidPassphraseException;
 import org.cryptomator.cryptolib.api.UnsupportedVaultFormatException;
 import org.cryptomator.frontend.webdav.ServerLifecycleException;
-import org.cryptomator.frontend.webdav.mount.Mounter.CommandFailedException;
 import org.cryptomator.keychain.KeychainAccess;
 import org.cryptomator.ui.controls.SecPasswordField;
 import org.cryptomator.ui.l10n.Localization;
@@ -38,7 +37,6 @@ import com.google.common.base.CharMatcher;
 import com.google.common.base.Strings;
 
 import javafx.application.Application;
-import javafx.application.Platform;
 import javafx.beans.value.ChangeListener;
 import javafx.beans.value.ObservableValue;
 import javafx.event.ActionEvent;
@@ -345,59 +343,42 @@ public class UnlockController implements ViewController {
 	private void didClickUnlockButton(ActionEvent event) {
 		advancedOptions.setDisable(true);
 		progressIndicator.setVisible(true);
-		downloadsPageLink.setVisible(false);
-		CharSequence password = passwordField.getCharacters();
-		asyncTaskService.asyncTaskOf(() -> this.unlock(password)).run();
-	}
 
-	private void unlock(CharSequence password) {
-		try {
+		CharSequence password = passwordField.getCharacters();
+		asyncTaskService.asyncTaskOf(() -> {
 			vault.unlock(password);
-			if (mountAfterUnlock.isSelected()) {
-				vault.mount();
-				if (revealAfterMount.isSelected()) {
-					vault.reveal();
-				}
-			}
-			Platform.runLater(() -> {
-				messageText.setText(null);
-				listener.ifPresent(lstnr -> lstnr.didUnlock(vault));
-			});
 			if (keychainAccess.isPresent() && savePassword.isSelected()) {
 				keychainAccess.get().storePassphrase(vault.getId(), password);
-			} else {
-				Platform.runLater(passwordField::swipe);
 			}
-		} catch (InvalidPassphraseException e) {
-			Platform.runLater(() -> {
-				messageText.setText(localization.getString("unlock.errorMessage.wrongPassword"));
-				passwordField.selectAll();
-				passwordField.requestFocus();
-			});
-		} catch (UnsupportedVaultFormatException e) {
-			Platform.runLater(() -> {
-				if (e.isVaultOlderThanSoftware()) {
-					// whitespace after localized text used as separator between text and hyperlink
-					messageText.setText(localization.getString("unlock.errorMessage.unsupportedVersion.vaultOlderThanSoftware") + " ");
-					downloadsPageLink.setVisible(true);
-				} else if (e.isSoftwareOlderThanVault()) {
-					messageText.setText(localization.getString("unlock.errorMessage.unsupportedVersion.softwareOlderThanVault") + " ");
-					downloadsPageLink.setVisible(true);
-				} else if (e.getDetectedVersion() == Integer.MAX_VALUE) {
-					messageText.setText(localization.getString("unlock.errorMessage.unauthenticVersionMac"));
-				}
-			});
-		} catch (ServerLifecycleException | CommandFailedException e) {
+		}).onSuccess(() -> {
+			messageText.setText(null);
+			downloadsPageLink.setVisible(false);
+			listener.ifPresent(lstnr -> lstnr.didUnlock(vault));
+		}).onError(InvalidPassphraseException.class, e -> {
+			messageText.setText(localization.getString("unlock.errorMessage.wrongPassword"));
+			passwordField.selectAll();
+			passwordField.requestFocus();
+		}).onError(UnsupportedVaultFormatException.class, e -> {
+			if (e.isVaultOlderThanSoftware()) {
+				// whitespace after localized text used as separator between text and hyperlink
+				messageText.setText(localization.getString("unlock.errorMessage.unsupportedVersion.vaultOlderThanSoftware") + " ");
+				downloadsPageLink.setVisible(true);
+			} else if (e.isSoftwareOlderThanVault()) {
+				messageText.setText(localization.getString("unlock.errorMessage.unsupportedVersion.softwareOlderThanVault") + " ");
+				downloadsPageLink.setVisible(true);
+			} else if (e.getDetectedVersion() == Integer.MAX_VALUE) {
+				messageText.setText(localization.getString("unlock.errorMessage.unauthenticVersionMac"));
+			}
+		}).onError(ServerLifecycleException.class, e -> {
 			LOG.error("Unlock failed for technical reasons.", e);
-			Platform.runLater(() -> {
-				messageText.setText(localization.getString("unlock.errorMessage.mountingFailed"));
-			});
-		} finally {
-			Platform.runLater(() -> {
-				advancedOptions.setDisable(false);
-				progressIndicator.setVisible(false);
-			});
-		}
+			messageText.setText(localization.getString("unlock.errorMessage.unlockFailed"));
+		}).andFinally(() -> {
+			if (!savePassword.isSelected()) {
+				passwordField.swipe();
+			}
+			advancedOptions.setDisable(false);
+			progressIndicator.setVisible(false);
+		}).run();
 	}
 
 	/* callback */

+ 88 - 42
main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockedController.java

@@ -10,10 +10,13 @@ package org.cryptomator.ui.controllers;
 
 import static java.lang.String.format;
 
+import java.io.IOException;
 import java.util.Optional;
 
 import javax.inject.Inject;
 
+import org.cryptomator.frontend.webdav.ServerLifecycleException;
+import org.cryptomator.frontend.webdav.mount.Mounter.CommandFailedException;
 import org.cryptomator.ui.l10n.Localization;
 import org.cryptomator.ui.model.Vault;
 import org.cryptomator.ui.util.AsyncTaskService;
@@ -22,9 +25,12 @@ import org.fxmisc.easybind.EasyBind;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import com.google.common.util.concurrent.Runnables;
+
 import javafx.animation.Animation;
 import javafx.animation.KeyFrame;
 import javafx.animation.Timeline;
+import javafx.beans.binding.BooleanExpression;
 import javafx.beans.property.ObjectProperty;
 import javafx.beans.property.SimpleObjectProperty;
 import javafx.event.ActionEvent;
@@ -58,6 +64,7 @@ public class UnlockedController implements ViewController {
 	private final Localization localization;
 	private final AsyncTaskService asyncTaskService;
 	private final ObjectProperty<Vault> vault = new SimpleObjectProperty<>();
+	private final BooleanExpression vaultMounted = BooleanExpression.booleanExpression(EasyBind.select(vault).selectObject(Vault::mountedProperty).orElse(false));
 	private Optional<LockListener> listener = Optional.empty();
 	private Timeline ioAnimation;
 
@@ -76,6 +83,12 @@ public class UnlockedController implements ViewController {
 	@FXML
 	private ContextMenu moreOptionsMenu;
 
+	@FXML
+	private MenuItem mountVaultMenuItem;
+
+	@FXML
+	private MenuItem unmountVaultMenuItem;
+
 	@FXML
 	private MenuItem revealVaultMenuItem;
 
@@ -90,7 +103,9 @@ public class UnlockedController implements ViewController {
 
 	@Override
 	public void initialize() {
-		revealVaultMenuItem.disableProperty().bind(EasyBind.map(vault, vault -> vault != null && !vault.isMounted()));
+		mountVaultMenuItem.disableProperty().bind(vaultMounted);
+		unmountVaultMenuItem.disableProperty().bind(vaultMounted.not());
+		revealVaultMenuItem.disableProperty().bind(vaultMounted.not());
 
 		EasyBind.subscribe(vault, this::vaultChanged);
 		EasyBind.subscribe(moreOptionsMenu.showingProperty(), moreOptionsButton::setSelected);
@@ -106,9 +121,8 @@ public class UnlockedController implements ViewController {
 			return;
 		}
 
-		if (newVault.getVaultSettings().mountAfterUnlock().get() && !newVault.isMounted()) {
-			// TODO Markus Kreusch #393: hyperlink auf FAQ oder sowas?
-			messageLabel.setText(localization.getString("unlocked.label.mountFailed"));
+		if (newVault.getVaultSettings().mountAfterUnlock().get()) {
+			mountVault(newVault);
 		}
 
 		// (re)start throughput statistics:
@@ -118,36 +132,80 @@ public class UnlockedController implements ViewController {
 
 	@FXML
 	private void didClickLockVault(ActionEvent event) {
-		regularLockVault();
+		regularUnmountVault(this::lockVault);
+	}
+
+	private void lockVault() {
+		try {
+			vault.get().lock();
+		} catch (ServerLifecycleException | IOException e) {
+			LOG.error("Lock failed", e);
+		}
+		listener.ifPresent(listener -> listener.didLock(this));
+	}
+
+	@FXML
+	private void didClickMoreOptions(ActionEvent event) {
+		if (moreOptionsMenu.isShowing()) {
+			moreOptionsMenu.hide();
+		} else {
+			moreOptionsMenu.setAnchorLocation(AnchorLocation.CONTENT_TOP_RIGHT);
+			moreOptionsMenu.show(moreOptionsButton, Side.BOTTOM, moreOptionsButton.getWidth(), 0.0);
+		}
+	}
+
+	@FXML
+	public void didClickMountVault(ActionEvent event) {
+		mountVault(vault.get());
+	}
+
+	private void mountVault(Vault vault) {
+		asyncTaskService.asyncTaskOf(() -> {
+			vault.mount();
+		}).onSuccess(() -> {
+			LOG.trace("Mount succeeded.");
+			messageLabel.setText(null);
+			if (vault.getVaultSettings().revealAfterMount().get()) {
+				revealVault(vault);
+			}
+		}).onError(CommandFailedException.class, e -> {
+			LOG.error("Mount failed.", e);
+			// TODO Markus Kreusch #393: hyperlink auf FAQ oder sowas?
+			messageLabel.setText(localization.getString("unlocked.label.mountFailed"));
+		}).run();
 	}
 
-	private void regularLockVault() {
+	@FXML
+	public void didClickUnmountVault(ActionEvent event) {
+		regularUnmountVault(Runnables.doNothing());
+	}
+
+	private void regularUnmountVault(Runnable onSuccess) {
 		asyncTaskService.asyncTaskOf(() -> {
 			vault.get().unmount();
-			vault.get().lock();
 		}).onSuccess(() -> {
-			listener.ifPresent(listener -> listener.didLock(this));
-			LOG.trace("Regular lock succeeded");
+			LOG.trace("Regular unmount succeeded.");
+			onSuccess.run();
 		}).onError(Exception.class, e -> {
-			onRegularLockVaultFailed(e);
+			onRegularUnmountVaultFailed(e, onSuccess);
 		}).run();
 	}
 
-	private void forcedLockVault() {
+	private void forcedUnmountVault(Runnable onSuccess) {
 		asyncTaskService.asyncTaskOf(() -> {
 			vault.get().unmountForced();
-			vault.get().lock();
 		}).onSuccess(() -> {
-			listener.ifPresent(listener -> listener.didLock(this));
-			LOG.trace("Forced lock succeeded");
+			LOG.trace("Forced unmount succeeded.");
+			onSuccess.run();
 		}).onError(Exception.class, e -> {
-			onForcedLockVaultFailed(e);
+			LOG.error("Forced unmount failed.", e);
+			messageLabel.setText(localization.getString("unlocked.label.unmountFailed"));
 		}).run();
 	}
 
-	private void onRegularLockVaultFailed(Exception e) {
+	private void onRegularUnmountVaultFailed(Exception e, Runnable onSuccess) {
 		if (vault.get().supportsForcedUnmount()) {
-			LOG.trace("Regular unmount failed", e);
+			LOG.trace("Regular unmount failed.", e);
 			Alert confirmDialog = DialogBuilderUtil.buildYesNoDialog( //
 					format(localization.getString("unlocked.lock.force.confirmation.title"), vault.get().name().getValue()), //
 					localization.getString("unlocked.lock.force.confirmation.header"), //
@@ -156,41 +214,29 @@ public class UnlockedController implements ViewController {
 
 			Optional<ButtonType> choice = confirmDialog.showAndWait();
 			if (ButtonType.YES.equals(choice.get())) {
-				forcedLockVault();
+				forcedUnmountVault(onSuccess);
 			} else {
-				LOG.trace("Unmount cancelled", e);
+				LOG.trace("Unmount cancelled.", e);
 			}
 		} else {
-			LOG.error("Regular unmount failed", e);
-			showUnmountFailedMessage();
+			LOG.error("Regular unmount failed.", e);
+			messageLabel.setText(localization.getString("unlocked.label.unmountFailed"));
 		}
 	}
 
-	private void onForcedLockVaultFailed(Exception e) {
-		LOG.error("Forced unmount failed", e);
-		showUnmountFailedMessage();
-	}
-
-	private void showUnmountFailedMessage() {
-		messageLabel.setText(localization.getString("unlocked.label.unmountFailed"));
-	}
-
 	@FXML
-	private void didClickMoreOptions(ActionEvent event) {
-		if (moreOptionsMenu.isShowing()) {
-			moreOptionsMenu.hide();
-		} else {
-			moreOptionsMenu.setAnchorLocation(AnchorLocation.CONTENT_TOP_RIGHT);
-			moreOptionsMenu.show(moreOptionsButton, Side.BOTTOM, moreOptionsButton.getWidth(), 0.0);
-		}
+	private void didClickRevealVault(ActionEvent event) {
+		revealVault(vault.get());
 	}
 
-	@FXML
-	private void didClickRevealVault(ActionEvent event) {
+	private void revealVault(Vault vault) {
 		asyncTaskService.asyncTaskOf(() -> {
-			vault.get().reveal();
-		}).onError(RuntimeException.class, () -> {
-			// TODO overheadhunter catch more specific exception type thrown by reveal()
+			vault.reveal();
+		}).onSuccess(() -> {
+			LOG.trace("Reveal succeeded.");
+			messageLabel.setText(null);
+		}).onError(CommandFailedException.class, e -> {
+			LOG.error("Reveal failed.", e);
 			messageLabel.setText(localization.getString("unlocked.label.revealFailed"));
 		}).run();
 	}

+ 9 - 5
main/ui/src/main/java/org/cryptomator/ui/model/Vault.java

@@ -16,6 +16,7 @@ import java.nio.file.FileSystem;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
+import java.util.EnumSet;
 import java.util.Objects;
 import java.util.concurrent.atomic.AtomicReference;
 import java.util.function.Function;
@@ -30,9 +31,11 @@ import org.cryptomator.common.settings.Settings;
 import org.cryptomator.common.settings.VaultSettings;
 import org.cryptomator.cryptofs.CryptoFileSystem;
 import org.cryptomator.cryptofs.CryptoFileSystemProperties;
+import org.cryptomator.cryptofs.CryptoFileSystemProperties.FileSystemFlags;
 import org.cryptomator.cryptofs.CryptoFileSystemProvider;
 import org.cryptomator.cryptolib.api.CryptoException;
 import org.cryptomator.cryptolib.api.InvalidPassphraseException;
+import org.cryptomator.frontend.webdav.ServerLifecycleException;
 import org.cryptomator.frontend.webdav.WebDavServer;
 import org.cryptomator.frontend.webdav.mount.MountParams;
 import org.cryptomator.frontend.webdav.mount.Mounter.CommandFailedException;
@@ -78,12 +81,13 @@ public class Vault {
 	// ********************************************************************************/
 
 	private CryptoFileSystem getCryptoFileSystem(CharSequence passphrase) throws IOException, CryptoException {
-		return LazyInitializer.initializeLazily(cryptoFileSystem, () -> createCryptoFileSystem(passphrase), IOException.class);
+		return LazyInitializer.initializeLazily(cryptoFileSystem, () -> unlockCryptoFileSystem(passphrase), IOException.class);
 	}
 
-	private CryptoFileSystem createCryptoFileSystem(CharSequence passphrase) throws IOException, CryptoException {
+	private CryptoFileSystem unlockCryptoFileSystem(CharSequence passphrase) throws IOException, CryptoException {
 		CryptoFileSystemProperties fsProps = CryptoFileSystemProperties.cryptoFileSystemProperties() //
 				.withPassphrase(passphrase) //
+				.withFlags(EnumSet.noneOf(FileSystemFlags.class)) // TODO: use withFlags() with CryptoFS 1.3.1
 				.withMasterkeyFilename(MASTERKEY_FILENAME) //
 				.build();
 		return CryptoFileSystemProvider.newFileSystem(getPath(), fsProps);
@@ -98,7 +102,7 @@ public class Vault {
 			}
 		}
 		if (!isValidVaultDirectory()) {
-			createCryptoFileSystem(passphrase).close(); // implicitly creates a non-existing vault
+			CryptoFileSystemProvider.initialize(getPath(), MASTERKEY_FILENAME, passphrase);
 		} else {
 			throw new FileAlreadyExistsException(getPath().toString());
 		}
@@ -108,7 +112,7 @@ public class Vault {
 		CryptoFileSystemProvider.changePassphrase(getPath(), MASTERKEY_FILENAME, oldPassphrase, newPassphrase);
 	}
 
-	public synchronized void unlock(CharSequence passphrase) {
+	public synchronized void unlock(CharSequence passphrase) throws ServerLifecycleException {
 		try {
 			FileSystem fs = getCryptoFileSystem(passphrase);
 			if (!server.isRunning()) {
@@ -161,7 +165,7 @@ public class Vault {
 		return mount != null && mount.forced().isPresent();
 	}
 
-	public synchronized void lock() throws Exception {
+	public synchronized void lock() throws ServerLifecycleException, IOException {
 		if (servlet != null) {
 			servlet.stop();
 		}

+ 40 - 0
main/ui/src/main/java/org/cryptomator/ui/util/AsyncTaskService.java

@@ -32,6 +32,12 @@ public class AsyncTaskService {
 		this.executor = executor;
 	}
 
+	/**
+	 * Creates a new async task
+	 * 
+	 * @param task Tasks to be invoked in a background thread.
+	 * @return The async task
+	 */
 	public AsyncTaskWithoutSuccessHandler<Void> asyncTaskOf(RunnableThrowingException<?> task) {
 		return new AsyncTaskImpl<>(() -> {
 			task.run();
@@ -39,6 +45,12 @@ public class AsyncTaskService {
 		});
 	}
 
+	/**
+	 * Creates a new async task
+	 * 
+	 * @param task Tasks to be invoked in a background thread.
+	 * @return The async task
+	 */
 	public <ResultType> AsyncTaskWithoutSuccessHandler<ResultType> asyncTaskOf(SupplierThrowingException<ResultType, ?> task) {
 		return new AsyncTaskImpl<>(task);
 	}
@@ -153,27 +165,55 @@ public class AsyncTaskService {
 
 	public interface AsyncTaskWithoutSuccessHandler<ResultType> extends AsyncTaskWithoutErrorHandler {
 
+		/**
+		 * @param handler Tasks to be invoked on the JavaFX application thread.
+		 * @return The async task
+		 */
 		AsyncTaskWithoutErrorHandler onSuccess(ConsumerThrowingException<ResultType, ?> handler);
 
+		/**
+		 * @param handler Tasks to be invoked on the JavaFX application thread.
+		 * @return The async task
+		 */
 		AsyncTaskWithoutErrorHandler onSuccess(RunnableThrowingException<?> handler);
 
 	}
 
 	public interface AsyncTaskWithoutErrorHandler extends AsyncTaskWithoutFinallyHandler {
 
+		/**
+		 * @param type Exception type to catch
+		 * @param handler Tasks to be invoked on the JavaFX application thread.
+		 * @return The async task
+		 */
 		<ErrorType extends Throwable> AsyncTaskWithoutErrorHandler onError(Class<ErrorType> type, ConsumerThrowingException<ErrorType, ?> handler);
 
+		/**
+		 * @param type Exception type to catch
+		 * @param handler Tasks to be invoked on the JavaFX application thread.
+		 * @return The async task
+		 */
 		<ErrorType extends Throwable> AsyncTaskWithoutErrorHandler onError(Class<ErrorType> type, RunnableThrowingException<?> handler);
 
 	}
 
 	public interface AsyncTaskWithoutFinallyHandler extends AsyncTask {
 
+		/**
+		 * @param handler Tasks to be invoked on the JavaFX application thread.
+		 * @return The async task
+		 */
 		AsyncTask andFinally(RunnableThrowingException<?> handler);
 
 	}
 
 	public interface AsyncTask extends Runnable {
+
+		/**
+		 * Starts the async task.
+		 */
+		@Override
+		void run();
 	}
 
 }

+ 127 - 0
main/ui/src/main/java/org/cryptomator/ui/util/EawtApplicationWrapper.java

@@ -0,0 +1,127 @@
+/*******************************************************************************
+ * 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.ui.util;
+
+import java.io.File;
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+import java.util.List;
+import java.util.Optional;
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+import org.apache.commons.lang3.SystemUtils;
+import org.cryptomator.common.SupplierThrowingException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Reflection-based wrapper for com.apple.eawt.Application.
+ */
+public class EawtApplicationWrapper {
+
+	private static final Logger LOG = LoggerFactory.getLogger(EawtApplicationWrapper.class);
+
+	private final Class<?> applicationClass;
+	private final Object application;
+
+	private EawtApplicationWrapper() throws ReflectiveOperationException {
+		this.applicationClass = Class.forName("com.apple.eawt.Application");
+		this.application = applicationClass.getMethod("getApplication").invoke(null);
+	}
+
+	/**
+	 * @return A wrapper for com.apple.ewat.Application if the current OS is macOS and the class is available in this JVM.
+	 */
+	public static Optional<EawtApplicationWrapper> getApplication() {
+		if (!SystemUtils.IS_OS_MAC_OSX) {
+			return Optional.empty();
+		}
+		try {
+			return Optional.of(new EawtApplicationWrapper());
+		} catch (ReflectiveOperationException e) {
+			return Optional.empty();
+		}
+	}
+
+	private void setOpenFileHandler(InvocationHandler handler) throws ReflectiveOperationException {
+		Class<?> handlerClass = Class.forName("com.apple.eawt.OpenFilesHandler");
+		Method setter = applicationClass.getMethod("setOpenFileHandler", handlerClass);
+		Object proxy = Proxy.newProxyInstance(applicationClass.getClassLoader(), new Class<?>[] {handlerClass}, handler);
+		setter.invoke(application, proxy);
+	}
+
+	public void setOpenFileHandler(Consumer<List<File>> handler) {
+		try {
+			Class<?> openFilesEventClass = Class.forName("com.apple.eawt.AppEvent$OpenFilesEvent");
+			Method getFiles = openFilesEventClass.getMethod("getFiles");
+			setOpenFileHandler(methodSpecificInvocationHandler("openFiles", args -> {
+				Object openFilesEvent = args[0];
+				assert openFilesEventClass.isInstance(openFilesEvent);
+				@SuppressWarnings("unchecked")
+				List<File> files = (List<File>) uncheckedReflectiveOperation(() -> getFiles.invoke(openFilesEvent));
+				handler.accept(files);
+				return null;
+			}));
+		} catch (ReflectiveOperationException e) {
+			LOG.error("Exception setting openFileHandler.", e);
+		}
+	}
+
+	private void setPreferencesHandler(InvocationHandler handler) throws ReflectiveOperationException {
+		Class<?> handlerClass = Class.forName("com.apple.eawt.PreferencesHandler");
+		Method setter = applicationClass.getMethod("setPreferencesHandler", handlerClass);
+		Object proxy = Proxy.newProxyInstance(applicationClass.getClassLoader(), new Class<?>[] {handlerClass}, handler);
+		setter.invoke(application, proxy);
+	}
+
+	public void setPreferencesHandler(Runnable handler) {
+		try {
+			setPreferencesHandler(methodSpecificInvocationHandler("handlePreferences", args -> {
+				handler.run();
+				return null;
+			}));
+		} catch (ReflectiveOperationException e) {
+			LOG.error("Exception setting preferencesHandler.", e);
+		}
+	}
+
+	private static InvocationHandler methodSpecificInvocationHandler(String methodName, Function<Object[], Object> handler) {
+		return new InvocationHandler() {
+			@Override
+			public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
+				if (method.getName().equals(methodName)) {
+					return handler.apply(args);
+				} else {
+					throw new UnsupportedOperationException("Unexpected invocation " + method.getName() + ", expected " + methodName);
+				}
+			}
+		};
+	}
+
+	/**
+	 * Wraps {@link ReflectiveOperationException}s as {@link UncheckedReflectiveOperationException}.
+	 * 
+	 * @param operation Invokation throwing an ReflectiveOperationException
+	 * @return Result returned by <code>operation</code>
+	 * @throws UncheckedReflectiveOperationException in case <code>operation</code> throws an ReflectiveOperationException.
+	 */
+	private static <T> T uncheckedReflectiveOperation(SupplierThrowingException<T, ReflectiveOperationException> operation) throws UncheckedReflectiveOperationException {
+		try {
+			return operation.get();
+		} catch (ReflectiveOperationException e) {
+			throw new UncheckedReflectiveOperationException(e);
+		}
+	}
+
+	private static class UncheckedReflectiveOperationException extends RuntimeException {
+		public UncheckedReflectiveOperationException(ReflectiveOperationException cause) {
+			super(cause);
+		}
+	}
+
+}

+ 6 - 0
main/ui/src/main/resources/fxml/unlocked.fxml

@@ -28,6 +28,12 @@
 	<fx:define>
 		<ContextMenu fx:id="moreOptionsMenu">
 			<items>
+				<MenuItem fx:id="mountVaultMenuItem" text="%unlocked.moreOptions.mount" onAction="#didClickMountVault">
+					<graphic><Label text="&#xf139;" styleClass="ionicons"/></graphic>
+				</MenuItem>
+				<MenuItem fx:id="unmountVaultMenuItem" text="%unlocked.moreOptions.unmount" onAction="#didClickUnmountVault">
+					<graphic><Label text="&#xf131;" styleClass="ionicons"/></graphic>
+				</MenuItem>
 				<MenuItem fx:id="revealVaultMenuItem" text="%unlocked.moreOptions.reveal" onAction="#didClickRevealVault">
 					<graphic><Label text="&#xf133;" styleClass="ionicons"/></graphic>
 				</MenuItem>

+ 3 - 1
main/ui/src/main/resources/localization/en.txt

@@ -72,7 +72,7 @@ unlock.savePassword.delete.confirmation.header=Do you really want to delete the
 unlock.savePassword.delete.confirmation.content=The saved password of this vault will be immediately deleted from your system keychain. If you'd like to save your password again, you'd have to unlock your vault with the "Save Password" option enabled.
 unlock.choicebox.winDriveLetter.auto=Assign automatically
 unlock.errorMessage.wrongPassword=Wrong password
-unlock.errorMessage.mountingFailed=Mounting failed. See log file for details.
+unlock.errorMessage.unlockFailed=Unlock failed. See log file for details.
 unlock.errorMessage.unsupportedVersion.vaultOlderThanSoftware=Unsupported vault. This vault has been created with an older version of Cryptomator.
 unlock.errorMessage.unsupportedVersion.softwareOlderThanVault=Unsupported vault. This vault has been created with a newer version of Cryptomator.
 unlock.errorMessage.unauthenticVersionMac=Could not authenticate version MAC.
@@ -89,6 +89,8 @@ changePassword.errorMessage.decryptionFailed=Decryption failed
 
 # unlocked.fxml
 unlocked.button.lock=Lock Vault
+unlocked.moreOptions.mount=Mount Drive
+unlocked.moreOptions.unmount=Eject Drive
 unlocked.moreOptions.reveal=Reveal Drive
 unlocked.moreOptions.copyUrl=Copy WebDAV URL
 unlocked.label.mountFailed=Connecting drive failed