diff --git a/main/ui/src/main/java/org/cryptomator/ui/common/FxmlFile.java b/main/ui/src/main/java/org/cryptomator/ui/common/FxmlFile.java index 43074a605..310e11747 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/common/FxmlFile.java +++ b/main/ui/src/main/java/org/cryptomator/ui/common/FxmlFile.java @@ -11,6 +11,8 @@ public enum FxmlFile { CHANGEPASSWORD("/fxml/changepassword.fxml"), // ERROR("/fxml/error.fxml"), // FORGET_PASSWORD("/fxml/forget_password.fxml"), // + LOCK_FORCED("/fxml/lock_forced.fxml"), // + LOCK_FAILED("/fxml/lock_failed.fxml"), // MAIN_WINDOW("/fxml/main_window.fxml"), // MIGRATION_CAPABILITY_ERROR("/fxml/migration_capability_error.fxml"), // MIGRATION_IMPOSSIBLE("/fxml/migration_impossible.fxml"), diff --git a/main/ui/src/main/java/org/cryptomator/ui/common/VaultService.java b/main/ui/src/main/java/org/cryptomator/ui/common/VaultService.java index 35ed49870..1f4ffc3fd 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/common/VaultService.java +++ b/main/ui/src/main/java/org/cryptomator/ui/common/VaultService.java @@ -65,6 +65,7 @@ public class VaultService { public Task createLockTask(Vault vault, boolean forced) { Task task = new LockVaultTask(vault, forced); task.setOnSucceeded(evt -> LOG.info("Locked {}", vault.getDisplayName())); + task.setOnFailed(evt -> LOG.info("Failed to lock {}.", vault.getDisplayName(), evt.getSource().getException())); return task; } diff --git a/main/ui/src/main/java/org/cryptomator/ui/fxapp/FxApplication.java b/main/ui/src/main/java/org/cryptomator/ui/fxapp/FxApplication.java index 264fda2f9..1a4cb84de 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/fxapp/FxApplication.java +++ b/main/ui/src/main/java/org/cryptomator/ui/fxapp/FxApplication.java @@ -11,6 +11,7 @@ import org.cryptomator.integrations.uiappearance.UiAppearanceException; import org.cryptomator.integrations.uiappearance.UiAppearanceListener; import org.cryptomator.integrations.uiappearance.UiAppearanceProvider; import org.cryptomator.ui.common.VaultService; +import org.cryptomator.ui.lock.LockComponent; import org.cryptomator.ui.mainwindow.MainWindowComponent; import org.cryptomator.ui.preferences.PreferencesComponent; import org.cryptomator.ui.preferences.SelectedPreferencesTab; @@ -42,6 +43,7 @@ public class FxApplication extends Application { private final Lazy preferencesWindow; private final Lazy quitWindow; private final Provider unlockWindowBuilderProvider; + private final Provider lockWindowBuilderProvider; private final Optional trayIntegration; private final Optional appearanceProvider; private final VaultService vaultService; @@ -51,11 +53,12 @@ public class FxApplication extends Application { private final UiAppearanceListener systemInterfaceThemeListener = this::systemInterfaceThemeChanged; @Inject - FxApplication(Settings settings, Lazy mainWindow, Lazy preferencesWindow, Provider unlockWindowBuilderProvider, Lazy quitWindow, Optional trayIntegration, Optional appearanceProvider, VaultService vaultService, LicenseHolder licenseHolder) { + FxApplication(Settings settings, Lazy mainWindow, Lazy preferencesWindow, Provider unlockWindowBuilderProvider, Provider lockWindowBuilderProvider, Lazy quitWindow, Optional trayIntegration, Optional appearanceProvider, VaultService vaultService, LicenseHolder licenseHolder) { this.settings = settings; this.mainWindow = mainWindow; this.preferencesWindow = preferencesWindow; this.unlockWindowBuilderProvider = unlockWindowBuilderProvider; + this.lockWindowBuilderProvider = lockWindowBuilderProvider; this.quitWindow = quitWindow; this.trayIntegration = trayIntegration; this.appearanceProvider = appearanceProvider; @@ -110,6 +113,13 @@ public class FxApplication extends Application { }); } + public void startLockWorkflow(Vault vault, Optional owner) { + Platform.runLater(() -> { + lockWindowBuilderProvider.get().vault(vault).owner(owner).build().startLockWorkflow(); + LOG.debug("Start lock workflow for {}", vault.getDisplayName()); + }); + } + public void showQuitWindow(QuitResponse response) { Platform.runLater(() -> { quitWindow.get().showQuitWindow(response); diff --git a/main/ui/src/main/java/org/cryptomator/ui/fxapp/FxApplicationModule.java b/main/ui/src/main/java/org/cryptomator/ui/fxapp/FxApplicationModule.java index 066d1e271..74c201372 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/fxapp/FxApplicationModule.java +++ b/main/ui/src/main/java/org/cryptomator/ui/fxapp/FxApplicationModule.java @@ -11,6 +11,7 @@ import dagger.Provides; import org.apache.commons.lang3.SystemUtils; import org.cryptomator.ui.common.ErrorComponent; import org.cryptomator.ui.common.StageFactory; +import org.cryptomator.ui.lock.LockComponent; import org.cryptomator.ui.mainwindow.MainWindowComponent; import org.cryptomator.ui.preferences.PreferencesComponent; import org.cryptomator.ui.quit.QuitComponent; @@ -27,7 +28,7 @@ import java.io.UncheckedIOException; import java.util.Collections; import java.util.List; -@Module(includes = {UpdateCheckerModule.class}, subcomponents = {MainWindowComponent.class, PreferencesComponent.class, UnlockComponent.class, QuitComponent.class, ErrorComponent.class}) +@Module(includes = {UpdateCheckerModule.class}, subcomponents = {MainWindowComponent.class, PreferencesComponent.class, UnlockComponent.class, LockComponent.class, QuitComponent.class, ErrorComponent.class}) abstract class FxApplicationModule { @Provides diff --git a/main/ui/src/main/java/org/cryptomator/ui/lock/LockComponent.java b/main/ui/src/main/java/org/cryptomator/ui/lock/LockComponent.java new file mode 100644 index 000000000..9796c88c7 --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/lock/LockComponent.java @@ -0,0 +1,39 @@ +package org.cryptomator.ui.lock; + +import dagger.BindsInstance; +import dagger.Subcomponent; +import org.cryptomator.common.vaults.Vault; + +import javax.inject.Named; +import javafx.stage.Stage; +import java.util.Optional; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; + + +@LockScoped +@Subcomponent(modules = {LockModule.class}) +public interface LockComponent { + + ExecutorService defaultExecutorService(); + + LockWorkflow lockWorkflow(); + + default Future startLockWorkflow() { + LockWorkflow workflow = lockWorkflow(); + defaultExecutorService().submit(workflow); + return workflow; + } + + @Subcomponent.Builder + interface Builder { + + @BindsInstance + LockComponent.Builder vault(@LockWindow Vault vault); + + @BindsInstance + LockComponent.Builder owner(@Named("lockWindowOwner") Optional owner); + + LockComponent build(); + } +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/lock/LockFailedController.java b/main/ui/src/main/java/org/cryptomator/ui/lock/LockFailedController.java new file mode 100644 index 000000000..e46d1d868 --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/lock/LockFailedController.java @@ -0,0 +1,31 @@ +package org.cryptomator.ui.lock; + +import org.cryptomator.common.vaults.Vault; +import org.cryptomator.ui.common.FxController; + +import javax.inject.Inject; +import javafx.fxml.FXML; +import javafx.stage.Stage; + +@LockScoped +public class LockFailedController implements FxController { + + private final Stage window; + private final Vault vault; + + @Inject + public LockFailedController(@LockWindow Stage window, @LockWindow Vault vault) { + this.window = window; + this.vault = vault; + } + + @FXML + public void close() { + window.close(); + } + + // ----- Getter & Setter ----- + public String getVaultName() { + return vault.getDisplayName(); + } +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/lock/LockForcedController.java b/main/ui/src/main/java/org/cryptomator/ui/lock/LockForcedController.java new file mode 100644 index 000000000..8d4ce32d3 --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/lock/LockForcedController.java @@ -0,0 +1,57 @@ +package org.cryptomator.ui.lock; + +import org.cryptomator.common.vaults.Vault; +import org.cryptomator.ui.common.FxController; +import org.cryptomator.ui.common.UserInteractionLock; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.inject.Inject; +import javafx.fxml.FXML; +import javafx.stage.Stage; +import javafx.stage.WindowEvent; + +@LockScoped +public class LockForcedController implements FxController { + + private static final Logger LOG = LoggerFactory.getLogger(LockForcedController.class); + + private final Stage window; + private final Vault vault; + private final UserInteractionLock forceLockDecisionLock; + + @Inject + public LockForcedController(@LockWindow Stage window, @LockWindow Vault vault, UserInteractionLock forceLockDecisionLock) { + this.window = window; + this.vault = vault; + this.forceLockDecisionLock = forceLockDecisionLock; + this.window.setOnHiding(this::windowClosed); + } + + @FXML + public void cancel() { + forceLockDecisionLock.interacted(LockModule.ForceLockDecision.CANCEL); + window.close(); + } + + @FXML + public void confirmForcedLock() { + forceLockDecisionLock.interacted(LockModule.ForceLockDecision.FORCE); + window.close(); + } + + private void windowClosed(WindowEvent windowEvent) { + // if not already interacted, set the decision to CANCEL + if (forceLockDecisionLock.awaitingInteraction().get()) { + LOG.debug("Lock canceled in force-lock-phase by user."); + forceLockDecisionLock.interacted(LockModule.ForceLockDecision.CANCEL); + } + } + + // ----- Getter & Setter ----- + + public String getVaultName() { + return vault.getDisplayName(); + } + +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/lock/LockModule.java b/main/ui/src/main/java/org/cryptomator/ui/lock/LockModule.java new file mode 100644 index 000000000..bbc8c2209 --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/lock/LockModule.java @@ -0,0 +1,89 @@ +package org.cryptomator.ui.lock; + +import dagger.Binds; +import dagger.Module; +import dagger.Provides; +import dagger.multibindings.IntoMap; +import org.cryptomator.common.vaults.Vault; +import org.cryptomator.ui.common.DefaultSceneFactory; +import org.cryptomator.ui.common.FXMLLoaderFactory; +import org.cryptomator.ui.common.FxController; +import org.cryptomator.ui.common.FxControllerKey; +import org.cryptomator.ui.common.FxmlFile; +import org.cryptomator.ui.common.FxmlScene; +import org.cryptomator.ui.common.StageFactory; +import org.cryptomator.ui.common.UserInteractionLock; + +import javax.inject.Named; +import javax.inject.Provider; +import javafx.scene.Scene; +import javafx.stage.Modality; +import javafx.stage.Stage; +import java.util.Map; +import java.util.Optional; +import java.util.ResourceBundle; + +@Module +abstract class LockModule { + + enum ForceLockDecision { + CANCEL, + FORCE; + } + + @Provides + @LockScoped + static UserInteractionLock provideForceLockDecisionLock() { + return new UserInteractionLock<>(null); + } + + @Provides + @LockWindow + @LockScoped + static FXMLLoaderFactory provideFxmlLoaderFactory(Map, Provider> factories, DefaultSceneFactory sceneFactory, ResourceBundle resourceBundle) { + return new FXMLLoaderFactory(factories, sceneFactory, resourceBundle); + } + + @Provides + @LockWindow + @LockScoped + static Stage provideWindow(StageFactory factory, @LockWindow Vault vault, @Named("lockWindowOwner") Optional owner) { + Stage stage = factory.create(); + stage.setTitle(vault.getDisplayName()); + stage.setResizable(false); + if (owner.isPresent()) { + stage.initOwner(owner.get()); + stage.initModality(Modality.WINDOW_MODAL); + } else { + stage.initModality(Modality.APPLICATION_MODAL); + } + return stage; + } + + @Provides + @FxmlScene(FxmlFile.LOCK_FORCED) + @LockScoped + static Scene provideForceLockScene(@LockWindow FXMLLoaderFactory fxmlLoaders) { + return fxmlLoaders.createScene("/fxml/lock_forced.fxml"); + } + + @Provides + @FxmlScene(FxmlFile.LOCK_FAILED) + @LockScoped + static Scene provideLockFailedScene(@LockWindow FXMLLoaderFactory fxmlLoaders) { + return fxmlLoaders.createScene("/fxml/lock_failed.fxml"); + } + + // ------------------ + + @Binds + @IntoMap + @FxControllerKey(LockForcedController.class) + abstract FxController bindLockForcedController(LockForcedController controller); + + @Binds + @IntoMap + @FxControllerKey(LockFailedController.class) + abstract FxController bindLockFailedController(LockFailedController controller); + +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/lock/LockScoped.java b/main/ui/src/main/java/org/cryptomator/ui/lock/LockScoped.java new file mode 100644 index 000000000..68d05f6e0 --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/lock/LockScoped.java @@ -0,0 +1,13 @@ +package org.cryptomator.ui.lock; + +import javax.inject.Scope; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Scope +@Documented +@Retention(RetentionPolicy.RUNTIME) +@interface LockScoped { + +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/lock/LockWindow.java b/main/ui/src/main/java/org/cryptomator/ui/lock/LockWindow.java new file mode 100644 index 000000000..10d6445ab --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/lock/LockWindow.java @@ -0,0 +1,14 @@ +package org.cryptomator.ui.lock; + +import javax.inject.Qualifier; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Qualifier +@Documented +@Retention(RUNTIME) +@interface LockWindow { + +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/lock/LockWorkflow.java b/main/ui/src/main/java/org/cryptomator/ui/lock/LockWorkflow.java new file mode 100644 index 000000000..acb6d1355 --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/lock/LockWorkflow.java @@ -0,0 +1,105 @@ +package org.cryptomator.ui.lock; + +import dagger.Lazy; +import org.cryptomator.common.vaults.Vault; +import org.cryptomator.common.vaults.VaultState; +import org.cryptomator.common.vaults.Volume; +import org.cryptomator.ui.common.FxmlFile; +import org.cryptomator.ui.common.FxmlScene; +import org.cryptomator.ui.common.UserInteractionLock; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.inject.Inject; +import javafx.application.Platform; +import javafx.concurrent.Task; +import javafx.scene.Scene; +import javafx.stage.Stage; +import javafx.stage.Window; + +/** + * The sequence of actions performed and checked during lock of a vault. + *

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

  • succeeded, the vault was successfully locked;
  • + *
  • canceled, the lock was canceled;
  • + *
  • failed, the lock failed due to an exception.
  • + */ +public class LockWorkflow extends Task { + + private static final Logger LOG = LoggerFactory.getLogger(LockWorkflow.class); + + private final Stage lockWindow; + private final Vault vault; + private final UserInteractionLock forceLockDecisionLock; + private final Lazy lockForcedScene; + private final Lazy lockFailedScene; + + @Inject + public LockWorkflow(@LockWindow Stage lockWindow, @LockWindow Vault vault, UserInteractionLock forceLockDecisionLock, @FxmlScene(FxmlFile.LOCK_FORCED) Lazy lockForcedScene, @FxmlScene(FxmlFile.LOCK_FAILED) Lazy lockFailedScene) { + this.lockWindow = lockWindow; + this.vault = vault; + this.forceLockDecisionLock = forceLockDecisionLock; + this.lockForcedScene = lockForcedScene; + this.lockFailedScene = lockFailedScene; + } + + @Override + protected Void call() throws Volume.VolumeException, InterruptedException { + try { + vault.lock(false); + } catch (Volume.VolumeException e) { + LOG.debug("Regular lock of {} failed.", vault.getDisplayName(), e); + var decision = askUserForAction(); + switch (decision) { + case FORCE -> vault.lock(true); + case CANCEL -> cancel(false); + } + } + return null; + } + + private LockModule.ForceLockDecision askUserForAction() throws InterruptedException { + // show forcedLock dialogue ... + Platform.runLater(() -> { + lockWindow.setScene(lockForcedScene.get()); + lockWindow.show(); + Window owner = lockWindow.getOwner(); + if (owner != null) { + lockWindow.setX(owner.getX() + (owner.getWidth() - lockWindow.getWidth()) / 2); + lockWindow.setY(owner.getY() + (owner.getHeight() - lockWindow.getHeight()) / 2); + } else { + lockWindow.centerOnScreen(); + } + }); + // ... and wait for answer + return forceLockDecisionLock.awaitInteraction(); + } + + @Override + protected void scheduled() { + vault.setState(VaultState.PROCESSING); + } + + @Override + protected void succeeded() { + LOG.info("Lock of {} succeeded.", vault.getDisplayName()); + vault.setState(VaultState.LOCKED); + } + + @Override + protected void failed() { + LOG.warn("Failed to lock {}.", vault.getDisplayName()); + vault.setState(VaultState.UNLOCKED); + lockWindow.setScene(lockFailedScene.get()); + lockWindow.show(); + } + + @Override + protected void cancelled() { + LOG.debug("Lock of {} canceled.", vault.getDisplayName()); + vault.setState(VaultState.UNLOCKED); + } + +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailUnlockedController.java b/main/ui/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailUnlockedController.java index 1806d9e5e..0af909bbc 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailUnlockedController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailUnlockedController.java @@ -6,25 +6,32 @@ import com.google.common.cache.LoadingCache; import org.cryptomator.common.vaults.Vault; import org.cryptomator.ui.common.FxController; import org.cryptomator.ui.common.VaultService; +import org.cryptomator.ui.fxapp.FxApplication; import org.cryptomator.ui.stats.VaultStatisticsComponent; import javax.inject.Inject; import javafx.beans.property.ObjectProperty; import javafx.beans.property.ReadOnlyObjectProperty; import javafx.fxml.FXML; +import javafx.stage.Stage; +import java.util.Optional; @MainWindowScoped public class VaultDetailUnlockedController implements FxController { private final ReadOnlyObjectProperty vault; + private final FxApplication application; private final VaultService vaultService; + private final Stage mainWindow; private final LoadingCache vaultStats; private final VaultStatisticsComponent.Builder vaultStatsBuilder; @Inject - public VaultDetailUnlockedController(ObjectProperty vault, VaultService vaultService, VaultStatisticsComponent.Builder vaultStatsBuilder) { + public VaultDetailUnlockedController(ObjectProperty vault, FxApplication application, VaultService vaultService, VaultStatisticsComponent.Builder vaultStatsBuilder, @MainWindow Stage mainWindow) { this.vault = vault; + this.application = application; this.vaultService = vaultService; + this.mainWindow = mainWindow; this.vaultStats = CacheBuilder.newBuilder().weakValues().build(CacheLoader.from(this::buildVaultStats)); this.vaultStatsBuilder = vaultStatsBuilder; } @@ -40,8 +47,7 @@ public class VaultDetailUnlockedController implements FxController { @FXML public void lock() { - vaultService.lock(vault.get(), false); - // TODO count lock attempts, and allow forced lock + application.startLockWorkflow(vault.get(), Optional.of(mainWindow)); } @FXML diff --git a/main/ui/src/main/java/org/cryptomator/ui/traymenu/TrayMenuController.java b/main/ui/src/main/java/org/cryptomator/ui/traymenu/TrayMenuController.java index 51f7c2225..a65f1e493 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/traymenu/TrayMenuController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/traymenu/TrayMenuController.java @@ -108,7 +108,7 @@ class TrayMenuController { } private void lockVault(Vault vault) { - fxApplicationStarter.get(true).thenAccept(app -> app.getVaultService().lock(vault, false)); + fxApplicationStarter.get(true).thenAccept(app -> app.startLockWorkflow(vault, Optional.empty())); } private void lockAllVaults(ActionEvent actionEvent) { diff --git a/main/ui/src/main/resources/fxml/lock_failed.fxml b/main/ui/src/main/resources/fxml/lock_failed.fxml new file mode 100644 index 000000000..f599b0fd8 --- /dev/null +++ b/main/ui/src/main/resources/fxml/lock_failed.fxml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + +