diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 459d3c52d..3acbc041d 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -50,6 +50,7 @@ open module org.cryptomator.desktop { requires io.github.coffeelibs.tinyoauth2client; requires org.slf4j; requires org.apache.commons.lang3; + requires org.purejava.portal; /* dagger bs */ requires jakarta.inject; diff --git a/src/main/java/org/cryptomator/common/updates/AppUpdateChecker.java b/src/main/java/org/cryptomator/common/updates/AppUpdateChecker.java index aaa90e737..238342e7d 100644 --- a/src/main/java/org/cryptomator/common/updates/AppUpdateChecker.java +++ b/src/main/java/org/cryptomator/common/updates/AppUpdateChecker.java @@ -35,7 +35,7 @@ public class AppUpdateChecker { } } - public String checkForUpdates(DistributionChannel.Value channel) { + public Object getUpdater(DistributionChannel.Value channel) { if (updateServices.isEmpty()) { LOG.error("No UpdateService found"); return null; @@ -47,7 +47,7 @@ public class AppUpdateChecker { LOG.error("Required service for channel LINUX_FLATPAK not available"); return null; } else { - return flatpakService.isUpdateAvailable(DistributionChannel.Value.LINUX_FLATPAK); + return flatpakService.getLatestReleaseChecker(DistributionChannel.Value.LINUX_FLATPAK); } } default -> throw new IllegalStateException("Unexpected value 'channel': " + channel); @@ -68,4 +68,10 @@ public class AppUpdateChecker { }).findFirst().orElse(null); } + public UpdateService getServiceForChannel(DistributionChannel.Value requiredChannel) { + return updateServices.stream().filter(service -> { + DistributionChannel annotation = service.getClass().getAnnotation(DistributionChannel.class); + return annotation != null && annotation.value() == requiredChannel; + }).findFirst().orElse(null); + } } diff --git a/src/main/java/org/cryptomator/ui/fxapp/UpdateChecker.java b/src/main/java/org/cryptomator/ui/fxapp/UpdateChecker.java index 07b401234..5e8f6eff7 100644 --- a/src/main/java/org/cryptomator/ui/fxapp/UpdateChecker.java +++ b/src/main/java/org/cryptomator/ui/fxapp/UpdateChecker.java @@ -5,7 +5,8 @@ import org.cryptomator.common.SemVerComparator; import org.cryptomator.common.settings.Settings; import org.cryptomator.common.updates.AppUpdateChecker; import org.cryptomator.integrations.common.DistributionChannel; -import org.cryptomator.integrations.update.UpdateService; +import org.cryptomator.integrations.update.UpdateFailedException; +import org.purejava.portal.rest.UpdateCheckerTask; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -22,6 +23,7 @@ import javafx.concurrent.Worker; import javafx.concurrent.WorkerStateEvent; import javafx.util.Duration; import java.time.Instant; +import java.time.temporal.ChronoUnit; import java.util.Comparator; @FxApplicationScoped @@ -33,11 +35,13 @@ public class UpdateChecker { private final Environment env; private final Settings settings; private final StringProperty latestVersion = new SimpleStringProperty(); + private final StringProperty latestAppUpdaterVersion = 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 appUpdateAvailable; private final BooleanBinding checkFailed; private final AppUpdateChecker updateChecker; @@ -51,6 +55,7 @@ public class UpdateChecker { this.updateCheckerService = updateCheckerService; this.lastSuccessfulUpdateCheck = settings.lastSuccessfulUpdateCheck; this.updateAvailable = Bindings.createBooleanBinding(this::isUpdateAvailable, latestVersion); + this.appUpdateAvailable = Bindings.createBooleanBinding(this::isAppUpdateAvailable, latestAppUpdaterVersion); this.checkFailed = Bindings.equal(UpdateCheckState.CHECK_FAILED, state); this.updateChecker = updateChecker; } @@ -58,14 +63,11 @@ public class UpdateChecker { public void automaticallyCheckForUpdatesIfEnabled() { if (!env.disableUpdateCheck() && settings.checkForUpdates.get()) { if (updateChecker.isUpdateServiceAvailable(env.getBuildNumber())) { // prefer AppUpdateChecker - String version = ""; switch (env.getBuildNumber().get()) { - case "flatpak-1" -> version = updateChecker.checkForUpdates(DistributionChannel.Value.LINUX_FLATPAK); + case "flatpak-1" -> startCheckingWithFlatpakUpdater((UpdateCheckerTask) updateChecker.getUpdater(DistributionChannel.Value.LINUX_FLATPAK), AUTO_CHECK_DELAY); default -> LOG.error("Unexpected value 'buildNumber': {}", env.getBuildNumber().get()); } - LOG.info("Retrieved version from Update Service {}", version); } else { // fallback is the "redirect user to website" approach - LOG.info("Common \"redirect user to website\" approach"); startCheckingForUpdates(AUTO_CHECK_DELAY); } } @@ -75,6 +77,11 @@ public class UpdateChecker { startCheckingForUpdates(Duration.ZERO); } + public void updateAppNow() throws UpdateFailedException { + var service = updateChecker.getServiceForChannel(DistributionChannel.Value.LINUX_FLATPAK); + service.triggerUpdate(); + } + private void startCheckingForUpdates(Duration initialDelay) { updateCheckerService.cancel(); updateCheckerService.reset(); @@ -85,11 +92,31 @@ public class UpdateChecker { updateCheckerService.start(); } + private void startCheckingWithFlatpakUpdater(UpdateCheckerTask service, Duration initialDelay) { + service.cancel(); + service.reset(); + service.setDelay(convertFxToJavaTime(initialDelay)); + service.setOnRunning(this::checkStarted); + service.setOnSucceeded(this::checkSucceeded); + service.setOnFailed(this::checkFailed); + service.start(); + } + + private java.time.Duration convertFxToJavaTime(javafx.util.Duration fxDuration) { + double millis = fxDuration.toMillis(); + return java.time.Duration.of((long) millis, ChronoUnit.MILLIS); + } + private void checkStarted(WorkerStateEvent event) { LOG.debug("Checking for updates..."); state.set(UpdateCheckState.IS_CHECKING); } + private void checkStarted() { + 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); @@ -98,15 +125,26 @@ public class UpdateChecker { state.set(UpdateCheckState.CHECK_SUCCESSFUL); } + private void checkSucceeded(String version) { + LOG.info("Current version: {}, latest version: {}", getCurrentVersion(), version); + lastSuccessfulUpdateCheck.set(Instant.now()); + latestAppUpdaterVersion.set(version); + state.set(UpdateCheckState.CHECK_SUCCESSFUL); + } + private void checkFailed(WorkerStateEvent event) { state.set(UpdateCheckState.CHECK_FAILED); } + private void checkFailed(Throwable throwable) { + state.set(UpdateCheckState.CHECK_FAILED); + } + public enum UpdateCheckState { NOT_CHECKED, IS_CHECKING, CHECK_SUCCESSFUL, - CHECK_FAILED; + CHECK_FAILED } /* Observable Properties */ @@ -118,23 +156,38 @@ public class UpdateChecker { return latestVersion; } + public ReadOnlyStringProperty latestAppUpdaterVersionProperty() { + return latestAppUpdaterVersion; + } + public BooleanBinding updateAvailableProperty() { return updateAvailable; } + public BooleanBinding appUpdateAvailableProperty() { + return appUpdateAvailable; + } + public BooleanBinding checkFailedProperty() { return checkFailed; } - public boolean isUpdateAvailable() { + public boolean isUpdateAvailable(StringProperty versionProperty) { String currentVersion = getCurrentVersion(); - String latestVersionString = latestVersion.get(); + String latestVersionString = versionProperty.get(); if (currentVersion == null || latestVersionString == null) { return false; - } else { - return versionComparator.compare(currentVersion, latestVersionString) < 0; } + return versionComparator.compare(currentVersion, latestVersionString) < 0; + } + + public boolean isUpdateAvailable() { + return isUpdateAvailable(latestVersion); + } + + public boolean isAppUpdateAvailable() { + return isUpdateAvailable(latestAppUpdaterVersion); } public ObjectProperty lastSuccessfulUpdateCheckProperty() { diff --git a/src/main/java/org/cryptomator/ui/preferences/UpdatesPreferencesController.java b/src/main/java/org/cryptomator/ui/preferences/UpdatesPreferencesController.java index f5a72290f..3c69db335 100644 --- a/src/main/java/org/cryptomator/ui/preferences/UpdatesPreferencesController.java +++ b/src/main/java/org/cryptomator/ui/preferences/UpdatesPreferencesController.java @@ -2,6 +2,7 @@ package org.cryptomator.ui.preferences; import org.cryptomator.common.Environment; import org.cryptomator.common.settings.Settings; +import org.cryptomator.integrations.update.UpdateFailedException; import org.cryptomator.ui.common.FxController; import org.cryptomator.ui.fxapp.UpdateChecker; @@ -51,6 +52,7 @@ public class UpdatesPreferencesController implements FxController { private final ObservableValue timeDifferenceMessage; private final String currentVersion; private final BooleanBinding updateAvailable; + private final BooleanBinding appUpdateAvailable; private final BooleanBinding checkFailed; private final BooleanProperty upToDateLabelVisible = new SimpleBooleanProperty(false); private final DateTimeFormatter formatter; @@ -73,6 +75,7 @@ public class UpdatesPreferencesController implements FxController { this.timeDifferenceMessage = Bindings.createStringBinding(this::getTimeDifferenceMessage, lastSuccessfulUpdateCheck); this.currentVersion = environment.getAppVersion(); this.updateAvailable = updateChecker.updateAvailableProperty(); + this.appUpdateAvailable = updateChecker.appUpdateAvailableProperty(); 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(); @@ -98,6 +101,11 @@ public class UpdatesPreferencesController implements FxController { updateChecker.checkForUpdatesNow(); } + @FXML + public void updateNow() throws UpdateFailedException { + updateChecker.updateAppNow(); + } + @FXML public void visitDownloadsPage() { application.getHostServices().showDocument(downloadsUri); @@ -174,10 +182,18 @@ public class UpdatesPreferencesController implements FxController { return updateAvailable; } + public BooleanBinding appUdateAvailableProperty() { + return appUpdateAvailable; + } + public boolean isUpdateAvailable() { return updateAvailable.get(); } + public boolean isAppUpdateAvailable() { + return appUpdateAvailable.get(); + } + public BooleanBinding checkFailedProperty() { return checkFailed; } diff --git a/src/main/resources/fxml/preferences_updates.fxml b/src/main/resources/fxml/preferences_updates.fxml index d0910949b..c0141f074 100644 --- a/src/main/resources/fxml/preferences_updates.fxml +++ b/src/main/resources/fxml/preferences_updates.fxml @@ -9,7 +9,6 @@ - @@ -53,5 +52,6 @@ +