Merge branch 'develop' into release/1.6.9

This commit is contained in:
Sebastian Stenzel 2022-04-27 15:30:59 +02:00
commit e9551a076d
No known key found for this signature in database
GPG Key ID: 667B866EA8240A09
17 changed files with 297 additions and 377 deletions

View File

@ -28,10 +28,10 @@
<!-- cryptomator dependencies -->
<cryptomator.cryptofs.version>2.4.1</cryptomator.cryptofs.version>
<cryptomator.integrations.version>1.1.0-beta1</cryptomator.integrations.version>
<cryptomator.integrations.win.version>1.0.0</cryptomator.integrations.win.version>
<cryptomator.integrations.mac.version>1.0.0</cryptomator.integrations.mac.version>
<cryptomator.integrations.linux.version>1.0.1</cryptomator.integrations.linux.version>
<cryptomator.integrations.version>1.1.0</cryptomator.integrations.version>
<cryptomator.integrations.win.version>1.1.0</cryptomator.integrations.win.version>
<cryptomator.integrations.mac.version>1.1.0</cryptomator.integrations.mac.version>
<cryptomator.integrations.linux.version>1.1.0</cryptomator.integrations.linux.version>
<cryptomator.fuse.version>1.3.3</cryptomator.fuse.version>
<cryptomator.dokany.version>1.3.3</cryptomator.dokany.version>
<cryptomator.webdav.version>1.2.7</cryptomator.webdav.version>

View File

@ -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;

View File

@ -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<Path> {
private final List<URL> 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;
}
}
}

View File

@ -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)) {

View File

@ -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<ServiceLoader.Provider<KeychainAccessProvider>> provideAvailableKeychainAccessProviderFactories(PluginClassLoader classLoader) {
return ServiceLoader.load(KeychainAccessProvider.class, classLoader).stream().collect(Collectors.toUnmodifiableSet());
static List<KeychainAccessProvider> provideSupportedKeychainAccessProviders() {
return KeychainAccessProvider.get().toList();
}
@Provides
@Singleton
static Set<KeychainAccessProvider> provideSupportedKeychainAccessProviders(Set<ServiceLoader.Provider<KeychainAccessProvider>> availableFactories) {
return availableFactories.stream() //
.map(ServiceLoader.Provider::get) //
.filter(KeychainAccessProvider::isSupported) //
.collect(Collectors.toUnmodifiableSet());
}
@Provides
@Singleton
static ObjectExpression<KeychainAccessProvider> provideKeychainAccessProvider(Settings settings, Set<KeychainAccessProvider> providers) {
static ObjectExpression<KeychainAccessProvider> provideKeychainAccessProvider(Settings settings, List<KeychainAccessProvider> 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());
}

View File

@ -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<UiAppearanceProvider> provideAppearanceProvider(PluginClassLoader classLoader) {
return ServiceLoader.load(UiAppearanceProvider.class, classLoader).findFirst();
static Optional<UiAppearanceProvider> provideAppearanceProvider() {
return UiAppearanceProvider.get();
}
@Provides
@Singleton
static Optional<AutoStartProvider> provideAutostartProvider(PluginClassLoader classLoader) {
return ServiceLoader.load(AutoStartProvider.class, classLoader).findFirst();
static Optional<AutoStartProvider> provideAutostartProvider() {
return AutoStartProvider.get();
}
@Provides
@Singleton
static Optional<TrayIntegrationProvider> provideTrayIntegrationProvider(PluginClassLoader classLoader) {
return ServiceLoader.load(TrayIntegrationProvider.class, classLoader).findFirst();
static Optional<TrayIntegrationProvider> provideTrayIntegrationProvider() {
return TrayIntegrationProvider.get();
}
}

View File

@ -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;

View File

@ -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> autoStartProvider;
private final Application application;
private final Environment environment;
private final Set<KeychainAccessProvider> keychainAccessProviders;
private final List<KeychainAccessProvider> keychainAccessProviders;
private final FxApplicationWindows appWindows;
public ChoiceBox<KeychainAccessProvider> 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> autoStartProvider, Set<KeychainAccessProvider> keychainAccessProviders, Application application, Environment environment, FxApplicationWindows appWindows) {
GeneralPreferencesController(@PreferencesWindow Stage window, Settings settings, Optional<AutoStartProvider> autoStartProvider, List<KeychainAccessProvider> 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<KeychainAccessProvider> {
private final Set<KeychainAccessProvider> keychainAccessProviders;
private final List<KeychainAccessProvider> keychainAccessProviders;
public KeychainProviderClassNameConverter(Set<KeychainAccessProvider> keychainAccessProviders) {
public KeychainProviderClassNameConverter(List<KeychainAccessProvider> keychainAccessProviders) {
this.keychainAccessProviders = keychainAccessProviders;
}

View File

@ -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<TrayMenuItem> items) {
menu.removeAll();
addChildren(menu, items);
}
private void addChildren(Menu menu, List<TrayMenuItem> 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);
}
}
}
}

View File

@ -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;
}
}

View File

@ -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<UiAppearanceProvider> appearanceProvider;
@Inject
TrayImageFactory(Optional<UiAppearanceProvider> 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";
}
}

View File

@ -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<Vault> vaults;
private final TrayMenuController trayMenu;
private volatile boolean initialized;
@Inject
TrayMenuBuilder(ResourceBundle resourceBundle, VaultService vaultService, FxApplicationWindows appWindows, FxApplicationTerminator appTerminator, ObservableList<Vault> vaults, Optional<TrayMenuController> 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<TrayMenuItem> 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<TrayMenuItem> 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<TrayMenuItem> 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);
}
}

View File

@ -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> trayIconController();
Optional<TrayMenuController> trayMenuController();
TrayMenuBuilder trayMenuBuilder();
/**
* @return <code>true</code> if a tray icon can be installed
*/
default boolean isSupported() {
return SystemTray.isSupported();
return trayMenuController().isPresent();
}
/**
* @return <code>true</code> 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

View File

@ -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<Vault> vaults;
private final PopupMenu menu;
@Inject
TrayMenuController(ResourceBundle resourceBundle, VaultService vaultService, FxApplicationWindows appWindows, FxApplicationTerminator appTerminator, ObservableList<Vault> 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<Vault> 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);
}
}

View File

@ -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<TrayMenuController> provideSupportedKeychainAccessProviders() {
return TrayMenuController.get();
}
}

View File

@ -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());
}
}

View File

@ -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