diff --git a/main/commons/src/main/java/org/cryptomator/common/CommonsModule.java b/main/commons/src/main/java/org/cryptomator/common/CommonsModule.java index f8e0e8078..705f98b8f 100644 --- a/main/commons/src/main/java/org/cryptomator/common/CommonsModule.java +++ b/main/commons/src/main/java/org/cryptomator/common/CommonsModule.java @@ -15,6 +15,7 @@ import org.cryptomator.common.settings.SettingsProvider; import org.cryptomator.common.vaults.Vault; import org.cryptomator.common.vaults.VaultComponent; import org.cryptomator.common.vaults.VaultListManager; +import org.cryptomator.common.vaults.VaultListModule; import org.cryptomator.cryptolib.common.MasterkeyFileAccess; import org.cryptomator.frontend.webdav.WebDavServer; import org.slf4j.Logger; @@ -37,7 +38,7 @@ import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; -@Module(subcomponents = {VaultComponent.class}, includes = {KeychainModule.class}) +@Module(subcomponents = {VaultComponent.class}, includes = {VaultListModule.class, KeychainModule.class}) public abstract class CommonsModule { private static final Logger LOG = LoggerFactory.getLogger(CommonsModule.class); @@ -87,12 +88,6 @@ public abstract class CommonsModule { return settingsProvider.get(); } - @Provides - @Singleton - static ObservableList provideVaultList(VaultListManager vaultListManager) { - return vaultListManager.getVaultList(); - } - @Provides @Singleton static ScheduledExecutorService provideScheduledExecutorService(ShutdownHook shutdownHook) { diff --git a/main/commons/src/main/java/org/cryptomator/common/vaults/AutoLocker.java b/main/commons/src/main/java/org/cryptomator/common/vaults/AutoLocker.java new file mode 100644 index 000000000..dcaafdb13 --- /dev/null +++ b/main/commons/src/main/java/org/cryptomator/common/vaults/AutoLocker.java @@ -0,0 +1,62 @@ +package org.cryptomator.common.vaults; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.inject.Inject; +import javax.inject.Singleton; +import javafx.collections.ObservableList; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +@Singleton +public class AutoLocker { + + private static final Logger LOG = LoggerFactory.getLogger(AutoLocker.class); + + private final ScheduledExecutorService scheduler; + private final ObservableList vaultList; + + @Inject + public AutoLocker(ScheduledExecutorService scheduler, ObservableList vaultList) { + this.scheduler = scheduler; + this.vaultList = vaultList; + } + + public void init() { + scheduler.scheduleAtFixedRate(this::tick, 0, 1, TimeUnit.MINUTES); + } + + private void tick() { + vaultList.stream() // all vaults + .filter(Vault::isUnlocked) // unlocked vaults + .filter(this::exceedsIdleTime) // idle vaults + .forEach(this::autolock); + } + + private void autolock(Vault vault) { + try { + vault.lock(false); + LOG.info("Autolocked {} after idle timeout", vault.getDisplayName()); + } catch (Volume.VolumeException | LockNotCompletedException e) { + LOG.error("Autolocking failed.", e); + } + } + + private boolean exceedsIdleTime(Vault vault) { + assert vault.isUnlocked(); + // TODO: shouldn't we read these properties from within FX Application Thread? + if (vault.getVaultSettings().lockAfterTime().get()) { + int maxIdleMinutes = vault.getVaultSettings().lockTimeInMinutes().get(); + var idleSince = vault.getStats().getLastActivity(); + var threshold = idleSince.plus(maxIdleMinutes, ChronoUnit.MINUTES); + return threshold.isBefore(Instant.now()); + } else { + return false; + } + } + + +} diff --git a/main/commons/src/main/java/org/cryptomator/common/vaults/VaultListManager.java b/main/commons/src/main/java/org/cryptomator/common/vaults/VaultListManager.java index d5038630a..05ea8ce07 100644 --- a/main/commons/src/main/java/org/cryptomator/common/vaults/VaultListManager.java +++ b/main/commons/src/main/java/org/cryptomator/common/vaults/VaultListManager.java @@ -37,22 +37,21 @@ public class VaultListManager { private static final Logger LOG = LoggerFactory.getLogger(VaultListManager.class); + private final AutoLocker autoLocker; private final VaultComponent.Builder vaultComponentBuilder; private final ObservableList vaultList; private final String defaultVaultName; @Inject - public VaultListManager(VaultComponent.Builder vaultComponentBuilder, ResourceBundle resourceBundle, Settings settings) { + public VaultListManager(ObservableList vaultList, AutoLocker autoLocker, VaultComponent.Builder vaultComponentBuilder, ResourceBundle resourceBundle, Settings settings) { + this.vaultList = vaultList; + this.autoLocker = autoLocker; this.vaultComponentBuilder = vaultComponentBuilder; this.defaultVaultName = resourceBundle.getString("defaults.vault.vaultName"); - this.vaultList = FXCollections.observableArrayList(Vault::observables); addAll(settings.getDirectories()); vaultList.addListener(new VaultListChangeListener(settings.getDirectories())); - } - - public ObservableList getVaultList() { - return vaultList; + autoLocker.init(); } public Vault add(Path pathToVault) throws IOException { diff --git a/main/commons/src/main/java/org/cryptomator/common/vaults/VaultListModule.java b/main/commons/src/main/java/org/cryptomator/common/vaults/VaultListModule.java new file mode 100644 index 000000000..644d05335 --- /dev/null +++ b/main/commons/src/main/java/org/cryptomator/common/vaults/VaultListModule.java @@ -0,0 +1,19 @@ +package org.cryptomator.common.vaults; + +import dagger.Module; +import dagger.Provides; + +import javax.inject.Singleton; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; + +@Module +public class VaultListModule { + + @Provides + @Singleton + public ObservableList provideVaultList() { + return FXCollections.observableArrayList(Vault::observables); + } + +} diff --git a/main/commons/src/main/java/org/cryptomator/common/vaults/VaultStats.java b/main/commons/src/main/java/org/cryptomator/common/vaults/VaultStats.java index 6dc86e8be..64205a0f2 100644 --- a/main/commons/src/main/java/org/cryptomator/common/vaults/VaultStats.java +++ b/main/commons/src/main/java/org/cryptomator/common/vaults/VaultStats.java @@ -13,9 +13,11 @@ import javafx.beans.property.LongProperty; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleDoubleProperty; import javafx.beans.property.SimpleLongProperty; +import javafx.beans.property.SimpleObjectProperty; import javafx.concurrent.ScheduledService; import javafx.concurrent.Task; import javafx.util.Duration; +import java.time.Instant; import java.util.Optional; import java.util.concurrent.ExecutorService; import java.util.concurrent.atomic.AtomicReference; @@ -39,6 +41,7 @@ public class VaultStats { private final LongProperty totalBytesDecrypted = new SimpleLongProperty(); private final LongProperty filesRead = new SimpleLongProperty(); private final LongProperty filesWritten = new SimpleLongProperty(); + private final ObjectProperty lastActivity = new SimpleObjectProperty<>(); @Inject VaultStats(AtomicReference fs, VaultState state, ExecutorService executor) { @@ -73,9 +76,16 @@ public class VaultStats { toalBytesWritten.set(stats.map(CryptoFileSystemStats::pollTotalBytesWritten).orElse(0L)); totalBytesEncrypted.set(stats.map(CryptoFileSystemStats::pollTotalBytesEncrypted).orElse(0L)); totalBytesDecrypted.set(stats.map(CryptoFileSystemStats::pollTotalBytesDecrypted).orElse(0L)); + var oldAccessCount = filesRead.get() + filesWritten.get(); filesRead.set(stats.map(CryptoFileSystemStats::pollAmountOfAccessesRead).orElse(0L)); filesWritten.set(stats.map(CryptoFileSystemStats::pollAmountOfAccessesWritten).orElse(0L)); + var newAccessCount = filesRead.get() + filesWritten.get(); + // check for any I/O activity + if (newAccessCount > oldAccessCount) { + LOG.info("ACTIVITY!"); + lastActivity.set(Instant.now()); + } } private double getCacheHitRate(CryptoFileSystemStats stats) { @@ -175,4 +185,12 @@ public class VaultStats { public LongProperty filesWritten() {return filesWritten;} public long getFilesWritten() {return filesWritten.get();} + + public ObjectProperty lastActivityProperty() { + return lastActivity; + } + + public Instant getLastActivity() { + return lastActivity.get(); + } } diff --git a/main/ui/src/main/java/org/cryptomator/ui/fxapp/FxApplication.java b/main/ui/src/main/java/org/cryptomator/ui/fxapp/FxApplication.java index a19a13bf1..02a2e5b9e 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/fxapp/FxApplication.java +++ b/main/ui/src/main/java/org/cryptomator/ui/fxapp/FxApplication.java @@ -59,11 +59,9 @@ public class FxApplication extends Application { private final ObservableList visibleWindows; private final BooleanBinding hasVisibleWindows; private final UiAppearanceListener systemInterfaceThemeListener = this::systemInterfaceThemeChanged; - private final VaultListManager vaultListManager; - private final ScheduledExecutorService scheduledExecutorService; @Inject - FxApplication(Settings settings, Lazy mainWindow, Lazy preferencesWindow, Provider unlockWorkflowBuilderProvider, Provider lockWorkflowBuilderProvider, Lazy quitWindow, ErrorComponent.Builder errorWindowBuilder, Optional trayIntegration, Optional appearanceProvider, VaultService vaultService, LicenseHolder licenseHolder, VaultListManager vaultListManager, ScheduledExecutorService scheduledExecutorService) { + FxApplication(Settings settings, Lazy mainWindow, Lazy preferencesWindow, Provider unlockWorkflowBuilderProvider, Provider lockWorkflowBuilderProvider, Lazy quitWindow, ErrorComponent.Builder errorWindowBuilder, Optional trayIntegration, Optional appearanceProvider, VaultService vaultService, LicenseHolder licenseHolder) { this.settings = settings; this.mainWindow = mainWindow; this.preferencesWindow = preferencesWindow; @@ -77,8 +75,6 @@ public class FxApplication extends Application { this.licenseHolder = licenseHolder; this.visibleWindows = Stage.getWindows().filtered(Window::isShowing); this.hasVisibleWindows = Bindings.isNotEmpty(visibleWindows); - this.vaultListManager = vaultListManager; - this.scheduledExecutorService = scheduledExecutorService; } public void start() { @@ -131,7 +127,6 @@ public class FxApplication extends Application { showMainWindow().thenAccept(mainWindow -> errorWindowBuilder.window(mainWindow).cause(new IllegalStateException("Unable to unlock vault in non-locked state."))); } }); - checkAutolock(vault, owner); } public void startLockWorkflow(Vault vault, Optional owner) { @@ -212,14 +207,4 @@ public class FxApplication extends Application { }); } - - private void checkAutolock(Vault vault, Optional owner) { - if (vault.getVaultSettings().lockAfterTime().get()) { - LOG.info("Locking after {} minutes.", vault.getVaultSettings().lockTimeInMinutes().get()); - scheduledExecutorService.schedule(() -> { - startLockWorkflow(vault, owner); - }, (long) (vault.getVaultSettings().lockTimeInMinutes().get()), TimeUnit.MINUTES); - } - } - } diff --git a/main/ui/src/main/resources/i18n/strings.properties b/main/ui/src/main/resources/i18n/strings.properties index 552131181..8ffc15c4c 100644 --- a/main/ui/src/main/resources/i18n/strings.properties +++ b/main/ui/src/main/resources/i18n/strings.properties @@ -331,8 +331,8 @@ vaultOptions.masterkey.showRecoveryKeyBtn=Display Recovery Key vaultOptions.masterkey.recoverPasswordBtn=Recover Password ## Auto Lock vaultOptions.autoLock=Auto-Lock -vaultOptions.autoLock.lockAfterTimePart1=Lock after -vaultOptions.autoLock.lockAfterTimePart2=minutes. +vaultOptions.autoLock.lockAfterTimePart1=Lock when idle for +vaultOptions.autoLock.lockAfterTimePart2=minutes # Recovery Key recoveryKey.title=Recovery Key