diff --git a/pom.xml b/pom.xml index b682f693d..36e19df5b 100644 --- a/pom.xml +++ b/pom.xml @@ -28,10 +28,10 @@ 2.4.1 - 1.1.0-beta1 - 1.0.0 - 1.0.0 - 1.0.1 + 1.1.0 + 1.1.0 + 1.1.0 + 1.1.0 1.3.3 1.3.3 1.2.7 diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 90125d7cc..dd3d7680b 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -1,7 +1,5 @@ -import org.cryptomator.integrations.autostart.AutoStartProvider; -import org.cryptomator.integrations.keychain.KeychainAccessProvider; -import org.cryptomator.integrations.tray.TrayIntegrationProvider; -import org.cryptomator.integrations.uiappearance.UiAppearanceProvider; +import org.cryptomator.integrations.tray.TrayMenuController; +import org.cryptomator.ui.traymenu.AwtTrayMenuController; module org.cryptomator.desktop { requires static org.jetbrains.annotations; @@ -31,10 +29,8 @@ module org.cryptomator.desktop { requires logback.classic; requires logback.core; - uses AutoStartProvider; - uses KeychainAccessProvider; - uses TrayIntegrationProvider; - uses UiAppearanceProvider; + exports org.cryptomator.ui.traymenu to org.cryptomator.integrations.api; + provides TrayMenuController with AwtTrayMenuController; opens org.cryptomator.common.settings to com.google.gson; diff --git a/src/main/java/org/cryptomator/common/PluginClassLoader.java b/src/main/java/org/cryptomator/common/PluginClassLoader.java deleted file mode 100644 index 16932923b..000000000 --- a/src/main/java/org/cryptomator/common/PluginClassLoader.java +++ /dev/null @@ -1,66 +0,0 @@ -package org.cryptomator.common; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.inject.Inject; -import javax.inject.Singleton; -import java.io.IOException; -import java.net.MalformedURLException; -import java.net.URL; -import java.net.URLClassLoader; -import java.nio.file.FileVisitOption; -import java.nio.file.FileVisitResult; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.SimpleFileVisitor; -import java.nio.file.attribute.BasicFileAttributes; -import java.util.ArrayList; -import java.util.EnumSet; -import java.util.List; - -@Singleton -public class PluginClassLoader extends URLClassLoader { - - private static final Logger LOG = LoggerFactory.getLogger(PluginClassLoader.class); - private static final String NAME = "PluginClassLoader"; - private static final String JAR_SUFFIX = ".jar"; - - @Inject - public PluginClassLoader(Environment env) { - super(NAME, env.getPluginDir().map(PluginClassLoader::findJars).orElse(new URL[0]), PluginClassLoader.class.getClassLoader()); - } - - private static URL[] findJars(Path path) { - if (!Files.isDirectory(path)) { - return new URL[0]; - } else { - try { - var visitor = new JarVisitor(); - Files.walkFileTree(path, EnumSet.of(FileVisitOption.FOLLOW_LINKS), Integer.MAX_VALUE, visitor); - return visitor.urls.toArray(URL[]::new); - } catch (IOException e) { - LOG.warn("Failed to scan plugin dir " + path, e); - return new URL[0]; - } - } - } - - private static final class JarVisitor extends SimpleFileVisitor { - - private final List urls = new ArrayList<>(); - - @Override - public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { - if (attrs.isRegularFile() && file.getFileName().toString().toLowerCase().endsWith(JAR_SUFFIX)) { - try { - urls.add(file.toUri().toURL()); - } catch (MalformedURLException e) { - LOG.warn("Failed to create URL for jar file {}", file); - } - } - return FileVisitResult.CONTINUE; - } - } - -} diff --git a/src/main/java/org/cryptomator/common/keychain/KeychainManager.java b/src/main/java/org/cryptomator/common/keychain/KeychainManager.java index d6adadfe4..48b0a0ed5 100644 --- a/src/main/java/org/cryptomator/common/keychain/KeychainManager.java +++ b/src/main/java/org/cryptomator/common/keychain/KeychainManager.java @@ -43,12 +43,6 @@ public class KeychainManager implements KeychainAccessProvider { return getClass().getName(); } - @Override - public void storePassphrase(String key, CharSequence passphrase) throws KeychainAccessException { - getKeychainOrFail().storePassphrase(key, passphrase); - setPassphraseStored(key, true); - } - @Override public void storePassphrase(String key, String displayName, CharSequence passphrase) throws KeychainAccessException { getKeychainOrFail().storePassphrase(key, displayName, passphrase); @@ -68,14 +62,6 @@ public class KeychainManager implements KeychainAccessProvider { setPassphraseStored(key, false); } - @Override - public void changePassphrase(String key, CharSequence passphrase) throws KeychainAccessException { - if (isPassphraseStored(key)) { - getKeychainOrFail().changePassphrase(key, passphrase); - setPassphraseStored(key, true); - } - } - @Override public void changePassphrase(String key, String displayName, CharSequence passphrase) throws KeychainAccessException { if (isPassphraseStored(key)) { diff --git a/src/main/java/org/cryptomator/common/keychain/KeychainModule.java b/src/main/java/org/cryptomator/common/keychain/KeychainModule.java index 6356c4966..63749b445 100644 --- a/src/main/java/org/cryptomator/common/keychain/KeychainModule.java +++ b/src/main/java/org/cryptomator/common/keychain/KeychainModule.java @@ -2,42 +2,30 @@ package org.cryptomator.common.keychain; import dagger.Module; import dagger.Provides; -import org.cryptomator.common.PluginClassLoader; import org.cryptomator.common.settings.Settings; import org.cryptomator.integrations.keychain.KeychainAccessProvider; import javax.inject.Singleton; import javafx.beans.binding.Bindings; import javafx.beans.binding.ObjectExpression; -import java.util.ServiceLoader; -import java.util.Set; -import java.util.stream.Collectors; +import java.util.List; @Module public class KeychainModule { @Provides @Singleton - static Set> provideAvailableKeychainAccessProviderFactories(PluginClassLoader classLoader) { - return ServiceLoader.load(KeychainAccessProvider.class, classLoader).stream().collect(Collectors.toUnmodifiableSet()); + static List provideSupportedKeychainAccessProviders() { + return KeychainAccessProvider.get().toList(); } @Provides @Singleton - static Set provideSupportedKeychainAccessProviders(Set> availableFactories) { - return availableFactories.stream() // - .map(ServiceLoader.Provider::get) // - .filter(KeychainAccessProvider::isSupported) // - .collect(Collectors.toUnmodifiableSet()); - } - - @Provides - @Singleton - static ObjectExpression provideKeychainAccessProvider(Settings settings, Set providers) { + static ObjectExpression provideKeychainAccessProvider(Settings settings, List providers) { return Bindings.createObjectBinding(() -> { var selectedProviderClass = settings.keychainProvider().get(); var selectedProvider = providers.stream().filter(provider -> provider.getClass().getName().equals(selectedProviderClass)).findAny(); - var fallbackProvider = providers.stream().findAny().orElse(null); + var fallbackProvider = providers.stream().findFirst().orElse(null); return selectedProvider.orElse(fallbackProvider); }, settings.keychainProvider()); } diff --git a/src/main/java/org/cryptomator/launcher/CryptomatorModule.java b/src/main/java/org/cryptomator/launcher/CryptomatorModule.java index e6aab0309..42e908df2 100644 --- a/src/main/java/org/cryptomator/launcher/CryptomatorModule.java +++ b/src/main/java/org/cryptomator/launcher/CryptomatorModule.java @@ -2,7 +2,6 @@ package org.cryptomator.launcher; import dagger.Module; import dagger.Provides; -import org.cryptomator.common.PluginClassLoader; import org.cryptomator.integrations.autostart.AutoStartProvider; import org.cryptomator.integrations.tray.TrayIntegrationProvider; import org.cryptomator.integrations.uiappearance.UiAppearanceProvider; @@ -12,7 +11,6 @@ import javax.inject.Named; import javax.inject.Singleton; import java.util.Optional; import java.util.ResourceBundle; -import java.util.ServiceLoader; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; @@ -32,25 +30,22 @@ class CryptomatorModule { return new ArrayBlockingQueue<>(10); } - // TODO: still needed after integrations-api 1.1.0? - @Provides @Singleton - static Optional provideAppearanceProvider(PluginClassLoader classLoader) { - return ServiceLoader.load(UiAppearanceProvider.class, classLoader).findFirst(); + static Optional provideAppearanceProvider() { + return UiAppearanceProvider.get(); } @Provides @Singleton - static Optional provideAutostartProvider(PluginClassLoader classLoader) { - return ServiceLoader.load(AutoStartProvider.class, classLoader).findFirst(); + static Optional provideAutostartProvider() { + return AutoStartProvider.get(); } @Provides @Singleton - static Optional provideTrayIntegrationProvider(PluginClassLoader classLoader) { - return ServiceLoader.load(TrayIntegrationProvider.class, classLoader).findFirst(); + static Optional provideTrayIntegrationProvider() { + return TrayIntegrationProvider.get(); } - } diff --git a/src/main/java/org/cryptomator/ui/fxapp/FxApplication.java b/src/main/java/org/cryptomator/ui/fxapp/FxApplication.java index fd9326c3d..3ddb7cba6 100644 --- a/src/main/java/org/cryptomator/ui/fxapp/FxApplication.java +++ b/src/main/java/org/cryptomator/ui/fxapp/FxApplication.java @@ -9,11 +9,6 @@ import org.slf4j.LoggerFactory; import javax.inject.Inject; import javax.inject.Named; import javafx.application.Platform; -import javafx.stage.Stage; -import javafx.stage.StageStyle; -import java.awt.SystemTray; -import java.io.IOException; -import java.io.UncheckedIOException; @FxApplicationScoped public class FxApplication { @@ -49,7 +44,7 @@ public class FxApplication { // init system tray final boolean hasTrayIcon; - if (SystemTray.isSupported() && settings.showTrayIcon().get()) { + if (settings.showTrayIcon().get() && trayMenu.get().isSupported()) { trayMenu.get().initializeTrayIcon(); Platform.setImplicitExit(false); // don't quit when closing all windows hasTrayIcon = true; diff --git a/src/main/java/org/cryptomator/ui/preferences/GeneralPreferencesController.java b/src/main/java/org/cryptomator/ui/preferences/GeneralPreferencesController.java index f2b7ef3b7..d33e919b6 100644 --- a/src/main/java/org/cryptomator/ui/preferences/GeneralPreferencesController.java +++ b/src/main/java/org/cryptomator/ui/preferences/GeneralPreferencesController.java @@ -19,8 +19,8 @@ import javafx.scene.control.ChoiceBox; import javafx.scene.control.ToggleGroup; import javafx.stage.Stage; import javafx.util.StringConverter; +import java.util.List; import java.util.Optional; -import java.util.Set; @PreferencesScoped public class GeneralPreferencesController implements FxController { @@ -32,7 +32,7 @@ public class GeneralPreferencesController implements FxController { private final Optional autoStartProvider; private final Application application; private final Environment environment; - private final Set keychainAccessProviders; + private final List keychainAccessProviders; private final FxApplicationWindows appWindows; public ChoiceBox keychainBackendChoiceBox; public CheckBox startHiddenCheckbox; @@ -41,7 +41,7 @@ public class GeneralPreferencesController implements FxController { public ToggleGroup nodeOrientation; @Inject - GeneralPreferencesController(@PreferencesWindow Stage window, Settings settings, Optional autoStartProvider, Set keychainAccessProviders, Application application, Environment environment, FxApplicationWindows appWindows) { + GeneralPreferencesController(@PreferencesWindow Stage window, Settings settings, Optional autoStartProvider, List keychainAccessProviders, Application application, Environment environment, FxApplicationWindows appWindows) { this.window = window; this.settings = settings; this.autoStartProvider = autoStartProvider; @@ -115,9 +115,9 @@ public class GeneralPreferencesController implements FxController { private static class KeychainProviderClassNameConverter extends StringConverter { - private final Set keychainAccessProviders; + private final List keychainAccessProviders; - public KeychainProviderClassNameConverter(Set keychainAccessProviders) { + public KeychainProviderClassNameConverter(List keychainAccessProviders) { this.keychainAccessProviders = keychainAccessProviders; } diff --git a/src/main/java/org/cryptomator/ui/traymenu/AwtTrayMenuController.java b/src/main/java/org/cryptomator/ui/traymenu/AwtTrayMenuController.java new file mode 100644 index 000000000..79f1dd628 --- /dev/null +++ b/src/main/java/org/cryptomator/ui/traymenu/AwtTrayMenuController.java @@ -0,0 +1,79 @@ +package org.cryptomator.ui.traymenu; + +import org.apache.commons.lang3.SystemUtils; +import org.cryptomator.integrations.common.CheckAvailability; +import org.cryptomator.integrations.common.Priority; +import org.cryptomator.integrations.tray.ActionItem; +import org.cryptomator.integrations.tray.SeparatorItem; +import org.cryptomator.integrations.tray.SubMenuItem; +import org.cryptomator.integrations.tray.TrayMenuController; +import org.cryptomator.integrations.tray.TrayMenuException; +import org.cryptomator.integrations.tray.TrayMenuItem; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.AWTException; +import java.awt.Menu; +import java.awt.MenuItem; +import java.awt.PopupMenu; +import java.awt.SystemTray; +import java.awt.Toolkit; +import java.awt.TrayIcon; +import java.util.List; + +@CheckAvailability +@Priority(Priority.FALLBACK) +public class AwtTrayMenuController implements TrayMenuController { + + private static final Logger LOG = LoggerFactory.getLogger(AwtTrayMenuController.class); + + private final PopupMenu menu = new PopupMenu(); + + @CheckAvailability + public static boolean isAvailable() { + return SystemTray.isSupported(); + } + + @Override + public void showTrayIcon(byte[] rawImageData, Runnable defaultAction, String tooltip) throws TrayMenuException { + var image = Toolkit.getDefaultToolkit().createImage(rawImageData); + var trayIcon = new TrayIcon(image, tooltip, menu); + + trayIcon.setImageAutoSize(true); + if (SystemUtils.IS_OS_WINDOWS) { + trayIcon.addActionListener(evt -> defaultAction.run()); + } + + try { + SystemTray.getSystemTray().add(trayIcon); + LOG.debug("initialized tray icon"); + } catch (AWTException e) { + throw new TrayMenuException("Failed to add icon to system tray.", e); + } + } + + @Override + public void updateTrayMenu(List items) { + menu.removeAll(); + addChildren(menu, items); + } + + private void addChildren(Menu menu, List items) { + for (var item : items) { + // TODO: use Pattern Matching for switch, once available + if (item instanceof ActionItem a) { + var menuItem = new MenuItem(a.title()); + menuItem.addActionListener(evt -> a.action().run()); + menuItem.setEnabled(a.enabled()); + menu.add(menuItem); + } else if (item instanceof SeparatorItem) { + menu.addSeparator(); + } else if (item instanceof SubMenuItem s) { + var submenu = new Menu(s.title()); + addChildren(submenu, s.items()); + menu.add(submenu); + } + } + } + +} diff --git a/src/main/java/org/cryptomator/ui/traymenu/TrayIconController.java b/src/main/java/org/cryptomator/ui/traymenu/TrayIconController.java deleted file mode 100644 index 2c176df76..000000000 --- a/src/main/java/org/cryptomator/ui/traymenu/TrayIconController.java +++ /dev/null @@ -1,51 +0,0 @@ -package org.cryptomator.ui.traymenu; - -import com.google.common.base.Preconditions; -import org.apache.commons.lang3.SystemUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.inject.Inject; -import java.awt.AWTException; -import java.awt.SystemTray; -import java.awt.TrayIcon; - -@TrayMenuScoped -public class TrayIconController { - - private static final Logger LOG = LoggerFactory.getLogger(TrayIconController.class); - - private final TrayMenuController trayMenuController; - private final TrayIcon trayIcon; - private volatile boolean initialized; - - @Inject - TrayIconController(TrayImageFactory imageFactory, TrayMenuController trayMenuController) { - this.trayMenuController = trayMenuController; - this.trayIcon = new TrayIcon(imageFactory.loadImage(), "Cryptomator", trayMenuController.getMenu()); - } - - public synchronized void initializeTrayIcon() throws IllegalStateException { - Preconditions.checkState(!initialized); - - trayIcon.setImageAutoSize(true); - if (SystemUtils.IS_OS_WINDOWS) { - trayIcon.addActionListener(trayMenuController::showMainWindow); - } - - try { - SystemTray.getSystemTray().add(trayIcon); - LOG.debug("initialized tray icon"); - } catch (AWTException e) { - LOG.error("Error adding tray icon", e); - } - - trayMenuController.initTrayMenu(); - - this.initialized = true; - } - - public boolean isInitialized() { - return initialized; - } -} diff --git a/src/main/java/org/cryptomator/ui/traymenu/TrayImageFactory.java b/src/main/java/org/cryptomator/ui/traymenu/TrayImageFactory.java deleted file mode 100644 index aa55ca766..000000000 --- a/src/main/java/org/cryptomator/ui/traymenu/TrayImageFactory.java +++ /dev/null @@ -1,35 +0,0 @@ -package org.cryptomator.ui.traymenu; - -import org.apache.commons.lang3.SystemUtils; -import org.cryptomator.integrations.uiappearance.Theme; -import org.cryptomator.integrations.uiappearance.UiAppearanceProvider; - -import javax.inject.Inject; -import java.awt.Image; -import java.awt.Toolkit; -import java.util.Optional; - -@TrayMenuScoped -class TrayImageFactory { - - private final Optional appearanceProvider; - - @Inject - TrayImageFactory(Optional appearanceProvider) { - this.appearanceProvider = appearanceProvider; - } - - public Image loadImage() { - String resourceName = SystemUtils.IS_OS_MAC_OSX ? getMacResourceName() : getWinOrLinuxResourceName(); - return Toolkit.getDefaultToolkit().getImage(getClass().getResource(resourceName)); - } - - private String getMacResourceName() { - return "/img/tray_icon_mac.png"; - } - - private String getWinOrLinuxResourceName() { - return "/img/tray_icon.png"; - } - -} diff --git a/src/main/java/org/cryptomator/ui/traymenu/TrayMenuBuilder.java b/src/main/java/org/cryptomator/ui/traymenu/TrayMenuBuilder.java new file mode 100644 index 000000000..ea8599b51 --- /dev/null +++ b/src/main/java/org/cryptomator/ui/traymenu/TrayMenuBuilder.java @@ -0,0 +1,151 @@ +package org.cryptomator.ui.traymenu; + +import com.google.common.base.Preconditions; +import org.apache.commons.lang3.SystemUtils; +import org.cryptomator.common.vaults.Vault; +import org.cryptomator.integrations.tray.ActionItem; +import org.cryptomator.integrations.tray.SeparatorItem; +import org.cryptomator.integrations.tray.SubMenuItem; +import org.cryptomator.integrations.tray.TrayMenuController; +import org.cryptomator.integrations.tray.TrayMenuException; +import org.cryptomator.integrations.tray.TrayMenuItem; +import org.cryptomator.ui.common.VaultService; +import org.cryptomator.ui.fxapp.FxApplicationTerminator; +import org.cryptomator.ui.fxapp.FxApplicationWindows; +import org.cryptomator.ui.preferences.SelectedPreferencesTab; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.inject.Inject; +import javafx.application.Platform; +import javafx.beans.Observable; +import javafx.collections.ObservableList; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.ResourceBundle; + +@TrayMenuScoped +public class TrayMenuBuilder { + + private static final Logger LOG = LoggerFactory.getLogger(TrayMenuBuilder.class); + private static final String TRAY_ICON_MAC = "/img/tray_icon_mac.png"; + private static final String TRAY_ICON = "/img/tray_icon.png"; + + private final ResourceBundle resourceBundle; + private final VaultService vaultService; + private final FxApplicationWindows appWindows; + private final FxApplicationTerminator appTerminator; + private final ObservableList vaults; + private final TrayMenuController trayMenu; + + private volatile boolean initialized; + + @Inject + TrayMenuBuilder(ResourceBundle resourceBundle, VaultService vaultService, FxApplicationWindows appWindows, FxApplicationTerminator appTerminator, ObservableList vaults, Optional trayMenu) { + this.resourceBundle = resourceBundle; + this.vaultService = vaultService; + this.appWindows = appWindows; + this.appTerminator = appTerminator; + this.vaults = vaults; + this.trayMenu = trayMenu.orElse(null); + } + + public synchronized void initTrayMenu() { + Preconditions.checkState(!initialized, "tray icon already initialized"); + + vaults.addListener(this::vaultListChanged); + vaults.forEach(v -> { + v.displayNameProperty().addListener(this::vaultListChanged); + }); + + try (var image = getClass().getResourceAsStream(SystemUtils.IS_OS_MAC_OSX ? TRAY_ICON_MAC : TRAY_ICON)) { + trayMenu.showTrayIcon(image.readAllBytes(), this::showMainWindow, "Cryptomator"); + rebuildMenu(); + initialized = true; + } catch (IOException e) { + throw new UncheckedIOException("Failed to load embedded resource", e); + } catch (TrayMenuException e) { + LOG.error("Adding tray icon failed", e); + } + } + + public boolean isInitialized() { + return initialized; + } + + private void vaultListChanged(@SuppressWarnings("unused") Observable observable) { + assert Platform.isFxApplicationThread(); + rebuildMenu(); + } + + private void rebuildMenu() { + List menu = new ArrayList<>(); + + menu.add(new ActionItem(resourceBundle.getString("traymenu.showMainWindow"), this::showMainWindow)); + menu.add(new ActionItem(resourceBundle.getString("traymenu.showPreferencesWindow"), this::showPreferencesWindow)); + menu.add(new SeparatorItem()); + for (Vault vault : vaults) { + List submenu = buildSubmenu(vault); + var label = vault.isUnlocked() ? "* ".concat(vault.getDisplayName()) : vault.getDisplayName(); + menu.add(new SubMenuItem(label, submenu)); + } + menu.add(new SeparatorItem()); + menu.add(new ActionItem(resourceBundle.getString("traymenu.lockAllVaults"), this::lockAllVaults, vaults.stream().anyMatch(Vault::isUnlocked))); + menu.add(new ActionItem(resourceBundle.getString("traymenu.quitApplication"), this::quitApplication)); + + try { + trayMenu.updateTrayMenu(menu); + } catch (TrayMenuException e) { + LOG.error("Updating tray menu failed", e); + } + } + + private List buildSubmenu(Vault vault) { + if (vault.isLocked()) { + return List.of( // + new ActionItem(resourceBundle.getString("traymenu.vault.unlock"), () -> this.unlockVault(vault)) // + ); + } else if (vault.isUnlocked()) { + return List.of( // + new ActionItem(resourceBundle.getString("traymenu.vault.lock"), () -> this.lockVault(vault)), // + new ActionItem(resourceBundle.getString("traymenu.vault.reveal"), () -> this.revealVault(vault)) // + ); + } else { + return List.of(); + } + } + + /* action listeners: */ + + private void quitApplication() { + appTerminator.terminate(); + } + + private void unlockVault(Vault vault) { + appWindows.startUnlockWorkflow(vault, null); + } + + private void lockVault(Vault vault) { + appWindows.startLockWorkflow(vault, null); + } + + private void lockAllVaults() { + vaultService.lockAll(vaults.filtered(Vault::isUnlocked), false); + } + + private void revealVault(Vault vault) { + vaultService.reveal(vault); + } + + void showMainWindow() { + appWindows.showMainWindow(); + } + + private void showPreferencesWindow() { + appWindows.showPreferencesWindow(SelectedPreferencesTab.ANY); + } + +} diff --git a/src/main/java/org/cryptomator/ui/traymenu/TrayMenuComponent.java b/src/main/java/org/cryptomator/ui/traymenu/TrayMenuComponent.java index c4cbfd456..02bf2aabd 100644 --- a/src/main/java/org/cryptomator/ui/traymenu/TrayMenuComponent.java +++ b/src/main/java/org/cryptomator/ui/traymenu/TrayMenuComponent.java @@ -5,38 +5,44 @@ *******************************************************************************/ package org.cryptomator.ui.traymenu; -import dagger.Lazy; +import com.google.common.base.Preconditions; import dagger.Subcomponent; -import java.awt.SystemTray; +import org.cryptomator.integrations.tray.TrayMenuController; + +import java.util.Optional; @TrayMenuScoped -@Subcomponent +@Subcomponent(modules = {TrayMenuModule.class}) public interface TrayMenuComponent { - Lazy trayIconController(); + Optional trayMenuController(); + + TrayMenuBuilder trayMenuBuilder(); /** * @return true if a tray icon can be installed */ default boolean isSupported() { - return SystemTray.isSupported(); + return trayMenuController().isPresent(); } /** * @return true if a tray icon has been installed */ default boolean isInitialized() { - return isSupported() && trayIconController().get().isInitialized(); + return isSupported() && trayMenuBuilder().isInitialized(); } /** * Installs a tray icon to the system tray. * - * @throws IllegalStateException If already added + * @throws IllegalStateException If not {@link #isSupported() supported} */ default void initializeTrayIcon() throws IllegalStateException { - assert isSupported(); - trayIconController().get().initializeTrayIcon(); + Preconditions.checkState(isSupported(), "system tray not supported"); + if (!trayMenuBuilder().isInitialized()) { + trayMenuBuilder().initTrayMenu(); + } } @Subcomponent.Builder diff --git a/src/main/java/org/cryptomator/ui/traymenu/TrayMenuController.java b/src/main/java/org/cryptomator/ui/traymenu/TrayMenuController.java deleted file mode 100644 index 32f6cbc52..000000000 --- a/src/main/java/org/cryptomator/ui/traymenu/TrayMenuController.java +++ /dev/null @@ -1,141 +0,0 @@ -package org.cryptomator.ui.traymenu; - -import org.cryptomator.common.vaults.Vault; -import org.cryptomator.ui.common.VaultService; -import org.cryptomator.ui.fxapp.FxApplicationTerminator; -import org.cryptomator.ui.fxapp.FxApplicationWindows; -import org.cryptomator.ui.preferences.SelectedPreferencesTab; - -import javax.inject.Inject; -import javafx.application.Platform; -import javafx.beans.Observable; -import javafx.collections.ObservableList; -import java.awt.Menu; -import java.awt.MenuItem; -import java.awt.PopupMenu; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import java.util.EventObject; -import java.util.ResourceBundle; -import java.util.function.Consumer; - -@TrayMenuScoped -class TrayMenuController { - - private final ResourceBundle resourceBundle; - private final VaultService vaultService; - private final FxApplicationWindows appWindows; - private final FxApplicationTerminator appTerminator; - private final ObservableList vaults; - private final PopupMenu menu; - - @Inject - TrayMenuController(ResourceBundle resourceBundle, VaultService vaultService, FxApplicationWindows appWindows, FxApplicationTerminator appTerminator, ObservableList vaults) { - this.resourceBundle = resourceBundle; - this.vaultService = vaultService; - this.appWindows = appWindows; - this.appTerminator = appTerminator; - this.vaults = vaults; - this.menu = new PopupMenu(); - } - - public PopupMenu getMenu() { - return menu; - } - - public void initTrayMenu() { - vaults.addListener(this::vaultListChanged); - vaults.forEach(v -> { - v.displayNameProperty().addListener(this::vaultListChanged); - }); - rebuildMenu(); - } - - private void vaultListChanged(@SuppressWarnings("unused") Observable observable) { - assert Platform.isFxApplicationThread(); - rebuildMenu(); - } - - private void rebuildMenu() { - menu.removeAll(); - - MenuItem showMainWindowItem = new MenuItem(resourceBundle.getString("traymenu.showMainWindow")); - showMainWindowItem.addActionListener(this::showMainWindow); - menu.add(showMainWindowItem); - - MenuItem showPreferencesItem = new MenuItem(resourceBundle.getString("traymenu.showPreferencesWindow")); - showPreferencesItem.addActionListener(this::showPreferencesWindow); - menu.add(showPreferencesItem); - - menu.addSeparator(); - for (Vault v : vaults) { - MenuItem submenu = buildSubmenu(v); - menu.add(submenu); - } - menu.addSeparator(); - - MenuItem lockAllItem = new MenuItem(resourceBundle.getString("traymenu.lockAllVaults")); - lockAllItem.addActionListener(this::lockAllVaults); - lockAllItem.setEnabled(!vaults.filtered(Vault::isUnlocked).isEmpty()); - menu.add(lockAllItem); - - MenuItem quitApplicationItem = new MenuItem(resourceBundle.getString("traymenu.quitApplication")); - quitApplicationItem.addActionListener(this::quitApplication); - menu.add(quitApplicationItem); - } - - private Menu buildSubmenu(Vault vault) { - Menu submenu = new Menu(vault.getDisplayName()); - - if (vault.isLocked()) { - MenuItem unlockItem = new MenuItem(resourceBundle.getString("traymenu.vault.unlock")); - unlockItem.addActionListener(createActionListenerForVault(vault, this::unlockVault)); - submenu.add(unlockItem); - } else if (vault.isUnlocked()) { - submenu.setLabel("* ".concat(submenu.getLabel())); - - MenuItem lockItem = new MenuItem(resourceBundle.getString("traymenu.vault.lock")); - lockItem.addActionListener(createActionListenerForVault(vault, this::lockVault)); - submenu.add(lockItem); - - MenuItem revealItem = new MenuItem(resourceBundle.getString("traymenu.vault.reveal")); - revealItem.addActionListener(createActionListenerForVault(vault, this::revealVault)); - submenu.add(revealItem); - } - - return submenu; - } - - private ActionListener createActionListenerForVault(Vault vault, Consumer consumer) { - return actionEvent -> consumer.accept(vault); - } - - private void quitApplication(EventObject actionEvent) { - appTerminator.terminate(); - } - - private void unlockVault(Vault vault) { - appWindows.startUnlockWorkflow(vault, null); - } - - private void lockVault(Vault vault) { - appWindows.startLockWorkflow(vault, null); - } - - private void lockAllVaults(ActionEvent actionEvent) { - vaultService.lockAll(vaults.filtered(Vault::isUnlocked), false); - } - - private void revealVault(Vault vault) { - vaultService.reveal(vault); - } - - void showMainWindow(@SuppressWarnings("unused") ActionEvent actionEvent) { - appWindows.showMainWindow(); - } - - private void showPreferencesWindow(@SuppressWarnings("unused") EventObject actionEvent) { - appWindows.showPreferencesWindow(SelectedPreferencesTab.ANY); - } - -} diff --git a/src/main/java/org/cryptomator/ui/traymenu/TrayMenuModule.java b/src/main/java/org/cryptomator/ui/traymenu/TrayMenuModule.java new file mode 100644 index 000000000..3110be883 --- /dev/null +++ b/src/main/java/org/cryptomator/ui/traymenu/TrayMenuModule.java @@ -0,0 +1,18 @@ +package org.cryptomator.ui.traymenu; + +import dagger.Module; +import dagger.Provides; +import org.cryptomator.integrations.tray.TrayMenuController; + +import java.util.Optional; + +@Module +public class TrayMenuModule { + + @Provides + @TrayMenuScoped + static Optional provideSupportedKeychainAccessProviders() { + return TrayMenuController.get(); + } + +} diff --git a/src/test/java/org/cryptomator/common/keychain/KeychainManagerTest.java b/src/test/java/org/cryptomator/common/keychain/KeychainManagerTest.java index aa8b6e1f3..abf803e1e 100644 --- a/src/test/java/org/cryptomator/common/keychain/KeychainManagerTest.java +++ b/src/test/java/org/cryptomator/common/keychain/KeychainManagerTest.java @@ -2,17 +2,16 @@ package org.cryptomator.common.keychain; import org.cryptomator.integrations.keychain.KeychainAccessException; -import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import javafx.application.Platform; import javafx.beans.property.ReadOnlyBooleanProperty; import javafx.beans.property.SimpleObjectProperty; +import java.time.Duration; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; @@ -23,7 +22,7 @@ public class KeychainManagerTest { @Test public void testStoreAndLoad() throws KeychainAccessException { KeychainManager keychainManager = new KeychainManager(new SimpleObjectProperty<>(new MapKeychainAccess())); - keychainManager.storePassphrase("test", "asd"); + keychainManager.storePassphrase("test", "Test", "asd"); Assertions.assertArrayEquals("asd".toCharArray(), keychainManager.loadPassphrase("test")); } @@ -42,9 +41,9 @@ public class KeychainManagerTest { public void testPropertyChangesWhenStoringPassword() throws KeychainAccessException, InterruptedException { KeychainManager keychainManager = new KeychainManager(new SimpleObjectProperty<>(new MapKeychainAccess())); ReadOnlyBooleanProperty property = keychainManager.getPassphraseStoredProperty("test"); - Assertions.assertEquals(false, property.get()); + Assertions.assertFalse(property.get()); - keychainManager.storePassphrase("test", "bar"); + keychainManager.storePassphrase("test", null,"bar"); AtomicBoolean result = new AtomicBoolean(false); CountDownLatch latch = new CountDownLatch(1); @@ -52,8 +51,8 @@ public class KeychainManagerTest { result.set(property.get()); latch.countDown(); }); - latch.await(1, TimeUnit.SECONDS); - Assertions.assertEquals(true, result.get()); + Assertions.assertTimeoutPreemptively(Duration.ofSeconds(1), () -> latch.await()); + Assertions.assertTrue(result.get()); } } diff --git a/src/test/java/org/cryptomator/common/keychain/MapKeychainAccess.java b/src/test/java/org/cryptomator/common/keychain/MapKeychainAccess.java index dbffae92e..c4baa96fa 100644 --- a/src/test/java/org/cryptomator/common/keychain/MapKeychainAccess.java +++ b/src/test/java/org/cryptomator/common/keychain/MapKeychainAccess.java @@ -20,7 +20,7 @@ class MapKeychainAccess implements KeychainAccessProvider { } @Override - public void storePassphrase(String key, CharSequence passphrase) { + public void storePassphrase(String key, String displayName,CharSequence passphrase) { char[] pw = new char[passphrase.length()]; for (int i = 0; i < passphrase.length(); i++) { pw[i] = passphrase.charAt(i); @@ -39,9 +39,9 @@ class MapKeychainAccess implements KeychainAccessProvider { } @Override - public void changePassphrase(String key, CharSequence passphrase) { + public void changePassphrase(String key, String displayName, CharSequence passphrase) { map.get(key); - storePassphrase(key, passphrase); + storePassphrase(key, displayName, passphrase); } @Override