From dab779cbef255f14f39584b84dba363d041ace07 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Wed, 31 Jul 2019 15:47:21 +0200 Subject: [PATCH 1/2] Added new UpdateChecker that checks periodically (fixes #272) and added it to the main window (references #925) --- .../ui/fxapp/FxApplicationModule.java | 6 +- .../cryptomator/ui/fxapp/UpdateChecker.java | 66 +++++++++++++++++++ .../ui/fxapp/UpdateCheckerModule.java | 63 ++++++++++++++++++ .../ui/fxapp/UpdateCheckerTask.java | 60 +++++++++++++++++ .../ui/mainwindow/MainWindowController.java | 21 +++++- .../ui/traymenu/TrayMenuController.java | 1 + .../src/main/resources/fxml/main_window.fxml | 7 +- 7 files changed, 219 insertions(+), 5 deletions(-) create mode 100644 main/ui/src/main/java/org/cryptomator/ui/fxapp/UpdateChecker.java create mode 100644 main/ui/src/main/java/org/cryptomator/ui/fxapp/UpdateCheckerModule.java create mode 100644 main/ui/src/main/java/org/cryptomator/ui/fxapp/UpdateCheckerTask.java diff --git a/main/ui/src/main/java/org/cryptomator/ui/fxapp/FxApplicationModule.java b/main/ui/src/main/java/org/cryptomator/ui/fxapp/FxApplicationModule.java index 6e3c94d9e..de07249e1 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/fxapp/FxApplicationModule.java +++ b/main/ui/src/main/java/org/cryptomator/ui/fxapp/FxApplicationModule.java @@ -19,7 +19,7 @@ import org.cryptomator.ui.unlock.UnlockComponent; import java.util.ResourceBundle; -@Module(includes = {KeychainModule.class}, subcomponents = {MainWindowComponent.class, PreferencesComponent.class, UnlockComponent.class}) +@Module(includes = {KeychainModule.class, UpdateCheckerModule.class}, subcomponents = {MainWindowComponent.class, PreferencesComponent.class, UnlockComponent.class}) abstract class FxApplicationModule { @Binds @@ -31,13 +31,13 @@ abstract class FxApplicationModule { static ObjectProperty provideSelectedVault() { return new SimpleObjectProperty<>(); } - + @Provides @FxApplicationScoped static ResourceBundle provideLocalization() { return ResourceBundle.getBundle("i18n.strings"); } - + @Provides static MainWindowComponent provideMainWindowComponent(MainWindowComponent.Builder builder) { return builder.build(); diff --git a/main/ui/src/main/java/org/cryptomator/ui/fxapp/UpdateChecker.java b/main/ui/src/main/java/org/cryptomator/ui/fxapp/UpdateChecker.java new file mode 100644 index 000000000..53a02130e --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/fxapp/UpdateChecker.java @@ -0,0 +1,66 @@ +package org.cryptomator.ui.fxapp; + +import javafx.beans.property.ReadOnlyStringProperty; +import javafx.beans.property.StringProperty; +import javafx.concurrent.ScheduledService; +import javafx.concurrent.WorkerStateEvent; +import javafx.util.Duration; +import org.cryptomator.common.settings.Settings; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.inject.Inject; +import javax.inject.Named; +import java.util.Comparator; +import java.util.Optional; + +@FxApplicationScoped +public class UpdateChecker { + + private static final Logger LOG = LoggerFactory.getLogger(UpdateChecker.class); + + private final Settings settings; + private final Optional applicationVersion; + private final StringProperty latestVersionProperty; + private final Comparator semVerComparator; + private final ScheduledService updateCheckerService; + + @Inject + UpdateChecker(Settings settings, @Named("applicationVersion") Optional applicationVersion, @Named("latestVersion") StringProperty latestVersionProperty, @Named("SemVer") Comparator semVerComparator, ScheduledService updateCheckerService) { + this.settings = settings; + this.applicationVersion = applicationVersion; + this.latestVersionProperty = latestVersionProperty; + this.semVerComparator = semVerComparator; + this.updateCheckerService = updateCheckerService; + } + + public void startCheckingForUpdates(Duration initialDelay) { + updateCheckerService.setDelay(initialDelay); + updateCheckerService.setOnSucceeded(this::checkSucceeded); + updateCheckerService.setOnFailed(this::checkFailed); + updateCheckerService.restart(); + } + + public ReadOnlyStringProperty latestVersionProperty() { + return latestVersionProperty; + } + + private void checkSucceeded(WorkerStateEvent event) { + String currentVersion = applicationVersion.orElse(null); + String latestVersion = updateCheckerService.getValue(); + LOG.info("Current version: {}, lastest version: {}", currentVersion, latestVersion); + + // TODO settings.lastVersionCheck = Instant.now() + if (currentVersion != null && semVerComparator.compare(currentVersion, latestVersion) < 0) { + // update is available! + latestVersionProperty.set(latestVersion); + } else { + latestVersionProperty.set(null); + } + } + + private void checkFailed(WorkerStateEvent event) { + LOG.warn("Error checking for updates", event.getSource().getException()); + } + +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/fxapp/UpdateCheckerModule.java b/main/ui/src/main/java/org/cryptomator/ui/fxapp/UpdateCheckerModule.java new file mode 100644 index 000000000..c5ee4ff5c --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/fxapp/UpdateCheckerModule.java @@ -0,0 +1,63 @@ +package org.cryptomator.ui.fxapp; + +import dagger.Module; +import dagger.Provides; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; +import javafx.concurrent.ScheduledService; +import javafx.concurrent.Task; +import javafx.util.Duration; +import org.apache.commons.lang3.SystemUtils; + +import javax.inject.Named; +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 URI LATEST_VERSION_URI = URI.create("https://api.cryptomator.org/updates/latestVersion.json"); + private static final Duration UPDATE_CHECK_INTERVAL = Duration.hours(3); + + @Provides + @Named("latestVersion") + @FxApplicationScoped + static StringProperty provideLatestVersion() { + return new SimpleStringProperty(); + } + + @Provides + @FxApplicationScoped + static HttpClient providesHttpClient() { + return HttpClient.newHttpClient(); + } + + @Provides + @FxApplicationScoped + static HttpRequest providesCheckForUpdatesRequest(@Named("applicationVersion") Optional applicationVersion) { + String userAgent = String.format("Cryptomator VersionChecker/%s %s %s (%s)", applicationVersion.orElse("SNAPSHOT"), SystemUtils.OS_NAME, SystemUtils.OS_VERSION, SystemUtils.OS_ARCH); + return HttpRequest.newBuilder() // + .uri(LATEST_VERSION_URI) // + .header("User-Agent", userAgent) + .build(); + } + + @Provides + @FxApplicationScoped + static ScheduledService provideCheckForUpdatesService(ExecutorService executor, HttpClient httpClient, HttpRequest checkForUpdatesRequest) { + ScheduledService service = new ScheduledService<>() { + @Override + protected Task createTask() { + return new UpdateCheckerTask(httpClient, checkForUpdatesRequest); + } + }; + service.setExecutor(executor); + service.setPeriod(UPDATE_CHECK_INTERVAL); + return service; + } + + +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/fxapp/UpdateCheckerTask.java b/main/ui/src/main/java/org/cryptomator/ui/fxapp/UpdateCheckerTask.java new file mode 100644 index 000000000..461a04fc6 --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/fxapp/UpdateCheckerTask.java @@ -0,0 +1,60 @@ +package org.cryptomator.ui.fxapp; + +import com.google.common.io.ByteStreams; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.reflect.TypeToken; +import javafx.concurrent.Task; +import org.apache.commons.lang3.SystemUtils; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.util.Map; + +public class UpdateCheckerTask extends Task { + + private static final long MAX_RESPONSE_SIZE = 10 * 1024; // 10kb should be sufficient. protect against flooding + private static final Gson GSON = new GsonBuilder().setLenient().create(); + + private final HttpClient httpClient; + private final HttpRequest checkForUpdatesRequest; + + UpdateCheckerTask(HttpClient httpClient, HttpRequest checkForUpdatesRequest) { + this.httpClient = httpClient; + this.checkForUpdatesRequest = checkForUpdatesRequest; + } + + @Override + protected String call() throws Exception { + 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); // + Reader reader = new InputStreamReader(limitedIn, StandardCharsets.UTF_8)) { + Map map = GSON.fromJson(reader, new TypeToken>() { + }.getType()); + if (SystemUtils.IS_OS_MAC_OSX) { + return map.get("mac"); + } else if (SystemUtils.IS_OS_WINDOWS) { + return map.get("win"); + } else if (SystemUtils.IS_OS_LINUX) { + return map.get("linux"); + } else { + throw new IllegalStateException("Unsupported operating system"); + } + } + } +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/mainwindow/MainWindowController.java b/main/ui/src/main/java/org/cryptomator/ui/mainwindow/MainWindowController.java index 5ecb6208c..50839a110 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/mainwindow/MainWindowController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/mainwindow/MainWindowController.java @@ -1,11 +1,14 @@ package org.cryptomator.ui.mainwindow; +import javafx.beans.binding.BooleanBinding; import javafx.fxml.FXML; import javafx.scene.layout.HBox; import javafx.scene.layout.Region; import javafx.stage.Stage; +import javafx.util.Duration; import org.cryptomator.ui.fxapp.FxApplication; import org.cryptomator.ui.common.FxController; +import org.cryptomator.ui.fxapp.UpdateChecker; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -17,20 +20,25 @@ import java.util.concurrent.CountDownLatch; public class MainWindowController implements FxController { private static final Logger LOG = LoggerFactory.getLogger(MainWindowController.class); + private static final Duration CHECK_FOR_UPDATES_DELAY = Duration.seconds(5); private final Stage window; private final FxApplication application; private final boolean minimizeToSysTray; + private final UpdateChecker updateChecker; + private final BooleanBinding updateAvailable; public HBox titleBar; public Region resizer; private double xOffset; private double yOffset; @Inject - public MainWindowController(@MainWindow Stage window, FxApplication application, @Named("trayMenuSupported") boolean minimizeToSysTray) { + public MainWindowController(@MainWindow Stage window, FxApplication application, @Named("trayMenuSupported") boolean minimizeToSysTray, UpdateChecker updateChecker) { this.window = window; this.application = application; this.minimizeToSysTray = minimizeToSysTray; + this.updateChecker = updateChecker; + this.updateAvailable = updateChecker.latestVersionProperty().isNotNull(); } @FXML @@ -49,6 +57,7 @@ public class MainWindowController implements FxController { window.setWidth(event.getSceneX()); window.setHeight(event.getSceneY()); }); + updateChecker.startCheckingForUpdates(CHECK_FOR_UPDATES_DELAY); } @FXML @@ -64,4 +73,14 @@ public class MainWindowController implements FxController { public void showPreferences() { application.showPreferencesWindow(); } + + /* Getter/Setter */ + + public BooleanBinding updateAvailableProperty() { + return updateAvailable; + } + + public boolean isUpdateAvailable() { + return updateAvailable.get(); + } } diff --git a/main/ui/src/main/java/org/cryptomator/ui/traymenu/TrayMenuController.java b/main/ui/src/main/java/org/cryptomator/ui/traymenu/TrayMenuController.java index 3402ea97f..330f1c2a6 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/traymenu/TrayMenuController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/traymenu/TrayMenuController.java @@ -48,6 +48,7 @@ class TrayMenuController { // show window on start? if (!settings.startHidden().get()) { + // TODO: schedule async to not delay tray menu initialization showMainWindow(null); } } diff --git a/main/ui/src/main/resources/fxml/main_window.fxml b/main/ui/src/main/resources/fxml/main_window.fxml index 819bccb53..b0587daa2 100644 --- a/main/ui/src/main/resources/fxml/main_window.fxml +++ b/main/ui/src/main/resources/fxml/main_window.fxml @@ -10,6 +10,7 @@ +