diff --git a/.github/workflows/mac-dmg-x64.yml b/.github/workflows/mac-dmg-x64.yml
index 6a9c3f644..0a2e51037 100644
--- a/.github/workflows/mac-dmg-x64.yml
+++ b/.github/workflows/mac-dmg-x64.yml
@@ -136,6 +136,7 @@ jobs:
--java-options "-Dcryptomator.integrationsMac.keychainServiceName=\"Cryptomator\""
--java-options "-Dcryptomator.mountPointsDir=\"@{userhome}/Library/Application Support/Cryptomator/mnt\""
--java-options "-Dcryptomator.showTrayIcon=true"
+ --java-options "-Dcryptomator.updateMechanism=org.cryptomator.macos.update.DmgUpdateMechanism"
--java-options "-Dcryptomator.buildNumber=\"dmg-${{ needs.get-version.outputs.revNum }}\""
--mac-package-identifier org.cryptomator
--resource-dir dist/mac/resources
diff --git a/.github/workflows/mac-dmg.yml b/.github/workflows/mac-dmg.yml
index 4f9ff6e71..dcc6bf749 100644
--- a/.github/workflows/mac-dmg.yml
+++ b/.github/workflows/mac-dmg.yml
@@ -134,6 +134,7 @@ jobs:
--java-options "-Dcryptomator.integrationsMac.keychainServiceName=\"Cryptomator\""
--java-options "-Dcryptomator.mountPointsDir=\"@{userhome}/Library/Application Support/Cryptomator/mnt\""
--java-options "-Dcryptomator.showTrayIcon=true"
+ --java-options "-Dcryptomator.updateMechanism=org.cryptomator.macos.update.DmgUpdateMechanism"
--java-options "-Dcryptomator.buildNumber=\"dmg-${{ needs.get-version.outputs.revNum }}\""
--java-options "-XX:ErrorFile=/cryptomator/cryptomator_crash.log"
--mac-package-identifier org.cryptomator
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
index 0634be039..31b119b32 100644
--- a/.idea/inspectionProfiles/Project_Default.xml
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -1,10 +1,8 @@
-
-
-
-
-
+
+
+
\ No newline at end of file
diff --git a/.idea/runConfigurations/Cryptomator_macOS.xml b/.idea/runConfigurations/Cryptomator_macOS.xml
index c777434a2..b1fd85746 100644
--- a/.idea/runConfigurations/Cryptomator_macOS.xml
+++ b/.idea/runConfigurations/Cryptomator_macOS.xml
@@ -5,7 +5,7 @@
-
+
diff --git a/dist/mac/dmg/build.sh b/dist/mac/dmg/build.sh
index 26673589b..fbe81931c 100755
--- a/dist/mac/dmg/build.sh
+++ b/dist/mac/dmg/build.sh
@@ -123,6 +123,7 @@ ${JAVA_HOME}/bin/jpackage \
--java-options "-Dcryptomator.integrationsMac.keychainServiceName=\"${APP_NAME}\"" \
--java-options "-Dcryptomator.mountPointsDir=\"@{userhome}/Library/Application Support${APP_NAME}/mnt\"" \
--java-options "-Dcryptomator.showTrayIcon=true" \
+ --java-options "-Dcryptomator.updateMechanism=org.cryptomator.macos.update.DmgUpdateMechanism" \
--java-options "-Dcryptomator.buildNumber=\"dmg-${REVISION_NO}\"" \
--mac-package-identifier ${PACKAGE_IDENTIFIER} \
--resource-dir ../resources
diff --git a/pom.xml b/pom.xml
index 1fc84d1f3..24f35de0a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -34,10 +34,10 @@
2.9.0
- 1.7.0
+ 1.8.0-beta1
1.5.1
- 1.4.1
- 1.6.1
+ 1.5.0-beta1
+ 1.7.0-beta1
5.1.0
3.0.0
@@ -75,6 +75,20 @@
+
+
+ Central Portal Snapshots
+ central-portal-snapshots
+ https://central.sonatype.com/repository/maven-snapshots/
+
+ false
+
+
+ true
+
+
+
+
diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java
index 459d3c52d..f1f2aa5c6 100644
--- a/src/main/java/module-info.java
+++ b/src/main/java/module-info.java
@@ -50,12 +50,12 @@ open module org.cryptomator.desktop {
requires io.github.coffeelibs.tinyoauth2client;
requires org.slf4j;
requires org.apache.commons.lang3;
+ requires com.github.benmanes.caffeine;
/* dagger bs */
requires jakarta.inject;
requires static javax.inject;
requires java.compiler;
- requires com.github.benmanes.caffeine;
uses org.cryptomator.common.locationpresets.LocationPresetsProvider;
uses SSLContextProvider;
diff --git a/src/main/java/org/cryptomator/common/CommonsModule.java b/src/main/java/org/cryptomator/common/CommonsModule.java
index a1e3c0950..739c358a9 100644
--- a/src/main/java/org/cryptomator/common/CommonsModule.java
+++ b/src/main/java/org/cryptomator/common/CommonsModule.java
@@ -74,13 +74,6 @@ public abstract class CommonsModule {
return new MasterkeyFileAccess(Constants.PEPPER, csprng);
}
- @Provides
- @Singleton
- @Named("SemVer")
- static Comparator providesSemVerComparator() {
- return new SemVerComparator();
- }
-
@Provides
@Singleton
static Optional provideRevealPathService() {
diff --git a/src/main/java/org/cryptomator/common/Environment.java b/src/main/java/org/cryptomator/common/Environment.java
index 39214e626..0a71b28a9 100644
--- a/src/main/java/org/cryptomator/common/Environment.java
+++ b/src/main/java/org/cryptomator/common/Environment.java
@@ -124,6 +124,15 @@ public class Environment {
return Optional.ofNullable(System.getProperty(BUILD_NUMBER_PROP_NAME));
}
+ /**
+ * Returns the app version concatenated with the build number (if defined).
+ *
+ * @return version string formatted like {@code 1.2.3-4567} or {@code 1.2.3} if no build number is defined.
+ */
+ public String getAppVersionWithBuildNumber() {
+ return getAppVersion() + getBuildNumber().map("-"::concat).orElse("");
+ }
+
public Optional getPluginDir() {
return getPath(PLUGIN_DIR_PROP_NAME);
}
diff --git a/src/main/java/org/cryptomator/common/SemVerComparator.java b/src/main/java/org/cryptomator/common/SemVerComparator.java
deleted file mode 100644
index 0f9148bd5..000000000
--- a/src/main/java/org/cryptomator/common/SemVerComparator.java
+++ /dev/null
@@ -1,81 +0,0 @@
-/*******************************************************************************
- * Copyright (c) 2016, 2017 Sebastian Stenzel and others.
- * All rights reserved.
- * This program and the accompanying materials are made available under the terms of the accompanying LICENSE file.
- *
- * Contributors:
- * Sebastian Stenzel - initial API and implementation
- *******************************************************************************/
-package org.cryptomator.common;
-
-import org.apache.commons.lang3.StringUtils;
-
-import java.util.Comparator;
-
-/**
- * Compares version strings according to SemVer 2.0.0.
- */
-public class SemVerComparator implements Comparator {
-
- private static final char VERSION_SEP = '.'; // http://semver.org/spec/v2.0.0.html#spec-item-2
- private static final String PRE_RELEASE_SEP = "-"; // http://semver.org/spec/v2.0.0.html#spec-item-9
- private static final String BUILD_SEP = "+"; // http://semver.org/spec/v2.0.0.html#spec-item-10
-
- @Override
- public int compare(String version1, String version2) {
- // "Build metadata SHOULD be ignored when determining version precedence.
- // Thus two versions that differ only in the build metadata, have the same precedence."
- String v1WithoutBuildMetadata = StringUtils.substringBefore(version1, BUILD_SEP);
- String v2WithoutBuildMetadata = StringUtils.substringBefore(version2, BUILD_SEP);
-
- if (v1WithoutBuildMetadata.equals(v2WithoutBuildMetadata)) {
- return 0;
- }
-
- String v1MajorMinorPatch = StringUtils.substringBefore(v1WithoutBuildMetadata, PRE_RELEASE_SEP);
- String v2MajorMinorPatch = StringUtils.substringBefore(v2WithoutBuildMetadata, PRE_RELEASE_SEP);
- String v1PreReleaseVersion = StringUtils.substringAfter(v1WithoutBuildMetadata, PRE_RELEASE_SEP);
- String v2PreReleaseVersion = StringUtils.substringAfter(v2WithoutBuildMetadata, PRE_RELEASE_SEP);
- return compare(v1MajorMinorPatch, v1PreReleaseVersion, v2MajorMinorPatch, v2PreReleaseVersion);
- }
-
- private int compare(String v1MajorMinorPatch, String v1PreReleaseVersion, String v2MajorMinorPatch, String v2PreReleaseVersion) {
- int comparisonResult = compareNumericallyThenLexicographically(v1MajorMinorPatch, v2MajorMinorPatch);
- if (comparisonResult == 0) {
- if (v1PreReleaseVersion.isEmpty()) {
- return 1; // 1.0.0 > 1.0.0-BETA
- } else if (v2PreReleaseVersion.isEmpty()) {
- return -1; // 1.0.0-BETA < 1.0.0
- } else {
- return compareNumericallyThenLexicographically(v1PreReleaseVersion, v2PreReleaseVersion);
- }
- } else {
- return comparisonResult;
- }
- }
-
- private int compareNumericallyThenLexicographically(String version1, String version2) {
- final String[] vComps1 = StringUtils.split(version1, VERSION_SEP);
- final String[] vComps2 = StringUtils.split(version2, VERSION_SEP);
- final int commonCompCount = Math.min(vComps1.length, vComps2.length);
-
- for (int i = 0; i < commonCompCount; i++) {
- int subversionComparisonResult = 0;
- try {
- final int v1 = Integer.parseInt(vComps1[i]);
- final int v2 = Integer.parseInt(vComps2[i]);
- subversionComparisonResult = v1 - v2;
- } catch (NumberFormatException ex) {
- // ok, lets compare this fragment lexicographically
- subversionComparisonResult = vComps1[i].compareTo(vComps2[i]);
- }
- if (subversionComparisonResult != 0) {
- return subversionComparisonResult;
- }
- }
-
- // all in common so far? longest version string is considered the higher version:
- return vComps1.length - vComps2.length;
- }
-
-}
\ No newline at end of file
diff --git a/src/main/java/org/cryptomator/common/settings/Settings.java b/src/main/java/org/cryptomator/common/settings/Settings.java
index a711e6536..90aedeeda 100644
--- a/src/main/java/org/cryptomator/common/settings/Settings.java
+++ b/src/main/java/org/cryptomator/common/settings/Settings.java
@@ -25,10 +25,8 @@ import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.NodeOrientation;
-
import java.nio.file.Path;
import java.time.Instant;
-import java.util.function.Consumer;
public class Settings {
@@ -53,6 +51,7 @@ public class Settings {
static final String DEFAULT_USER_INTERFACE_ORIENTATION = NodeOrientation.LEFT_TO_RIGHT.name();
public static final Instant DEFAULT_TIMESTAMP = Instant.parse("2000-01-01T00:00:00Z");
+ private final SettingsProvider provider;
public final ObservableList directories;
public final BooleanProperty startHidden;
public final BooleanProperty autoCloseVaults;
@@ -78,13 +77,12 @@ public class Settings {
public final ObjectProperty lastUpdateCheckReminder;
public final ObjectProperty lastSuccessfulUpdateCheck;
public final ObjectProperty previouslyUsedVaultDirectory;
+ public final StringProperty lastUpdateAttemptedByVersion;
- private Consumer saveCmd;
-
- public static Settings create(Environment env) {
+ public static Settings create(SettingsProvider provider, Environment env) {
var defaults = new SettingsJson();
defaults.showTrayIcon = env.showTrayIcon();
- return new Settings(defaults);
+ return new Settings(provider, defaults);
}
/**
@@ -92,7 +90,8 @@ public class Settings {
*
* @param json The parsed settings.json
*/
- Settings(SettingsJson json) {
+ Settings(SettingsProvider provider, SettingsJson json) {
+ this.provider = provider;
this.directories = FXCollections.observableArrayList(VaultSettings::observables);
this.startHidden = new SimpleBooleanProperty(this, "startHidden", json.startHidden);
this.autoCloseVaults = new SimpleBooleanProperty(this, "autoCloseVaults", json.autoCloseVaults);
@@ -118,6 +117,7 @@ public class Settings {
this.lastUpdateCheckReminder = new SimpleObjectProperty<>(this, "lastUpdateCheckReminder", json.lastReminderForUpdateCheck);
this.lastSuccessfulUpdateCheck = new SimpleObjectProperty<>(this, "lastSuccessfulUpdateCheck", json.lastSuccessfulUpdateCheck);
this.previouslyUsedVaultDirectory = new SimpleObjectProperty<>(this, "previouslyUsedVaultDirectory", json.previouslyUsedVaultDirectory);
+ this.lastUpdateAttemptedByVersion = new SimpleStringProperty(this, "lastUpdateAttemptedByVersion", json.lastUpdateAttemptedByVersion);
this.directories.addAll(json.directories.stream().map(VaultSettings::new).toList());
@@ -148,6 +148,7 @@ public class Settings {
lastUpdateCheckReminder.addListener(this::somethingChanged);
lastSuccessfulUpdateCheck.addListener(this::somethingChanged);
previouslyUsedVaultDirectory.addListener(this::somethingChanged);
+ lastUpdateAttemptedByVersion.addListener(this::somethingChanged);
}
@SuppressWarnings("deprecation")
@@ -210,6 +211,7 @@ public class Settings {
json.lastReminderForUpdateCheck = lastUpdateCheckReminder.get();
json.lastSuccessfulUpdateCheck = lastSuccessfulUpdateCheck.get();
json.previouslyUsedVaultDirectory = previouslyUsedVaultDirectory.get();
+ json.lastUpdateAttemptedByVersion = lastUpdateAttemptedByVersion.get();
return json;
}
@@ -222,20 +224,12 @@ public class Settings {
}
}
-
- // TODO rename to setChangeListener
- void setSaveCmd(Consumer saveCmd) {
- this.saveCmd = saveCmd;
- }
-
private void somethingChanged(@SuppressWarnings("unused") Observable observable) {
- this.save();
+ provider.scheduleSave(this);
}
- void save() {
- if (saveCmd != null) {
- saveCmd.accept(this);
- }
+ public void saveNow() {
+ provider.saveNow(this);
}
}
diff --git a/src/main/java/org/cryptomator/common/settings/SettingsJson.java b/src/main/java/org/cryptomator/common/settings/SettingsJson.java
index feb8a0bf2..e0cdb7b5e 100644
--- a/src/main/java/org/cryptomator/common/settings/SettingsJson.java
+++ b/src/main/java/org/cryptomator/common/settings/SettingsJson.java
@@ -96,4 +96,7 @@ class SettingsJson {
@JsonProperty("previouslyUsedVaultDirectory")
Path previouslyUsedVaultDirectory;
+
+ @JsonProperty("lastUpdateAttemptedByVersion")
+ String lastUpdateAttemptedByVersion;
}
diff --git a/src/main/java/org/cryptomator/common/settings/SettingsProvider.java b/src/main/java/org/cryptomator/common/settings/SettingsProvider.java
index d9fab7108..0a4a0f190 100644
--- a/src/main/java/org/cryptomator/common/settings/SettingsProvider.java
+++ b/src/main/java/org/cryptomator/common/settings/SettingsProvider.java
@@ -26,7 +26,9 @@ import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
-import java.util.Optional;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
@@ -61,8 +63,7 @@ public class SettingsProvider implements Supplier {
Settings settings = env.getSettingsPath() //
.flatMap(this::tryLoad) //
.findFirst() //
- .orElseGet(() -> Settings.create(env));
- settings.setSaveCmd(this::scheduleSave);
+ .orElseGet(() -> Settings.create(this, env));
return settings;
}
@@ -71,7 +72,7 @@ public class SettingsProvider implements Supplier {
try (InputStream in = Files.newInputStream(path, StandardOpenOption.READ)) {
var json = JSON.reader().readValue(in, SettingsJson.class);
LOG.info("Settings loaded from {}", path);
- var settings = new Settings(json);
+ var settings = new Settings(this, json);
return Stream.of(settings);
} catch (JacksonException e) {
LOG.warn("Failed to parse json file {}", path, e);
@@ -84,19 +85,33 @@ public class SettingsProvider implements Supplier {
}
}
- private void scheduleSave(Settings settings) {
- if (settings == null) {
- return;
+ void saveNow(Settings settings) {
+ try {
+ scheduleSave(settings, 0L).get();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ LOG.error("Saving settings was interrupted.", e);
+ } catch (ExecutionException e) {
+ LOG.error("Unexpected exception while saving.", e);
}
- final Optional settingsPath = env.getSettingsPath().findFirst(); // always save to preferred (first) path
- settingsPath.ifPresent(path -> {
- Runnable saveCommand = () -> this.save(settings, path);
- ScheduledFuture> scheduledTask = scheduler.schedule(saveCommand, SAVE_DELAY_MS, TimeUnit.MILLISECONDS);
- ScheduledFuture> previouslyScheduledTask = scheduledSaveCmd.getAndSet(scheduledTask);
- if (previouslyScheduledTask != null) {
- previouslyScheduledTask.cancel(false);
- }
- });
+ }
+
+ void scheduleSave(Settings settings) {
+ scheduleSave(settings, SAVE_DELAY_MS);
+ }
+
+ private Future> scheduleSave(Settings settings, long delayMillis) {
+ if (settings == null) {
+ return CompletableFuture.completedFuture(null);
+ }
+ final Path settingsPath = env.getSettingsPath().findFirst().orElseThrow(); // always save to preferred (first) path
+ Runnable saveCommand = () -> this.save(settings, settingsPath);
+ ScheduledFuture> scheduledTask = scheduler.schedule(saveCommand, delayMillis, TimeUnit.MILLISECONDS);
+ ScheduledFuture> previouslyScheduledTask = scheduledSaveCmd.getAndSet(scheduledTask);
+ if (previouslyScheduledTask != null) {
+ previouslyScheduledTask.cancel(false);
+ }
+ return scheduledTask;
}
private void save(Settings settings, Path settingsPath) {
@@ -107,7 +122,7 @@ public class SettingsProvider implements Supplier {
Path tmpPath = settingsPath.resolveSibling(settingsPath.getFileName().toString() + ".tmp");
try (OutputStream out = Files.newOutputStream(tmpPath, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE)) {
var jsonObj = settings.serialized();
- jsonObj.writtenByVersion = env.getAppVersion() + env.getBuildNumber().map("-"::concat).orElse("");
+ jsonObj.writtenByVersion = env.getAppVersionWithBuildNumber();
JSON.writerWithDefaultPrettyPrinter().writeValue(out, jsonObj);
}
Files.move(tmpPath, settingsPath, StandardCopyOption.REPLACE_EXISTING);
diff --git a/src/main/java/org/cryptomator/ui/fxapp/FxApplicationModule.java b/src/main/java/org/cryptomator/ui/fxapp/FxApplicationModule.java
index 74abac546..70319df5b 100644
--- a/src/main/java/org/cryptomator/ui/fxapp/FxApplicationModule.java
+++ b/src/main/java/org/cryptomator/ui/fxapp/FxApplicationModule.java
@@ -26,7 +26,7 @@ import javafx.scene.image.Image;
import java.io.IOException;
import java.io.InputStream;
-@Module(includes = {UpdateCheckerModule.class}, subcomponents = {TrayMenuComponent.class, //
+@Module(subcomponents = {TrayMenuComponent.class, //
DecryptNameComponent.class, //
MainWindowComponent.class, //
PreferencesComponent.class, //
diff --git a/src/main/java/org/cryptomator/ui/fxapp/UpdateChecker.java b/src/main/java/org/cryptomator/ui/fxapp/UpdateChecker.java
deleted file mode 100644
index b857adcae..000000000
--- a/src/main/java/org/cryptomator/ui/fxapp/UpdateChecker.java
+++ /dev/null
@@ -1,135 +0,0 @@
-package org.cryptomator.ui.fxapp;
-
-import org.cryptomator.common.Environment;
-import org.cryptomator.common.SemVerComparator;
-import org.cryptomator.common.settings.Settings;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import javax.inject.Inject;
-import javafx.beans.binding.Bindings;
-import javafx.beans.binding.BooleanBinding;
-import javafx.beans.property.ObjectProperty;
-import javafx.beans.property.ReadOnlyStringProperty;
-import javafx.beans.property.SimpleObjectProperty;
-import javafx.beans.property.SimpleStringProperty;
-import javafx.beans.property.StringProperty;
-import javafx.concurrent.ScheduledService;
-import javafx.concurrent.Worker;
-import javafx.concurrent.WorkerStateEvent;
-import javafx.util.Duration;
-import java.time.Instant;
-import java.util.Comparator;
-
-@FxApplicationScoped
-public class UpdateChecker {
-
- private static final Logger LOG = LoggerFactory.getLogger(UpdateChecker.class);
- private static final Duration AUTO_CHECK_DELAY = Duration.seconds(5);
-
- private final Environment env;
- private final Settings settings;
- private final StringProperty latestVersion = new SimpleStringProperty();
- private final ScheduledService updateCheckerService;
- private final ObjectProperty state = new SimpleObjectProperty<>(UpdateCheckState.NOT_CHECKED);
- private final ObjectProperty lastSuccessfulUpdateCheck;
- private final Comparator versionComparator = new SemVerComparator();
- private final BooleanBinding updateAvailable;
- private final BooleanBinding checkFailed;
-
- @Inject
- UpdateChecker(Settings settings, //
- Environment env, //
- ScheduledService updateCheckerService) {
- this.env = env;
- this.settings = settings;
- this.updateCheckerService = updateCheckerService;
- this.lastSuccessfulUpdateCheck = settings.lastSuccessfulUpdateCheck;
- this.updateAvailable = Bindings.createBooleanBinding(this::isUpdateAvailable, latestVersion);
- this.checkFailed = Bindings.equal(UpdateCheckState.CHECK_FAILED, state);
- }
-
- public void automaticallyCheckForUpdatesIfEnabled() {
- if (!env.disableUpdateCheck() && settings.checkForUpdates.get()) {
- startCheckingForUpdates(AUTO_CHECK_DELAY);
- }
- }
-
- public void checkForUpdatesNow() {
- startCheckingForUpdates(Duration.ZERO);
- }
-
- private void startCheckingForUpdates(Duration initialDelay) {
- updateCheckerService.cancel();
- updateCheckerService.reset();
- updateCheckerService.setDelay(initialDelay);
- updateCheckerService.setOnRunning(this::checkStarted);
- updateCheckerService.setOnSucceeded(this::checkSucceeded);
- updateCheckerService.setOnFailed(this::checkFailed);
- updateCheckerService.start();
- }
-
- private void checkStarted(WorkerStateEvent event) {
- LOG.debug("Checking for updates...");
- state.set(UpdateCheckState.IS_CHECKING);
- }
-
- private void checkSucceeded(WorkerStateEvent event) {
- var latestVersionString = updateCheckerService.getValue();
- LOG.info("Current version: {}, latest version: {}", getCurrentVersion(), latestVersionString);
- lastSuccessfulUpdateCheck.set(Instant.now());
- latestVersion.set(latestVersionString);
- state.set(UpdateCheckState.CHECK_SUCCESSFUL);
- }
-
- private void checkFailed(WorkerStateEvent event) {
- state.set(UpdateCheckState.CHECK_FAILED);
- }
-
- public enum UpdateCheckState {
- NOT_CHECKED,
- IS_CHECKING,
- CHECK_SUCCESSFUL,
- CHECK_FAILED;
- }
-
- /* Observable Properties */
- public BooleanBinding checkingForUpdatesProperty() {
- return updateCheckerService.stateProperty().isEqualTo(Worker.State.RUNNING);
- }
-
- public ReadOnlyStringProperty latestVersionProperty() {
- return latestVersion;
- }
-
- public BooleanBinding updateAvailableProperty() {
- return updateAvailable;
- }
-
- public BooleanBinding checkFailedProperty() {
- return checkFailed;
- }
-
- public boolean isUpdateAvailable() {
- String currentVersion = getCurrentVersion();
- String latestVersionString = latestVersion.get();
-
- if (currentVersion == null || latestVersionString == null) {
- return false;
- } else {
- return versionComparator.compare(currentVersion, latestVersionString) < 0;
- }
- }
-
- public ObjectProperty lastSuccessfulUpdateCheckProperty() {
- return lastSuccessfulUpdateCheck;
- }
-
- public ObjectProperty updateCheckStateProperty() {
- return state;
- }
-
- public String getCurrentVersion() {
- return env.getAppVersion();
- }
-}
diff --git a/src/main/java/org/cryptomator/ui/fxapp/UpdateCheckerModule.java b/src/main/java/org/cryptomator/ui/fxapp/UpdateCheckerModule.java
deleted file mode 100644
index 585180662..000000000
--- a/src/main/java/org/cryptomator/ui/fxapp/UpdateCheckerModule.java
+++ /dev/null
@@ -1,93 +0,0 @@
-package org.cryptomator.ui.fxapp;
-
-import dagger.Module;
-import dagger.Provides;
-import org.apache.commons.lang3.SystemUtils;
-import org.cryptomator.common.Environment;
-import org.cryptomator.common.settings.Settings;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import javax.inject.Named;
-import javafx.beans.binding.Bindings;
-import javafx.beans.binding.ObjectBinding;
-import javafx.concurrent.ScheduledService;
-import javafx.concurrent.Task;
-import javafx.util.Duration;
-import java.io.UncheckedIOException;
-import java.net.URI;
-import java.net.http.HttpClient;
-import java.net.http.HttpRequest;
-import java.util.Optional;
-import java.util.concurrent.ExecutorService;
-
-@Module
-public abstract class UpdateCheckerModule {
-
- private static final Logger LOG = LoggerFactory.getLogger(UpdateCheckerModule.class);
-
- private static final URI LATEST_VERSION_URI = URI.create("https://api.cryptomator.org/desktop/latest-version.json");
- private static final Duration UPDATE_CHECK_INTERVAL = Duration.hours(3);
- private static final Duration DISABLED_UPDATE_CHECK_INTERVAL = Duration.hours(100000); // Duration.INDEFINITE leads to overflows...
-
- @Provides
- @FxApplicationScoped
- static Optional provideHttpClient() {
- try {
- return Optional.of(HttpClient.newBuilder() //
- .followRedirects(HttpClient.Redirect.NORMAL) // from version 1.6.11 onwards, Cryptomator can follow redirects, in case this URL ever changes
- .build());
- } catch (UncheckedIOException e) {
- LOG.error("HttpClient for update check cannot be created.", e);
- return Optional.empty();
- }
- }
-
- @Provides
- @FxApplicationScoped
- static HttpRequest provideCheckForUpdatesRequest(Environment env) {
- String userAgent = String.format("Cryptomator VersionChecker/%s %s %s (%s)", //
- env.getAppVersion(), //
- SystemUtils.OS_NAME, //
- SystemUtils.OS_VERSION, //
- SystemUtils.OS_ARCH); //
- return HttpRequest.newBuilder() //
- .uri(LATEST_VERSION_URI) //
- .header("User-Agent", userAgent) //
- .timeout(java.time.Duration.ofSeconds(10))
- .build();
- }
-
- @Provides
- @Named("checkForUpdatesInterval")
- @FxApplicationScoped
- static ObjectBinding provideCheckForUpdateInterval(Settings settings) {
- return Bindings.when(settings.checkForUpdates).then(UPDATE_CHECK_INTERVAL).otherwise(DISABLED_UPDATE_CHECK_INTERVAL);
- }
-
- @Provides
- @FxApplicationScoped
- static ScheduledService provideCheckForUpdatesService(ExecutorService executor, Optional httpClient, HttpRequest checkForUpdatesRequest, @Named("checkForUpdatesInterval") ObjectBinding period) {
- ScheduledService service = new ScheduledService<>() {
- @Override
- protected Task createTask() {
- if (httpClient.isPresent()) {
- return new UpdateCheckerTask(httpClient.get(), checkForUpdatesRequest);
- } else {
- return new Task<>() {
- @Override
- protected String call() {
- throw new NullPointerException("No HttpClient present.");
- }
- };
- }
- }
- };
- service.setOnFailed(event -> LOG.error("Failed to execute update service", service.getException()));
- service.setExecutor(executor);
- service.periodProperty().bind(period);
- return service;
- }
-
-
-}
diff --git a/src/main/java/org/cryptomator/ui/fxapp/UpdateCheckerTask.java b/src/main/java/org/cryptomator/ui/fxapp/UpdateCheckerTask.java
deleted file mode 100644
index 36ad53331..000000000
--- a/src/main/java/org/cryptomator/ui/fxapp/UpdateCheckerTask.java
+++ /dev/null
@@ -1,58 +0,0 @@
-package org.cryptomator.ui.fxapp;
-
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.google.common.io.ByteStreams;
-import org.apache.commons.lang3.SystemUtils;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import javafx.concurrent.Task;
-import java.io.IOException;
-import java.io.InputStream;
-import java.net.http.HttpClient;
-import java.net.http.HttpRequest;
-import java.net.http.HttpResponse;
-
-public class UpdateCheckerTask extends Task {
-
- private static final ObjectMapper JSON = new ObjectMapper();
- private static final Logger LOG = LoggerFactory.getLogger(UpdateCheckerTask.class);
-
- private static final long MAX_RESPONSE_SIZE = 10L * 1024; // 10kb should be sufficient. protect against flooding
-
- private final HttpClient httpClient;
- private final HttpRequest checkForUpdatesRequest;
-
- UpdateCheckerTask(HttpClient httpClient, HttpRequest checkForUpdatesRequest) {
- this.httpClient = httpClient;
- this.checkForUpdatesRequest = checkForUpdatesRequest;
-
- setOnFailed(event -> LOG.error("Failed to check for updates", getException()));
- }
-
- @Override
- protected String call() throws IOException, InterruptedException {
- HttpResponse response = httpClient.send(checkForUpdatesRequest, HttpResponse.BodyHandlers.ofInputStream());
- if (response.statusCode() == 200) {
- return processBody(response);
- } else {
- throw new IOException("Unexpected HTTP response code " + response.statusCode());
- }
- }
-
- private String processBody(HttpResponse response) throws IOException {
- try (InputStream in = response.body(); //
- InputStream limitedIn = ByteStreams.limit(in, MAX_RESPONSE_SIZE)) {
- var json = JSON.reader().readTree(limitedIn);
- if (SystemUtils.IS_OS_MAC_OSX) {
- return json.get("mac").asText();
- } else if (SystemUtils.IS_OS_WINDOWS) {
- return json.get("win").asText();
- } else if (SystemUtils.IS_OS_LINUX) {
- return json.get("linux").asText();
- } else {
- throw new IllegalStateException("Unsupported operating system");
- }
- }
- }
-}
diff --git a/src/main/java/org/cryptomator/ui/mainwindow/MainWindowController.java b/src/main/java/org/cryptomator/ui/mainwindow/MainWindowController.java
index c6e084518..a642dcee9 100644
--- a/src/main/java/org/cryptomator/ui/mainwindow/MainWindowController.java
+++ b/src/main/java/org/cryptomator/ui/mainwindow/MainWindowController.java
@@ -7,7 +7,7 @@ import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultListManager;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.fxapp.FxApplicationWindows;
-import org.cryptomator.ui.fxapp.UpdateChecker;
+import org.cryptomator.updater.UpdateChecker;
import org.cryptomator.ui.preferences.SelectedPreferencesTab;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
diff --git a/src/main/java/org/cryptomator/ui/preferences/AboutController.java b/src/main/java/org/cryptomator/ui/preferences/AboutController.java
index 1f1b1864b..0598e0d38 100644
--- a/src/main/java/org/cryptomator/ui/preferences/AboutController.java
+++ b/src/main/java/org/cryptomator/ui/preferences/AboutController.java
@@ -3,7 +3,7 @@ package org.cryptomator.ui.preferences;
import com.google.common.io.CharStreams;
import org.cryptomator.common.Environment;
import org.cryptomator.ui.common.FxController;
-import org.cryptomator.ui.fxapp.UpdateChecker;
+import org.cryptomator.updater.UpdateChecker;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
diff --git a/src/main/java/org/cryptomator/ui/preferences/PreferencesController.java b/src/main/java/org/cryptomator/ui/preferences/PreferencesController.java
index ad3d08491..57d087643 100644
--- a/src/main/java/org/cryptomator/ui/preferences/PreferencesController.java
+++ b/src/main/java/org/cryptomator/ui/preferences/PreferencesController.java
@@ -2,7 +2,7 @@ package org.cryptomator.ui.preferences;
import org.cryptomator.common.Environment;
import org.cryptomator.ui.common.FxController;
-import org.cryptomator.ui.fxapp.UpdateChecker;
+import org.cryptomator.updater.UpdateChecker;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
diff --git a/src/main/java/org/cryptomator/ui/preferences/UpdatesPreferencesController.java b/src/main/java/org/cryptomator/ui/preferences/UpdatesPreferencesController.java
index f5a72290f..89c03800e 100644
--- a/src/main/java/org/cryptomator/ui/preferences/UpdatesPreferencesController.java
+++ b/src/main/java/org/cryptomator/ui/preferences/UpdatesPreferencesController.java
@@ -2,25 +2,37 @@ package org.cryptomator.ui.preferences;
import org.cryptomator.common.Environment;
import org.cryptomator.common.settings.Settings;
+import org.cryptomator.common.vaults.Vault;
+import org.cryptomator.integrations.update.UpdateStep;
import org.cryptomator.ui.common.FxController;
-import org.cryptomator.ui.fxapp.UpdateChecker;
+import org.cryptomator.ui.common.VaultService;
+import org.cryptomator.updater.UpdateChecker;
+import org.cryptomator.updater.FallbackUpdateInfo;
+import org.cryptomator.updater.UpdateService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javafx.animation.PauseTransition;
import javafx.application.Application;
+import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
+import javafx.beans.binding.BooleanExpression;
import javafx.beans.binding.ObjectBinding;
import javafx.beans.binding.StringBinding;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ReadOnlyStringProperty;
import javafx.beans.property.SimpleBooleanProperty;
+import javafx.beans.property.SimpleStringProperty;
+import javafx.beans.property.StringProperty;
import javafx.beans.value.ObservableValue;
+import javafx.collections.ObservableList;
+import javafx.concurrent.Worker;
+import javafx.concurrent.WorkerStateEvent;
import javafx.fxml.FXML;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ContentDisplay;
-import java.net.URLEncoder;
-import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDateTime;
@@ -34,73 +46,64 @@ import java.util.ResourceBundle;
@PreferencesScoped
public class UpdatesPreferencesController implements FxController {
- private static final String DOWNLOADS_URI_TEMPLATE = "https://cryptomator.org/downloads/" //
- + "?utm_source=cryptomator-desktop" //
- + "&utm_medium=update-notification&" //
- + "utm_campaign=app-update-%s";
+ private static final Logger LOG = LoggerFactory.getLogger(UpdatesPreferencesController.class);
+ private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM).withLocale(Locale.getDefault());
private final Application application;
private final Environment environment;
private final ResourceBundle resourceBundle;
private final Settings settings;
private final UpdateChecker updateChecker;
- private final ObjectBinding checkForUpdatesButtonState;
- private final ReadOnlyStringProperty latestVersion;
- private final ObservableValue lastSuccessfulUpdateCheck;
- private final StringBinding lastUpdateCheckMessage;
+ private final UpdateService updateService;
+ private final ObservableList unlockedVaults;
+ private final VaultService vaultService;
+ private final ObjectBinding> worker;
+ private final BooleanExpression running;
+ private final StringBinding updateButtonTitle;
+ private final ObjectBinding updateButtonState;
private final ObservableValue timeDifferenceMessage;
- private final String currentVersion;
- private final BooleanBinding updateAvailable;
- private final BooleanBinding checkFailed;
+ private final StringBinding lastUpdateCheckMessage;
+ private final BooleanBinding prohibitUpdateWhileUnlocked;
+ private final BooleanBinding updateButtonDisabled;
+ private final StringProperty errorMessage = new SimpleStringProperty("");
private final BooleanProperty upToDateLabelVisible = new SimpleBooleanProperty(false);
- private final DateTimeFormatter formatter;
- private final BooleanBinding upToDate;
- private final String downloadsUri;
/* FXML */
public CheckBox checkForUpdatesCheckbox;
@Inject
- UpdatesPreferencesController(Application application, Environment environment, ResourceBundle resourceBundle, Settings settings, UpdateChecker updateChecker) {
+ UpdatesPreferencesController(Application application, Environment environment, ResourceBundle resourceBundle, Settings settings, UpdateChecker updateChecker, ObservableList vaults, VaultService vaultService) {
this.application = application;
this.environment = environment;
this.resourceBundle = resourceBundle;
this.settings = settings;
this.updateChecker = updateChecker;
- this.checkForUpdatesButtonState = Bindings.when(updateChecker.checkingForUpdatesProperty()).then(ContentDisplay.LEFT).otherwise(ContentDisplay.TEXT_ONLY);
- this.latestVersion = updateChecker.latestVersionProperty();
- this.lastSuccessfulUpdateCheck = updateChecker.lastSuccessfulUpdateCheckProperty();
- this.timeDifferenceMessage = Bindings.createStringBinding(this::getTimeDifferenceMessage, lastSuccessfulUpdateCheck);
- this.currentVersion = environment.getAppVersion();
- this.updateAvailable = updateChecker.updateAvailableProperty();
- this.formatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM).withLocale(Locale.getDefault());
- this.upToDate = updateChecker.updateCheckStateProperty().isEqualTo(UpdateChecker.UpdateCheckState.CHECK_SUCCESSFUL).and(latestVersion.isEqualTo(currentVersion));
- this.checkFailed = updateChecker.checkFailedProperty();
- this.lastUpdateCheckMessage = Bindings.createStringBinding(this::getLastUpdateCheckMessage, lastSuccessfulUpdateCheck);
- this.downloadsUri = DOWNLOADS_URI_TEMPLATE.formatted(URLEncoder.encode(currentVersion, StandardCharsets.US_ASCII));
+ this.updateService = new UpdateService(updateChecker.updateProperty());
+ this.unlockedVaults = vaults.filtered(Vault::isUnlocked);
+ this.vaultService = vaultService;
+ this.worker = Bindings.when(updateChecker.updateAvailableProperty()).>then(this.updateService).otherwise(this.updateChecker);
+ this.running = Bindings.createBooleanBinding(this::isRunning, updateService.stateProperty(), updateChecker.stateProperty());
+ this.updateButtonTitle = Bindings.createStringBinding(this::getUpdateButtonTitle, worker, updateService.stateProperty(), updateService.messageProperty());
+ this.updateButtonState = Bindings.createObjectBinding(this::getUpdateButtonState, updateChecker.stateProperty(), updateService.stateProperty());
+ this.timeDifferenceMessage = Bindings.createStringBinding(this::getTimeDifferenceMessage, updateChecker.lastSuccessfulUpdateCheckProperty());
+ this.lastUpdateCheckMessage = Bindings.createStringBinding(this::getLastUpdateCheckMessage, updateChecker.lastSuccessfulUpdateCheckProperty());
+ this.prohibitUpdateWhileUnlocked = Bindings.createBooleanBinding(this::isProhibitUpdateWhileUnlocked, unlockedVaults, updateChecker.updateProperty());
+ this.updateButtonDisabled = Bindings.when(worker.isEqualTo(updateChecker)).then(running).otherwise(prohibitUpdateWhileUnlocked.or(running));
}
public void initialize() {
checkForUpdatesCheckbox.selectedProperty().bindBidirectional(settings.checkForUpdates);
-
- upToDate.addListener((_, _, newVal) -> {
- if (newVal) {
+ updateChecker.updateAvailableProperty().addListener((_, _, hasUpdate) -> {
+ if (!hasUpdate) {
upToDateLabelVisible.set(true);
PauseTransition delay = new PauseTransition(javafx.util.Duration.seconds(5));
delay.setOnFinished(_ -> upToDateLabelVisible.set(false));
delay.play();
}
});
- }
-
- @FXML
- public void checkNow() {
- updateChecker.checkForUpdatesNow();
- }
-
- @FXML
- public void visitDownloadsPage() {
- application.getHostServices().showDocument(downloadsUri);
+ updateChecker.setOnFailed(this::checkFailed);
+ updateService.setOnSucceeded(this::updateSucceeded);
+ updateService.setOnFailed(this::updateFailed);
}
@FXML
@@ -108,38 +111,104 @@ public class UpdatesPreferencesController implements FxController {
environment.getLogDir().ifPresent(logDirPath -> application.getHostServices().showDocument(logDirPath.toUri().toString()));
}
+ @FXML
+ public void startWork() {
+ if (worker.get().equals(updateChecker)) {
+ updateChecker.checkForUpdatesNow();
+ } else if (!unlockedVaults.isEmpty()) {
+ LOG.warn("Cannot start update due to unlocked vaults.");
+ } else if (worker.get().equals(updateService)) {
+ LOG.info("User started update to version {}", updateChecker.getUpdate().version());
+ updateService.start();
+ }
+ }
+
+ private void checkFailed(WorkerStateEvent workerStateEvent) {
+ assert workerStateEvent.getSource() == updateChecker;
+ LOG.error("Update check failed.", updateChecker.getException());
+ errorMessage.set(resourceBundle.getString("preferences.updates.checkFailed"));
+ }
+
+ private void updateSucceeded(WorkerStateEvent workerStateEvent) {
+ assert workerStateEvent.getSource() == updateService;
+ var lastStep = updateService.getValue();
+ if (lastStep == UpdateStep.EXIT) {
+ // Record that this version attempted an update, so next launch can choose fallback if needed
+ settings.lastUpdateAttemptedByVersion.set(environment.getAppVersionWithBuildNumber());
+ settings.saveNow();
+ LOG.info("Exiting app to update...");
+ Platform.exit();
+ } else if (lastStep == UpdateStep.RETRY) {
+ updateService.reset();
+ } else {
+ LOG.info("Update succeeded.");
+ }
+ }
+
+ private void updateFailed(WorkerStateEvent workerStateEvent) {
+ assert workerStateEvent.getSource() == updateService;
+ LOG.error("Update failed.", updateService.getException());
+ updateService.reset();
+ errorMessage.set(resourceBundle.getString("preferences.updates.updateFailed"));
+ // try fallback mechanism:
+ updateChecker.recheckWithFallbackMechanism();
+ }
+
+ @FXML
+ public void lockAllGracefully() {
+ vaultService.lockAll(unlockedVaults, false);
+ }
+
/* Observable Properties */
- public ObjectBinding checkForUpdatesButtonStateProperty() {
- return checkForUpdatesButtonState;
+ public UpdateChecker getUpdateChecker() {
+ return updateChecker;
}
- public ContentDisplay getCheckForUpdatesButtonState() {
- return checkForUpdatesButtonState.get();
+ public ObjectBinding> workerProperty() {
+ return worker;
}
- public ReadOnlyStringProperty latestVersionProperty() {
- return latestVersion;
+ public Worker> getWorker() {
+ return worker.get();
}
- public String getLatestVersion() {
- return latestVersion.get();
+ public BooleanExpression runningProperty() {
+ return running;
}
- public String getCurrentVersion() {
- return currentVersion;
+ public boolean isRunning() {
+ return updateChecker.getState() == Worker.State.RUNNING || updateService.getState() == Worker.State.RUNNING;
}
- public StringBinding lastUpdateCheckMessageProperty() {
- return lastUpdateCheckMessage;
+ public StringBinding updateButtonTitleProperty() {
+ return updateButtonTitle;
}
- public String getLastUpdateCheckMessage() {
- Instant lastCheck = lastSuccessfulUpdateCheck.getValue();
- if (lastCheck != null && !lastCheck.equals(Settings.DEFAULT_TIMESTAMP)) {
- return formatter.format(LocalDateTime.ofInstant(lastCheck, ZoneId.systemDefault()));
+ public String getUpdateButtonTitle() {
+ if (worker.get() == updateChecker) {
+ return resourceBundle.getString("preferences.updates.checkNowBtn");
} else {
- return "-";
+ return switch (updateService.getState()) {
+ case READY -> updateChecker.getUpdate().updateMechanism().getName();
+ case SCHEDULED, RUNNING -> updateService.getMessage();
+ case SUCCEEDED -> resourceBundle.getString("generic.button.done");
+ case FAILED, CANCELLED -> "failed"; // should never be visible
+ };
+ }
+ }
+
+ public ObjectBinding updateButtonStateProperty() {
+ return updateButtonState;
+ }
+
+ public ContentDisplay getUpdateButtonState() {
+ if (updateService.isRunning()) { // isRunning() covers RUNNING and SCHEDULED states
+ return ContentDisplay.BOTTOM;
+ } else if (updateChecker.getState() == Worker.State.RUNNING) {
+ return ContentDisplay.LEFT;
+ } else {
+ return ContentDisplay.TEXT_ONLY;
}
}
@@ -148,7 +217,7 @@ public class UpdatesPreferencesController implements FxController {
}
public String getTimeDifferenceMessage() {
- var lastSuccessCheck = lastSuccessfulUpdateCheck.getValue();
+ var lastSuccessCheck = updateChecker.getLastSuccessfulUpdateCheck();
var duration = Duration.between(lastSuccessCheck, Instant.now());
var hours = duration.toHours();
if (lastSuccessCheck.equals(Settings.DEFAULT_TIMESTAMP)) {
@@ -162,6 +231,44 @@ public class UpdatesPreferencesController implements FxController {
}
}
+ public StringBinding lastUpdateCheckMessageProperty() {
+ return lastUpdateCheckMessage;
+ }
+
+ public String getLastUpdateCheckMessage() {
+ Instant lastCheck = updateChecker.getLastSuccessfulUpdateCheck();
+ if (lastCheck != null && !lastCheck.equals(Settings.DEFAULT_TIMESTAMP)) {
+ return FORMATTER.format(LocalDateTime.ofInstant(lastCheck, ZoneId.systemDefault()));
+ } else {
+ return "-";
+ }
+ }
+
+ public String getErrorMessage() {
+ return errorMessage.get();
+ }
+
+ public ReadOnlyStringProperty errorMessageProperty() {
+ return errorMessage;
+ }
+
+ public boolean isProhibitUpdateWhileUnlocked() {
+ // If the result of the last update check was from the fallback mechanism, we don't need to show the warning
+ return !unlockedVaults.isEmpty() && !FallbackUpdateInfo.class.isInstance(updateChecker.getUpdate());
+ }
+
+ public BooleanBinding prohibitUpdateWhileUnlockedProperty() {
+ return prohibitUpdateWhileUnlocked;
+ }
+
+ public boolean isUpdateButtonDisabled() {
+ return updateButtonDisabled.get();
+ }
+
+ public BooleanBinding updateButtonDisabledProperty() {
+ return updateButtonDisabled;
+ }
+
public BooleanProperty upToDateLabelVisibleProperty() {
return upToDateLabelVisible;
}
@@ -170,20 +277,4 @@ public class UpdatesPreferencesController implements FxController {
return upToDateLabelVisible.get();
}
- public BooleanBinding updateAvailableProperty() {
- return updateAvailable;
- }
-
- public boolean isUpdateAvailable() {
- return updateAvailable.get();
- }
-
- public BooleanBinding checkFailedProperty() {
- return checkFailed;
- }
-
- public boolean isCheckFailed() {
- return checkFailed.getValue();
- }
-
}
diff --git a/src/main/java/org/cryptomator/ui/updatereminder/UpdateReminderController.java b/src/main/java/org/cryptomator/ui/updatereminder/UpdateReminderController.java
index a6fce0b79..91b333279 100644
--- a/src/main/java/org/cryptomator/ui/updatereminder/UpdateReminderController.java
+++ b/src/main/java/org/cryptomator/ui/updatereminder/UpdateReminderController.java
@@ -2,7 +2,7 @@ package org.cryptomator.ui.updatereminder;
import org.cryptomator.common.settings.Settings;
import org.cryptomator.ui.common.FxController;
-import org.cryptomator.ui.fxapp.UpdateChecker;
+import org.cryptomator.updater.UpdateChecker;
import javax.inject.Inject;
import javafx.fxml.FXML;
diff --git a/src/main/java/org/cryptomator/updater/DelegatingHttpClient.java b/src/main/java/org/cryptomator/updater/DelegatingHttpClient.java
new file mode 100644
index 000000000..b86b72ed3
--- /dev/null
+++ b/src/main/java/org/cryptomator/updater/DelegatingHttpClient.java
@@ -0,0 +1,86 @@
+package org.cryptomator.updater;
+
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLParameters;
+import java.io.IOException;
+import java.net.Authenticator;
+import java.net.CookieHandler;
+import java.net.ProxySelector;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.time.Duration;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Executor;
+
+abstract class DelegatingHttpClient extends HttpClient {
+
+ private final HttpClient delegate;
+
+ public DelegatingHttpClient(HttpClient delegate) {
+ this.delegate = Objects.requireNonNull(delegate, "delegate must not be null");
+ }
+
+ @Override
+ public Optional cookieHandler() {
+ return delegate.cookieHandler();
+ }
+
+ @Override
+ public Optional connectTimeout() {
+ return delegate.connectTimeout();
+ }
+
+ @Override
+ public Redirect followRedirects() {
+ return delegate.followRedirects();
+ }
+
+ @Override
+ public Optional proxy() {
+ return delegate.proxy();
+ }
+
+ @Override
+ public SSLContext sslContext() {
+ return delegate.sslContext();
+ }
+
+ @Override
+ public SSLParameters sslParameters() {
+ return delegate.sslParameters();
+ }
+
+ @Override
+ public Optional authenticator() {
+ return delegate.authenticator();
+ }
+
+ @Override
+ public Version version() {
+ return delegate.version();
+ }
+
+ @Override
+ public Optional executor() {
+ return delegate.executor();
+ }
+
+ @Override
+ public HttpResponse send(HttpRequest request, HttpResponse.BodyHandler responseBodyHandler) throws IOException, InterruptedException {
+ return delegate.send(request, responseBodyHandler);
+ }
+
+ @Override
+ public CompletableFuture> sendAsync(HttpRequest request, HttpResponse.BodyHandler responseBodyHandler) {
+ return delegate.sendAsync(request, responseBodyHandler);
+ }
+
+ @Override
+ public CompletableFuture> sendAsync(HttpRequest request, HttpResponse.BodyHandler responseBodyHandler, HttpResponse.PushPromiseHandler pushPromiseHandler) {
+ return delegate.sendAsync(request, responseBodyHandler, pushPromiseHandler);
+ }
+
+}
diff --git a/src/main/java/org/cryptomator/updater/FallbackUpdateInfo.java b/src/main/java/org/cryptomator/updater/FallbackUpdateInfo.java
new file mode 100644
index 000000000..b0f5d83b0
--- /dev/null
+++ b/src/main/java/org/cryptomator/updater/FallbackUpdateInfo.java
@@ -0,0 +1,6 @@
+package org.cryptomator.updater;
+
+import org.cryptomator.integrations.update.UpdateInfo;
+import org.cryptomator.integrations.update.UpdateMechanism;
+
+public record FallbackUpdateInfo(String version, UpdateMechanism updateMechanism) implements UpdateInfo {}
diff --git a/src/main/java/org/cryptomator/updater/FallbackUpdateMechanism.java b/src/main/java/org/cryptomator/updater/FallbackUpdateMechanism.java
new file mode 100644
index 000000000..2fff9ab18
--- /dev/null
+++ b/src/main/java/org/cryptomator/updater/FallbackUpdateMechanism.java
@@ -0,0 +1,105 @@
+package org.cryptomator.updater;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.commons.lang3.SystemUtils;
+import org.cryptomator.common.Environment;
+import org.cryptomator.integrations.common.LocalizedDisplayName;
+import org.cryptomator.integrations.update.UpdateMechanism;
+import org.cryptomator.integrations.update.UpdateStep;
+import org.cryptomator.ui.fxapp.FxApplicationScoped;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.inject.Inject;
+import javafx.application.Application;
+import javafx.application.Platform;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.net.URLEncoder;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.nio.charset.StandardCharsets;
+
+@FxApplicationScoped
+@LocalizedDisplayName(bundle = "i18n.strings", key = "preferences.updates.visitDownloadPage")
+public class FallbackUpdateMechanism implements UpdateMechanism {
+
+ private static final Logger LOG = LoggerFactory.getLogger(FallbackUpdateMechanism.class);
+ private static final String LATEST_VERSION_API_URL = "https://api.cryptomator.org/connect/apps/desktop/latest-version";
+ private static final ObjectMapper MAPPER = new ObjectMapper();
+ private static final String DOWNLOADS_URI_TEMPLATE = "https://cryptomator.org/downloads/" //
+ + "?utm_source=cryptomator-desktop" //
+ + "&utm_medium=update-notification&" //
+ + "utm_campaign=app-update-%s";
+
+ private final Application app;
+ private final Environment env;
+
+ @Inject
+ public FallbackUpdateMechanism(Application app, Environment env) {
+ this.app = app;
+ this.env = env;
+ }
+
+ @Override
+ public FallbackUpdateInfo checkForUpdate(String currentVersion, HttpClient httpClient) {
+ try {
+ HttpRequest request = HttpRequest.newBuilder().uri(URI.create(LATEST_VERSION_API_URL)).build();
+ HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofInputStream());
+ if (response.statusCode() != 200) {
+ throw new RuntimeException("Failed to fetch release: " + response.statusCode());
+ }
+ var release = MAPPER.readValue(response.body(), LatestVersion.class);
+ var updateVersion = release.versionForCurrentOS();
+ if (UpdateMechanism.isUpdateAvailable(updateVersion, currentVersion)) {
+ return new FallbackUpdateInfo(updateVersion, this);
+ } else {
+ return null;
+ }
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ LOG.warn("Update check interrupted", e);
+ return null;
+ } catch (IOException e) {
+ LOG.warn("Update check failed", e);
+ return null;
+ }
+ }
+
+ @Override
+ public UpdateStep firstStep(FallbackUpdateInfo updateInfo) {
+ return UpdateStep.of("Go to download page", this::openDownloadPage); // TODO localize
+ }
+
+ private UpdateStep openDownloadPage() {
+ var downloadUrl = DOWNLOADS_URI_TEMPLATE.formatted(URLEncoder.encode(env.getAppVersion(), StandardCharsets.US_ASCII));
+ Platform.runLater(() -> {
+ app.getHostServices().showDocument(downloadUrl);
+ });
+ return UpdateStep.RETRY; // allow running this "update mechanism" as many times as the user wants
+ }
+
+ @JsonIgnoreProperties(ignoreUnknown = true)
+ public record LatestVersion(
+ @JsonProperty("mac") String macVersion,
+ @JsonProperty("win") String winVersion,
+ @JsonProperty("linux") String linuxVersion
+ ) {
+ public String versionForCurrentOS() {
+ if (SystemUtils.IS_OS_MAC_OSX) {
+ return macVersion;
+ } else if (SystemUtils.IS_OS_WINDOWS) {
+ return winVersion;
+ } else if (SystemUtils.IS_OS_LINUX) {
+ return linuxVersion;
+ } else {
+ throw new IllegalStateException("Unsupported operating system");
+ }
+ }
+ }
+
+}
diff --git a/src/main/java/org/cryptomator/updater/UpdateChecker.java b/src/main/java/org/cryptomator/updater/UpdateChecker.java
new file mode 100644
index 000000000..9c1665e25
--- /dev/null
+++ b/src/main/java/org/cryptomator/updater/UpdateChecker.java
@@ -0,0 +1,198 @@
+package org.cryptomator.updater;
+
+import org.cryptomator.common.Environment;
+import org.cryptomator.common.settings.Settings;
+import org.cryptomator.integrations.update.UpdateFailedException;
+import org.cryptomator.integrations.update.UpdateInfo;
+import org.cryptomator.integrations.update.UpdateMechanism;
+import org.cryptomator.ui.fxapp.FxApplicationScoped;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.inject.Inject;
+import javafx.beans.binding.Bindings;
+import javafx.beans.binding.BooleanBinding;
+import javafx.beans.binding.ObjectBinding;
+import javafx.beans.binding.StringExpression;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.concurrent.ScheduledService;
+import javafx.concurrent.Task;
+import javafx.util.Duration;
+import java.time.Instant;
+import java.util.concurrent.Executors;
+
+@FxApplicationScoped
+public class UpdateChecker extends ScheduledService> {
+
+ private static final Logger LOG = LoggerFactory.getLogger(UpdateChecker.class);
+ private static final Duration AUTO_CHECK_DELAY = Duration.seconds(5);
+ private static final Duration UPDATE_CHECK_INTERVAL = Duration.hours(3);
+ private static final Duration DISABLED_UPDATE_CHECK_INTERVAL = Duration.hours(100000); // Duration.INDEFINITE leads to overflows...
+
+ public enum UpdateCheckState {
+ NOT_CHECKED,
+ IS_CHECKING,
+ CHECK_SUCCESSFUL,
+ CHECK_FAILED
+ }
+
+ private final Environment env;
+ private final Settings settings;
+ private final ObjectProperty lastSuccessfulUpdateCheck;
+ private final ObjectProperty> update = new SimpleObjectProperty<>();
+ private final StringExpression latestVersion = StringExpression.stringExpression(update.map(UpdateInfo::version));
+ private final BooleanBinding updateAvailable = update.isNotNull();
+ private final ObjectBinding updateState = Bindings.createObjectBinding(this::getUpdateCheckState, stateProperty());
+ private final BooleanBinding checkFailed = Bindings.equal(UpdateCheckState.CHECK_FAILED, updateState);
+ private final UpdateMechanism> fallbackUpdateMechanism;
+ private UpdateMechanism> updateMechanism;
+
+ @Inject
+ UpdateChecker(Settings settings, //
+ Environment env,
+ FallbackUpdateMechanism fallbackUpdateMechanism) {
+ this.env = env;
+ this.settings = settings;
+ this.lastSuccessfulUpdateCheck = settings.lastSuccessfulUpdateCheck;
+ this.fallbackUpdateMechanism = fallbackUpdateMechanism;
+
+ // Prefer the safer fallback mechanism if the last update attempt was already made by this app version
+ var currentVersion = env.getAppVersionWithBuildNumber();
+ var lastAttemptedBy = settings.lastUpdateAttemptedByVersion.get();
+ if (currentVersion != null && currentVersion.equals(lastAttemptedBy)) {
+ this.updateMechanism = fallbackUpdateMechanism; // immediately use fallback mechanism
+ } else {
+ this.updateMechanism = UpdateMechanism.get().orElse(fallbackUpdateMechanism);
+ }
+
+ setExecutor(Executors.newVirtualThreadPerTaskExecutor());
+ periodProperty().bind(Bindings.when(settings.checkForUpdates).then(UPDATE_CHECK_INTERVAL).otherwise(DISABLED_UPDATE_CHECK_INTERVAL));
+ }
+
+ public void automaticallyCheckForUpdatesIfEnabled() {
+ if (!env.disableUpdateCheck() && settings.checkForUpdates.get()) {
+ startCheckingForUpdates(AUTO_CHECK_DELAY);
+ }
+ }
+
+ public void recheckWithFallbackMechanism() {
+ if (updateMechanism == fallbackUpdateMechanism) {
+ return; // already using fallback mechanism
+ }
+ updateMechanism = fallbackUpdateMechanism;
+ checkForUpdatesNow();
+ }
+
+ public void checkForUpdatesNow() {
+ startCheckingForUpdates(Duration.ZERO);
+ }
+
+ private void startCheckingForUpdates(Duration initialDelay) {
+ cancel();
+ reset();
+ setDelay(initialDelay);
+ start();
+ }
+
+ @Override
+ protected void succeeded() {
+ var updateInfo = getValue();
+ super.succeeded(); // this will nil the value property!
+ lastSuccessfulUpdateCheck.set(Instant.now());
+ if (updateInfo != null) {
+ LOG.info("Current version: {}, latest version: {}", getCurrentVersion(), updateInfo.version());
+ update.set(updateInfo);
+ }
+ }
+
+ @Override
+ protected Task> createTask() {
+ return new UpdateCheckTask();
+ }
+
+ /* Observable Properties */
+
+ public UpdateInfo> getUpdate() {
+ return update.get();
+ }
+
+ public ObjectProperty> updateProperty() {
+ return update;
+ }
+
+ public String getLatestVersion() {
+ return latestVersion.get();
+ }
+
+ public StringExpression latestVersionProperty() {
+ return latestVersion;
+ }
+
+ public boolean isUpdateAvailable() {
+ return updateAvailable.get();
+ }
+
+ public BooleanBinding updateAvailableProperty() {
+ return updateAvailable;
+ }
+
+ public boolean isCheckFailed() {
+ return checkFailed.get();
+ }
+
+ public BooleanBinding checkFailedProperty() {
+ return checkFailed;
+ }
+
+ public Instant getLastSuccessfulUpdateCheck() {
+ return lastSuccessfulUpdateCheck.get();
+ }
+
+ public ObjectProperty lastSuccessfulUpdateCheckProperty() {
+ return lastSuccessfulUpdateCheck;
+ }
+
+ public ObjectBinding updateCheckStateProperty() {
+ return updateState;
+ }
+
+ private UpdateCheckState getUpdateCheckState() {
+ return switch (getState()) {
+ case READY -> UpdateCheckState.NOT_CHECKED;
+ case SCHEDULED, RUNNING -> UpdateCheckState.IS_CHECKING;
+ case SUCCEEDED -> UpdateCheckState.CHECK_SUCCESSFUL;
+ case FAILED, CANCELLED -> UpdateCheckState.CHECK_FAILED;
+ };
+ }
+
+ public String getCurrentVersion() {
+ return env.getAppVersion();
+ }
+
+ private class UpdateCheckTask extends Task> {
+
+ @Override
+ protected UpdateInfo> call() {
+ try (var httpClient = new UpdateCheckerHttpClient(env)) {
+ var result = updateMechanism.checkForUpdate(env.getAppVersion(), httpClient);
+ if (result != null) {
+ return result;
+ }
+ } catch (UpdateFailedException e) {
+ LOG.error("Update check using {} failed.", updateMechanism.getClass(), e);
+ }
+ if (updateMechanism == fallbackUpdateMechanism) {
+ return null;
+ }
+ LOG.debug("Trying fallback update check...");
+ try (var httpClient = new UpdateCheckerHttpClient(env)) {
+ return fallbackUpdateMechanism.checkForUpdate(env.getAppVersion(), httpClient);
+ } catch (UpdateFailedException e) {
+ LOG.error("Fallback update check failed.", e);
+ return null;
+ }
+ }
+ }
+
+}
diff --git a/src/main/java/org/cryptomator/updater/UpdateCheckerHttpClient.java b/src/main/java/org/cryptomator/updater/UpdateCheckerHttpClient.java
new file mode 100644
index 000000000..4f32d1953
--- /dev/null
+++ b/src/main/java/org/cryptomator/updater/UpdateCheckerHttpClient.java
@@ -0,0 +1,50 @@
+package org.cryptomator.updater;
+
+import org.apache.commons.lang3.SystemUtils;
+import org.cryptomator.common.Environment;
+
+import java.io.IOException;
+import java.net.ProxySelector;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.time.Duration;
+import java.util.concurrent.CompletableFuture;
+
+public class UpdateCheckerHttpClient extends DelegatingHttpClient {
+
+ private final String userAgent;
+
+ public UpdateCheckerHttpClient(Environment env) {
+ var delegate = HttpClient.newBuilder() //
+ .followRedirects(HttpClient.Redirect.NORMAL) // from version 1.6.11 onwards, Cryptomator can follow redirects, in case this URL ever changes
+ .proxy(ProxySelector.getDefault()).build();
+ super(delegate);
+ this.userAgent = String.format("Cryptomator VersionChecker/%s %s %s (%s)", env.getAppVersion(), SystemUtils.OS_NAME, SystemUtils.OS_VERSION, SystemUtils.OS_ARCH);
+ }
+
+ @Override
+ public HttpResponse send(HttpRequest request, HttpResponse.BodyHandler responseBodyHandler) throws IOException, InterruptedException {
+ return super.send(decorateRequest(request), responseBodyHandler);
+ }
+
+ @Override
+ public CompletableFuture> sendAsync(HttpRequest request, HttpResponse.BodyHandler responseBodyHandler) {
+ return super.sendAsync(decorateRequest(request), responseBodyHandler);
+ }
+
+ @Override
+ public CompletableFuture> sendAsync(HttpRequest request, HttpResponse.BodyHandler responseBodyHandler, HttpResponse.PushPromiseHandler pushPromiseHandler) {
+ return super.sendAsync(decorateRequest(request), responseBodyHandler, pushPromiseHandler);
+ }
+
+ private HttpRequest decorateRequest(HttpRequest request) {
+ return HttpRequest.newBuilder(request, (_, _) -> true) //
+ .header("User-Agent", this.userAgent) //
+ .timeout(Duration.ofSeconds(10)) //
+ .build();
+
+ }
+
+
+}
diff --git a/src/main/java/org/cryptomator/updater/UpdateService.java b/src/main/java/org/cryptomator/updater/UpdateService.java
new file mode 100644
index 000000000..fbb2d75f4
--- /dev/null
+++ b/src/main/java/org/cryptomator/updater/UpdateService.java
@@ -0,0 +1,81 @@
+package org.cryptomator.updater;
+
+import org.cryptomator.integrations.update.UpdateInfo;
+import org.cryptomator.integrations.update.UpdateMechanism;
+import org.cryptomator.integrations.update.UpdateStep;
+
+import javafx.beans.binding.Bindings;
+import javafx.beans.binding.BooleanBinding;
+import javafx.beans.value.ObservableValue;
+import javafx.concurrent.Service;
+import javafx.concurrent.Task;
+import java.io.IOException;
+import java.io.InterruptedIOException;
+import java.util.Objects;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * A service that performs all update steps provided by the given {@link UpdateMechanism} in sequence.
+ */
+public class UpdateService extends Service {
+
+ private final BooleanBinding updateFailed = Bindings.equal(State.FAILED, stateProperty());
+
+ private ObservableValue> updateInfo;
+
+ public UpdateService(ObservableValue> updateInfo) {
+ setExecutor(Executors.newVirtualThreadPerTaskExecutor());
+ this.updateInfo = updateInfo;
+ }
+
+ @Override
+ protected Task createTask() {
+ return new RunAllStepsTask(updateInfo.getValue());
+ }
+
+ private static class RunAllStepsTask extends Task {
+
+ @SuppressWarnings("rawtypes")
+ private final UpdateInfo updateInfo;
+
+ public RunAllStepsTask(UpdateInfo> updateInfo) {
+ this.updateInfo = Objects.requireNonNull(updateInfo);
+ }
+
+ @Override
+ protected UpdateStep call() throws IOException {
+ try {
+ UpdateStep step = updateInfo.useToPrepareFirstStep();
+ UpdateStep lastStep;
+ do {
+ step.start();
+ observeAndWaitFor(step);
+ lastStep = step;
+ step = step.nextStep();
+ } while (step != null);
+ return lastStep;
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new InterruptedIOException("Update interrupted");
+ }
+ }
+
+ private void observeAndWaitFor(UpdateStep step) throws InterruptedException {
+ do {
+ updateProgress(step.preparationProgress(), 1.0);
+ updateMessage(step.description());
+ } while (!step.await(100, TimeUnit.MILLISECONDS));
+ }
+ }
+
+ /* Observable Properties */
+
+ public boolean isUpdateFailed() {
+ return updateFailed.get();
+ }
+
+ public BooleanBinding updateFailedProperty() {
+ return updateFailed;
+ }
+}
diff --git a/src/main/resources/fxml/preferences_updates.fxml b/src/main/resources/fxml/preferences_updates.fxml
index d0910949b..c1d2dd071 100644
--- a/src/main/resources/fxml/preferences_updates.fxml
+++ b/src/main/resources/fxml/preferences_updates.fxml
@@ -3,55 +3,73 @@
-
-
-
+
-
+
+
-
-
-
-
+
-
diff --git a/src/main/resources/i18n/strings.properties b/src/main/resources/i18n/strings.properties
index 66f473248..c9825c965 100644
--- a/src/main/resources/i18n/strings.properties
+++ b/src/main/resources/i18n/strings.properties
@@ -330,8 +330,13 @@ preferences.updates.lastUpdateCheck.never=never
preferences.updates.lastUpdateCheck.recently=recently
preferences.updates.lastUpdateCheck.daysAgo=%s days ago
preferences.updates.lastUpdateCheck.hoursAgo=%s hours ago
+preferences.updates.prohibitedDueToUnlockedVaults.1=Please
+preferences.updates.prohibitedDueToUnlockedVaults.2=lock your vaults
+preferences.updates.prohibitedDueToUnlockedVaults.3=to install the update.
preferences.updates.checkFailed=Looking for updates failed. Please check your internet connection or try again later.
+preferences.updates.updateFailed=Update failed. Please install the update manually.
preferences.updates.upToDate=Cryptomator is up-to-date.
+preferences.updates.visitDownloadPage=Visit Download Page
## Contribution
preferences.contribute=Support Us
diff --git a/src/test/java/org/cryptomator/common/SemVerComparatorTest.java b/src/test/java/org/cryptomator/common/SemVerComparatorTest.java
deleted file mode 100644
index 52a9366c9..000000000
--- a/src/test/java/org/cryptomator/common/SemVerComparatorTest.java
+++ /dev/null
@@ -1,58 +0,0 @@
-/*******************************************************************************
- * Copyright (c) 2016, 2017 Sebastian Stenzel and others.
- * All rights reserved.
- * This program and the accompanying materials are made available under the terms of the accompanying LICENSE file.
- *
- * Contributors:
- * Sebastian Stenzel - initial API and implementation
- *******************************************************************************/
-package org.cryptomator.common;
-
-import org.junit.jupiter.api.Assertions;
-import org.junit.jupiter.api.Test;
-
-import java.util.Comparator;
-
-public class SemVerComparatorTest {
-
- private final Comparator semVerComparator = new SemVerComparator();
-
- // equal versions
-
- @Test
- public void compareEqualVersions() {
- Assertions.assertEquals(0, Integer.signum(semVerComparator.compare("1.23.4", "1.23.4")));
- Assertions.assertEquals(0, Integer.signum(semVerComparator.compare("1.23.4-alpha", "1.23.4-alpha")));
- Assertions.assertEquals(0, Integer.signum(semVerComparator.compare("1.23.4+20170101", "1.23.4+20171231")));
- Assertions.assertEquals(0, Integer.signum(semVerComparator.compare("1.23.4-alpha+20170101", "1.23.4-alpha+20171231")));
- }
-
- // newer versions in first argument
-
- @Test
- public void compareHigherToLowerVersions() {
- Assertions.assertEquals(1, Integer.signum(semVerComparator.compare("1.23.5", "1.23.4")));
- Assertions.assertEquals(1, Integer.signum(semVerComparator.compare("1.24.4", "1.23.4")));
- Assertions.assertEquals(1, Integer.signum(semVerComparator.compare("1.23.4", "1.23")));
- Assertions.assertEquals(1, Integer.signum(semVerComparator.compare("1.23.4", "1.23.4-SNAPSHOT")));
- Assertions.assertEquals(1, Integer.signum(semVerComparator.compare("1.23.4", "1.23.4-56.78")));
- Assertions.assertEquals(1, Integer.signum(semVerComparator.compare("1.23.4-beta", "1.23.4-alpha")));
- Assertions.assertEquals(1, Integer.signum(semVerComparator.compare("1.23.4-alpha.1", "1.23.4-alpha")));
- Assertions.assertEquals(1, Integer.signum(semVerComparator.compare("1.23.4-56.79", "1.23.4-56.78")));
- }
-
- // newer versions in second argument
-
- @Test
- public void compareLowerToHigherVersions() {
- Assertions.assertEquals(-1, Integer.signum(semVerComparator.compare("1.23.4", "1.23.5")));
- Assertions.assertEquals(-1, Integer.signum(semVerComparator.compare("1.23.4", "1.24.4")));
- Assertions.assertEquals(-1, Integer.signum(semVerComparator.compare("1.23", "1.23.4")));
- Assertions.assertEquals(-1, Integer.signum(semVerComparator.compare("1.23.4-SNAPSHOT", "1.23.4")));
- Assertions.assertEquals(-1, Integer.signum(semVerComparator.compare("1.23.4-56.78", "1.23.4")));
- Assertions.assertEquals(-1, Integer.signum(semVerComparator.compare("1.23.4-alpha", "1.23.4-beta")));
- Assertions.assertEquals(-1, Integer.signum(semVerComparator.compare("1.23.4-alpha", "1.23.4-alpha.1")));
- Assertions.assertEquals(-1, Integer.signum(semVerComparator.compare("1.23.4-56.78", "1.23.4-56.79")));
- }
-
-}
diff --git a/src/test/java/org/cryptomator/common/settings/SettingsTest.java b/src/test/java/org/cryptomator/common/settings/SettingsTest.java
index cd737ac11..a7ccf2e7a 100644
--- a/src/test/java/org/cryptomator/common/settings/SettingsTest.java
+++ b/src/test/java/org/cryptomator/common/settings/SettingsTest.java
@@ -9,31 +9,28 @@ import org.cryptomator.common.Environment;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
-import java.util.function.Consumer;
-
public class SettingsTest {
@Test
public void testAutoSave() {
Environment env = Mockito.mock(Environment.class);
- @SuppressWarnings("unchecked") Consumer changeListener = Mockito.mock(Consumer.class);
+ SettingsProvider provider = Mockito.mock(SettingsProvider.class);
- Settings settings = Settings.create(env);
- settings.setSaveCmd(changeListener);
+ Settings settings = Settings.create(provider, env);
VaultSettings vaultSettings = VaultSettings.withRandomId();
- Mockito.verify(changeListener, Mockito.times(0)).accept(settings);
+ Mockito.verify(provider, Mockito.times(0)).scheduleSave(settings);
// first change (to property):
settings.port.set(42428);
- Mockito.verify(changeListener, Mockito.times(1)).accept(settings);
+ Mockito.verify(provider, Mockito.times(1)).scheduleSave(settings);
// second change (to list):
settings.directories.add(vaultSettings);
- Mockito.verify(changeListener, Mockito.times(2)).accept(settings);
+ Mockito.verify(provider, Mockito.times(2)).scheduleSave(settings);
// third change (to property of list item):
vaultSettings.displayName.set("asd");
- Mockito.verify(changeListener, Mockito.times(3)).accept(settings);
+ Mockito.verify(provider, Mockito.times(3)).scheduleSave(settings);
}
}