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..509b07b17 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 @@ -14,6 +14,7 @@ import java.util.Optional; import javax.inject.Inject; +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; @@ -25,6 +26,7 @@ import org.slf4j.LoggerFactory; 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 +60,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 +79,9 @@ public class UnlockedController implements ViewController { @FXML private ContextMenu moreOptionsMenu; + @FXML + private MenuItem mountVaultMenuItem; + @FXML private MenuItem revealVaultMenuItem; @@ -90,7 +96,8 @@ public class UnlockedController implements ViewController { @Override public void initialize() { - revealVaultMenuItem.disableProperty().bind(EasyBind.map(vault, vault -> vault != null && !vault.isMounted())); + revealVaultMenuItem.disableProperty().bind(vaultMounted.not()); + mountVaultMenuItem.disableProperty().bind(vaultMounted); EasyBind.subscribe(vault, this::vaultChanged); EasyBind.subscribe(moreOptionsMenu.showingProperty(), moreOptionsButton::setSelected); @@ -106,9 +113,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: @@ -186,11 +192,35 @@ 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(() -> { + messageLabel.setText(null); + if (vault.getVaultSettings().revealAfterMount().get()) { + revealVault(vault); + } + }).onError(CommandFailedException.class, e -> { + // TODO Markus Kreusch #393: hyperlink auf FAQ oder sowas? + messageLabel.setText(localization.getString("unlocked.label.mountFailed")); + }).run(); + } + + @FXML + private void didClickRevealVault(ActionEvent event) { + revealVault(vault.get()); + } + + private void revealVault(Vault vault) { + asyncTaskService.asyncTaskOf(() -> { + vault.reveal(); + }).onSuccess(() -> { + messageLabel.setText(null); + }).onError(CommandFailedException.class, () -> { 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..07d3ccc74 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 @@ -33,6 +33,7 @@ import org.cryptomator.cryptofs.CryptoFileSystemProperties; 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; @@ -108,7 +109,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()) { 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/resources/fxml/unlocked.fxml b/main/ui/src/main/resources/fxml/unlocked.fxml index 91f65ecc7..72eec0a7b 100644 --- a/main/ui/src/main/resources/fxml/unlocked.fxml +++ b/main/ui/src/main/resources/fxml/unlocked.fxml @@ -28,6 +28,9 @@ + + + diff --git a/main/ui/src/main/resources/localization/en.txt b/main/ui/src/main/resources/localization/en.txt index 541808120..716e58b35 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,7 @@ changePassword.errorMessage.decryptionFailed=Decryption failed # unlocked.fxml unlocked.button.lock=Lock Vault +unlocked.moreOptions.mount=Mount Drive unlocked.moreOptions.reveal=Reveal Drive unlocked.moreOptions.copyUrl=Copy WebDAV URL unlocked.label.mountFailed=Connecting drive failed