Forráskód Böngészése

Added new UpdateChecker that checks periodically (fixes #272) and added it to the main window (references #925)

Sebastian Stenzel 6 éve
szülő
commit
dab779cbef

+ 3 - 3
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<Vault> provideSelectedVault() {
 		return new SimpleObjectProperty<>();
 	}
-	
+
 	@Provides
 	@FxApplicationScoped
 	static ResourceBundle provideLocalization() {
 		return ResourceBundle.getBundle("i18n.strings");
 	}
-	
+
 	@Provides
 	static MainWindowComponent provideMainWindowComponent(MainWindowComponent.Builder builder) {
 		return builder.build();

+ 66 - 0
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<String> applicationVersion;
+	private final StringProperty latestVersionProperty;
+	private final Comparator<String> semVerComparator;
+	private final ScheduledService<String> updateCheckerService;
+
+	@Inject
+	UpdateChecker(Settings settings, @Named("applicationVersion") Optional<String> applicationVersion, @Named("latestVersion") StringProperty latestVersionProperty, @Named("SemVer") Comparator<String> semVerComparator, ScheduledService<String> 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());
+	}
+
+}

+ 63 - 0
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<String> 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<String> provideCheckForUpdatesService(ExecutorService executor, HttpClient httpClient, HttpRequest checkForUpdatesRequest) {
+		ScheduledService<String> service = new ScheduledService<>() {
+			@Override
+			protected Task<String> createTask() {
+				return new UpdateCheckerTask(httpClient, checkForUpdatesRequest);
+			}
+		};
+		service.setExecutor(executor);
+		service.setPeriod(UPDATE_CHECK_INTERVAL);
+		return service;
+	}
+
+
+}

+ 60 - 0
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<String> {
+
+	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<InputStream> 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<InputStream> 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<String, String> map = GSON.fromJson(reader, new TypeToken<Map<String, String>>() {
+			}.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");
+			}
+		}
+	}
+}

+ 20 - 1
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();
+	}
 }

+ 1 - 0
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);
 		}
 	}

+ 6 - 1
main/ui/src/main/resources/fxml/main_window.fxml

@@ -10,6 +10,7 @@
 <?import javafx.scene.layout.Region?>
 <?import javafx.scene.layout.StackPane?>
 <?import javafx.scene.layout.VBox?>
+<?import javafx.scene.shape.Circle?>
 <VBox xmlns="http://javafx.com/javafx"
 	  xmlns:fx="http://javafx.com/fxml"
 	  fx:controller="org.cryptomator.ui.mainwindow.MainWindowController"
@@ -23,7 +24,11 @@
 			<Region HBox.hgrow="ALWAYS"/>
 			<Button contentDisplay="GRAPHIC_ONLY" mnemonicParsing="false" onAction="#showPreferences">
 				<graphic>
-					<FontAwesomeIconView styleClass="fa-icon" glyphName="COGS"/>
+					<StackPane>
+						<FontAwesomeIconView styleClass="fa-icon" glyphName="COGS"/>
+						<!-- TODO style: -->
+						<Circle visible="${controller.updateAvailable}" StackPane.alignment="TOP_RIGHT" styleClass="update-indicator" fill="red" radius="4"/>
+					</StackPane>
 				</graphic>
 				<tooltip>
 					<Tooltip text="%main.settingsBtn.tooltip"/>