Handling launch events (such as doubleclicking a .cryptomator file)

Also refactored dagger component graph during launch
This commit is contained in:
Sebastian Stenzel
2019-09-11 16:44:41 +02:00
parent 2cf97b5f77
commit 161a4cd511
18 changed files with 227 additions and 91 deletions
@@ -8,7 +8,7 @@ package org.cryptomator.launcher;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.logging.DebugMode;
import org.cryptomator.logging.LoggerConfiguration;
import org.cryptomator.ui.traymenu.TrayMenuComponent;
import org.cryptomator.ui.launcher.UiLauncher;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -33,17 +33,17 @@ public class Cryptomator {
private final Optional<String> applicationVersion;
private final CountDownLatch shutdownLatch;
private final CleanShutdownPerformer shutdownPerformer;
private final TrayMenuComponent.Builder trayComponent;
private final UiLauncher uiLauncher;
@Inject
Cryptomator(LoggerConfiguration logConfig, DebugMode debugMode, IpcFactory ipcFactory, @Named("applicationVersion") Optional<String> applicationVersion, @Named("shutdownLatch") CountDownLatch shutdownLatch, CleanShutdownPerformer shutdownPerformer, TrayMenuComponent.Builder trayComponent) {
Cryptomator(LoggerConfiguration logConfig, DebugMode debugMode, IpcFactory ipcFactory, @Named("applicationVersion") Optional<String> applicationVersion, @Named("shutdownLatch") CountDownLatch shutdownLatch, CleanShutdownPerformer shutdownPerformer, UiLauncher uiLauncher) {
this.logConfig = logConfig;
this.debugMode = debugMode;
this.ipcFactory = ipcFactory;
this.applicationVersion = applicationVersion;
this.shutdownLatch = shutdownLatch;
this.shutdownPerformer = shutdownPerformer;
this.trayComponent = trayComponent;
this.uiLauncher = uiLauncher;
}
public static void main(String[] args) {
@@ -69,6 +69,7 @@ public class Cryptomator {
try (IpcFactory.IpcEndpoint endpoint = ipcFactory.create()) {
endpoint.getRemote().handleLaunchArgs(args); // if we are the server, getRemote() returns self.
if (endpoint.isConnectedToRemote()) {
endpoint.getRemote().revealRunningApp();
LOG.info("Found running application instance. Shutting down...");
return 2;
} else {
@@ -90,7 +91,7 @@ public class Cryptomator {
private int runGuiApplication() {
try {
shutdownPerformer.registerShutdownHook();
trayComponent.build().addIconToSystemTray();
uiLauncher.launch();
shutdownLatch.await();
LOG.info("UI shut down");
return 0;
@@ -3,12 +3,12 @@ package org.cryptomator.launcher;
import dagger.Component;
import org.cryptomator.common.CommonsModule;
import org.cryptomator.logging.LoggerModule;
import org.cryptomator.ui.fxapp.FxApplicationComponent;
import org.cryptomator.ui.launcher.UiLauncherModule;
import javax.inject.Singleton;
@Singleton
@Component(modules = {CryptomatorModule.class, CommonsModule.class, LoggerModule.class})
@Component(modules = {CryptomatorModule.class, CommonsModule.class, LoggerModule.class, UiLauncherModule.class})
public interface CryptomatorComponent {
Cryptomator application();
@@ -2,20 +2,14 @@ package org.cryptomator.launcher;
import dagger.Module;
import dagger.Provides;
import org.cryptomator.common.settings.Settings;
import org.cryptomator.common.settings.SettingsProvider;
import org.cryptomator.ui.model.AppLaunchEvent;
import org.cryptomator.ui.traymenu.TrayMenuComponent;
import javax.inject.Named;
import javax.inject.Singleton;
import java.util.Optional;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CountDownLatch;
import java.util.function.Consumer;
@Module(subcomponents = {TrayMenuComponent.class})
@Module
class CryptomatorModule {
@Provides
@@ -32,13 +26,6 @@ class CryptomatorModule {
return new CountDownLatch(1);
}
@Provides
@Singleton
@Named("launchEventQueue")
static BlockingQueue<AppLaunchEvent> provideFileOpenRequests() {
return new ArrayBlockingQueue<>(10);
}
@Provides
@Singleton
@Named("applicationVersion")
@@ -6,7 +6,7 @@
*******************************************************************************/
package org.cryptomator.launcher;
import org.cryptomator.ui.model.AppLaunchEvent;
import org.cryptomator.ui.launcher.AppLaunchEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -39,9 +39,9 @@ class FileOpenRequestHandler {
}
}
private void openFiles(final OpenFilesEvent evt) {
private void openFiles(OpenFilesEvent evt) {
Stream<Path> pathsToOpen = evt.getFiles().stream().map(File::toPath);
AppLaunchEvent launchEvent = new AppLaunchEvent(pathsToOpen);
AppLaunchEvent launchEvent = new AppLaunchEvent(AppLaunchEvent.EventType.OPEN_FILE, pathsToOpen);
tryToEnqueueFileOpenRequest(launchEvent);
}
@@ -59,7 +59,7 @@ class FileOpenRequestHandler {
return null;
}
}).filter(Objects::nonNull);
AppLaunchEvent launchEvent = new AppLaunchEvent(pathsToOpen);
AppLaunchEvent launchEvent = new AppLaunchEvent(AppLaunchEvent.EventType.OPEN_FILE, pathsToOpen);
tryToEnqueueFileOpenRequest(launchEvent);
}
@@ -10,6 +10,8 @@ import java.rmi.RemoteException;
interface IpcProtocol extends Remote {
void handleLaunchArgs(String[] args) throws RemoteException;
void revealRunningApp() throws RemoteException;
void handleLaunchArgs(String... args) throws RemoteException;
}
@@ -1,11 +1,15 @@
package org.cryptomator.launcher;
import org.cryptomator.ui.launcher.AppLaunchEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import java.util.Arrays;
import java.util.concurrent.BlockingQueue;
import java.util.stream.Stream;
@Singleton
class IpcProtocolImpl implements IpcProtocol {
@@ -13,15 +17,22 @@ class IpcProtocolImpl implements IpcProtocol {
private static final Logger LOG = LoggerFactory.getLogger(IpcProtocolImpl.class);
private final FileOpenRequestHandler fileOpenRequestHandler;
private final BlockingQueue<AppLaunchEvent> launchEventQueue;
@Inject
public IpcProtocolImpl(FileOpenRequestHandler fileOpenRequestHandler) {
public IpcProtocolImpl(FileOpenRequestHandler fileOpenRequestHandler, @Named("launchEventQueue") BlockingQueue<AppLaunchEvent> launchEventQueue) {
this.fileOpenRequestHandler = fileOpenRequestHandler;
this.launchEventQueue = launchEventQueue;
}
@Override
public void handleLaunchArgs(String[] args) {
LOG.info("Received launch args: {}", Arrays.stream(args).reduce((a, b) -> a + ", " + b).orElse(""));
public void revealRunningApp() {
launchEventQueue.add(new AppLaunchEvent(AppLaunchEvent.EventType.REVEAL_APP, Stream.empty()));
}
@Override
public void handleLaunchArgs(String... args) {
LOG.debug("Received launch args: {}", Arrays.stream(args).reduce((a, b) -> a + ", " + b).orElse(""));
fileOpenRequestHandler.handleLaunchArgs(args);
}
@@ -5,7 +5,7 @@
*******************************************************************************/
package org.cryptomator.launcher;
import org.cryptomator.ui.model.AppLaunchEvent;
import org.cryptomator.ui.launcher.AppLaunchEvent;
import org.hamcrest.CoreMatchers;
import org.hamcrest.MatcherAssert;
import org.junit.jupiter.api.Assertions;
@@ -64,7 +64,7 @@ public class FileOpenRequestHandlerTest {
@Test
@DisplayName("./cryptomator.exe foo (with full event queue)")
public void testOpenArgsWithFullQueue() throws IOException {
queue.add(new AppLaunchEvent(Stream.empty()));
queue.add(new AppLaunchEvent(AppLaunchEvent.EventType.OPEN_FILE, Stream.empty()));
Assumptions.assumeTrue(queue.remainingCapacity() == 0);
inTest.handleLaunchArgs(new String[]{"foo"});
@@ -50,7 +50,7 @@ import org.cryptomator.common.settings.VaultSettings;
import org.cryptomator.ui.ExitUtil;
import org.cryptomator.ui.controls.DirectoryListCell;
import org.cryptomator.ui.l10n.Localization;
import org.cryptomator.ui.model.AppLaunchEvent;
import org.cryptomator.ui.launcher.AppLaunchEvent;
import org.cryptomator.ui.model.AutoUnlocker;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultFactory;
@@ -89,15 +89,12 @@ public class FxApplication extends Application {
});
}
public CompletionStage<Stage> showMainWindow() {
CompletableFuture<Stage> future = new CompletableFuture<>();
public void showMainWindow() {
Platform.runLater(() -> {
Stage stage = mainWindow.get().showMainWindow();
addVisibleStage(stage);
LOG.debug("Showing MainWindow");
future.complete(stage);
});
return future;
}
public void showUnlockWindow(Vault vault) {
@@ -0,0 +1,25 @@
package org.cryptomator.ui.launcher;
import java.nio.file.Path;
import java.util.stream.Stream;
public class AppLaunchEvent {
private final Stream<Path> pathsToOpen;
private final EventType type;
public enum EventType {REVEAL_APP, OPEN_FILE}
public AppLaunchEvent(EventType type, Stream<Path> pathsToOpen) {
this.type = type;
this.pathsToOpen = pathsToOpen;
}
public EventType getType() {
return type;
}
public Stream<Path> getPathsToOpen() {
return pathsToOpen;
}
}
@@ -0,0 +1,84 @@
package org.cryptomator.ui.launcher;
import javafx.application.Platform;
import javafx.collections.ObservableList;
import org.cryptomator.common.settings.VaultSettings;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultFactory;
import org.cryptomator.ui.fxapp.FxApplication;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import java.nio.file.Path;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
@Singleton
class AppLaunchEventHandler {
private static final Logger LOG = LoggerFactory.getLogger(AppLaunchEventHandler.class);
private final BlockingQueue<AppLaunchEvent> launchEventQueue;
private final ExecutorService executorService;
private final FxApplicationStarter fxApplicationStarter;
private final VaultFactory vaultFactory;
private final ObservableList<Vault> vaults;
@Inject
public AppLaunchEventHandler(@Named("launchEventQueue") BlockingQueue<AppLaunchEvent> launchEventQueue, ExecutorService executorService, FxApplicationStarter fxApplicationStarter, VaultFactory vaultFactory, ObservableList<Vault> vaults) {
this.launchEventQueue = launchEventQueue;
this.executorService = executorService;
this.fxApplicationStarter = fxApplicationStarter;
this.vaultFactory = vaultFactory;
this.vaults = vaults;
}
public void startHandlingLaunchEvents(boolean hasTrayIcon) {
executorService.submit(() -> handleLaunchEvents(hasTrayIcon));
}
private void handleLaunchEvents(boolean hasTrayIcon) {
try {
while (!Thread.interrupted()) {
AppLaunchEvent event = launchEventQueue.take();
handleLaunchEvent(hasTrayIcon, event);
}
} catch (InterruptedException e) {
LOG.warn("Interrupted launch event handler.");
Thread.currentThread().interrupt();
}
}
private void handleLaunchEvent(boolean hasTrayIcon, AppLaunchEvent event) {
switch (event.getType()) {
case REVEAL_APP:
fxApplicationStarter.get(hasTrayIcon).thenAccept(FxApplication::showMainWindow);
break;
case OPEN_FILE:
fxApplicationStarter.get(hasTrayIcon).thenRun(() -> {
Platform.runLater(() -> {
event.getPathsToOpen().forEach(this::addVault);
});
});
break;
default:
LOG.warn("Unsupported event type: {}", event.getType());
break;
}
}
// TODO dedup MainWindowController...
private void addVault(Path potentialVaultPath) {
assert Platform.isFxApplicationThread();
// TODO CryptoFileSystemProvider.containsVault(potentialVaultPath, "masterkey.cryptomator");
VaultSettings settings = VaultSettings.withRandomId();
settings.path().set(potentialVaultPath);
Vault vault = vaultFactory.get(settings);
vaults.add(vault);
LOG.debug("Added vault {}", potentialVaultPath);
}
}
@@ -1,4 +1,4 @@
package org.cryptomator.ui.traymenu;
package org.cryptomator.ui.launcher;
import javafx.application.Platform;
import org.cryptomator.ui.fxapp.FxApplication;
@@ -7,11 +7,12 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.ExecutorService;
@TrayMenuScoped
@Singleton
public class FxApplicationStarter {
private static final Logger LOG = LoggerFactory.getLogger(FxApplicationStarter.class);
@@ -27,20 +28,20 @@ public class FxApplicationStarter {
this.future = new CompletableFuture<>();
}
public synchronized CompletionStage<FxApplication> get(boolean fromTrayMenu) {
public synchronized CompletionStage<FxApplication> get(boolean hasTrayIcon) {
if (!future.isDone()) {
start(fromTrayMenu);
start(hasTrayIcon);
}
return future;
}
private void start(boolean fromTrayMenu) {
private void start(boolean hasTrayIcon) {
executor.submit(() -> {
LOG.debug("Starting JavaFX runtime...");
Platform.startup(() -> {
assert Platform.isFxApplicationThread();
LOG.info("JavaFX Runtime started.");
FxApplication app = fxAppComponent.trayMenuSupported(fromTrayMenu).build().application();
FxApplication app = fxAppComponent.trayMenuSupported(hasTrayIcon).build().application();
app.start();
future.complete(app);
});
@@ -0,0 +1,42 @@
package org.cryptomator.ui.launcher;
import org.cryptomator.common.settings.Settings;
import org.cryptomator.ui.fxapp.FxApplication;
import org.cryptomator.ui.traymenu.TrayMenuComponent;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.awt.SystemTray;
@Singleton
public class UiLauncher {
private final Settings settings;
private final TrayMenuComponent.Builder trayComponent;
private final FxApplicationStarter fxApplicationStarter;
private final AppLaunchEventHandler launchEventHandler;
@Inject
public UiLauncher(Settings settings, TrayMenuComponent.Builder trayComponent, FxApplicationStarter fxApplicationStarter, AppLaunchEventHandler launchEventHandler) {
this.settings = settings;
this.trayComponent = trayComponent;
this.fxApplicationStarter = fxApplicationStarter;
this.launchEventHandler = launchEventHandler;
}
public void launch() {
boolean hasTrayIcon = false;
if (SystemTray.isSupported()) {
trayComponent.build().addIconToSystemTray();
hasTrayIcon = true;
}
// show window on start?
if (!settings.startHidden().get()) {
fxApplicationStarter.get(hasTrayIcon).thenAccept(FxApplication::showMainWindow);
}
launchEventHandler.startHandlingLaunchEvents(hasTrayIcon);
}
}
@@ -0,0 +1,31 @@
package org.cryptomator.ui.launcher;
import dagger.Module;
import dagger.Provides;
import org.cryptomator.common.JniModule;
import org.cryptomator.ui.fxapp.FxApplicationComponent;
import org.cryptomator.ui.traymenu.TrayMenuComponent;
import javax.inject.Named;
import javax.inject.Singleton;
import java.util.ResourceBundle;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
@Module(includes = {JniModule.class}, subcomponents = {TrayMenuComponent.class, FxApplicationComponent.class})
public abstract class UiLauncherModule {
@Provides
@Singleton
static ResourceBundle provideLocalization() {
return ResourceBundle.getBundle("i18n.strings");
}
@Provides
@Singleton
@Named("launchEventQueue")
static BlockingQueue<AppLaunchEvent> provideFileOpenRequests() {
return new ArrayBlockingQueue<>(10);
}
}
@@ -1,15 +0,0 @@
package org.cryptomator.ui.model;
import java.nio.file.Path;
import java.util.stream.Stream;
public class AppLaunchEvent {
private final Stream<Path> pathsToOpen;
public AppLaunchEvent(Stream<Path> pathsToOpen) {this.pathsToOpen = pathsToOpen;}
public Stream<Path> getPathsToOpen() {
return pathsToOpen;
}
}
@@ -6,25 +6,18 @@
package org.cryptomator.ui.traymenu;
import dagger.Subcomponent;
import org.cryptomator.ui.fxapp.FxApplication;
import java.awt.SystemTray;
@TrayMenuScoped
@Subcomponent(modules = {TrayMenuModule.class})
@Subcomponent
public interface TrayMenuComponent {
TrayIconController trayIconController();
FxApplicationStarter fxAppStarter();
default void addIconToSystemTray() {
if (SystemTray.isSupported()) {
trayIconController().initializeTrayIcon();
} else {
// show main window directly without any tray support:
fxAppStarter().get(false).thenAccept(FxApplication::showMainWindow);
}
assert SystemTray.isSupported();
trayIconController().initializeTrayIcon();
}
@Subcomponent.Builder
@@ -7,6 +7,7 @@ import org.cryptomator.common.settings.Settings;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultState;
import org.cryptomator.ui.fxapp.FxApplication;
import org.cryptomator.ui.launcher.FxApplicationStarter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -75,11 +76,6 @@ class TrayMenuController {
if (Desktop.getDesktop().isSupported(Desktop.Action.APP_SUDDEN_TERMINATION)) {
Desktop.getDesktop().enableSuddenTermination();
}
// show window on start?
if (!settings.startHidden().get()) {
showMainWindow(null);
}
}
private void vaultListChanged(@SuppressWarnings("unused") Observable observable) {
@@ -1,19 +0,0 @@
package org.cryptomator.ui.traymenu;
import dagger.Module;
import dagger.Provides;
import org.cryptomator.common.JniModule;
import org.cryptomator.ui.fxapp.FxApplicationComponent;
import java.util.ResourceBundle;
@Module(includes = {JniModule.class}, subcomponents = {FxApplicationComponent.class})
abstract class TrayMenuModule {
@Provides
@TrayMenuScoped
static ResourceBundle provideLocalization() {
return ResourceBundle.getBundle("i18n.strings");
}
}