diff --git a/README.md b/README.md index bcff4c978..5e847208c 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ Download native binaries of Cryptomator on [cryptomator.org](https://cryptomator - File names get encrypted - Folder structure gets obfuscated - Use as many vaults in your Dropbox as you want, each having individual passwords -- Two thousand commits for the security of your data!! :tada: +- Three thousand commits for the security of your data!! :tada: ### Privacy diff --git a/pom.xml b/pom.xml index 1a86c335e..a6bc06257 100644 --- a/pom.xml +++ b/pom.xml @@ -27,7 +27,7 @@ com.github.serceman,com.github.jnr,org.ow2.asm,net.java.dev.jna,org.apache.jackrabbit,org.apache.httpcomponents - 2.1.0-beta7 + 2.1.0-beta8 1.0.0-rc1 1.0.0-beta2 1.0.0-beta2 diff --git a/src/main/java/org/cryptomator/common/keychain/KeychainManager.java b/src/main/java/org/cryptomator/common/keychain/KeychainManager.java index 64db08018..57af6c685 100644 --- a/src/main/java/org/cryptomator/common/keychain/KeychainManager.java +++ b/src/main/java/org/cryptomator/common/keychain/KeychainManager.java @@ -59,8 +59,10 @@ public class KeychainManager implements KeychainAccessProvider { @Override public void changePassphrase(String key, CharSequence passphrase) throws KeychainAccessException { - getKeychainOrFail().changePassphrase(key, passphrase); - setPassphraseStored(key, true); + if (isPassphraseStored(key)) { + getKeychainOrFail().changePassphrase(key, passphrase); + setPassphraseStored(key, true); + } } @Override diff --git a/src/main/java/org/cryptomator/ui/addvaultwizard/CreateNewVaultPasswordController.java b/src/main/java/org/cryptomator/ui/addvaultwizard/CreateNewVaultPasswordController.java index 4b4e02ed2..28b8384bb 100644 --- a/src/main/java/org/cryptomator/ui/addvaultwizard/CreateNewVaultPasswordController.java +++ b/src/main/java/org/cryptomator/ui/addvaultwizard/CreateNewVaultPasswordController.java @@ -5,8 +5,8 @@ import org.cryptomator.common.vaults.Vault; import org.cryptomator.common.vaults.VaultListManager; import org.cryptomator.cryptofs.CryptoFileSystemProperties; import org.cryptomator.cryptofs.CryptoFileSystemProvider; -import org.cryptomator.cryptofs.VaultCipherCombo; import org.cryptomator.cryptolib.api.CryptoException; +import org.cryptomator.cryptolib.api.CryptorProvider; import org.cryptomator.cryptolib.api.Masterkey; import org.cryptomator.cryptolib.api.MasterkeyLoader; import org.cryptomator.cryptolib.common.MasterkeyFileAccess; @@ -182,7 +182,7 @@ public class CreateNewVaultPasswordController implements FxController { // 2. initialize vault: try { MasterkeyLoader loader = ignored -> masterkey.clone(); - CryptoFileSystemProperties fsProps = CryptoFileSystemProperties.cryptoFileSystemProperties().withCipherCombo(VaultCipherCombo.SIV_CTRMAC).withKeyLoader(loader).build(); + CryptoFileSystemProperties fsProps = CryptoFileSystemProperties.cryptoFileSystemProperties().withCipherCombo(CryptorProvider.Scheme.SIV_CTRMAC).withKeyLoader(loader).build(); CryptoFileSystemProvider.initialize(path, fsProps, DEFAULT_KEY_ID); // 3. write vault-internal readme file: diff --git a/src/main/java/org/cryptomator/ui/controls/FontAwesome5Icon.java b/src/main/java/org/cryptomator/ui/controls/FontAwesome5Icon.java index a273447bb..6ec3cddb4 100644 --- a/src/main/java/org/cryptomator/ui/controls/FontAwesome5Icon.java +++ b/src/main/java/org/cryptomator/ui/controls/FontAwesome5Icon.java @@ -39,7 +39,7 @@ public enum FontAwesome5Icon { REDO("\uF01E"), // SEARCH("\uF002"), // SPINNER("\uF110"), // - STOPWATCH("\uF2F2"), // + STETHOSCOPE("\uF0f1"), // SYNC("\uF021"), // TIMES("\uF00D"), // TRASH("\uF1F8"), // diff --git a/src/main/java/org/cryptomator/ui/controls/FontAwesome5IconView.java b/src/main/java/org/cryptomator/ui/controls/FontAwesome5IconView.java index 4d0797eaa..3bfa70c46 100644 --- a/src/main/java/org/cryptomator/ui/controls/FontAwesome5IconView.java +++ b/src/main/java/org/cryptomator/ui/controls/FontAwesome5IconView.java @@ -21,8 +21,8 @@ public class FontAwesome5IconView extends Text { private static final String FONT_PATH = "/css/fontawesome5-free-solid.otf"; private static final Font FONT; - private ObjectProperty glyph = new SimpleObjectProperty<>(this, "glyph", DEFAULT_GLYPH); - private DoubleProperty glyphSize = new SimpleDoubleProperty(this, "glyphSize", DEFAULT_GLYPH_SIZE); + private final ObjectProperty glyph = new SimpleObjectProperty<>(this, "glyph", DEFAULT_GLYPH); + private final DoubleProperty glyphSize = new SimpleDoubleProperty(this, "glyphSize", DEFAULT_GLYPH_SIZE); static { try { @@ -42,7 +42,7 @@ public class FontAwesome5IconView extends Text { } private void glyphChanged(@SuppressWarnings("unused") ObservableValue observable, @SuppressWarnings("unused") FontAwesome5Icon oldValue, FontAwesome5Icon newValue) { - setText(newValue.unicode()); + setText(newValue == null ? null : newValue.unicode()); } private void glyphSizeChanged(@SuppressWarnings("unused") ObservableValue observable, @SuppressWarnings("unused") Number oldValue, Number newValue) { diff --git a/src/main/java/org/cryptomator/ui/health/BatchService.java b/src/main/java/org/cryptomator/ui/health/BatchService.java index f3968c27d..40f4e173f 100644 --- a/src/main/java/org/cryptomator/ui/health/BatchService.java +++ b/src/main/java/org/cryptomator/ui/health/BatchService.java @@ -16,7 +16,6 @@ public class BatchService extends Service { private final Iterator remainingTasks; - @Inject public BatchService(Iterable tasks) { this.remainingTasks = tasks.iterator(); } diff --git a/src/main/java/org/cryptomator/ui/health/CheckDetailController.java b/src/main/java/org/cryptomator/ui/health/CheckDetailController.java index 68a74c4a0..7ccc9e09c 100644 --- a/src/main/java/org/cryptomator/ui/health/CheckDetailController.java +++ b/src/main/java/org/cryptomator/ui/health/CheckDetailController.java @@ -23,7 +23,7 @@ import java.util.stream.Stream; @HealthCheckScoped public class CheckDetailController implements FxController { - private final EasyObservableList results; + private final EasyObservableList results; private final OptionalBinding taskState; private final Binding taskName; private final Binding taskDuration; @@ -39,7 +39,7 @@ public class CheckDetailController implements FxController { private final ResultListCellFactory resultListCellFactory; private final ResourceBundle resourceBundle; - public ListView resultsListView; + public ListView resultsListView; private Subscription resultSubscription; @Inject @@ -71,8 +71,8 @@ public class CheckDetailController implements FxController { } } - private Function, Long> countSeverity(DiagnosticResult.Severity severity) { - return stream -> stream.filter(item -> severity.equals(item.getSeverity())).count(); + private Function, Long> countSeverity(DiagnosticResult.Severity severity) { + return stream -> stream.filter(item -> severity.equals(item.diagnosis().getSeverity())).count(); } @FXML diff --git a/src/main/java/org/cryptomator/ui/health/CheckListCell.java b/src/main/java/org/cryptomator/ui/health/CheckListCell.java index 63d9d8d6b..76a8f3c27 100644 --- a/src/main/java/org/cryptomator/ui/health/CheckListCell.java +++ b/src/main/java/org/cryptomator/ui/health/CheckListCell.java @@ -1,13 +1,10 @@ package org.cryptomator.ui.health; +import org.cryptomator.cryptofs.health.api.DiagnosticResult; import org.cryptomator.ui.controls.FontAwesome5Icon; import org.cryptomator.ui.controls.FontAwesome5IconView; import javafx.beans.binding.Bindings; -import javafx.beans.property.BooleanProperty; -import javafx.beans.property.ObjectProperty; -import javafx.beans.property.SimpleObjectProperty; -import javafx.beans.value.ObservableValue; import javafx.concurrent.Worker; import javafx.geometry.Insets; import javafx.geometry.Pos; @@ -15,101 +12,60 @@ import javafx.scene.Node; import javafx.scene.control.CheckBox; import javafx.scene.control.ContentDisplay; import javafx.scene.control.ListCell; -import javafx.util.Callback; +import java.util.function.Predicate; class CheckListCell extends ListCell { private final FontAwesome5IconView stateIcon = new FontAwesome5IconView(); - private final Callback selectedGetter; - private final ObjectProperty stateProperty; - private CheckBox checkBox = new CheckBox(); - private BooleanProperty selectedProperty; - CheckListCell(Callback selectedGetter, ObservableValue switchIndicator) { - this.selectedGetter = selectedGetter; - this.stateProperty = new SimpleObjectProperty<>(State.SELECTION); - switchIndicator.addListener(this::changeState); + CheckListCell() { setPadding(new Insets(6)); setAlignment(Pos.CENTER_LEFT); setContentDisplay(ContentDisplay.LEFT); getStyleClass().add("label"); } - private void changeState(ObservableValue observableValue, boolean oldValue, boolean newValue) { - if (newValue) { - stateProperty.set(State.RUN); - } else { - stateProperty.set(State.SELECTION); - } - } - @Override protected void updateItem(HealthCheckTask item, boolean empty) { super.updateItem(item, empty); if (item != null) { setText(item.getTitle()); - } - switch (stateProperty.get()) { - case SELECTION -> updateItemSelection(item, empty); - case RUN -> updateItemRun(item, empty); - } - } - - private void updateItemSelection(HealthCheckTask item, boolean empty) { - if (!empty) { - setGraphic(checkBox); - - if (selectedProperty != null) { - checkBox.selectedProperty().unbindBidirectional(selectedProperty); - } - selectedProperty = selectedGetter.call(item); - if (selectedProperty != null) { - checkBox.selectedProperty().bindBidirectional(selectedProperty); - } - } else { - setGraphic(null); - setText(null); - } - } - - private void updateItemRun(HealthCheckTask item, boolean empty) { - if (item != null) { - item.stateProperty().addListener(this::stateChanged); graphicProperty().bind(Bindings.createObjectBinding(() -> graphicForState(item.getState()), item.stateProperty())); - stateIcon.setGlyph(glyphForState(item.getState())); + stateIcon.glyphProperty().bind(Bindings.createObjectBinding(() -> glyphForState(item), item.stateProperty())); + checkBox.selectedProperty().bindBidirectional(item.chosenForExecutionProperty()); } else { graphicProperty().unbind(); setGraphic(null); setText(null); + checkBox.selectedProperty().unbind(); } } - private void stateChanged(ObservableValue observable, Worker.State oldState, Worker.State newState) { - stateIcon.setGlyph(glyphForState(newState)); - stateIcon.setVisible(true); - } - private Node graphicForState(Worker.State state) { return switch (state) { - case READY -> null; + case READY -> checkBox; case SCHEDULED, RUNNING, FAILED, CANCELLED, SUCCEEDED -> stateIcon; }; } - private FontAwesome5Icon glyphForState(Worker.State state) { - return switch (state) { + private FontAwesome5Icon glyphForState(HealthCheckTask item) { + return switch (item.getState()) { case READY -> FontAwesome5Icon.COG; //just a placeholder case SCHEDULED -> FontAwesome5Icon.CLOCK; case RUNNING -> FontAwesome5Icon.SPINNER; case FAILED -> FontAwesome5Icon.EXCLAMATION_TRIANGLE; case CANCELLED -> FontAwesome5Icon.BAN; - case SUCCEEDED -> FontAwesome5Icon.CHECK; + case SUCCEEDED -> checkFoundProblems(item) ? FontAwesome5Icon.EXCLAMATION_TRIANGLE : FontAwesome5Icon.CHECK; }; } - private enum State { - SELECTION, - RUN; + private boolean checkFoundProblems(HealthCheckTask item) { + Predicate isProblem = severity -> switch (severity) { + case WARN, CRITICAL -> true; + case INFO, GOOD -> false; + }; + return item.results().stream().map(Result::diagnosis).map(DiagnosticResult::getSeverity).anyMatch(isProblem); } + } diff --git a/src/main/java/org/cryptomator/ui/health/CheckListController.java b/src/main/java/org/cryptomator/ui/health/CheckListController.java index 5c7e5af7d..7710eb14d 100644 --- a/src/main/java/org/cryptomator/ui/health/CheckListController.java +++ b/src/main/java/org/cryptomator/ui/health/CheckListController.java @@ -1,6 +1,7 @@ package org.cryptomator.ui.health; import com.google.common.base.Preconditions; +import com.google.common.base.Predicates; import com.tobiasdiez.easybind.EasyBind; import dagger.Lazy; import org.cryptomator.ui.common.ErrorComponent; @@ -10,28 +11,24 @@ import org.slf4j.LoggerFactory; import javax.inject.Inject; import javafx.beans.binding.Binding; +import javafx.beans.binding.Bindings; import javafx.beans.binding.BooleanBinding; +import javafx.beans.binding.IntegerBinding; import javafx.beans.property.BooleanProperty; -import javafx.beans.property.IntegerProperty; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleBooleanProperty; -import javafx.beans.property.SimpleIntegerProperty; import javafx.beans.property.SimpleObjectProperty; -import javafx.beans.value.ObservableValue; import javafx.collections.FXCollections; import javafx.collections.ObservableList; +import javafx.collections.transformation.FilteredList; import javafx.concurrent.Worker; import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.scene.control.CheckBox; import javafx.scene.control.ListView; -import javafx.scene.control.cell.CheckBoxListCell; import javafx.stage.Stage; -import javafx.util.StringConverter; import java.io.IOException; -import java.util.Collection; -import java.util.HashMap; -import java.util.Map; +import java.util.List; import java.util.Set; import java.util.concurrent.ExecutorService; @@ -43,6 +40,7 @@ public class CheckListController implements FxController { private final Stage window; private final ObservableList tasks; + private final FilteredList chosenTasks; private final ReportWriter reportWriter; private final ExecutorService executorService; private final ObjectProperty selectedTask; @@ -50,19 +48,18 @@ public class CheckListController implements FxController { private final SimpleObjectProperty> runningTask; private final Binding running; private final Binding finished; - private final Map listPickIndicators; - private final IntegerProperty numberOfPickedChecks; + private final IntegerBinding chosenTaskCount; private final BooleanBinding anyCheckSelected; private final BooleanProperty showResultScreen; /* FXML */ public ListView checksListView; - @Inject - public CheckListController(@HealthCheckWindow Stage window, Lazy> tasks, ReportWriter reportWriteTask, ObjectProperty selectedTask, ExecutorService executorService, Lazy errorComponentBuilder) { + public CheckListController(@HealthCheckWindow Stage window, Lazy> tasks, ReportWriter reportWriteTask, ObjectProperty selectedTask, ExecutorService executorService, Lazy errorComponentBuilder) { this.window = window; - this.tasks = FXCollections.observableArrayList(tasks.get()); + this.tasks = FXCollections.observableList(tasks.get(), HealthCheckTask::observables); + this.chosenTasks = this.tasks.filtered(HealthCheckTask::isChosenForExecution); this.reportWriter = reportWriteTask; this.executorService = executorService; this.selectedTask = selectedTask; @@ -70,13 +67,7 @@ public class CheckListController implements FxController { this.runningTask = new SimpleObjectProperty<>(); this.running = EasyBind.wrapNullable(runningTask).mapObservable(Worker::runningProperty).orElse(false); this.finished = EasyBind.wrapNullable(runningTask).mapObservable(Worker::stateProperty).map(END_STATES::contains).orElse(false); - this.listPickIndicators = new HashMap<>(); - this.numberOfPickedChecks = new SimpleIntegerProperty(0); - this.tasks.forEach(task -> { - var entrySelectedProp = new SimpleBooleanProperty(false); - entrySelectedProp.addListener((observable, oldValue, newValue) -> numberOfPickedChecks.set(numberOfPickedChecks.get() + (newValue ? 1 : -1))); - listPickIndicators.put(task, entrySelectedProp); - }); + this.chosenTaskCount = Bindings.size(this.chosenTasks); this.anyCheckSelected = selectedTask.isNotNull(); this.showResultScreen = new SimpleBooleanProperty(false); } @@ -84,27 +75,31 @@ public class CheckListController implements FxController { @FXML public void initialize() { checksListView.setItems(tasks); - checksListView.setCellFactory(view -> new CheckListCell(listPickIndicators::get, showResultScreen)); + checksListView.setCellFactory(view -> new CheckListCell()); selectedTask.bind(checksListView.getSelectionModel().selectedItemProperty()); } @FXML public void toggleSelectAll(ActionEvent event) { if (event.getSource() instanceof CheckBox c) { - listPickIndicators.forEach( (task, pickProperty) -> pickProperty.set(c.isSelected())); + tasks.forEach(t -> t.chosenForExecutionProperty().set(c.isSelected())); } } @FXML public void runSelectedChecks() { Preconditions.checkState(runningTask.get() == null); - var batch = checksListView.getItems().filtered(item -> listPickIndicators.get(item).get()); - var batchService = new BatchService(batch); + + // prevent further interaction by cancelling non-chosen tasks: + tasks.filtered(Predicates.not(chosenTasks::contains)).forEach(HealthCheckTask::cancel); + + // run chosen tasks: + var batchService = new BatchService(chosenTasks); batchService.setExecutor(executorService); batchService.start(); runningTask.set(batchService); showResultScreen.set(true); - checksListView.getSelectionModel().select(batch.get(0)); + checksListView.getSelectionModel().select(chosenTasks.get(0)); checksListView.refresh(); window.sizeToScene(); } @@ -158,13 +153,12 @@ public class CheckListController implements FxController { return showResultScreen; } - public int getNumberOfPickedChecks() { - return numberOfPickedChecks.get(); + public int getChosenTaskCount() { + return chosenTaskCount.getValue(); } - public IntegerProperty numberOfPickedChecksProperty() { - return numberOfPickedChecks; + public IntegerBinding chosenTaskCountProperty() { + return chosenTaskCount; } - } diff --git a/src/main/java/org/cryptomator/ui/health/DummyHealthChecks.java b/src/main/java/org/cryptomator/ui/health/DummyHealthChecks.java new file mode 100644 index 000000000..d2bccff37 --- /dev/null +++ b/src/main/java/org/cryptomator/ui/health/DummyHealthChecks.java @@ -0,0 +1,41 @@ +package org.cryptomator.ui.health; + +import org.cryptomator.cryptofs.VaultConfig; +import org.cryptomator.cryptofs.health.api.DiagnosticResult; +import org.cryptomator.cryptofs.health.api.HealthCheck; +import org.cryptomator.cryptolib.api.Cryptor; +import org.cryptomator.cryptolib.api.Masterkey; + +import java.nio.file.Path; +import java.util.function.Consumer; + +/** + * FIXME: Remove in production release + */ +public class DummyHealthChecks { + + public static class DummyCheck1 implements HealthCheck { + + @Override + public void check(Path path, VaultConfig vaultConfig, Masterkey masterkey, Cryptor cryptor, Consumer consumer) { + // no-op + } + } + + public static class DummyCheck2 implements HealthCheck { + + @Override + public void check(Path path, VaultConfig vaultConfig, Masterkey masterkey, Cryptor cryptor, Consumer consumer) { + // no-op + } + } + + public static class DummyCheck3 implements HealthCheck { + + @Override + public void check(Path path, VaultConfig vaultConfig, Masterkey masterkey, Cryptor cryptor, Consumer consumer) { + // no-op + } + } + +} diff --git a/src/main/java/org/cryptomator/ui/health/HealthCheckComponent.java b/src/main/java/org/cryptomator/ui/health/HealthCheckComponent.java index 48b16f694..365ab63df 100644 --- a/src/main/java/org/cryptomator/ui/health/HealthCheckComponent.java +++ b/src/main/java/org/cryptomator/ui/health/HealthCheckComponent.java @@ -7,6 +7,7 @@ import org.cryptomator.common.vaults.Vault; import org.cryptomator.ui.common.FxmlFile; import org.cryptomator.ui.common.FxmlScene; +import javax.inject.Named; import javafx.scene.Scene; import javafx.stage.Stage; @@ -17,6 +18,9 @@ public interface HealthCheckComponent { @HealthCheckWindow Stage window(); + @Named("windowToClose") + Stage windowToClose(); + @FxmlScene(FxmlFile.HEALTH_START) Lazy scene(); @@ -24,6 +28,7 @@ public interface HealthCheckComponent { Stage stage = window(); stage.setScene(scene().get()); stage.show(); + windowToClose().close(); return stage; } @@ -33,6 +38,9 @@ public interface HealthCheckComponent { @BindsInstance Builder vault(@HealthCheckWindow Vault vault); + @BindsInstance + Builder windowToClose(@Named("windowToClose") Stage window); + HealthCheckComponent build(); } diff --git a/src/main/java/org/cryptomator/ui/health/HealthCheckModule.java b/src/main/java/org/cryptomator/ui/health/HealthCheckModule.java index e33a9f2f1..78643f011 100644 --- a/src/main/java/org/cryptomator/ui/health/HealthCheckModule.java +++ b/src/main/java/org/cryptomator/ui/health/HealthCheckModule.java @@ -28,6 +28,7 @@ import javafx.stage.Modality; import javafx.stage.Stage; import java.security.SecureRandom; import java.util.Collection; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.ResourceBundle; @@ -63,7 +64,7 @@ abstract class HealthCheckModule { /* Only inject with Lazy-Wrapper!*/ @Provides @HealthCheckScoped - static Collection provideAvailableHealthCheckTasks(Collection availableHealthChecks, @HealthCheckWindow Vault vault, AtomicReference masterkeyRef, AtomicReference vaultConfigRef, SecureRandom csprng, ResourceBundle resourceBundle) { + static List provideAvailableHealthCheckTasks(Collection availableHealthChecks, @HealthCheckWindow Vault vault, AtomicReference masterkeyRef, AtomicReference vaultConfigRef, SecureRandom csprng, ResourceBundle resourceBundle) { return availableHealthChecks.stream().map(check -> new HealthCheckTask(vault.getPath(), vaultConfigRef.get(), masterkeyRef.get(), csprng, check, resourceBundle)).toList(); } @@ -86,10 +87,10 @@ abstract class HealthCheckModule { @HealthCheckScoped static Stage provideStage(StageFactory factory, @MainWindow Stage owner, ResourceBundle resourceBundle, ChangeListener showingListener) { Stage stage = factory.create(); - stage.setTitle(resourceBundle.getString("health.title")); - stage.setResizable(true); stage.initModality(Modality.WINDOW_MODAL); stage.initOwner(owner); + stage.setTitle(resourceBundle.getString("health.title")); + stage.setResizable(true); stage.showingProperty().addListener(showingListener); // bind masterkey lifecycle to window return stage; } diff --git a/src/main/java/org/cryptomator/ui/health/HealthCheckTask.java b/src/main/java/org/cryptomator/ui/health/HealthCheckTask.java index 7acbfc1c2..3efe333b3 100644 --- a/src/main/java/org/cryptomator/ui/health/HealthCheckTask.java +++ b/src/main/java/org/cryptomator/ui/health/HealthCheckTask.java @@ -3,12 +3,16 @@ package org.cryptomator.ui.health; import org.cryptomator.cryptofs.VaultConfig; import org.cryptomator.cryptofs.health.api.DiagnosticResult; import org.cryptomator.cryptofs.health.api.HealthCheck; +import org.cryptomator.cryptolib.api.CryptorProvider; import org.cryptomator.cryptolib.api.Masterkey; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javafx.application.Platform; +import javafx.beans.Observable; +import javafx.beans.property.BooleanProperty; import javafx.beans.property.LongProperty; +import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleLongProperty; import javafx.collections.FXCollections; import javafx.collections.ObservableList; @@ -31,8 +35,9 @@ class HealthCheckTask extends Task { private final Masterkey masterkey; private final SecureRandom csprng; private final HealthCheck check; - private final ObservableList results; + private final ObservableList results; private final LongProperty durationInMillis; + private final BooleanProperty chosenForExecution; public HealthCheckTask(Path vaultPath, VaultConfig vaultConfig, Masterkey masterkey, SecureRandom csprng, HealthCheck check, ResourceBundle resourceBundle) { this.vaultPath = Objects.requireNonNull(vaultPath); @@ -40,7 +45,7 @@ class HealthCheckTask extends Task { this.masterkey = Objects.requireNonNull(masterkey); this.csprng = Objects.requireNonNull(csprng); this.check = Objects.requireNonNull(check); - this.results = FXCollections.observableArrayList(); + this.results = FXCollections.observableArrayList(Result::observables); try { updateTitle(resourceBundle.getString("health." + check.identifier())); } catch (MissingResourceException e) { @@ -48,32 +53,22 @@ class HealthCheckTask extends Task { updateTitle(check.identifier()); } this.durationInMillis = new SimpleLongProperty(-1); + this.chosenForExecution = new SimpleBooleanProperty(); } @Override protected Void call() { Instant start = Instant.now(); try (var masterkeyClone = masterkey.clone(); // - var cryptor = vaultConfig.getCipherCombo().getCryptorProvider(csprng).withKey(masterkeyClone)) { - check.check(vaultPath, vaultConfig, masterkeyClone, cryptor, result -> { + var cryptor = CryptorProvider.forScheme(vaultConfig.getCipherCombo()).provide(masterkeyClone, csprng)) { + check.check(vaultPath, vaultConfig, masterkeyClone, cryptor, diagnosis -> { if (isCancelled()) { throw new CancellationException(); } - // FIXME: slowdown for demonstration purposes only: - try { - Thread.sleep(2000); - } catch (InterruptedException e) { - if (isCancelled()) { - return; - } else { - Thread.currentThread().interrupt(); - throw new RuntimeException(e); - } - } - Platform.runLater(() -> results.add(result)); + Platform.runLater(() -> results.add(Result.create(diagnosis))); }); } - Platform.runLater(() ->durationInMillis.set(Duration.between(start, Instant.now()).toMillis())); + Platform.runLater(() -> durationInMillis.set(Duration.between(start, Instant.now()).toMillis())); return null; } @@ -89,7 +84,11 @@ class HealthCheckTask extends Task { /* Getter */ - public ObservableList results() { + Observable[] observables() { + return new Observable[]{results, chosenForExecution}; + } + + public ObservableList results() { return results; } @@ -105,4 +104,11 @@ class HealthCheckTask extends Task { return durationInMillis.get(); } + public BooleanProperty chosenForExecutionProperty() { + return chosenForExecution; + } + + public boolean isChosenForExecution() { + return chosenForExecution.get(); + } } diff --git a/src/main/java/org/cryptomator/ui/health/ReportWriter.java b/src/main/java/org/cryptomator/ui/health/ReportWriter.java index 2bc4c28d9..6039901ef 100644 --- a/src/main/java/org/cryptomator/ui/health/ReportWriter.java +++ b/src/main/java/org/cryptomator/ui/health/ReportWriter.java @@ -72,7 +72,7 @@ public class ReportWriter { case SUCCEEDED -> { writer.write("STATUS: SUCCESS\nRESULTS:\n"); for (var result : task.results()) { - writer.write(REPORT_CHECK_RESULT.formatted(result.getSeverity(), result.toString())); + writer.write(REPORT_CHECK_RESULT.formatted(result.diagnosis().getSeverity(), result.getDescription())); } } case CANCELLED -> writer.write("STATUS: CANCELED\n"); diff --git a/src/main/java/org/cryptomator/ui/health/Result.java b/src/main/java/org/cryptomator/ui/health/Result.java new file mode 100644 index 000000000..23d812f50 --- /dev/null +++ b/src/main/java/org/cryptomator/ui/health/Result.java @@ -0,0 +1,43 @@ +package org.cryptomator.ui.health; + +import org.cryptomator.cryptofs.health.api.DiagnosticResult; + +import javafx.beans.Observable; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; + +record Result(DiagnosticResult diagnosis, ObjectProperty fixState) { + + enum FixState { + NOT_FIXABLE, + FIXABLE, + FIXING, + FIXED, + FIX_FAILED + } + + public static Result create(DiagnosticResult diagnosis) { + FixState initialState = switch (diagnosis.getSeverity()) { + case WARN -> FixState.FIXABLE; + default -> FixState.NOT_FIXABLE; + }; + return new Result(diagnosis, new SimpleObjectProperty<>(initialState)); + } + + public Observable[] observables() { + return new Observable[]{fixState}; + } + + public String getDescription() { + return diagnosis.toString(); + } + + public FixState getState() { + return fixState.get(); + } + + public void setState(FixState state) { + this.fixState.set(state); + } + +} diff --git a/src/main/java/org/cryptomator/ui/health/ResultFixApplier.java b/src/main/java/org/cryptomator/ui/health/ResultFixApplier.java index 3c5403ab7..841a8f5c4 100644 --- a/src/main/java/org/cryptomator/ui/health/ResultFixApplier.java +++ b/src/main/java/org/cryptomator/ui/health/ResultFixApplier.java @@ -4,25 +4,30 @@ import com.google.common.base.Preconditions; import org.cryptomator.common.vaults.Vault; import org.cryptomator.cryptofs.VaultConfig; import org.cryptomator.cryptofs.health.api.DiagnosticResult; +import org.cryptomator.cryptolib.api.CryptorProvider; import org.cryptomator.cryptolib.api.Masterkey; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.inject.Inject; -import javafx.scene.control.Alert; +import javafx.application.Platform; import java.nio.file.Path; import java.security.SecureRandom; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicReference; @HealthCheckScoped class ResultFixApplier { - private static final Logger LOG = LoggerFactory.getLogger(ResultFixApplier.class); - private final Path vaultPath; private final SecureRandom csprng; private final Masterkey masterkey; private final VaultConfig vaultConfig; + private final ExecutorService sequentialExecutor; @Inject public ResultFixApplier(@HealthCheckWindow Vault vault, AtomicReference masterkeyRef, AtomicReference vaultConfigRef, SecureRandom csprng) { @@ -30,18 +35,32 @@ class ResultFixApplier { this.masterkey = masterkeyRef.get(); this.vaultConfig = vaultConfigRef.get(); this.csprng = csprng; + this.sequentialExecutor = Executors.newSingleThreadExecutor(); } - public void fix(DiagnosticResult result) { - Preconditions.checkArgument(result.getSeverity() == DiagnosticResult.Severity.WARN, "Unfixable result"); + public CompletionStage fix(Result result) { + Preconditions.checkArgument(result.getState() == Result.FixState.FIXABLE); + result.setState(Result.FixState.FIXING); + return CompletableFuture.runAsync(() -> fix(result.diagnosis()), sequentialExecutor) + .whenCompleteAsync((unused, throwable) -> { + var fixed = throwable == null ? Result.FixState.FIXED : Result.FixState.FIX_FAILED; + result.setState(fixed); + }, Platform::runLater); + } + + public void fix(DiagnosticResult diagnosis) { + Preconditions.checkArgument(diagnosis.getSeverity() == DiagnosticResult.Severity.WARN, "Unfixable result"); try (var masterkeyClone = masterkey.clone(); // - var cryptor = vaultConfig.getCipherCombo().getCryptorProvider(csprng).withKey(masterkeyClone)) { - result.fix(vaultPath, vaultConfig, masterkeyClone, cryptor); + var cryptor = CryptorProvider.forScheme(vaultConfig.getCipherCombo()).provide(masterkeyClone, csprng)) { + diagnosis.fix(vaultPath, vaultConfig, masterkeyClone, cryptor); } catch (Exception e) { - LOG.error("Failed to apply fix", e); - Alert alert = new Alert(Alert.AlertType.ERROR, e.getMessage()); - alert.showAndWait(); - //TODO: real error/not supported handling + throw new FixFailedException(e); + } + } + + public static class FixFailedException extends CompletionException { + private FixFailedException(Throwable cause) { + super(cause); } } } diff --git a/src/main/java/org/cryptomator/ui/health/ResultListCellController.java b/src/main/java/org/cryptomator/ui/health/ResultListCellController.java index 4506bc602..683d0fb93 100644 --- a/src/main/java/org/cryptomator/ui/health/ResultListCellController.java +++ b/src/main/java/org/cryptomator/ui/health/ResultListCellController.java @@ -1,84 +1,89 @@ package org.cryptomator.ui.health; import com.tobiasdiez.easybind.EasyBind; -import org.cryptomator.cryptofs.health.api.DiagnosticResult; +import com.tobiasdiez.easybind.optional.OptionalBinding; import org.cryptomator.ui.common.FxController; import org.cryptomator.ui.controls.FontAwesome5Icon; import org.cryptomator.ui.controls.FontAwesome5IconView; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import javax.inject.Inject; +import javafx.application.Platform; import javafx.beans.binding.Binding; +import javafx.beans.binding.Bindings; +import javafx.beans.binding.BooleanBinding; +import javafx.beans.binding.ObjectBinding; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; -import javafx.beans.value.ObservableValue; import javafx.fxml.FXML; import javafx.scene.control.Button; // unscoped because each cell needs its own controller public class ResultListCellController implements FxController { - private final ResultFixApplier fixApplier; - private final ObjectProperty result; + private final Logger LOG = LoggerFactory.getLogger(ResultListCellController.class); + + private final ObjectProperty result; private final Binding description; + private final ResultFixApplier fixApplier; + private final OptionalBinding fixState; + private final ObjectBinding glyph; + private final BooleanBinding fixable; + private final BooleanBinding fixing; + private final BooleanBinding fixed; public FontAwesome5IconView iconView; - public Button actionButton; + public Button fixButton; @Inject public ResultListCellController(ResultFixApplier fixApplier) { this.result = new SimpleObjectProperty<>(null); - this.description = EasyBind.wrapNullable(result).map(DiagnosticResult::toString).orElse(""); + this.description = EasyBind.wrapNullable(result).map(Result::getDescription).orElse(""); this.fixApplier = fixApplier; - result.addListener(this::updateCellContent); - } - - private void updateCellContent(ObservableValue observable, DiagnosticResult oldVal, DiagnosticResult newVal) { - iconView.getStyleClass().clear(); - actionButton.setVisible(false); - //TODO: see comment in case WARN - actionButton.setManaged(false); - switch (newVal.getSeverity()) { - case INFO -> { - iconView.setGlyph(FontAwesome5Icon.INFO_CIRCLE); - iconView.getStyleClass().add("glyph-icon-muted"); - } - case GOOD -> { - iconView.setGlyph(FontAwesome5Icon.CHECK); - iconView.getStyleClass().add("glyph-icon-primary"); - } - case WARN -> { - iconView.setGlyph(FontAwesome5Icon.EXCLAMATION_TRIANGLE); - iconView.getStyleClass().add("glyph-icon-orange"); - //TODO: Neither is any fix implemented, nor it is ensured, that only fix is executed at a time with good ui indication - // before both are not fix, do not show the button - //actionButton.setVisible(true); - } - case CRITICAL -> { - iconView.setGlyph(FontAwesome5Icon.TIMES); - iconView.getStyleClass().add("glyph-icon-red"); - } - } + this.fixState = EasyBind.wrapNullable(result).mapObservable(Result::fixState); + this.glyph = Bindings.createObjectBinding(this::getGlyph, result); + this.fixable = Bindings.createBooleanBinding(this::isFixable, fixState); + this.fixing = Bindings.createBooleanBinding(this::isFixing, fixState); + this.fixed = Bindings.createBooleanBinding(this::isFixed, fixState); } @FXML - public void runResultAction() { - final var realResult = result.get(); - if (realResult != null) { - fixApplier.fix(realResult); + public void initialize() { + // see getGlyph() for relevant glyphs: + EasyBind.includeWhen(iconView.getStyleClass(), "glyph-icon-muted", iconView.glyphProperty().isEqualTo(FontAwesome5Icon.INFO_CIRCLE)); + EasyBind.includeWhen(iconView.getStyleClass(), "glyph-icon-primary", iconView.glyphProperty().isEqualTo(FontAwesome5Icon.CHECK)); + EasyBind.includeWhen(iconView.getStyleClass(), "glyph-icon-orange", iconView.glyphProperty().isEqualTo(FontAwesome5Icon.EXCLAMATION_TRIANGLE)); + EasyBind.includeWhen(iconView.getStyleClass(), "glyph-icon-red", iconView.glyphProperty().isEqualTo(FontAwesome5Icon.TIMES)); + } + + @FXML + public void fix() { + Result r = result.get(); + if (r != null) { + fixApplier.fix(r).whenCompleteAsync(this::fixFinished, Platform::runLater); } } + + private void fixFinished(Void unused, Throwable exception) { + if (exception != null) { + LOG.error("Failed to apply fix", exception); + // TODO ... + } + } + + /* Getter & Setter */ - - public DiagnosticResult getResult() { + public Result getResult() { return result.get(); } - public void setResult(DiagnosticResult result) { + public void setResult(Result result) { this.result.set(result); } - public ObjectProperty resultProperty() { + public ObjectProperty resultProperty() { return result; } @@ -86,7 +91,49 @@ public class ResultListCellController implements FxController { return description.getValue(); } + public ObjectBinding glyphProperty() { + return glyph; + } + + public FontAwesome5Icon getGlyph() { + var r = result.get(); + if (r == null) { + return null; + } + return switch (r.diagnosis().getSeverity()) { + case INFO -> FontAwesome5Icon.INFO_CIRCLE; + case GOOD -> FontAwesome5Icon.CHECK; + case WARN -> FontAwesome5Icon.EXCLAMATION_TRIANGLE; + case CRITICAL -> FontAwesome5Icon.TIMES; + }; + } + public Binding descriptionProperty() { return description; } + + public BooleanBinding fixableProperty() { + return fixable; + } + + public boolean isFixable() { + return fixState.get().map(Result.FixState.FIXABLE::equals).orElse(false); + } + + public BooleanBinding fixingProperty() { + return fixing; + } + + public boolean isFixing() { + return fixState.get().map(Result.FixState.FIXING::equals).orElse(false); + } + + public BooleanBinding fixedProperty() { + return fixed; + } + + public boolean isFixed() { + return fixState.get().map(Result.FixState.FIXED::equals).orElse(false); + } + } diff --git a/src/main/java/org/cryptomator/ui/health/ResultListCellFactory.java b/src/main/java/org/cryptomator/ui/health/ResultListCellFactory.java index 7acada487..86c793bf7 100644 --- a/src/main/java/org/cryptomator/ui/health/ResultListCellFactory.java +++ b/src/main/java/org/cryptomator/ui/health/ResultListCellFactory.java @@ -1,7 +1,6 @@ package org.cryptomator.ui.health; -import org.cryptomator.cryptofs.health.api.DiagnosticResult; import org.cryptomator.ui.common.FxmlLoaderFactory; import javax.inject.Inject; @@ -15,7 +14,7 @@ import java.io.IOException; import java.io.UncheckedIOException; @HealthCheckScoped -public class ResultListCellFactory implements Callback, ListCell> { +public class ResultListCellFactory implements Callback, ListCell> { private final FxmlLoaderFactory fxmlLoaders; @@ -25,7 +24,7 @@ public class ResultListCellFactory implements Callback call(ListView param) { + public ListCell call(ListView param) { try { FXMLLoader fxmlLoader = fxmlLoaders.load("/fxml/health_result_listcell.fxml"); return new ResultListCellFactory.Cell(fxmlLoader.getRoot(), fxmlLoader.getController()); @@ -34,7 +33,7 @@ public class ResultListCellFactory implements Callback { + private static class Cell extends ListCell { private final Parent node; private final ResultListCellController controller; @@ -45,7 +44,7 @@ public class ResultListCellFactory implements Callback unverifiedVaultConfig; + private final CompletableFuture unverifiedVaultConfig; private final KeyLoadingStrategy keyLoadingStrategy; private final ExecutorService executor; private final AtomicReference masterkeyRef; private final AtomicReference vaultConfigRef; private final Lazy checkScene; private final Lazy errorComponent; + private final ObjectProperty state = new SimpleObjectProperty<>(State.LOADING); + private final BooleanBinding loading = state.isEqualTo(State.LOADING); + private final BooleanBinding failed = state.isEqualTo(State.FAILED); + private final BooleanBinding loaded = state.isEqualTo(State.LOADED); + + public enum State { + LOADING, + FAILED, + LOADED + } /* FXML */ @Inject public StartController(@HealthCheckWindow Vault vault, @HealthCheckWindow Stage window, @HealthCheckWindow KeyLoadingStrategy keyLoadingStrategy, ExecutorService executor, AtomicReference masterkeyRef, AtomicReference vaultConfigRef, @FxmlScene(FxmlFile.HEALTH_CHECK_LIST) Lazy checkScene, Lazy errorComponent) { + this.vault = vault; this.window = window; + this.unverifiedVaultConfig = CompletableFuture.supplyAsync(this::loadConfig, executor); this.keyLoadingStrategy = keyLoadingStrategy; this.executor = executor; this.masterkeyRef = masterkeyRef; this.vaultConfigRef = vaultConfigRef; this.checkScene = checkScene; this.errorComponent = errorComponent; - - //TODO: this is ugly - //idea: delay the loading of the vault config and show a spinner (something like "check/load config") and react to the result of the loading - //or: load vault config in a previous step to see if it is loadable. - VaultConfig.UnverifiedVaultConfig tmp; - try { - tmp = vault.getUnverifiedVaultConfig(); - } catch (IOException e) { - e.printStackTrace(); - tmp = null; - } - this.unverifiedVaultConfig = Optional.ofNullable(tmp); + this.unverifiedVaultConfig.whenCompleteAsync(this::loadedConfig, Platform::runLater); } @FXML @@ -74,41 +81,64 @@ public class StartController implements FxController { @FXML public void next() { LOG.trace("StartController.next()"); - executor.submit(this::loadKey); + CompletableFuture.runAsync(this::loadKey, executor).whenCompleteAsync(this::loadedKey, Platform::runLater); + } + + private VaultConfig.UnverifiedVaultConfig loadConfig() { + assert !Platform.isFxApplicationThread(); + try { + return this.vault.getUnverifiedVaultConfig(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + private void loadedConfig(VaultConfig.UnverifiedVaultConfig cfg, Throwable exception) { + assert Platform.isFxApplicationThread(); + if (exception != null) { + state.set(State.FAILED); + } else { + assert cfg != null; + state.set(State.LOADED); + } } private void loadKey() { assert !Platform.isFxApplicationThread(); - assert unverifiedVaultConfig.isPresent(); - try (var masterkey = keyLoadingStrategy.loadKey(unverifiedVaultConfig.orElseThrow().getKeyId())) { - var unverifiedCfg = unverifiedVaultConfig.get(); + assert unverifiedVaultConfig.isDone(); + var unverifiedCfg = unverifiedVaultConfig.join(); + try (var masterkey = keyLoadingStrategy.loadKey(unverifiedCfg.getKeyId())) { var verifiedCfg = unverifiedCfg.verify(masterkey.getEncoded(), unverifiedCfg.allegedVaultVersion()); vaultConfigRef.set(verifiedCfg); var old = masterkeyRef.getAndSet(masterkey.clone()); if (old != null) { old.destroy(); } - Platform.runLater(this::loadedKey); } catch (MasterkeyLoadingFailedException e) { if (keyLoadingStrategy.recoverFromException(e)) { // retry loadKey(); } else { - Platform.runLater(() -> loadingKeyFailed(e)); + throw new LoadingFailedException(e); } - } catch (VaultKeyInvalidException e) { - Platform.runLater(() -> loadingKeyFailed(e)); } catch (VaultConfigLoadException e) { - Platform.runLater(() -> loadingKeyFailed(e)); + throw new LoadingFailedException(e); } } - private void loadedKey() { - LOG.debug("Loaded valid key"); - window.setScene(checkScene.get()); + private void loadedKey(Void unused, Throwable exception) { + assert Platform.isFxApplicationThread(); + if (exception instanceof LoadingFailedException) { + loadingKeyFailed(exception.getCause()); + } else if (exception != null) { + loadingKeyFailed(exception); + } else { + LOG.debug("Loaded valid key"); + window.setScene(checkScene.get()); + } } - private void loadingKeyFailed(Exception e) { + private void loadingKeyFailed(Throwable e) { if (e instanceof UnlockCancelledException) { // ok } else if (e instanceof VaultKeyInvalidException) { @@ -120,8 +150,37 @@ public class StartController implements FxController { } } - public boolean isInvalidConfig() { - return unverifiedVaultConfig.isEmpty(); + /* Getter */ + + public BooleanBinding loadingProperty() { + return loading; } + public boolean isLoading() { + return loading.get(); + } + + public BooleanBinding failedProperty() { + return failed; + } + + public boolean isFailed() { + return failed.get(); + } + + public BooleanBinding loadedProperty() { + return loaded; + } + + public boolean isLoaded() { + return loaded.get(); + } + + /* internal types */ + + private static class LoadingFailedException extends CompletionException { + LoadingFailedException(Throwable cause) { + super(cause); + } + } } diff --git a/src/main/java/org/cryptomator/ui/vaultoptions/AutoLockVaultOptionsController.java b/src/main/java/org/cryptomator/ui/vaultoptions/AutoLockVaultOptionsController.java deleted file mode 100644 index 1bb74690f..000000000 --- a/src/main/java/org/cryptomator/ui/vaultoptions/AutoLockVaultOptionsController.java +++ /dev/null @@ -1,51 +0,0 @@ -package org.cryptomator.ui.vaultoptions; - -import org.cryptomator.common.vaults.Vault; -import org.cryptomator.ui.common.FxController; -import org.cryptomator.ui.controls.NumericTextField; - -import javax.inject.Inject; -import javafx.beans.binding.Bindings; -import javafx.fxml.FXML; -import javafx.scene.control.CheckBox; -import javafx.util.StringConverter; - -@VaultOptionsScoped -public class AutoLockVaultOptionsController implements FxController { - - private final Vault vault; - - public CheckBox lockAfterTimeCheckbox; - public NumericTextField lockTimeInMinutesTextField; - - @Inject - AutoLockVaultOptionsController(@VaultOptionsWindow Vault vault) { - this.vault = vault; - } - - @FXML - public void initialize() { - lockAfterTimeCheckbox.selectedProperty().bindBidirectional(vault.getVaultSettings().autoLockWhenIdle()); - Bindings.bindBidirectional(lockTimeInMinutesTextField.textProperty(), vault.getVaultSettings().autoLockIdleSeconds(), new IdleTimeSecondsConverter()); - } - - private static class IdleTimeSecondsConverter extends StringConverter { - - @Override - public String toString(Number seconds) { - int minutes = seconds.intValue() / 60; // int-truncate - return Integer.toString(minutes); - } - - @Override - public Number fromString(String string) { - try { - int minutes = Integer.valueOf(string); - return minutes * 60; - } catch (NumberFormatException e) { - return 0; - } - } - } - -} diff --git a/src/main/java/org/cryptomator/ui/vaultoptions/GeneralVaultOptionsController.java b/src/main/java/org/cryptomator/ui/vaultoptions/GeneralVaultOptionsController.java index 79d3b53ad..0ccea096e 100644 --- a/src/main/java/org/cryptomator/ui/vaultoptions/GeneralVaultOptionsController.java +++ b/src/main/java/org/cryptomator/ui/vaultoptions/GeneralVaultOptionsController.java @@ -3,10 +3,11 @@ package org.cryptomator.ui.vaultoptions; import org.cryptomator.common.settings.WhenUnlocked; import org.cryptomator.common.vaults.Vault; import org.cryptomator.ui.common.FxController; -import org.cryptomator.ui.health.HealthCheckComponent; +import org.cryptomator.ui.controls.NumericTextField; import javax.inject.Inject; import javafx.beans.Observable; +import javafx.beans.binding.Bindings; import javafx.fxml.FXML; import javafx.scene.control.CheckBox; import javafx.scene.control.ChoiceBox; @@ -23,18 +24,18 @@ public class GeneralVaultOptionsController implements FxController { private final Stage window; private final Vault vault; - private final HealthCheckComponent.Builder healthCheckWindow; private final ResourceBundle resourceBundle; public TextField vaultName; public CheckBox unlockOnStartupCheckbox; public ChoiceBox actionAfterUnlockChoiceBox; + public CheckBox lockAfterTimeCheckbox; + public NumericTextField lockTimeInMinutesTextField; @Inject - GeneralVaultOptionsController(@VaultOptionsWindow Stage window, @VaultOptionsWindow Vault vault, HealthCheckComponent.Builder healthCheckWindow, ResourceBundle resourceBundle) { + GeneralVaultOptionsController(@VaultOptionsWindow Stage window, @VaultOptionsWindow Vault vault, ResourceBundle resourceBundle) { this.window = window; this.vault = vault; - this.healthCheckWindow = healthCheckWindow; this.resourceBundle = resourceBundle; } @@ -47,6 +48,8 @@ public class GeneralVaultOptionsController implements FxController { actionAfterUnlockChoiceBox.getItems().addAll(WhenUnlocked.values()); actionAfterUnlockChoiceBox.valueProperty().bindBidirectional(vault.getVaultSettings().actionAfterUnlock()); actionAfterUnlockChoiceBox.setConverter(new WhenUnlockedConverter(resourceBundle)); + lockAfterTimeCheckbox.selectedProperty().bindBidirectional(vault.getVaultSettings().autoLockWhenIdle()); + Bindings.bindBidirectional(lockTimeInMinutesTextField.textProperty(), vault.getVaultSettings().autoLockIdleSeconds(), new IdleTimeSecondsConverter()); } private void trimVaultNameOnFocusLoss(Observable observable, Boolean wasFocussed, Boolean isFocussed) { @@ -64,12 +67,6 @@ public class GeneralVaultOptionsController implements FxController { } } - @FXML - public void showHealthCheck() { - healthCheckWindow.vault(vault).build().showHealthCheckWindow(); - } - - private static class WhenUnlockedConverter extends StringConverter { private final ResourceBundle resourceBundle; @@ -89,4 +86,22 @@ public class GeneralVaultOptionsController implements FxController { } } + private static class IdleTimeSecondsConverter extends StringConverter { + + @Override + public String toString(Number seconds) { + int minutes = seconds.intValue() / 60; // int-truncate + return Integer.toString(minutes); + } + + @Override + public Number fromString(String string) { + try { + int minutes = Integer.valueOf(string); + return minutes * 60; + } catch (NumberFormatException e) { + return 0; + } + } + } } diff --git a/src/main/java/org/cryptomator/ui/vaultoptions/HealthVaultOptionsController.java b/src/main/java/org/cryptomator/ui/vaultoptions/HealthVaultOptionsController.java new file mode 100644 index 000000000..7b0842e97 --- /dev/null +++ b/src/main/java/org/cryptomator/ui/vaultoptions/HealthVaultOptionsController.java @@ -0,0 +1,30 @@ +package org.cryptomator.ui.vaultoptions; + +import org.cryptomator.common.vaults.Vault; +import org.cryptomator.ui.common.FxController; +import org.cryptomator.ui.health.HealthCheckComponent; + +import javax.inject.Inject; +import javafx.event.ActionEvent; +import javafx.fxml.FXML; +import javafx.stage.Stage; + +@VaultOptionsScoped +public class HealthVaultOptionsController implements FxController { + + private final Stage window; + private final Vault vault; + private final HealthCheckComponent.Builder healthCheckWindow; + + @Inject + public HealthVaultOptionsController(@VaultOptionsWindow Stage window, @VaultOptionsWindow Vault vault, HealthCheckComponent.Builder healthCheckWindow) { + this.window = window; + this.vault = vault; + this.healthCheckWindow = healthCheckWindow; + } + + @FXML + public void startHealthCheck(ActionEvent event) { + healthCheckWindow.vault(vault).windowToClose(window).build().showHealthCheckWindow(); + } +} diff --git a/src/main/java/org/cryptomator/ui/vaultoptions/SelectedVaultOptionsTab.java b/src/main/java/org/cryptomator/ui/vaultoptions/SelectedVaultOptionsTab.java index 03a4922d4..bfaff147a 100644 --- a/src/main/java/org/cryptomator/ui/vaultoptions/SelectedVaultOptionsTab.java +++ b/src/main/java/org/cryptomator/ui/vaultoptions/SelectedVaultOptionsTab.java @@ -23,7 +23,11 @@ public enum SelectedVaultOptionsTab { /** * Show Auto-Lock tab - * */ AUTOLOCK, + + /** + * Show health tab + */ + HEALTH; } diff --git a/src/main/java/org/cryptomator/ui/vaultoptions/VaultOptionsController.java b/src/main/java/org/cryptomator/ui/vaultoptions/VaultOptionsController.java index 20dac7594..662232a49 100644 --- a/src/main/java/org/cryptomator/ui/vaultoptions/VaultOptionsController.java +++ b/src/main/java/org/cryptomator/ui/vaultoptions/VaultOptionsController.java @@ -24,6 +24,7 @@ public class VaultOptionsController implements FxController { public Tab mountTab; public Tab keyTab; public Tab autoLockTab; + public Tab healthTab; @Inject VaultOptionsController(@VaultOptionsWindow Stage window, ObjectProperty selectedTabProperty) { @@ -49,6 +50,7 @@ public class VaultOptionsController implements FxController { case MOUNT -> mountTab; case KEY -> keyTab; case AUTOLOCK -> autoLockTab; + case HEALTH -> healthTab; }; } diff --git a/src/main/java/org/cryptomator/ui/vaultoptions/VaultOptionsModule.java b/src/main/java/org/cryptomator/ui/vaultoptions/VaultOptionsModule.java index 89cef234b..2de9421a0 100644 --- a/src/main/java/org/cryptomator/ui/vaultoptions/VaultOptionsModule.java +++ b/src/main/java/org/cryptomator/ui/vaultoptions/VaultOptionsModule.java @@ -86,7 +86,6 @@ abstract class VaultOptionsModule { @Binds @IntoMap - @FxControllerKey(AutoLockVaultOptionsController.class) - abstract FxController bindAutoLockVaultOptionsController(AutoLockVaultOptionsController controller); - + @FxControllerKey(HealthVaultOptionsController.class) + abstract FxController bindHealthOptionsController(HealthVaultOptionsController controller); } diff --git a/src/main/resources/META-INF/services/org.cryptomator.cryptofs.health.api.HealthCheck b/src/main/resources/META-INF/services/org.cryptomator.cryptofs.health.api.HealthCheck new file mode 100644 index 000000000..e78e709b4 --- /dev/null +++ b/src/main/resources/META-INF/services/org.cryptomator.cryptofs.health.api.HealthCheck @@ -0,0 +1,3 @@ +org.cryptomator.ui.health.DummyHealthChecks$DummyCheck1 +org.cryptomator.ui.health.DummyHealthChecks$DummyCheck2 +org.cryptomator.ui.health.DummyHealthChecks$DummyCheck3 \ No newline at end of file diff --git a/src/main/resources/fxml/health_check_list.fxml b/src/main/resources/fxml/health_check_list.fxml index 407627891..91cd02975 100644 --- a/src/main/resources/fxml/health_check_list.fxml +++ b/src/main/resources/fxml/health_check_list.fxml @@ -3,17 +3,18 @@ + + - - @@ -28,7 +29,7 @@ - + @@ -39,7 +40,7 @@