- * Use {@link EventMap#put(VaultEvent)} to add an element and {@link EventMap#remove(VaultEvent)} to remove it.
- *
- * The map is size restricted to {@value MAX_SIZE} elements. If a _new_ element (i.e. not already present) is added, the least recently added is removed.
- */
-@Singleton
-public class EventMap implements ObservableMap {
-
- private static final int MAX_SIZE = 300;
-
- public record EventKey(Path ciphertextPath, Class extends FilesystemEvent> c) {}
-
- private final ObservableMap delegate;
-
- @Inject
- public EventMap() {
- delegate = FXCollections.observableHashMap();
- }
-
- @Override
- public void addListener(MapChangeListener super EventKey, ? super VaultEvent> mapChangeListener) {
- delegate.addListener(mapChangeListener);
- }
-
- @Override
- public void removeListener(MapChangeListener super EventKey, ? super VaultEvent> mapChangeListener) {
- delegate.removeListener(mapChangeListener);
- }
-
- @Override
- public int size() {
- return delegate.size();
- }
-
- @Override
- public boolean isEmpty() {
- return delegate.isEmpty();
- }
-
- @Override
- public boolean containsKey(Object key) {
- return delegate.containsKey(key);
- }
-
- @Override
- public boolean containsValue(Object value) {
- return delegate.containsValue(value);
- }
-
- @Override
- public VaultEvent get(Object key) {
- return delegate.get(key);
- }
-
- @Override
- public @Nullable VaultEvent put(EventKey key, VaultEvent value) {
- return delegate.put(key, value);
- }
-
- @Override
- public VaultEvent remove(Object key) {
- return delegate.remove(key);
- }
-
- @Override
- public void putAll(@NotNull Map extends EventKey, ? extends VaultEvent> m) {
- delegate.putAll(m);
- }
-
- @Override
- public void clear() {
- delegate.clear();
- }
-
- @Override
- public @NotNull Set keySet() {
- return delegate.keySet();
- }
-
- @Override
- public @NotNull Collection values() {
- return delegate.values();
- }
-
- @Override
- public @NotNull Set> entrySet() {
- return delegate.entrySet();
- }
-
- @Override
- public void addListener(InvalidationListener invalidationListener) {
- delegate.addListener(invalidationListener);
- }
-
- @Override
- public void removeListener(InvalidationListener invalidationListener) {
- delegate.removeListener(invalidationListener);
- }
-
- public synchronized void put(VaultEvent e) {
- //compute key
- var key = computeKey(e.actualEvent());
- //if-else
- var nullOrEntry = delegate.get(key);
- if (nullOrEntry == null) {
- if (size() == MAX_SIZE) {
- delegate.entrySet().stream() //
- .min(Comparator.comparing(entry -> entry.getValue().actualEvent().getTimestamp())) //
- .ifPresent(oldestEntry -> delegate.remove(oldestEntry.getKey()));
- }
- delegate.put(key, e);
- } else {
- delegate.put(key, nullOrEntry.incrementCount(e.actualEvent()));
- }
- }
-
- public synchronized VaultEvent remove(VaultEvent similar) {
- //compute key
- var key = computeKey(similar.actualEvent());
- return this.remove(key);
- }
-
- private EventKey computeKey(FilesystemEvent e) {
- var p = switch (e) {
- case DecryptionFailedEvent(_, Path ciphertextPath, _) -> ciphertextPath;
- case ConflictResolvedEvent(_, _, _, _, Path resolvedCiphertext) -> resolvedCiphertext;
- case ConflictResolutionFailedEvent(_, _, Path conflictingCiphertext, _) -> conflictingCiphertext;
- case BrokenDirFileEvent(_, Path ciphertext) -> ciphertext;
- case BrokenFileNodeEvent(_, _, Path ciphertext) -> ciphertext;
- };
- return new EventKey(p, e.getClass());
- }
-}
diff --git a/src/main/java/org/cryptomator/common/FilesystemOwnerSupplier.java b/src/main/java/org/cryptomator/common/FilesystemOwnerSupplier.java
new file mode 100644
index 000000000..3e18e1981
--- /dev/null
+++ b/src/main/java/org/cryptomator/common/FilesystemOwnerSupplier.java
@@ -0,0 +1,18 @@
+package org.cryptomator.common;
+
+import java.util.function.Supplier;
+
+/**
+ * Interface marking a class to be used in {@link org.cryptomator.cryptofs.CryptoFileSystemProperties.Builder#withOwnerGetter(Supplier)}.
+ */
+@FunctionalInterface
+public interface FilesystemOwnerSupplier {
+
+ /**
+ * Get the filesystem owner.
+ *
+ * @return the filesystem owner
+ */
+ String getOwner();
+
+}
diff --git a/src/main/java/org/cryptomator/common/mount/Mounter.java b/src/main/java/org/cryptomator/common/mount/Mounter.java
index 89f8fb782..def53cb9a 100644
--- a/src/main/java/org/cryptomator/common/mount/Mounter.java
+++ b/src/main/java/org/cryptomator/common/mount/Mounter.java
@@ -167,6 +167,7 @@ public class Mounter {
usedMountServices.add(mountService);
var builder = mountService.forFileSystem(cryptoFsRoot);
+ LOG.debug("Using mount service {} for mounting vault {}", mountService.getClass().getName(), vaultSettings.displayName);
var internal = new SettledMounter(mountService, builder, vaultSettings); // FIXME: no need for an inner class
var cleanup = internal.prepare();
return new MountHandle(builder.mount(), mountService.hasCapability(UNMOUNT_FORCED), cleanup);
diff --git a/src/main/java/org/cryptomator/common/vaults/Vault.java b/src/main/java/org/cryptomator/common/vaults/Vault.java
index 2fa613e3e..64b763aeb 100644
--- a/src/main/java/org/cryptomator/common/vaults/Vault.java
+++ b/src/main/java/org/cryptomator/common/vaults/Vault.java
@@ -10,7 +10,7 @@ package org.cryptomator.common.vaults;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.common.Constants;
-import org.cryptomator.event.FileSystemEventAggregator;
+import org.cryptomator.common.FilesystemOwnerSupplier;
import org.cryptomator.common.mount.Mounter;
import org.cryptomator.common.settings.Settings;
import org.cryptomator.common.settings.VaultSettings;
@@ -23,6 +23,8 @@ import org.cryptomator.cryptofs.event.FilesystemEvent;
import org.cryptomator.cryptolib.api.CryptoException;
import org.cryptomator.cryptolib.api.MasterkeyLoader;
import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
+import org.cryptomator.event.FileSystemEventAggregator;
+import org.cryptomator.event.NotificationManager;
import org.cryptomator.integrations.mount.MountFailedException;
import org.cryptomator.integrations.mount.Mountpoint;
import org.cryptomator.integrations.mount.UnmountFailedException;
@@ -78,6 +80,7 @@ public class Vault {
private final Mounter mounter;
private final Settings settings;
private final FileSystemEventAggregator fileSystemEventAggregator;
+ private final NotificationManager notificationManager;
private final BooleanProperty showingStats;
private final AtomicReference mountHandle = new AtomicReference<>(null);
@@ -90,7 +93,8 @@ public class Vault {
@Named("lastKnownException") ObjectProperty lastKnownException, //
VaultStats stats, //
Mounter mounter, Settings settings, //
- FileSystemEventAggregator fileSystemEventAggregator) {
+ FileSystemEventAggregator fileSystemEventAggregator, //
+ NotificationManager notificationManager) {
this.vaultSettings = vaultSettings;
this.configCache = configCache;
this.cryptoFileSystem = cryptoFileSystem;
@@ -109,6 +113,7 @@ public class Vault {
this.mounter = mounter;
this.settings = settings;
this.fileSystemEventAggregator = fileSystemEventAggregator;
+ this.notificationManager = notificationManager;
this.showingStats = new SimpleBooleanProperty(false);
this.quickAccessEntry = new AtomicReference<>(null);
}
@@ -145,14 +150,17 @@ public class Vault {
LOG.warn("Limiting cleartext filename length on this device to {}.", vaultSettings.maxCleartextFilenameLength.get());
}
- CryptoFileSystemProperties fsProps = CryptoFileSystemProperties.cryptoFileSystemProperties() //
+ var fsPropsBuilder = CryptoFileSystemProperties.cryptoFileSystemProperties() //
.withKeyLoader(keyLoader) //
.withFlags(flags) //
.withMaxCleartextNameLength(vaultSettings.maxCleartextFilenameLength.get()) //
.withVaultConfigFilename(Constants.VAULTCONFIG_FILENAME) //
- .withFilesystemEventConsumer(this::consumeVaultEvent) //
- .build();
- return CryptoFileSystemProvider.newFileSystem(getPath(), fsProps);
+ .withFilesystemEventConsumer(this::consumeVaultEvent);
+ if (keyLoader instanceof FilesystemOwnerSupplier oo) {
+ fsPropsBuilder.withOwnerGetter(oo::getOwner);
+ }
+
+ return CryptoFileSystemProvider.newFileSystem(getPath(), fsPropsBuilder.build());
}
private void destroyCryptoFileSystem() {
@@ -262,6 +270,7 @@ public class Vault {
private void consumeVaultEvent(FilesystemEvent e) {
fileSystemEventAggregator.put(this, e);
+ notificationManager.offer(this, e);
}
// ******************************************************************************
diff --git a/src/main/java/org/cryptomator/event/FileSystemEventAggregator.java b/src/main/java/org/cryptomator/event/FileSystemEventAggregator.java
index c871436fd..b27238cee 100644
--- a/src/main/java/org/cryptomator/event/FileSystemEventAggregator.java
+++ b/src/main/java/org/cryptomator/event/FileSystemEventAggregator.java
@@ -6,6 +6,7 @@ import org.cryptomator.cryptofs.event.BrokenFileNodeEvent;
import org.cryptomator.cryptofs.event.ConflictResolutionFailedEvent;
import org.cryptomator.cryptofs.event.ConflictResolvedEvent;
import org.cryptomator.cryptofs.event.DecryptionFailedEvent;
+import org.cryptomator.cryptofs.event.FileIsInUseEvent;
import org.cryptomator.cryptofs.event.FilesystemEvent;
import javax.inject.Inject;
@@ -101,6 +102,7 @@ public class FileSystemEventAggregator {
case ConflictResolutionFailedEvent(_, _, Path conflictingCiphertext, _) -> conflictingCiphertext;
case BrokenDirFileEvent(_, Path ciphertext) -> ciphertext;
case BrokenFileNodeEvent(_, _, Path ciphertext) -> ciphertext;
+ case FileIsInUseEvent(_, _, Path ciphertext, _, _, _) -> ciphertext;
};
return new FSEventBucket(v, p, event.getClass());
}
diff --git a/src/main/java/org/cryptomator/event/NotificationManager.java b/src/main/java/org/cryptomator/event/NotificationManager.java
new file mode 100644
index 000000000..7cd5ae1e1
--- /dev/null
+++ b/src/main/java/org/cryptomator/event/NotificationManager.java
@@ -0,0 +1,85 @@
+package org.cryptomator.event;
+
+import com.github.benmanes.caffeine.cache.Cache;
+import com.github.benmanes.caffeine.cache.Caffeine;
+import org.cryptomator.common.vaults.Vault;
+import org.cryptomator.cryptofs.event.FileIsInUseEvent;
+import org.cryptomator.cryptofs.event.FilesystemEvent;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+import java.nio.file.Path;
+import java.time.Duration;
+import java.util.List;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * Manager for notifications.
+ *
+ * To add (filesystem) events, use method {@link #offer(Vault, FilesystemEvent)}. If the input event is eligible, it is added to an internal queue.
+ * An event is eligible, if
+ *
+ *
the event should trigger a notification and
+ *
it is not added within the last {@value DEBOUNCE_THRESHOLD_SECONDS} seconds
+ *
+ *
+ * @see org.cryptomator.ui.fxapp.FxNotificationManager
+ */
+@Singleton
+public class NotificationManager {
+
+ private static final int DEBOUNCE_THRESHOLD_SECONDS = 5;
+
+ private final Cache debounceCache;
+ private final ConcurrentLinkedQueue pendingEvents;
+
+ @Inject
+ public NotificationManager() {
+ debounceCache = Caffeine.newBuilder().expireAfterWrite(Duration.ofSeconds(DEBOUNCE_THRESHOLD_SECONDS)).build();
+ pendingEvents = new ConcurrentLinkedQueue<>();
+ }
+
+ /**
+ * Offers the given filesystem event to the notification manager.
+ *
+ * @param v The vault where the filesystem event happened
+ * @param e the actual filesystem event
+ * @return {@code true} if the filesystem event is accepted, otherwise {@code false}.
+ */
+ public boolean offer(Vault v, FilesystemEvent e) {
+ return switch (e) {
+ case FileIsInUseEvent fiiue -> addEvent(v, fiiue.ciphertextPath(), fiiue);
+ default -> false;
+ };
+ }
+
+ boolean addEvent(Vault v, Path keyPath, FilesystemEvent e) {
+ var key = new FSEventBucket(v, keyPath, e.getClass());
+ var isAdded = new AtomicBoolean(false);
+ debounceCache.asMap().computeIfAbsent(key, _ -> {
+ synchronized (this) {
+ pendingEvents.add(new VaultEvent(v, e));
+ isAdded.set(true);
+ }
+ return e;
+ });
+ return isAdded.get();
+ }
+
+ /**
+ * Adds all events to the target list and clears afterward the pending-event-queue
+ *
+ * @param target list where the filesystem events are copied to
+ * @return {@code true}, if elements were copied
+ */
+ public boolean appendToAndClear(List target) {
+ //it is not clear, if addAll iterates thread-safe over the pendingEvents
+ //hence we synchronize moving (copy then clear) and adding-single-element operations
+ synchronized (this) {
+ var result = target.addAll(pendingEvents);
+ pendingEvents.clear();
+ return result;
+ }
+ }
+}
diff --git a/src/main/java/org/cryptomator/event/VaultEvent.java b/src/main/java/org/cryptomator/event/VaultEvent.java
index 8b31747cf..1095c812b 100644
--- a/src/main/java/org/cryptomator/event/VaultEvent.java
+++ b/src/main/java/org/cryptomator/event/VaultEvent.java
@@ -3,25 +3,6 @@ package org.cryptomator.event;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.cryptofs.event.FilesystemEvent;
-import java.time.Instant;
+public record VaultEvent(Vault v, FilesystemEvent actualEvent) {
-public record VaultEvent(Vault v, FilesystemEvent actualEvent, int count) implements Comparable {
-
- public VaultEvent(Vault v, FilesystemEvent actualEvent) {
- this(v, actualEvent, 1);
- }
-
- @Override
- public int compareTo(VaultEvent other) {
- var timeResult = actualEvent.getTimestamp().compareTo(other.actualEvent().getTimestamp());
- if(timeResult != 0) {
- return timeResult;
- } else {
- return this.equals(other) ? 0 : this.actualEvent.getClass().getName().compareTo(other.actualEvent.getClass().getName());
- }
- }
-
- public VaultEvent incrementCount(FilesystemEvent update) {
- return new VaultEvent(v, update, count+1);
- }
}
diff --git a/src/main/java/org/cryptomator/ui/common/FxmlFile.java b/src/main/java/org/cryptomator/ui/common/FxmlFile.java
index c3cd9c1ef..3246370b9 100644
--- a/src/main/java/org/cryptomator/ui/common/FxmlFile.java
+++ b/src/main/java/org/cryptomator/ui/common/FxmlFile.java
@@ -39,6 +39,7 @@ public enum FxmlFile {
MIGRATION_RUN("/fxml/migration_run.fxml"), //
MIGRATION_START("/fxml/migration_start.fxml"), //
MIGRATION_SUCCESS("/fxml/migration_success.fxml"), //
+ NOTIFICATION("/fxml/notification.fxml"), //
PREFERENCES("/fxml/preferences.fxml"), //
QUIT("/fxml/quit.fxml"), //
QUIT_FORCED("/fxml/quit_forced.fxml"), //
@@ -60,6 +61,7 @@ public enum FxmlFile {
VAULT_STATISTICS("/fxml/stats.fxml"), //
WRONGFILEALERT("/fxml/wrongfilealert.fxml");
+
private final String ressourcePathString;
FxmlFile(String ressourcePathString) {
diff --git a/src/main/java/org/cryptomator/ui/common/SystemBarUtil.java b/src/main/java/org/cryptomator/ui/common/SystemBarUtil.java
new file mode 100644
index 000000000..9faaa60db
--- /dev/null
+++ b/src/main/java/org/cryptomator/ui/common/SystemBarUtil.java
@@ -0,0 +1,56 @@
+package org.cryptomator.ui.common;
+
+import javafx.stage.Screen;
+
+/**
+ * Utility class providing methods regarding the OS bar.
+ */
+public class SystemBarUtil {
+
+ public enum Placement {
+ /**
+ * OS Bar placed at the left screen edge
+ */
+ LEFT,
+ /**
+ * OS Bar placed at the top screen edge
+ */
+ TOP,
+ /**
+ * OS Bar placed at the right screen edge
+ */
+ RIGHT,
+ /**
+ * OS Bar placed at the bottom screen edge
+ */
+ BOTTOM;
+ }
+
+ /**
+ * Determines the placement of the OS bar on the given screen.
+ *
+ * Assuming the OS bar fills one screen edge completely,
+ * this method determines that screen edge by comparing the actual screen bounds with the visual ones.
+ *
+ * If the screen does not have a system bar, the bottom placement is returned.
+ * If the screen does have multiple system bars, the first in following priority is returned:
+ * LEFT, TOP, RIGHT, BOTTOM.
+ *
+ * @param screen a {@link Screen} where an OS bar exists
+ * @return {@link Placement} indicating the screen edge.
+ */
+ public static Placement getPlacementOfSystembar(Screen screen) {
+ var bounds = screen.getBounds();
+ var vBounds = screen.getVisualBounds();
+ //assumption: the system bar fills a whole screen side
+ if (bounds.getMinX() != vBounds.getMinX()) {
+ return Placement.LEFT;
+ } else if (bounds.getMinY() != vBounds.getMinY()) {
+ return Placement.TOP;
+ } else if (bounds.getMaxX() != vBounds.getMaxX()) {
+ return Placement.RIGHT;
+ } else {
+ return Placement.BOTTOM;
+ }
+ }
+}
diff --git a/src/main/java/org/cryptomator/ui/controls/FontAwesome5Icon.java b/src/main/java/org/cryptomator/ui/controls/FontAwesome5Icon.java
index 348b3a26b..ca1793e6d 100644
--- a/src/main/java/org/cryptomator/ui/controls/FontAwesome5Icon.java
+++ b/src/main/java/org/cryptomator/ui/controls/FontAwesome5Icon.java
@@ -12,6 +12,8 @@ public enum FontAwesome5Icon {
CARET_DOWN("\uF0D7"), //
CARET_RIGHT("\uF0Da"), //
CHECK("\uF00C"), //
+ CHEVRON_LEFT("\uF053"), //
+ CHEVRON_RIGHT("\uF054"), //
CLOCK("\uF017"), //
CLIPBOARD("\uF328"), //
COG("\uF013"), //
@@ -60,6 +62,7 @@ public enum FontAwesome5Icon {
TRASH("\uF1F8"), //
UNLINK("\uf127"), //
USER_COG("\uf4fe"), //
+ USER_LOCK("\uf502"), //
WRENCH("\uF0AD"), //
WINDOW_MINIMIZE("\uF2D1"), //
;
diff --git a/src/main/java/org/cryptomator/ui/eventview/EventListCellController.java b/src/main/java/org/cryptomator/ui/eventview/EventListCellController.java
index 3157f9f40..2327cc262 100644
--- a/src/main/java/org/cryptomator/ui/eventview/EventListCellController.java
+++ b/src/main/java/org/cryptomator/ui/eventview/EventListCellController.java
@@ -1,5 +1,8 @@
package org.cryptomator.ui.eventview;
+import org.apache.commons.lang3.SystemUtils;
+import org.cryptomator.common.Constants;
+import org.cryptomator.cryptofs.event.FileIsInUseEvent;
import org.cryptomator.event.FSEventBucket;
import org.cryptomator.event.FSEventBucketContent;
import org.cryptomator.event.FileSystemEventAggregator;
@@ -115,7 +118,7 @@ public class EventListCellController implements FxController {
eventActionsMenu.hide();
eventActionsMenu.getItems().clear();
eventTooltip.setText(item.getKey().vault().getDisplayName());
- addAction("generic.action.dismiss", () -> {
+ addLocalizedAction("generic.action.dismiss", () -> {
fileSystemEventAggregator.remove(item.getKey());
});
switch (item.getValue().mostRecentEvent()) {
@@ -124,20 +127,41 @@ public class EventListCellController implements FxController {
case DecryptionFailedEvent fse -> this.adjustToDecryptionFailedEvent(fse);
case BrokenDirFileEvent fse -> this.adjustToBrokenDirFileEvent(fse);
case BrokenFileNodeEvent fse -> this.adjustToBrokenFileNodeEvent(fse);
+ case FileIsInUseEvent fse -> this.adjustToFileInUseEvent(fse);
}
}
+ private void adjustToFileInUseEvent(FileIsInUseEvent fiiue) {
+ eventIcon.setValue(FontAwesome5Icon.USER_LOCK);
+ eventMessage.setValue(resourceBundle.getString("eventView.entry.inUse.message"));
+ var indexFileName = fiiue.cleartextPath().lastIndexOf("/");
+ eventDescription.setValue(fiiue.cleartextPath().substring(indexFileName + 1));
+ if (revealService != null) {
+ addLocalizedAction("eventView.entry.inUse.showDecrypted", () -> reveal(revealService, convertVaultPathToSystemPath(fiiue.cleartextPath())));
+ addLocalizedAction("eventView.entry.inUse.showEncrypted", () -> reveal(revealService, fiiue.ciphertextPath()));
+ } else {
+ addLocalizedAction("eventView.entry.inUse.copyDecrypted", () -> copyToClipboard(convertVaultPathToSystemPath(fiiue.cleartextPath()).toString()));
+ addLocalizedAction("eventView.entry.inUse.copyEncrypted", () -> copyToClipboard(fiiue.ciphertextPath().toString()));
+ }
+
+ var userAndDevice = fiiue.owner().split(Constants.HUB_USER_DEVICE_SEPARATOR);
+ var user = userAndDevice[0];
+ var device = userAndDevice.length == 1 ? userAndDevice[0] : userAndDevice[1];
+ addLocalizedAction("eventView.entry.inUse.copyUserAndDevice", () -> copyToClipboard(user + ", " + device));
+ addLocalizedAction("eventView.entry.inUse.ignoreLock", fiiue.ignoreMethod());
+ }
+
private void adjustToBrokenFileNodeEvent(BrokenFileNodeEvent bfe) {
eventIcon.setValue(FontAwesome5Icon.TIMES);
eventMessage.setValue(resourceBundle.getString("eventView.entry.brokenFileNode.message"));
eventDescription.setValue(bfe.ciphertextPath().getFileName().toString());
if (revealService != null) {
- addAction("eventView.entry.brokenFileNode.showEncrypted", () -> reveal(revealService, bfe.ciphertextPath()));
+ addLocalizedAction("eventView.entry.brokenFileNode.showEncrypted", () -> reveal(revealService, bfe.ciphertextPath()));
} else {
- addAction("eventView.entry.brokenFileNode.copyEncrypted", () -> copyToClipboard(bfe.ciphertextPath().toString()));
+ addLocalizedAction("eventView.entry.brokenFileNode.copyEncrypted", () -> copyToClipboard(bfe.ciphertextPath().toString()));
}
- addAction("eventView.entry.brokenFileNode.copyDecrypted", () -> copyToClipboard(convertVaultPathToSystemPath(bfe.cleartextPath()).toString()));
+ addLocalizedAction("eventView.entry.brokenFileNode.copyDecrypted", () -> copyToClipboard(convertVaultPathToSystemPath(bfe.cleartextPath()).toString()));
}
private void adjustToConflictResolvedEvent(ConflictResolvedEvent cre) {
@@ -145,9 +169,9 @@ public class EventListCellController implements FxController {
eventMessage.setValue(resourceBundle.getString("eventView.entry.conflictResolved.message"));
eventDescription.setValue(cre.resolvedCiphertextPath().getFileName().toString());
if (revealService != null) {
- addAction("eventView.entry.conflictResolved.showDecrypted", () -> reveal(revealService, convertVaultPathToSystemPath(cre.resolvedCleartextPath())));
+ addLocalizedAction("eventView.entry.conflictResolved.showDecrypted", () -> reveal(revealService, convertVaultPathToSystemPath(cre.resolvedCleartextPath())));
} else {
- addAction("eventView.entry.conflictResolved.copyDecrypted", () -> copyToClipboard(convertVaultPathToSystemPath(cre.resolvedCleartextPath()).toString()));
+ addLocalizedAction("eventView.entry.conflictResolved.copyDecrypted", () -> copyToClipboard(convertVaultPathToSystemPath(cre.resolvedCleartextPath()).toString()));
}
}
@@ -156,11 +180,11 @@ public class EventListCellController implements FxController {
eventMessage.setValue(resourceBundle.getString("eventView.entry.conflict.message"));
eventDescription.setValue(cfe.conflictingCiphertextPath().getFileName().toString());
if (revealService != null) {
- addAction("eventView.entry.conflict.showDecrypted", () -> reveal(revealService, convertVaultPathToSystemPath(cfe.canonicalCleartextPath())));
- addAction("eventView.entry.conflict.showEncrypted", () -> reveal(revealService, cfe.conflictingCiphertextPath()));
+ addLocalizedAction("eventView.entry.conflict.showDecrypted", () -> reveal(revealService, convertVaultPathToSystemPath(cfe.canonicalCleartextPath())));
+ addLocalizedAction("eventView.entry.conflict.showEncrypted", () -> reveal(revealService, cfe.conflictingCiphertextPath()));
} else {
- addAction("eventView.entry.conflict.copyDecrypted", () -> copyToClipboard(convertVaultPathToSystemPath(cfe.canonicalCleartextPath()).toString()));
- addAction("eventView.entry.conflict.copyEncrypted", () -> copyToClipboard(cfe.conflictingCiphertextPath().toString()));
+ addLocalizedAction("eventView.entry.conflict.copyDecrypted", () -> copyToClipboard(convertVaultPathToSystemPath(cfe.canonicalCleartextPath()).toString()));
+ addLocalizedAction("eventView.entry.conflict.copyEncrypted", () -> copyToClipboard(cfe.conflictingCiphertextPath().toString()));
}
}
@@ -169,9 +193,9 @@ public class EventListCellController implements FxController {
eventMessage.setValue(resourceBundle.getString("eventView.entry.decryptionFailed.message"));
eventDescription.setValue(dfe.ciphertextPath().getFileName().toString());
if (revealService != null) {
- addAction("eventView.entry.decryptionFailed.showEncrypted", () -> reveal(revealService, dfe.ciphertextPath()));
+ addLocalizedAction("eventView.entry.decryptionFailed.showEncrypted", () -> reveal(revealService, dfe.ciphertextPath()));
} else {
- addAction("eventView.entry.decryptionFailed.copyEncrypted", () -> copyToClipboard(dfe.ciphertextPath().toString()));
+ addLocalizedAction("eventView.entry.decryptionFailed.copyEncrypted", () -> copyToClipboard(dfe.ciphertextPath().toString()));
}
}
@@ -180,14 +204,19 @@ public class EventListCellController implements FxController {
eventMessage.setValue(resourceBundle.getString("eventView.entry.brokenDirFile.message"));
eventDescription.setValue(bde.ciphertextPath().getParent().getFileName().toString());
if (revealService != null) {
- addAction("eventView.entry.brokenDirFile.showEncrypted", () -> reveal(revealService, bde.ciphertextPath()));
+ addLocalizedAction("eventView.entry.brokenDirFile.showEncrypted", () -> reveal(revealService, bde.ciphertextPath()));
} else {
- addAction("eventView.entry.brokenDirFile.copyEncrypted", () -> copyToClipboard(bde.ciphertextPath().toString()));
+ addLocalizedAction("eventView.entry.brokenDirFile.copyEncrypted", () -> copyToClipboard(bde.ciphertextPath().toString()));
}
}
- private void addAction(String localizationKey, Runnable action) {
- var entry = new MenuItem(resourceBundle.getString(localizationKey));
+ private void addLocalizedAction(String localizationKey, Runnable action) {
+ var entryText = resourceBundle.getString(localizationKey);
+ addAction(entryText, action);
+ }
+
+ private void addAction(String entryText, Runnable action) {
+ var entry = new MenuItem(entryText);
entry.getStyleClass().addLast("dropdown-button-context-menu-item");
entry.setOnAction(_ -> action.run());
eventActionsMenu.getItems().addLast(entry);
@@ -234,18 +263,17 @@ public class EventListCellController implements FxController {
}
}
- private Path convertVaultPathToSystemPath(Path p) {
- if (!(p instanceof CryptoPath)) {
- throw new IllegalArgumentException("Path " + p + " is not a vault path");
- }
+ private Path convertVaultPathToSystemPath(String vaultInternalPath) {
var v = eventEntry.getValue().getKey().vault();
if (!v.isUnlocked()) {
return Path.of(System.getProperty("user.home"));
}
- var mountUri = v.getMountPoint().uri();
- var internalPath = p.toString().substring(1);
- return Path.of(mountUri.getPath().concat(internalPath).substring(1));
+ var mountPoint = v.getMountPoint().uri().getPath();
+ if(SystemUtils.IS_OS_WINDOWS) {
+ mountPoint = mountPoint.substring(1); //strip away any leading "/", otherwise there are errors
+ }
+ return Path.of(mountPoint, vaultInternalPath.substring(1)); //vaultPaths are always absolute
}
private void reveal(RevealPathService s, Path p) {
diff --git a/src/main/java/org/cryptomator/ui/fxapp/FxApplication.java b/src/main/java/org/cryptomator/ui/fxapp/FxApplication.java
index ccc0684af..7fb5b523b 100644
--- a/src/main/java/org/cryptomator/ui/fxapp/FxApplication.java
+++ b/src/main/java/org/cryptomator/ui/fxapp/FxApplication.java
@@ -30,9 +30,20 @@ public class FxApplication {
private final FxApplicationTerminator applicationTerminator;
private final AutoUnlocker autoUnlocker;
private final FxFSEventList fxFSEventList;
+ private final FxNotificationManager notificationManager;
@Inject
- FxApplication(@Named("startupTime") long startupTime, Environment environment, Settings settings, AppLaunchEventHandler launchEventHandler, Lazy trayMenu, FxApplicationWindows appWindows, FxApplicationStyle applicationStyle, FxApplicationTerminator applicationTerminator, AutoUnlocker autoUnlocker, FxFSEventList fxFSEventList) {
+ FxApplication(@Named("startupTime") long startupTime, //
+ Environment environment, //
+ Settings settings, //
+ AppLaunchEventHandler launchEventHandler, //
+ Lazy trayMenu, //
+ FxApplicationWindows appWindows, //
+ FxApplicationStyle applicationStyle, //
+ FxApplicationTerminator applicationTerminator, //
+ AutoUnlocker autoUnlocker, //
+ FxFSEventList fxFSEventList, //
+ FxNotificationManager notificationManager) {
this.startupTime = startupTime;
this.environment = environment;
this.settings = settings;
@@ -43,6 +54,7 @@ public class FxApplication {
this.applicationTerminator = applicationTerminator;
this.autoUnlocker = autoUnlocker;
this.fxFSEventList = fxFSEventList;
+ this.notificationManager = notificationManager;
}
public void start() {
@@ -88,6 +100,7 @@ public class FxApplication {
launchEventHandler.startHandlingLaunchEvents();
fxFSEventList.schedulePollForUpdates();
+ notificationManager.schedulePollForUpdates();
autoUnlocker.tryUnlockForTimespan(2, TimeUnit.MINUTES);
}
diff --git a/src/main/java/org/cryptomator/ui/fxapp/FxApplicationModule.java b/src/main/java/org/cryptomator/ui/fxapp/FxApplicationModule.java
index 70319df5b..80a261bb8 100644
--- a/src/main/java/org/cryptomator/ui/fxapp/FxApplicationModule.java
+++ b/src/main/java/org/cryptomator/ui/fxapp/FxApplicationModule.java
@@ -13,6 +13,7 @@ import org.cryptomator.ui.eventview.EventViewComponent;
import org.cryptomator.ui.health.HealthCheckComponent;
import org.cryptomator.ui.lock.LockComponent;
import org.cryptomator.ui.mainwindow.MainWindowComponent;
+import org.cryptomator.ui.notification.NotificationComponent;
import org.cryptomator.ui.preferences.PreferencesComponent;
import org.cryptomator.ui.quit.QuitComponent;
import org.cryptomator.ui.recoverykey.RecoveryKeyComponent;
@@ -39,7 +40,8 @@ import java.io.InputStream;
UpdateReminderComponent.class, //
ShareVaultComponent.class, //
EventViewComponent.class, //
- RecoveryKeyComponent.class})
+ RecoveryKeyComponent.class, //
+ NotificationComponent.class })
abstract class FxApplicationModule {
private static Image createImageFromResource(String resourceName) throws IOException {
@@ -78,4 +80,10 @@ abstract class FxApplicationModule {
return factory.create();
}
+ @Provides
+ @FxApplicationScoped
+ static NotificationComponent provideNotificationComponent(NotificationComponent.Factory factory) {
+ return factory.create();
+ }
+
}
\ No newline at end of file
diff --git a/src/main/java/org/cryptomator/ui/fxapp/FxApplicationWindows.java b/src/main/java/org/cryptomator/ui/fxapp/FxApplicationWindows.java
index c8a870fd8..94c4fe330 100644
--- a/src/main/java/org/cryptomator/ui/fxapp/FxApplicationWindows.java
+++ b/src/main/java/org/cryptomator/ui/fxapp/FxApplicationWindows.java
@@ -11,6 +11,7 @@ import org.cryptomator.ui.error.ErrorComponent;
import org.cryptomator.ui.eventview.EventViewComponent;
import org.cryptomator.ui.lock.LockComponent;
import org.cryptomator.ui.mainwindow.MainWindowComponent;
+import org.cryptomator.ui.notification.NotificationComponent;
import org.cryptomator.ui.preferences.PreferencesComponent;
import org.cryptomator.ui.preferences.SelectedPreferencesTab;
import org.cryptomator.ui.quit.QuitComponent;
@@ -54,6 +55,7 @@ public class FxApplicationWindows {
private final LockComponent.Factory lockWorkflowFactory;
private final ErrorComponent.Factory errorWindowFactory;
private final Lazy eventViewWindow;
+ private final Lazy notificationWindow;
private final ExecutorService executor;
private final VaultOptionsComponent.Factory vaultOptionsWindow;
private final ShareVaultComponent.Factory shareVaultWindow;
@@ -73,6 +75,7 @@ public class FxApplicationWindows {
VaultOptionsComponent.Factory vaultOptionsWindow, //
ShareVaultComponent.Factory shareVaultWindow, //
Lazy eventViewWindow, //
+ Lazy notificationWindow,
ExecutorService executor, //
Dialogs dialogs) {
this.primaryStage = primaryStage;
@@ -85,6 +88,7 @@ public class FxApplicationWindows {
this.lockWorkflowFactory = lockWorkflowFactory;
this.errorWindowFactory = errorWindowFactory;
this.eventViewWindow = eventViewWindow;
+ this.notificationWindow = notificationWindow;
this.executor = executor;
this.vaultOptionsWindow = vaultOptionsWindow;
this.shareVaultWindow = shareVaultWindow;
@@ -193,6 +197,10 @@ public class FxApplicationWindows {
return CompletableFuture.supplyAsync(() -> eventViewWindow.get().showEventViewerWindow(), Platform::runLater).whenComplete(this::reportErrors);
}
+ public CompletionStage showNotification() {
+ return CompletableFuture.supplyAsync(() -> notificationWindow.get().showNotification(), Platform::runLater).whenComplete(this::reportErrors);
+ }
+
/**
* Displays the generic error scene in the given window.
*
diff --git a/src/main/java/org/cryptomator/ui/fxapp/FxNotificationManager.java b/src/main/java/org/cryptomator/ui/fxapp/FxNotificationManager.java
new file mode 100644
index 000000000..c1100a2f4
--- /dev/null
+++ b/src/main/java/org/cryptomator/ui/fxapp/FxNotificationManager.java
@@ -0,0 +1,57 @@
+package org.cryptomator.ui.fxapp;
+
+import org.cryptomator.event.NotificationManager;
+import org.cryptomator.event.VaultEvent;
+
+import javax.inject.Inject;
+import javafx.application.Platform;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Notification manager inside the UI domain.
+ *
+ * Polls the {@link NotificationManager} for pending events every {@value POLL_INTERVAL_SECONDS } seconds and
+ * triggers the notification window display when events are available.
+ * Returns an observable list of events requiring a user notification with {@link #getEventsRequiringNotification()}.
+ *
+ * @see NotificationManager
+ */
+@FxApplicationScoped
+public class FxNotificationManager {
+
+ private static final int POLL_INTERVAL_SECONDS = 1;
+
+ private final NotificationManager notificationManager;
+ private final ScheduledExecutorService scheduler;
+ private final FxApplicationWindows applicationWindows;
+ private final ObservableList eventsRequiringNotification;
+
+ @Inject
+ public FxNotificationManager(NotificationManager notificationManager, ScheduledExecutorService scheduler, FxApplicationWindows applicationWindows) {
+ this.notificationManager = notificationManager;
+ this.scheduler = scheduler;
+ this.applicationWindows = applicationWindows;
+ this.eventsRequiringNotification = FXCollections.observableArrayList();
+ }
+
+ public void schedulePollForUpdates() {
+ scheduler.scheduleAtFixedRate(this::checkForPendingNotifications, 0, POLL_INTERVAL_SECONDS, TimeUnit.SECONDS);
+ }
+
+ private void checkForPendingNotifications() {
+ Platform.runLater(() -> {
+ if (notificationManager.appendToAndClear(eventsRequiringNotification)) {
+ applicationWindows.showNotification();
+ }
+ });
+
+ }
+
+ public ObservableList getEventsRequiringNotification() {
+ return eventsRequiringNotification;
+ }
+
+}
diff --git a/src/main/java/org/cryptomator/ui/health/HealthCheckModule.java b/src/main/java/org/cryptomator/ui/health/HealthCheckModule.java
index c36f486e0..19595e0e9 100644
--- a/src/main/java/org/cryptomator/ui/health/HealthCheckModule.java
+++ b/src/main/java/org/cryptomator/ui/health/HealthCheckModule.java
@@ -63,8 +63,8 @@ abstract class HealthCheckModule {
@Provides
@HealthCheckWindow
@HealthCheckScoped
- static KeyLoadingStrategy provideKeyLoadingStrategy(KeyLoadingComponent.Builder compBuilder, @HealthCheckWindow Vault vault, @Named("unlockWindow") Stage window ) {
- return compBuilder.vault(vault).window(window).build().keyloadingStrategy();
+ static KeyLoadingStrategy provideKeyLoadingStrategy(KeyLoadingComponent.Factory compFactory, @HealthCheckWindow Vault vault, @Named("unlockWindow") Stage window ) {
+ return compFactory.create(vault, window).keyloadingStrategy();
}
@Provides
diff --git a/src/main/java/org/cryptomator/ui/keyloading/KeyLoadingComponent.java b/src/main/java/org/cryptomator/ui/keyloading/KeyLoadingComponent.java
index 190bf0e1f..f035388f6 100644
--- a/src/main/java/org/cryptomator/ui/keyloading/KeyLoadingComponent.java
+++ b/src/main/java/org/cryptomator/ui/keyloading/KeyLoadingComponent.java
@@ -3,11 +3,8 @@ package org.cryptomator.ui.keyloading;
import dagger.BindsInstance;
import dagger.Subcomponent;
import org.cryptomator.common.vaults.Vault;
-import org.cryptomator.cryptolib.api.MasterkeyLoader;
import javafx.stage.Stage;
-import java.util.Map;
-import java.util.function.Supplier;
@KeyLoadingScoped
@Subcomponent(modules = {KeyLoadingModule.class})
@@ -16,16 +13,10 @@ public interface KeyLoadingComponent {
@KeyLoading
KeyLoadingStrategy keyloadingStrategy();
- @Subcomponent.Builder
- interface Builder {
+ @Subcomponent.Factory
+ interface Factory {
- @BindsInstance
- Builder vault(@KeyLoading Vault vault);
-
- @BindsInstance
- Builder window(@KeyLoading Stage window);
-
- KeyLoadingComponent build();
+ KeyLoadingComponent create(@BindsInstance @KeyLoading Vault vault, @KeyLoading @BindsInstance Stage window);
}
}
diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingModule.java b/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingModule.java
index 806e99a0f..929a34a2c 100644
--- a/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingModule.java
+++ b/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingModule.java
@@ -66,6 +66,13 @@ public abstract class HubKeyLoadingModule {
return new AtomicReference<>();
}
+ @Provides
+ @Named("filesystemOwnerId")
+ @KeyLoadingScoped
+ static AtomicReference provideFilesystemOwnerIdRef() {
+ return new AtomicReference<>();
+ }
+
@Provides
@KeyLoadingScoped
static CompletableFuture provideResult() {
diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingStrategy.java b/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingStrategy.java
index 40f845a63..4b48169c0 100644
--- a/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingStrategy.java
+++ b/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingStrategy.java
@@ -2,6 +2,7 @@ package org.cryptomator.ui.keyloading.hub;
import com.google.common.base.Preconditions;
import dagger.Lazy;
+import org.cryptomator.common.FilesystemOwnerSupplier;
import org.cryptomator.common.keychain.KeychainManager;
import org.cryptomator.common.keychain.NoKeychainAccessProviderException;
import org.cryptomator.common.settings.DeviceKey;
@@ -23,25 +24,28 @@ import java.net.URI;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
+import java.util.concurrent.atomic.AtomicReference;
@KeyLoading
-public class HubKeyLoadingStrategy implements KeyLoadingStrategy {
+public class HubKeyLoadingStrategy implements KeyLoadingStrategy, FilesystemOwnerSupplier {
- private static final String SCHEME_PREFIX = "hub+";
+ public static final String SCHEME_PREFIX = "hub+";
public static final String SCHEME_HUB_HTTP = SCHEME_PREFIX + "http";
public static final String SCHEME_HUB_HTTPS = SCHEME_PREFIX + "https";
private final Stage window;
private final KeychainManager keychainManager;
+ private final AtomicReference fsOwnerId;
private final Lazy authFlowScene;
private final Lazy noKeychainScene;
private final CompletableFuture result;
private final DeviceKey deviceKey;
@Inject
- public HubKeyLoadingStrategy(@KeyLoading Stage window, @FxmlScene(FxmlFile.HUB_AUTH_FLOW) Lazy authFlowScene, @FxmlScene(FxmlFile.HUB_NO_KEYCHAIN) Lazy noKeychainScene, CompletableFuture result, DeviceKey deviceKey, KeychainManager keychainManager, @Named("windowTitle") String windowTitle) {
+ public HubKeyLoadingStrategy(@KeyLoading Stage window, @FxmlScene(FxmlFile.HUB_AUTH_FLOW) Lazy authFlowScene, @FxmlScene(FxmlFile.HUB_NO_KEYCHAIN) Lazy noKeychainScene, CompletableFuture result, DeviceKey deviceKey, KeychainManager keychainManager, @Named("windowTitle") String windowTitle, @Named("filesystemOwnerId") AtomicReference fsOwnerId) {
this.window = window;
this.keychainManager = keychainManager;
+ this.fsOwnerId = fsOwnerId;
window.setTitle(windowTitle);
window.setOnCloseRequest(_ -> result.cancel(true));
this.authFlowScene = authFlowScene;
@@ -90,4 +94,13 @@ public class HubKeyLoadingStrategy implements KeyLoadingStrategy {
});
}
+ @Override
+ public String getOwner() {
+ var name = fsOwnerId.get();
+ if (name == null) {
+ throw new IllegalStateException("Owner is not yet determined");
+ }
+ return name;
+ }
+
}
diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/ReceiveKeyController.java b/src/main/java/org/cryptomator/ui/keyloading/hub/ReceiveKeyController.java
index a93d942cb..6dfde8c52 100644
--- a/src/main/java/org/cryptomator/ui/keyloading/hub/ReceiveKeyController.java
+++ b/src/main/java/org/cryptomator/ui/keyloading/hub/ReceiveKeyController.java
@@ -6,6 +6,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Preconditions;
import com.nimbusds.jose.JWEObject;
import dagger.Lazy;
+import org.cryptomator.common.Constants;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxmlFile;
@@ -41,7 +42,6 @@ import java.util.concurrent.atomic.AtomicReference;
public class ReceiveKeyController implements FxController {
private static final Logger LOG = LoggerFactory.getLogger(ReceiveKeyController.class);
- private static final String SCHEME_PREFIX = "hub+";
private static final ObjectMapper JSON = new ObjectMapper().setDefaultLeniency(true);
private static final Duration REQ_TIMEOUT = Duration.ofSeconds(10);
@@ -50,6 +50,7 @@ public class ReceiveKeyController implements FxController {
private final String vaultId;
private final String deviceId;
private final String bearerToken;
+ private final AtomicReference fsOwnerId;
private final CompletableFuture result;
private final Lazy registerDeviceScene;
private final Lazy legacyRegisterDeviceScene;
@@ -66,6 +67,7 @@ public class ReceiveKeyController implements FxController {
HubConfig hubConfig,
@Named("deviceId") String deviceId,
@Named("bearerToken") AtomicReference tokenRef,
+ @Named("filesystemOwnerId") AtomicReference fsOwnerId, //
CompletableFuture result,
@FxmlScene(FxmlFile.HUB_REGISTER_DEVICE) Lazy registerDeviceScene,
@FxmlScene(FxmlFile.HUB_LEGACY_REGISTER_DEVICE) Lazy legacyRegisterDeviceScene,
@@ -78,6 +80,7 @@ public class ReceiveKeyController implements FxController {
this.vaultId = extractVaultId(vault.getVaultConfigCache().getUnchecked().getKeyId()); // TODO: access vault config's JTI directly (requires changes in cryptofs)
this.deviceId = deviceId;
this.bearerToken = Objects.requireNonNull(tokenRef.get());
+ this.fsOwnerId = fsOwnerId;
this.result = result;
this.registerDeviceScene = registerDeviceScene;
this.legacyRegisterDeviceScene = legacyRegisterDeviceScene;
@@ -95,7 +98,34 @@ public class ReceiveKeyController implements FxController {
}
public void receiveKey() {
- requestApiConfig();
+ requestUserData();
+ }
+
+ private void requestUserData() {
+ var userUri = hubConfig.URIs.API.resolve("users/me?withDevices=false");
+ var request = HttpRequest.newBuilder(userUri) //
+ .header("Authorization", "Bearer " + bearerToken) //
+ .GET() //
+ .timeout(REQ_TIMEOUT) //
+ .build();
+ httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)) //
+ .thenAcceptAsync(this::receivedUserData) //
+ .exceptionally(this::retrievalFailed);
+ }
+
+ private void receivedUserData(HttpResponse response) {
+ LOG.debug("GET {} -> Status Code {}", response.request().uri(), response.statusCode());
+ try {
+ if (response.statusCode() == 200) {
+ var user = JSON.reader().readValue(response.body(), UserDto.class);
+ fsOwnerId.set(user.name);
+ requestApiConfig();
+ } else {
+ throw new IllegalStateException("Unexpected response " + response.statusCode());
+ }
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
}
/**
@@ -158,6 +188,7 @@ public class ReceiveKeyController implements FxController {
switch (response.statusCode()) {
case 200 -> {
var device = JSON.reader().readValue(response.body(), DeviceDto.class);
+ fsOwnerId.accumulateAndGet(device.name, (s1, s2) -> s1 + Constants.HUB_USER_DEVICE_SEPARATOR + s2);
requestVaultMasterkey(device.userPrivateKey);
}
case 404 -> Platform.runLater(this::needsDeviceRegistration);
@@ -309,13 +340,16 @@ public class ReceiveKeyController implements FxController {
}
private static String extractVaultId(URI vaultKeyUri) {
- assert vaultKeyUri.getScheme().startsWith(SCHEME_PREFIX);
+ assert vaultKeyUri.getScheme().startsWith(HubKeyLoadingStrategy.SCHEME_PREFIX);
var path = vaultKeyUri.getPath();
return path.substring(path.lastIndexOf('/') + 1);
}
@JsonIgnoreProperties(ignoreUnknown = true)
- private record DeviceDto(@JsonProperty(value = "userPrivateKey", required = true) String userPrivateKey) {}
+ private record UserDto(@JsonProperty(value = "name", required = true) String name) {}
+
+ @JsonIgnoreProperties(ignoreUnknown = true)
+ private record DeviceDto(@JsonProperty(value = "name", required = true) String name, @JsonProperty(value = "userPrivateKey", required = true) String userPrivateKey) {}
@JsonIgnoreProperties(ignoreUnknown = true)
private record ConfigDto(@JsonProperty(value = "apiLevel") int apiLevel) {}
diff --git a/src/main/java/org/cryptomator/ui/mainwindow/VaultListController.java b/src/main/java/org/cryptomator/ui/mainwindow/VaultListController.java
index f25528498..f3c3ccb83 100644
--- a/src/main/java/org/cryptomator/ui/mainwindow/VaultListController.java
+++ b/src/main/java/org/cryptomator/ui/mainwindow/VaultListController.java
@@ -15,8 +15,8 @@ import org.cryptomator.ui.addvaultwizard.AddVaultWizardComponent;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.VaultService;
import org.cryptomator.ui.dialogs.Dialogs;
-import org.cryptomator.ui.fxapp.FxFSEventList;
import org.cryptomator.ui.fxapp.FxApplicationWindows;
+import org.cryptomator.ui.fxapp.FxFSEventList;
import org.cryptomator.ui.preferences.SelectedPreferencesTab;
import org.cryptomator.ui.recoverykey.RecoveryKeyComponent;
import org.slf4j.Logger;
@@ -139,8 +139,8 @@ public class VaultListController implements FxController {
vaultList.setItems(vaults);
vaultList.setCellFactory(cellFactory);
- vaultList.prefHeightProperty().bind(
- vaultList.fixedCellSizeProperty().multiply(Bindings.size(vaultList.getItems()))
+ vaultList.prefHeightProperty().bind( //
+ vaultList.fixedCellSizeProperty().multiply(Bindings.size(vaultList.getItems())) //
);
selectedVault.bind(vaultList.getSelectionModel().selectedItemProperty());
@@ -157,11 +157,11 @@ public class VaultListController implements FxController {
//unlock vault on double click
vaultList.addEventFilter(MouseEvent.MOUSE_CLICKED, click -> {
if (click.getClickCount() >= 2) {
- Optional.ofNullable(selectedVault.get())
- .filter(Vault::isLocked)
+ Optional.ofNullable(selectedVault.get()) //
+ .filter(Vault::isLocked) //
.ifPresent(vault -> appWindows.startUnlockWorkflow(vault, mainWindow));
- Optional.ofNullable(selectedVault.get())
- .filter(Vault::isUnlocked)
+ Optional.ofNullable(selectedVault.get()) //
+ .filter(Vault::isUnlocked) //
.ifPresent(vaultService::reveal);
}
});
diff --git a/src/main/java/org/cryptomator/ui/notification/NotificationComponent.java b/src/main/java/org/cryptomator/ui/notification/NotificationComponent.java
new file mode 100644
index 000000000..3ebcdf4ab
--- /dev/null
+++ b/src/main/java/org/cryptomator/ui/notification/NotificationComponent.java
@@ -0,0 +1,35 @@
+package org.cryptomator.ui.notification;
+
+import dagger.Lazy;
+import dagger.Subcomponent;
+import org.cryptomator.ui.common.FxmlFile;
+import org.cryptomator.ui.common.FxmlScene;
+
+import javafx.scene.Scene;
+import javafx.stage.Stage;
+
+@NotificationScoped
+@Subcomponent(modules = {NotificationModule.class})
+public interface NotificationComponent {
+
+ @NotificationWindow
+ Stage window();
+
+ @FxmlScene(FxmlFile.NOTIFICATION)
+ Lazy scene();
+
+ default Stage showNotification() {
+ var window = window();
+ window.setScene(scene().get());
+ window.sizeToScene();
+ window.show();
+ window.requestFocus();
+ return window;
+ }
+
+ @Subcomponent.Factory
+ interface Factory {
+ NotificationComponent create();
+ }
+
+}
diff --git a/src/main/java/org/cryptomator/ui/notification/NotificationController.java b/src/main/java/org/cryptomator/ui/notification/NotificationController.java
new file mode 100644
index 000000000..fe96e3721
--- /dev/null
+++ b/src/main/java/org/cryptomator/ui/notification/NotificationController.java
@@ -0,0 +1,245 @@
+package org.cryptomator.ui.notification;
+
+import org.cryptomator.common.Constants;
+import org.cryptomator.common.settings.Settings;
+import org.cryptomator.cryptofs.event.FileIsInUseEvent;
+import org.cryptomator.event.VaultEvent;
+import org.cryptomator.ui.common.FxController;
+import org.cryptomator.ui.fxapp.FxNotificationManager;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.inject.Inject;
+import javafx.beans.binding.Bindings;
+import javafx.beans.property.IntegerProperty;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleIntegerProperty;
+import javafx.beans.property.SimpleListProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.beans.property.SimpleStringProperty;
+import javafx.beans.property.StringProperty;
+import javafx.beans.value.ObservableStringValue;
+import javafx.beans.value.ObservableValue;
+import javafx.fxml.FXML;
+import javafx.stage.Stage;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.time.format.FormatStyle;
+import java.util.Locale;
+import java.util.Objects;
+import java.util.ResourceBundle;
+import java.util.concurrent.ExecutorService;
+
+@NotificationScoped
+public class NotificationController implements FxController {
+
+ private static final Logger LOG = LoggerFactory.getLogger(NotificationController.class);
+ private static final DateTimeFormatter LOCAL_TIME_FORMATTER_TEMPLATE = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT).withZone(ZoneId.systemDefault());
+ private static final String BUG_MSG = "If you see this message, please report it on the Cryptomator issue tracker.";
+
+ private final Stage window;
+ private final SimpleListProperty events;
+ private final ResourceBundle resourceBundle;
+ private final IntegerProperty selectionIndex;
+ private final ObservableStringValue paging;
+ private final ObjectProperty selectedEvent;
+ private final ObservableValue singleEvent;
+ private final StringProperty fileName;
+ private final StringProperty vaultName;
+ private final StringProperty eventTimestamp;
+ private final StringProperty message;
+ private final StringProperty description;
+ private final StringProperty actionText;
+ private final ExecutorService executorService;
+ private final DateTimeFormatter localizedTimeFormatter;
+
+ @Inject
+ public NotificationController(@NotificationWindow Stage window, FxNotificationManager notificationManager, ExecutorService executorService, ResourceBundle resourceBundle, Settings settings) {
+ this.window = window;
+ var preferredLanguage = settings.language.get();
+ this.localizedTimeFormatter = LOCAL_TIME_FORMATTER_TEMPLATE.withLocale(preferredLanguage == null ? Locale.getDefault() : Locale.forLanguageTag(preferredLanguage));
+ this.events = new SimpleListProperty<>(notificationManager.getEventsRequiringNotification());
+ this.resourceBundle = resourceBundle;
+ this.selectionIndex = new SimpleIntegerProperty(-1);
+ this.selectedEvent = new SimpleObjectProperty<>();
+ this.singleEvent = events.sizeProperty().map(size -> size.intValue() == 1);
+ this.paging = Bindings.createStringBinding(() -> selectionIndex.get() + 1 + "/" + events.size(), selectionIndex, events);
+ this.vaultName = new SimpleStringProperty();
+ this.eventTimestamp = new SimpleStringProperty();
+ this.message = new SimpleStringProperty();
+ this.fileName = new SimpleStringProperty("");
+ this.description = new SimpleStringProperty();
+ this.actionText = new SimpleStringProperty();
+ this.executorService = executorService;
+ }
+
+ @FXML
+ public void initialize() {
+ window.setOnShowing(_ -> selectionIndex.set(0));
+ selectionIndex.addListener((_, _, n) -> {
+ if (!events.isEmpty()) {
+ selectedEvent.setValue(events.get(n.intValue()));
+ }
+ });
+ selectedEvent.addListener(this::selectTexts);
+ }
+
+ private void selectTexts(ObservableValue extends VaultEvent> observable, VaultEvent oldEvent, VaultEvent newEvent) {
+ if (newEvent == null) {
+ vaultName.set("");
+ message.set("NO CONTENT");
+ description.set(BUG_MSG);
+ actionText.set(null);
+ return;
+ }
+
+ vaultName.set(newEvent.v().getDisplayName());
+ switch (newEvent.actualEvent()) {
+ case FileIsInUseEvent fiiue -> {
+ var userAndDevice = fiiue.owner().split(Constants.HUB_USER_DEVICE_SEPARATOR);
+ var user = userAndDevice[0];
+ var device = userAndDevice.length == 1 ? userAndDevice[0] : userAndDevice[1];
+ var cleartextFileName = fiiue.cleartextPath().substring(fiiue.cleartextPath().lastIndexOf('/') + 1);
+ eventTimestamp.set(localizedTimeFormatter.format(fiiue.lastUpdated()));
+ message.set("File is locked by another device");
+ fileName.set(cleartextFileName);
+ description.set("The file is opened by %s on device %s. Ask the user to close the file and sync again. Otherwise, you can ignore the lock and open it anyway.".formatted(user, device));
+ actionText.set("Ignore Lock");
+ /* TODO: Once feature is out of beta, activate translations
+ message.set(resourceBundle.getString("notification.inUse.message"));
+ description.set(resourceBundle.getString("notification.inUse.description").formatted(fiiue.cleartextPath(), user, device));
+ actionText.set(resourceBundle.getString("notification.inUse.action"));
+ */
+ }
+ default -> {
+ message.set("NO CONTENT");
+ description.set(BUG_MSG);
+ actionText.set(null);
+ }
+ }
+ }
+
+ @FXML
+ public void processSelectedEvent() {
+ try {
+ var ev = selectedEvent.get();
+ switch (ev.actualEvent()) {
+ case FileIsInUseEvent fiiue -> {
+ executorService.submit(fiiue.ignoreMethod());
+ }
+ default -> {
+ } //normally nothing
+ }
+ } finally {
+ removeSelectedEvent();
+ }
+ }
+
+ private void removeSelectedEvent() {
+ int i = selectionIndex.get();
+ var size = events.size();
+ if (i < 0 || i >= size) {
+ LOG.error("Selection index {} is out of bounds of list size {} during event removal. Closing Window.", i, size);
+ window.close();
+ return;
+ }
+
+ events.remove(i);
+ if (events.isEmpty()) {
+ window.close(); //no more events
+ } else if (events.size() == i) {
+ selectionIndex.set(i - 1); //triggers event update
+ } else {
+ selectedEvent.set(events.get(i));
+ }
+ }
+
+ @FXML
+ public void previousNotification() {
+ int i = selectionIndex.get();
+ if (i != 0) {
+ selectionIndex.set(i - 1);
+ }
+ }
+
+ @FXML
+ public void nextNotification() {
+ int i = selectionIndex.get();
+ if (i != events.size() - 1) {
+ selectionIndex.set(i + 1);
+ }
+ }
+
+ @FXML
+ public void close() {
+ events.clear();
+ window.close();
+ }
+
+
+ //FXML bindings
+ public ObservableValue eventTimeProperty() {
+ return eventTimestamp;
+ }
+
+ public String getEventTime() {
+ return eventTimestamp.get();
+ }
+
+ public ObservableValue vaultNameProperty() {
+ return vaultName;
+ }
+
+ public String getVaultName() {
+ return vaultName.get();
+ }
+
+ public ObservableValue fileNameProperty() {
+ return fileName;
+ }
+
+ public String getFileName() {
+ return fileName.get();
+ }
+
+ public ObservableValue messageProperty() {
+ return message;
+ }
+
+ public String getMessage() {
+ return message.get();
+ }
+
+ public ObservableValue descriptionProperty() {
+ return description;
+ }
+
+ public String getDescription() {
+ return description.get();
+ }
+
+ public StringProperty actionTextProperty() {
+ return actionText;
+ }
+
+ public String getActionText() {
+ return Objects.requireNonNullElse(actionText.get(), "");
+ }
+
+ public ObservableStringValue pagingProperty() {
+ return paging;
+ }
+
+ public String getPaging() {
+ return paging.get();
+ }
+
+ public ObservableValue singleEventProperty() {
+ return singleEvent;
+ }
+
+ public boolean isSingleEvent() {
+ return singleEvent.getValue();
+ }
+
+}
diff --git a/src/main/java/org/cryptomator/ui/notification/NotificationModule.java b/src/main/java/org/cryptomator/ui/notification/NotificationModule.java
new file mode 100644
index 000000000..64895a40c
--- /dev/null
+++ b/src/main/java/org/cryptomator/ui/notification/NotificationModule.java
@@ -0,0 +1,99 @@
+package org.cryptomator.ui.notification;
+
+import dagger.Binds;
+import dagger.Module;
+import dagger.Provides;
+import dagger.multibindings.IntoMap;
+import org.apache.commons.lang3.SystemUtils;
+import org.cryptomator.ui.common.DefaultSceneFactory;
+import org.cryptomator.ui.common.FxController;
+import org.cryptomator.ui.common.FxControllerKey;
+import org.cryptomator.ui.common.FxmlFile;
+import org.cryptomator.ui.common.FxmlLoaderFactory;
+import org.cryptomator.ui.common.FxmlScene;
+import org.cryptomator.ui.common.StageInitializer;
+import org.cryptomator.ui.common.SystemBarUtil;
+
+import javax.inject.Provider;
+import javafx.scene.Scene;
+import javafx.stage.Modality;
+import javafx.stage.Screen;
+import javafx.stage.Stage;
+import javafx.stage.StageStyle;
+import java.util.Map;
+import java.util.ResourceBundle;
+
+@Module
+abstract class NotificationModule {
+
+ @Provides
+ @NotificationWindow
+ @NotificationScoped
+ static Stage provideStage(StageInitializer initializer) {
+ Stage stage = new Stage(StageStyle.TRANSPARENT);
+ stage.setTitle("Filesystem notification"); //TODO: translate
+ stage.setResizable(false);
+ stage.initModality(Modality.NONE);
+ stage.setAlwaysOnTop(true);
+ initializer.accept(stage);
+ stage.setOnShown(_ -> placeWindow(stage));
+ return stage;
+ }
+
+ /**
+ * Places the notification window on the screen according to some heuristic based on operating system and system bar placement.
+ *
+ * On macOS, the window is placed in the top-right corner of the primary screen, following platform conventions.
+ * On other operating systems, the window placement depends on the location of the system bar:
+ *
+ *
If the system bar is at the top, the window is centered horizontally at the top of the screen.
+ *
Otherwise (e.g., system bar at the bottom or elsewhere), the window is placed in the bottom-right corner.
+
diff --git a/src/main/resources/i18n/strings.properties b/src/main/resources/i18n/strings.properties
index d49b4b7d6..897fbd7da 100644
--- a/src/main/resources/i18n/strings.properties
+++ b/src/main/resources/i18n/strings.properties
@@ -13,6 +13,7 @@ generic.button.close=Close
generic.button.copy=Copy
generic.button.copied=Copied!
generic.button.done=Done
+generic.button.previous=Previous
generic.button.next=Next
generic.button.print=Print
generic.button.remove=Remove
@@ -698,3 +699,17 @@ eventView.entry.brokenFileNode.message=Broken filesystem node
eventView.entry.brokenFileNode.showEncrypted=Show broken, encrypted node
eventView.entry.brokenFileNode.copyEncrypted=Copy path of broken, encrypted node
eventView.entry.brokenFileNode.copyDecrypted=Copy decrypted path
+eventView.entry.inUse.message=Locked File
+eventView.entry.inUse.showDecrypted=Show decrypted file
+eventView.entry.inUse.copyDecrypted=Copy decrypted path
+eventView.entry.inUse.showEncrypted=Show encrypted file
+eventView.entry.inUse.copyEncrypted=Copy encrypted path
+eventView.entry.inUse.copyUserAndDevice=Copy locking user and device name
+eventView.entry.inUse.ignoreLock=Ignore Lock
+
+
+# Notifications
+## FileIsInUse Notification
+#notification.inUse.message=File is locked
+#notification.inUse.description=File %s is opened by user %s (%s). Ask the user to close the file. Otherwise, you can ignore the lock and open it anyway, but be aware of the data loss risk.
+#notification.inUse.action=Ignore Lock
\ No newline at end of file
diff --git a/suppression.xml b/suppression.xml
index 308280a8d..2b351269a 100644
--- a/suppression.xml
+++ b/suppression.xml
@@ -82,13 +82,22 @@
- ^pkg:maven/org\.eclipse\.jetty/jetty-.*$
- CVE-2024-6763
+ CVE-2024-6763CVE-2024-6763
+
+
+
+
+ ^pkg:maven/org\.purejava/flatpak-update-portal@.*$
+ cpe:/a:flatpak:flatpak
+