diff --git a/main/ant-kit/pom.xml b/main/ant-kit/pom.xml index be78f8f92..e14674f85 100644 --- a/main/ant-kit/pom.xml +++ b/main/ant-kit/pom.xml @@ -8,7 +8,7 @@ org.cryptomator main - 1.3.0-rc6 + 1.3.0-rc7 ant-kit pom diff --git a/main/ant-kit/src/main/resources/package/linux/postinst b/main/ant-kit/src/main/resources/package/linux/postinst index e77fa6366..ee09f3b1d 100644 --- a/main/ant-kit/src/main/resources/package/linux/postinst +++ b/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 diff --git a/main/commons/pom.xml b/main/commons/pom.xml index 8ac4900df..6ed07dfac 100644 --- a/main/commons/pom.xml +++ b/main/commons/pom.xml @@ -10,7 +10,7 @@ org.cryptomator main - 1.3.0-rc6 + 1.3.0-rc7 commons Cryptomator Commons diff --git a/main/jacoco-report/pom.xml b/main/jacoco-report/pom.xml index 62749fd3d..fd685566b 100644 --- a/main/jacoco-report/pom.xml +++ b/main/jacoco-report/pom.xml @@ -5,7 +5,7 @@ org.cryptomator main - 1.3.0-rc6 + 1.3.0-rc7 jacoco-report Cryptomator Code Coverage Report diff --git a/main/keychain/pom.xml b/main/keychain/pom.xml index 2f19f9c57..bea437ab9 100644 --- a/main/keychain/pom.xml +++ b/main/keychain/pom.xml @@ -3,7 +3,7 @@ org.cryptomator main - 1.3.0-rc6 + 1.3.0-rc7 keychain System Keychain Access diff --git a/main/launcher/pom.xml b/main/launcher/pom.xml index c0420d976..ea5bf3603 100644 --- a/main/launcher/pom.xml +++ b/main/launcher/pom.xml @@ -4,7 +4,7 @@ org.cryptomator main - 1.3.0-rc6 + 1.3.0-rc7 launcher Cryptomator Launcher diff --git a/main/launcher/src/main/java/org/cryptomator/launcher/FileOpenRequestHandler.java b/main/launcher/src/main/java/org/cryptomator/launcher/FileOpenRequestHandler.java index fdaafe483..3a0ac97c9 100644 --- a/main/launcher/src/main/java/org/cryptomator/launcher/FileOpenRequestHandler.java +++ b/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 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 ff = (List) 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; - } - } - } diff --git a/main/pom.xml b/main/pom.xml index 98e37939f..ff9103ce2 100644 --- a/main/pom.xml +++ b/main/pom.xml @@ -6,7 +6,7 @@ 4.0.0 org.cryptomator main - 1.3.0-rc6 + 1.3.0-rc7 pom Cryptomator @@ -27,8 +27,8 @@ UTF-8 - 1.1.1 - 1.2.2 + 1.1.2 + 1.3.0 0.6.1 1.0.2 1.7.25 @@ -42,9 +42,9 @@ 1.10 4.5.3 2.7.21 - 2.10 + 2.11 1.0.3 - 21.0 + 22.0 2.8.0 diff --git a/main/uber-jar/pom.xml b/main/uber-jar/pom.xml index f59da0bc8..54f7b106a 100644 --- a/main/uber-jar/pom.xml +++ b/main/uber-jar/pom.xml @@ -5,7 +5,7 @@ org.cryptomator main - 1.3.0-rc6 + 1.3.0-rc7 uber-jar Single über jar with all dependencies diff --git a/main/ui/pom.xml b/main/ui/pom.xml index 8fa5cd2f5..1631e43ff 100644 --- a/main/ui/pom.xml +++ b/main/ui/pom.xml @@ -12,7 +12,7 @@ org.cryptomator main - 1.3.0-rc6 + 1.3.0-rc7 ui Cryptomator GUI 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 ba15b47e1..7a41bf1b7 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 @@ -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")); } 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 a0554c3e6..f8859c305 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 @@ -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 */ diff --git a/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockedController.java b/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockedController.java index 52f70da77..ff8a57028 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockedController.java +++ b/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 = new SimpleObjectProperty<>(); + private final BooleanExpression vaultMounted = BooleanExpression.booleanExpression(EasyBind.select(vault).selectObject(Vault::mountedProperty).orElse(false)); private Optional 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,61 +132,16 @@ public class UnlockedController implements ViewController { @FXML private void didClickLockVault(ActionEvent event) { - regularLockVault(); + regularUnmountVault(this::lockVault); } - private void regularLockVault() { - asyncTaskService.asyncTaskOf(() -> { - vault.get().unmount(); + private void lockVault() { + try { vault.get().lock(); - }).onSuccess(() -> { - listener.ifPresent(listener -> listener.didLock(this)); - LOG.trace("Regular lock succeeded"); - }).onError(Exception.class, e -> { - onRegularLockVaultFailed(e); - }).run(); - } - - private void forcedLockVault() { - asyncTaskService.asyncTaskOf(() -> { - vault.get().unmountForced(); - vault.get().lock(); - }).onSuccess(() -> { - listener.ifPresent(listener -> listener.didLock(this)); - LOG.trace("Forced lock succeeded"); - }).onError(Exception.class, e -> { - onForcedLockVaultFailed(e); - }).run(); - } - - private void onRegularLockVaultFailed(Exception e) { - if (vault.get().supportsForcedUnmount()) { - 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"), // - localization.getString("unlocked.lock.force.confirmation.content"), // - ButtonType.NO); - - Optional choice = confirmDialog.showAndWait(); - if (ButtonType.YES.equals(choice.get())) { - forcedLockVault(); - } else { - LOG.trace("Unmount cancelled", e); - } - } else { - LOG.error("Regular unmount failed", e); - showUnmountFailedMessage(); + } catch (ServerLifecycleException | IOException e) { + LOG.error("Lock failed", e); } - } - - private void onForcedLockVaultFailed(Exception e) { - LOG.error("Forced unmount failed", e); - showUnmountFailedMessage(); - } - - private void showUnmountFailedMessage() { - messageLabel.setText(localization.getString("unlocked.label.unmountFailed")); + listener.ifPresent(listener -> listener.didLock(this)); } @FXML @@ -186,11 +155,88 @@ public class UnlockedController implements ViewController { } @FXML - private void didClickRevealVault(ActionEvent event) { + public void didClickMountVault(ActionEvent event) { + mountVault(vault.get()); + } + + private void mountVault(Vault vault) { asyncTaskService.asyncTaskOf(() -> { - vault.get().reveal(); - }).onError(RuntimeException.class, () -> { - // TODO overheadhunter catch more specific exception type thrown by reveal() + 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(); + } + + @FXML + public void didClickUnmountVault(ActionEvent event) { + regularUnmountVault(Runnables.doNothing()); + } + + private void regularUnmountVault(Runnable onSuccess) { + asyncTaskService.asyncTaskOf(() -> { + vault.get().unmount(); + }).onSuccess(() -> { + LOG.trace("Regular unmount succeeded."); + onSuccess.run(); + }).onError(Exception.class, e -> { + onRegularUnmountVaultFailed(e, onSuccess); + }).run(); + } + + private void forcedUnmountVault(Runnable onSuccess) { + asyncTaskService.asyncTaskOf(() -> { + vault.get().unmountForced(); + }).onSuccess(() -> { + LOG.trace("Forced unmount succeeded."); + onSuccess.run(); + }).onError(Exception.class, e -> { + LOG.error("Forced unmount failed.", e); + messageLabel.setText(localization.getString("unlocked.label.unmountFailed")); + }).run(); + } + + private void onRegularUnmountVaultFailed(Exception e, Runnable onSuccess) { + if (vault.get().supportsForcedUnmount()) { + 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"), // + localization.getString("unlocked.lock.force.confirmation.content"), // + ButtonType.NO); + + Optional choice = confirmDialog.showAndWait(); + if (ButtonType.YES.equals(choice.get())) { + forcedUnmountVault(onSuccess); + } else { + LOG.trace("Unmount cancelled.", e); + } + } else { + LOG.error("Regular unmount failed.", e); + messageLabel.setText(localization.getString("unlocked.label.unmountFailed")); + } + } + + @FXML + private void didClickRevealVault(ActionEvent event) { + revealVault(vault.get()); + } + + private void revealVault(Vault vault) { + asyncTaskService.asyncTaskOf(() -> { + 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(); } 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 d615dac5a..0f7e89e91 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 @@ -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(); } diff --git a/main/ui/src/main/java/org/cryptomator/ui/util/AsyncTaskService.java b/main/ui/src/main/java/org/cryptomator/ui/util/AsyncTaskService.java index 391d3ae77..a4aa3f96c 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/util/AsyncTaskService.java +++ b/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 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 AsyncTaskWithoutSuccessHandler asyncTaskOf(SupplierThrowingException task) { return new AsyncTaskImpl<>(task); } @@ -153,27 +165,55 @@ public class AsyncTaskService { public interface AsyncTaskWithoutSuccessHandler extends AsyncTaskWithoutErrorHandler { + /** + * @param handler Tasks to be invoked on the JavaFX application thread. + * @return The async task + */ AsyncTaskWithoutErrorHandler onSuccess(ConsumerThrowingException 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 + */ AsyncTaskWithoutErrorHandler onError(Class type, ConsumerThrowingException handler); + /** + * @param type Exception type to catch + * @param handler Tasks to be invoked on the JavaFX application thread. + * @return The async task + */ AsyncTaskWithoutErrorHandler onError(Class 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(); } } diff --git a/main/ui/src/main/java/org/cryptomator/ui/util/EawtApplicationWrapper.java b/main/ui/src/main/java/org/cryptomator/ui/util/EawtApplicationWrapper.java new file mode 100644 index 000000000..3c524e418 --- /dev/null +++ b/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 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> 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 files = (List) 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 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 operation + * @throws UncheckedReflectiveOperationException in case operation throws an ReflectiveOperationException. + */ + private static T uncheckedReflectiveOperation(SupplierThrowingException 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); + } + } + +} diff --git a/main/ui/src/main/resources/fxml/unlocked.fxml b/main/ui/src/main/resources/fxml/unlocked.fxml index 91f65ecc7..c29268378 100644 --- a/main/ui/src/main/resources/fxml/unlocked.fxml +++ b/main/ui/src/main/resources/fxml/unlocked.fxml @@ -28,6 +28,12 @@ + + + + + + diff --git a/main/ui/src/main/resources/localization/en.txt b/main/ui/src/main/resources/localization/en.txt index 541808120..3cad2a15d 100644 --- a/main/ui/src/main/resources/localization/en.txt +++ b/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