diff --git a/main/launcher/src/main/java/org/cryptomator/launcher/CryptomatorModule.java b/main/launcher/src/main/java/org/cryptomator/launcher/CryptomatorModule.java index 4c9971471..8b93a2f08 100644 --- a/main/launcher/src/main/java/org/cryptomator/launcher/CryptomatorModule.java +++ b/main/launcher/src/main/java/org/cryptomator/launcher/CryptomatorModule.java @@ -4,10 +4,10 @@ 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 javax.inject.Named; import javax.inject.Singleton; -import java.nio.file.Path; import java.util.Optional; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; @@ -23,8 +23,8 @@ class CryptomatorModule { @Provides @Singleton - @Named("fileOpenRequests") - BlockingQueue provideFileOpenRequests() { + @Named("launchEventQueue") + BlockingQueue provideFileOpenRequests() { return new ArrayBlockingQueue<>(10); } diff --git a/main/launcher/src/main/java/org/cryptomator/launcher/FileOpenRequestHandler.java b/main/launcher/src/main/java/org/cryptomator/launcher/FileOpenRequestHandler.java index 663f01b3c..535e2767c 100644 --- a/main/launcher/src/main/java/org/cryptomator/launcher/FileOpenRequestHandler.java +++ b/main/launcher/src/main/java/org/cryptomator/launcher/FileOpenRequestHandler.java @@ -13,8 +13,13 @@ import java.nio.file.FileSystem; import java.nio.file.FileSystems; import java.nio.file.InvalidPathException; import java.nio.file.Path; +import java.util.Arrays; +import java.util.Objects; import java.util.concurrent.BlockingQueue; +import java.util.function.Function; +import java.util.stream.Stream; +import org.cryptomator.ui.model.AppLaunchEvent; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -26,11 +31,11 @@ import javax.inject.Singleton; class FileOpenRequestHandler { private static final Logger LOG = LoggerFactory.getLogger(FileOpenRequestHandler.class); - private final BlockingQueue fileOpenRequests; + private final BlockingQueue launchEventQueue; @Inject - public FileOpenRequestHandler(@Named("fileOpenRequests") BlockingQueue fileOpenRequests) { - this.fileOpenRequests = fileOpenRequests; + public FileOpenRequestHandler(@Named("launchEventQueue") BlockingQueue launchEventQueue) { + this.launchEventQueue = launchEventQueue; try { Desktop.getDesktop().setOpenFileHandler(this::openFiles); } catch (UnsupportedOperationException e) { @@ -39,7 +44,9 @@ class FileOpenRequestHandler { } private void openFiles(final OpenFilesEvent evt) { - evt.getFiles().stream().map(File::toPath).forEach(fileOpenRequests::add); + Stream pathsToOpen = evt.getFiles().stream().map(File::toPath); + AppLaunchEvent launchEvent = new AppLaunchEvent(pathsToOpen); + tryToEnqueueFileOpenRequest(launchEvent); } public void handleLaunchArgs(String[] args) { @@ -48,19 +55,22 @@ class FileOpenRequestHandler { // visible for testing void handleLaunchArgs(FileSystem fs, String[] args) { - for (String arg : args) { + Stream pathsToOpen = Arrays.stream(args).map(str -> { try { - Path path = fs.getPath(arg); - tryToEnqueueFileOpenRequest(path); + return fs.getPath(str); } catch (InvalidPathException e) { - LOG.trace("{} not a valid path", arg); + LOG.trace("Argument not a valid path: {}", str); + return null; } - } + }).filter(Objects::nonNull); + AppLaunchEvent launchEvent = new AppLaunchEvent(pathsToOpen); + tryToEnqueueFileOpenRequest(launchEvent); } - private void tryToEnqueueFileOpenRequest(Path path) { - if (!fileOpenRequests.offer(path)) { - LOG.warn("{} could not be enqueued for opening.", path); + + private void tryToEnqueueFileOpenRequest(AppLaunchEvent launchEvent) { + if (!launchEventQueue.offer(launchEvent)) { + LOG.warn("Could not enqueue application launch event.", launchEvent); } } diff --git a/main/launcher/src/test/java/org/cryptomator/launcher/FileOpenRequestHandlerTest.java b/main/launcher/src/test/java/org/cryptomator/launcher/FileOpenRequestHandlerTest.java index 9bf8cd2be..673f23c6d 100644 --- a/main/launcher/src/test/java/org/cryptomator/launcher/FileOpenRequestHandlerTest.java +++ b/main/launcher/src/test/java/org/cryptomator/launcher/FileOpenRequestHandlerTest.java @@ -5,7 +5,13 @@ *******************************************************************************/ package org.cryptomator.launcher; +import org.cryptomator.ui.model.AppLaunchEvent; +import org.hamcrest.CoreMatchers; +import org.hamcrest.MatcherAssert; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.mockito.Mockito; @@ -13,64 +19,55 @@ import java.io.IOException; import java.nio.file.FileSystem; import java.nio.file.InvalidPathException; import java.nio.file.Path; -import java.nio.file.attribute.BasicFileAttributes; -import java.nio.file.spi.FileSystemProvider; +import java.nio.file.Paths; +import java.util.List; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; +import java.util.stream.Collectors; +import java.util.stream.Stream; public class FileOpenRequestHandlerTest { - @Test - public void testOpenArgsWithCorrectPaths() throws IOException { - Path p1 = Mockito.mock(Path.class); - Path p2 = Mockito.mock(Path.class); - FileSystem fs = Mockito.mock(FileSystem.class); - FileSystemProvider provider = Mockito.mock(FileSystemProvider.class); - BasicFileAttributes attrs = Mockito.mock(BasicFileAttributes.class); - Mockito.when(p1.getFileSystem()).thenReturn(fs); - Mockito.when(p2.getFileSystem()).thenReturn(fs); - Mockito.when(fs.provider()).thenReturn(provider); - Mockito.when(fs.getPath(Mockito.anyString())).thenReturn(p1, p2); - Mockito.when(provider.readAttributes(Mockito.any(), Mockito.eq(BasicFileAttributes.class))).thenReturn(attrs); + private FileOpenRequestHandler inTest; + private BlockingQueue queue; - BlockingQueue queue = new ArrayBlockingQueue<>(10); - FileOpenRequestHandler handler = new FileOpenRequestHandler(queue); - handler.handleLaunchArgs(fs, new String[] {"foo", "bar"}); - - Assertions.assertEquals(p1, queue.poll()); - Assertions.assertEquals(p2, queue.poll()); + @BeforeEach + public void setup() { + queue = new ArrayBlockingQueue<>(1); + inTest = new FileOpenRequestHandler(queue); } @Test + @DisplayName("./cryptomator.exe foo bar") + public void testOpenArgsWithCorrectPaths() throws IOException { + inTest.handleLaunchArgs(new String[]{"foo", "bar"}); + + AppLaunchEvent evt = queue.poll(); + Assertions.assertNotNull(evt); + List paths = evt.getPathsToOpen().collect(Collectors.toList()); + MatcherAssert.assertThat(paths, CoreMatchers.hasItems(Paths.get("foo"), Paths.get("bar"))); + } + + @Test + @DisplayName("./cryptomator.exe foo (with 'foo' being an invalid path)") public void testOpenArgsWithIncorrectPaths() throws IOException { FileSystem fs = Mockito.mock(FileSystem.class); - Mockito.when(fs.getPath(Mockito.anyString())).thenThrow(new InvalidPathException("foo", "foo is not a path")); + Mockito.when(fs.getPath("foo")).thenThrow(new InvalidPathException("foo", "foo is not a path")); + inTest.handleLaunchArgs(fs, new String[]{"foo"}); - @SuppressWarnings("unchecked") - BlockingQueue queue = Mockito.mock(BlockingQueue.class); - FileOpenRequestHandler handler = new FileOpenRequestHandler(queue); - handler.handleLaunchArgs(fs, new String[] {"foo"}); - - Mockito.verifyNoMoreInteractions(queue); + AppLaunchEvent evt = queue.poll(); + Assertions.assertNotNull(evt); + List paths = evt.getPathsToOpen().collect(Collectors.toList()); + Assertions.assertTrue(paths.isEmpty()); } @Test + @DisplayName("./cryptomator.exe foo (with full event queue)") public void testOpenArgsWithFullQueue() throws IOException { - Path p = Mockito.mock(Path.class); - FileSystem fs = Mockito.mock(FileSystem.class); - FileSystemProvider provider = Mockito.mock(FileSystemProvider.class); - BasicFileAttributes attrs = Mockito.mock(BasicFileAttributes.class); - Mockito.when(p.getFileSystem()).thenReturn(fs); - Mockito.when(fs.provider()).thenReturn(provider); - Mockito.when(fs.getPath(Mockito.anyString())).thenReturn(p); - Mockito.when(provider.readAttributes(Mockito.eq(p), Mockito.eq(BasicFileAttributes.class))).thenReturn(attrs); - Mockito.when(attrs.isRegularFile()).thenReturn(true); + queue.add(new AppLaunchEvent(Stream.empty())); + Assumptions.assumeTrue(queue.remainingCapacity() == 0); - @SuppressWarnings("unchecked") - BlockingQueue queue = Mockito.mock(BlockingQueue.class); - Mockito.when(queue.offer(Mockito.any())).thenReturn(false); - FileOpenRequestHandler handler = new FileOpenRequestHandler(queue); - handler.handleLaunchArgs(fs, new String[] {"foo"}); + inTest.handleLaunchArgs(new String[]{"foo"}); } } diff --git a/main/ui/src/main/java/org/cryptomator/ui/controllers/MainController.java b/main/ui/src/main/java/org/cryptomator/ui/controllers/MainController.java index c66712818..7fc86867e 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/controllers/MainController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/controllers/MainController.java @@ -50,6 +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.model.AutoUnlocker; import org.cryptomator.ui.model.UpgradeStrategies; import org.cryptomator.ui.model.UpgradeStrategy; @@ -93,7 +94,7 @@ public class MainController implements ViewController { private final ExitUtil exitUtil; private final Localization localization; private final ExecutorService executorService; - private final BlockingQueue fileOpenRequests; + private final BlockingQueue launchEventQueue; private final VaultFactory vaultFactoy; private final ViewControllerLoader viewControllerLoader; private final ObjectProperty activeController = new SimpleObjectProperty<>(); @@ -110,11 +111,11 @@ public class MainController implements ViewController { private Subscription subs = Subscription.EMPTY; @Inject - public MainController(@Named("mainWindow") Stage mainWindow, ExecutorService executorService, @Named("fileOpenRequests") BlockingQueue fileOpenRequests, ExitUtil exitUtil, Localization localization, + public MainController(@Named("mainWindow") Stage mainWindow, ExecutorService executorService, @Named("launchEventQueue") BlockingQueue launchEventQueue, ExitUtil exitUtil, Localization localization, VaultFactory vaultFactoy, ViewControllerLoader viewControllerLoader, UpgradeStrategies upgradeStrategies, VaultList vaults, AutoUnlocker autoUnlocker) { this.mainWindow = mainWindow; this.executorService = executorService; - this.fileOpenRequests = fileOpenRequests; + this.launchEventQueue = launchEventQueue; this.exitUtil = exitUtil; this.localization = localization; this.vaultFactoy = vaultFactoy; @@ -249,12 +250,12 @@ public class MainController implements ViewController { } private void listenToFileOpenRequests(Stage stage) { - Tasks.create(fileOpenRequests::take).onSuccess(path -> { - addVault(path, true); + Tasks.create(launchEventQueue::take).onSuccess(event -> { stage.setIconified(false); stage.show(); stage.toFront(); stage.requestFocus(); + event.getPathsToOpen().forEach(path -> addVault(path, true)); }).schedulePeriodically(executorService, Duration.ZERO, Duration.ZERO); } diff --git a/main/ui/src/main/java/org/cryptomator/ui/model/AppLaunchEvent.java b/main/ui/src/main/java/org/cryptomator/ui/model/AppLaunchEvent.java new file mode 100644 index 000000000..e01cf40da --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/model/AppLaunchEvent.java @@ -0,0 +1,15 @@ +package org.cryptomator.ui.model; + +import java.nio.file.Path; +import java.util.stream.Stream; + +public class AppLaunchEvent { + + private final Stream pathsToOpen; + + public AppLaunchEvent(Stream pathsToOpen) {this.pathsToOpen = pathsToOpen;} + + public Stream getPathsToOpen() { + return pathsToOpen; + } +}