/*******************************************************************************
* Copyright (c) 2014, 2017 Sebastian Stenzel
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Sebastian Stenzel - initial API and implementation
******************************************************************************/
package org.cryptomator.ui.controllers;
import static java.lang.String.format;
import java.util.Optional;
import javax.inject.Inject;
import org.cryptomator.ui.l10n.Localization;
import org.cryptomator.ui.model.Vault;
import org.cryptomator.ui.util.AsyncTaskService;
import org.cryptomator.ui.util.DialogBuilderUtil;
import org.fxmisc.easybind.EasyBind;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javafx.animation.Animation;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.geometry.Side;
import javafx.scene.Parent;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart.Data;
import javafx.scene.chart.XYChart.Series;
import javafx.scene.control.Alert;
import javafx.scene.control.ButtonType;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.Label;
import javafx.scene.control.MenuItem;
import javafx.scene.control.ToggleButton;
import javafx.scene.input.Clipboard;
import javafx.scene.input.ClipboardContent;
import javafx.scene.layout.VBox;
import javafx.stage.PopupWindow.AnchorLocation;
import javafx.util.Duration;
public class UnlockedController implements ViewController {
private static final Logger LOG = LoggerFactory.getLogger(UnlockedController.class);
private static final int IO_SAMPLING_STEPS = 100;
private static final double IO_SAMPLING_INTERVAL = 0.25;
private final Localization localization;
private final AsyncTaskService asyncTaskService;
private final ObjectProperty<Vault> vault = new SimpleObjectProperty<>();
private Optional<LockListener> listener = Optional.empty();
private Timeline ioAnimation;
@FXML
private Label messageLabel;
@FXML
private LineChart<Number, Number> ioGraph;
@FXML
private NumberAxis xAxis;
@FXML
private ToggleButton moreOptionsButton;
@FXML
private ContextMenu moreOptionsMenu;
@FXML
private MenuItem revealVaultMenuItem;
@FXML
private VBox root;
@Inject
public UnlockedController(Localization localization, AsyncTaskService asyncTaskService) {
this.localization = localization;
this.asyncTaskService = asyncTaskService;
}
@Override
public void initialize() {
revealVaultMenuItem.disableProperty().bind(EasyBind.map(vault, vault -> vault != null && !vault.isMounted()));
EasyBind.subscribe(vault, this::vaultChanged);
EasyBind.subscribe(moreOptionsMenu.showingProperty(), moreOptionsButton::setSelected);
}
@Override
public Parent getRoot() {
return root;
}
private void vaultChanged(Vault newVault) {
if (newVault == null) {
return;
}
if (newVault.getVaultSettings().mountAfterUnlock().get() && !newVault.isMounted()) {
// TODO Markus Kreusch #393: hyperlink auf FAQ oder sowas?
messageLabel.setText(localization.getString("unlocked.label.mountFailed"));
}
// (re)start throughput statistics:
stopIoSampling();
startIoSampling();
}
@FXML
private void didClickLockVault(ActionEvent event) {
regularLockVault();
}
private void regularLockVault() {
asyncTaskService.asyncTaskOf(() -> {
vault.get().unmount();
vault.get().lock();
}).onSuccess(() -> {
listener.ifPresent(listener -> listener.didLock(this));
LOG.trace("Regular lock succeeded");
}).onError(Exception.class, e -> {
onRegularLockVaultFailed(e);
}).run();
}
private void forcedLockVault() {
asyncTaskService.asyncTaskOf(() -> {
vault.get().unmountForced();
vault.get().lock();
}).onSuccess(() -> {
listener.ifPresent(listener -> listener.didLock(this));
LOG.trace("Forced lock succeeded");
}).onError(Exception.class, e -> {
onForcedLockVaultFailed(e);
}).run();
}
private void onRegularLockVaultFailed(Exception e) {
if (vault.get().supportsForcedUnmount()) {
LOG.trace("Regular unmount failed", e);
Alert confirmDialog = DialogBuilderUtil.buildYesNoDialog( //
format(localization.getString("unlocked.lock.force.confirmation.title"), vault.get().name().getValue()), //
localization.getString("unlocked.lock.force.confirmation.header"), //
localization.getString("unlocked.lock.force.confirmation.content"), //
ButtonType.NO);
Optional<ButtonType> choice = confirmDialog.showAndWait();
if (ButtonType.YES.equals(choice.get())) {
forcedLockVault();
} else {
LOG.trace("Unmount cancelled", e);
}
} else {
LOG.error("Regular unmount failed", e);
showUnmountFailedMessage();
}
}
private void onForcedLockVaultFailed(Exception e) {
LOG.error("Forced unmount failed", e);
showUnmountFailedMessage();
}
private void showUnmountFailedMessage() {
messageLabel.setText(localization.getString("unlocked.label.unmountFailed"));
}
@FXML
private void didClickMoreOptions(ActionEvent event) {
if (moreOptionsMenu.isShowing()) {
moreOptionsMenu.hide();
} else {
moreOptionsMenu.setAnchorLocation(AnchorLocation.CONTENT_TOP_RIGHT);
moreOptionsMenu.show(moreOptionsButton, Side.BOTTOM, moreOptionsButton.getWidth(), 0.0);
}
}
@FXML
private void didClickRevealVault(ActionEvent event) {
asyncTaskService.asyncTaskOf(() -> {
vault.get().reveal();
}).onError(RuntimeException.class, () -> {
// TODO overheadhunter catch more specific exception type thrown by reveal()
messageLabel.setText(localization.getString("unlocked.label.revealFailed"));
}).run();
}
@FXML
private void didClickCopyUrl(ActionEvent event) {
ClipboardContent clipboardContent = new ClipboardContent();
clipboardContent.putUrl(vault.get().getWebDavUrl());
clipboardContent.putString(vault.get().getWebDavUrl());
Clipboard.getSystemClipboard().setContent(clipboardContent);
}
// ****************************************
// IO Graph
// ****************************************
private void startIoSampling() {
final Series<Number, Number> decryptedBytes = new Series<>();
decryptedBytes.setName(localization.getString("unlocked.label.statsDecrypted"));
final Series<Number, Number> encryptedBytes = new Series<>();
encryptedBytes.setName(localization.getString("unlocked.label.statsEncrypted"));
ioGraph.getData().add(decryptedBytes);
ioGraph.getData().add(encryptedBytes);
ioAnimation = new Timeline();
ioAnimation.getKeyFrames().add(new KeyFrame(Duration.seconds(IO_SAMPLING_INTERVAL), new IoSamplingAnimationHandler(decryptedBytes, encryptedBytes)));
ioAnimation.setCycleCount(Animation.INDEFINITE);
ioAnimation.play();
}
private void stopIoSampling() {
if (ioAnimation != null) {
ioGraph.getData().clear();
ioAnimation.stop();
}
}
private class IoSamplingAnimationHandler implements EventHandler<ActionEvent> {
private static final double BYTES_TO_MEGABYTES_FACTOR = 1.0 / IO_SAMPLING_INTERVAL / 1024.0 / 1024.0;
private static final double SMOOTHING_FACTOR = 0.3;
private static final long EFFECTIVELY_ZERO = 100000; // 100kb
private final Series<Number, Number> decryptedBytes;
private final Series<Number, Number> encryptedBytes;
private int step = 0;
private long oldDecBytes = 0;
private long oldEncBytes = 0;
public IoSamplingAnimationHandler(Series<Number, Number> decryptedBytes, Series<Number, Number> encryptedBytes) {
this.decryptedBytes = decryptedBytes;
this.encryptedBytes = encryptedBytes;
}
@Override
public void handle(ActionEvent event) {
step++;
final long decBytes = vault.get().pollBytesRead();
final double smoothedDecBytes = oldDecBytes + SMOOTHING_FACTOR * (decBytes - oldDecBytes);
final double smoothedDecMb = smoothedDecBytes * BYTES_TO_MEGABYTES_FACTOR;
oldDecBytes = smoothedDecBytes > EFFECTIVELY_ZERO ? (long) smoothedDecBytes : 0l;
decryptedBytes.getData().add(new Data<Number, Number>(step, smoothedDecMb));
if (decryptedBytes.getData().size() > IO_SAMPLING_STEPS) {
decryptedBytes.getData().remove(0);
}
final long encBytes = vault.get().pollBytesWritten();
final double smoothedEncBytes = oldEncBytes + SMOOTHING_FACTOR * (encBytes - oldEncBytes);
final double smoothedEncMb = smoothedEncBytes * BYTES_TO_MEGABYTES_FACTOR;
oldEncBytes = smoothedEncBytes > EFFECTIVELY_ZERO ? (long) smoothedEncBytes : 0l;
encryptedBytes.getData().add(new Data<Number, Number>(step, smoothedEncMb));
if (encryptedBytes.getData().size() > IO_SAMPLING_STEPS) {
encryptedBytes.getData().remove(0);
}
xAxis.setLowerBound(step - IO_SAMPLING_STEPS);
xAxis.setUpperBound(step);
}
}
/* Getter/Setter */
public Vault getVault() {
return this.vault.get();
}
public void setVault(Vault vault) {
this.vault.set(vault);
}
/* callback */
public void setListener(LockListener listener) {
this.listener = Optional.ofNullable(listener);
}
@FunctionalInterface
interface LockListener {
void didLock(UnlockedController ctrl);
}
}