Java tutorial
/* * MainController.java - Copyright(c) 2013 Joe Pasqua * Provided under the MIT License. See the LICENSE file for details. * Created: Jul 22, 2013 */ package org.noroomattheinn.visibletesla; import com.google.common.collect.Range; import java.io.File; import java.io.IOException; import java.util.Arrays; import java.util.Calendar; import java.util.Date; import java.util.List; import java.util.NavigableMap; import java.util.concurrent.Callable; import java.util.logging.Level; import javafx.application.Platform; import javafx.beans.property.BooleanProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.scene.Node; import javafx.scene.control.*; import javafx.scene.image.Image; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyCodeCombination; import javafx.scene.input.KeyCombination; import javafx.scene.layout.Pane; import javafx.stage.FileChooser; import javafx.stage.Stage; import org.apache.commons.lang3.SystemUtils; import org.noroomattheinn.tesla.ChargeState; import org.noroomattheinn.tesla.GUIState; import org.noroomattheinn.tesla.Result; import org.noroomattheinn.tesla.Vehicle; import org.noroomattheinn.tesla.VehicleState; import org.noroomattheinn.timeseries.Row; import org.noroomattheinn.utils.ThreadManager; import org.noroomattheinn.utils.TrackedObject; import org.noroomattheinn.utils.Utils; import org.noroomattheinn.visibletesla.data.RestCycle; import org.noroomattheinn.visibletesla.data.VTData; import org.noroomattheinn.visibletesla.dialogs.*; import org.noroomattheinn.visibletesla.vehicle.VTVehicle; import static org.noroomattheinn.tesla.Tesla.logger; import static org.noroomattheinn.utils.Utils.timeSince; /** * This is the main application code for VisibleTesla. It does not contain * the main function. Main is in VisibleTesla.java which is mostly just a shell. * This controller is associated with the Tab panel in which all of the * individual tabs live. * * @author Joe Pasqua <joe at NoRoomAtTheInn dot org> */ public class MainController extends BaseController { /*------------------------------------------------------------------------------ * * Constants and Enums * *----------------------------------------------------------------------------*/ private static final String DocumentationURL = "http://visibletesla.com/Doc_v2/pages/GettingStarted.html"; private static final String ReleaseNotesURL = "http://visibletesla.com/Doc_v2/ReleaseNotes.html"; /*------------------------------------------------------------------------------ * * Internal State * *----------------------------------------------------------------------------*/ private final BooleanProperty forceWakeup = new SimpleBooleanProperty(false); /*------------------------------------------------------------------------------ * * UI Elements * *----------------------------------------------------------------------------*/ // The top level AnchorPane and the TabPane that sits inside it @FXML private TabPane tabPane; // The individual tabs that comprise the overall UI @FXML private Tab notifierTab; @FXML private Tab prefsTab; @FXML private Tab schedulerTab; @FXML private Tab graphTab; @FXML private Tab chargeTab; @FXML private Tab hvacTab; @FXML private Tab locationTab; @FXML private Tab loginTab; @FXML private Tab overviewTab; @FXML private Tab tripsTab; @FXML private Pane wakePane; private List<Tab> tabs; @FXML private MenuItem exportStatsMenuItem, exportLocMenuItem, exportAllMenuItem, exportChargeMenuItem, exportRestMenuItem; @FXML private MenuItem vampireLossMenuItem; @FXML private MenuItem remoteStartMenuItem; // The menu items that are handled in this controller directly @FXML private RadioMenuItem allowSleepMenuItem; @FXML private RadioMenuItem stayAwakeMenuItem; /*============================================================================== * ------- ------- * ------- Public Interface To This Class ------- * ------- ------- *============================================================================*/ /** * Called by the main application to allow us to store away the fxApp context * and perform any other fxApp startup tasks. In particular, we (1) distribute * fxApp context to all of the controllers, and (2) we set a listener for login * completion and try and automatic login. */ void start(App theApp, VTVehicle v, VTData data, Prefs prefs) { this.app = theApp; this.vtVehicle = v; // This is defined in BaseController this.vtData = data; // This is defined in BaseController this.prefs = prefs; // This is defined in BaseController logAppInfo(); addSystemSpecificHandlers(app.stage); refreshTitle(); app.stage.getIcons().add(new Image(getClass().getClassLoader() .getResourceAsStream("org/noroomattheinn/TeslaResources/Icon-72@2x.png"))); tabPane.getSelectionModel().selectedItemProperty().addListener(new ChangeListener<Tab>() { @Override public void changed(ObservableValue<? extends Tab> ov, Tab t, Tab t1) { BaseController c = controllerFromTab(t1); if (c != null) { c.activate(); } } }); tabs = Arrays.asList(prefsTab, loginTab, schedulerTab, graphTab, chargeTab, hvacTab, locationTab, overviewTab, tripsTab, notifierTab); for (Tab t : tabs) { controllerFromTab(t).setAppContext(theApp, v, data, prefs); } // Handle font scaling int fontScale = prefs.fontScale.get(); if (fontScale != 100) { for (Tab t : tabs) { Node n = t.getContent(); n.setStyle(String.format("-fx-font-size: %d%%;", fontScale)); } } showDisclaimer(); // Watch for changes to the inactivity mode and state in order to update the UI App.addTracker(app.api.mode, new Runnable() { @Override public void run() { setAppModeMenu(); } }); App.addTracker(app.api.state, new Runnable() { @Override public void run() { refreshTitle(); } }); // Kick off the login process LoginController lc = Utils.cast(controllerFromTab(loginTab)); App.addTracker(lc.loggedIn, new LoginStateChange(lc.loggedIn, false)); lc.activate(); } /*------------------------------------------------------------------------------ * * Methods overridden from BaseController. We implement BaseController so that * we can perform issueCommand operations. * *----------------------------------------------------------------------------*/ @Override protected void fxInitialize() { } @Override protected void initializeState() { } @Override protected void activateTab() { } @Override protected void refresh() { } /*------------------------------------------------------------------------------ * * Dealing with a Login Event * *----------------------------------------------------------------------------*/ private void fetchInitialCarState() { issueCommand(new Callable<Result>() { @Override public Result call() { Result r = cacheBasics(); if (!r.success) { if (r.explanation.equals("mobile_access_disabled")) exitWithMobileAccessError(); else exitWithCachingError(); return Result.Failed; } Platform.runLater(finishAppStartup); return Result.Succeeded; } }, "Cache Basics"); } private class LoginStateChange implements Runnable { private final TrackedObject<Boolean> loggedIn; private final boolean assumeAwake; LoginStateChange(TrackedObject<Boolean> loggedIn, boolean assumeAwake) { this.loggedIn = loggedIn; this.assumeAwake = assumeAwake; } @Override public void run() { if (!loggedIn.get()) { vtVehicle.setVehicle(null); setTabsEnabled(false); return; } if (assumeAwake) { wakePane.setVisible(false); } else { Vehicle v = SelectVehicleDialog.select(app.stage, app.tesla.getVehicles()); vtVehicle.setVehicle(v); try { upgradeDataStoreIfNeeded(v); vtData.setVehicle(v); vtData.setWakeEarly(new WakeEarlyPredicate()); vtData.setPassiveCollection(new PassiveCollectionPredicate()); vtData.setCollectNow(new CollectNowPredicate()); } catch (IOException e) { logger.severe("Unable to establish VTData: " + e.getMessage()); Dialogs.showErrorDialog(app.stage, "VisibleTesla has encountered a severe error\n" + "while trying to access its data files. Another\n" + "copy of VisibleTesla may already be writing to them\n" + "or they may be missing.\n\n" + "VisibleTesla will close when you close this window.", "Problem accessing data files", "Problem launching application"); Platform.exit(); } if (!app.lock(v.getVIN())) { showLockError(); Platform.exit(); } logger.info("Vehicle Info: " + vtVehicle.getVehicle().getUnderlyingValues()); if (vtVehicle.getVehicle().status().equals("asleep")) { if (letItSleep()) { logger.info("Allowing vehicle to remain in sleep mode"); wakePane.setVisible(true); vtVehicle.waitForWakeup(new LoginStateChange(loggedIn, true), forceWakeup); return; } else { logger.log(Level.INFO, "Waking up your vehicle"); } } } conditionalCheckVersion(); app.restoreMode(); fetchInitialCarState(); } private void upgradeDataStoreIfNeeded(Vehicle v) { if (vtData.upgradeRequired(v)) { Dialogs.showInformationDialog(app.stage, "Your data files must be upgraded\nPress OK to begin the process.", "Data Upgrade Process", "Data File Upgrade"); vtData.doUpgrade(v); Dialogs.showInformationDialog(app.stage, "Your data files have been upgraded\nPress OK to continue.", "Data Upgrade Process", "Process Complete"); } } } private void conditionalCheckVersion() { String key = vinKey("LastVersionCheck"); long lastVersionCheck = prefs.storage().getLong(key, 0); long now = System.currentTimeMillis(); if (now - lastVersionCheck > (7 * 24 * 60 * 60 * 1000)) { VersionUpdater.checkForNewerVersion(App.productVersion(), app.stage, app.getHostServices(), prefs.offerExperimental.get()); prefs.storage().putLong(key, now); } } private Runnable finishAppStartup = new Runnable() { @Override public void run() { boolean remoteStartEnabled = vtVehicle.getVehicle().remoteStartEnabled(); remoteStartMenuItem.setDisable(!remoteStartEnabled); app.watchForUserActivity(Arrays.asList(overviewTab, hvacTab, locationTab, chargeTab)); // TO DO: Isn't the following line redundant? vtVehicle.setVehicle(vtVehicle.getVehicle()); refreshTitle(); // Start the Scheduler and the Notifier controllerFromTab(schedulerTab).activate(); controllerFromTab(notifierTab).activate(); setTabsEnabled(true); jumpToTab(overviewTab); } }; /*------------------------------------------------------------------------------ * * Private Utility Methods waking the car and initiating contact * *----------------------------------------------------------------------------*/ /** * Make contact with the car for the first time. It may need to be woken up * in the process. Since we need to do some command as part of this process, * we grab the GUIState and store it away. * @return */ private Result establishContact() { Vehicle v = vtVehicle.getVehicle(); long MaxWaitTime = 70 * 1000; long now = System.currentTimeMillis(); while (timeSince(now) < MaxWaitTime) { if (ThreadManager.get().shuttingDown()) { return new Result(false, "shutting down"); } GUIState gs = v.queryGUI(); if (gs.valid) { if (gs.rawState.optString("reason").equals("mobile_access_disabled")) { return new Result(false, "mobile_access_disabled"); } vtVehicle.noteUpdatedState(gs); return Result.Succeeded; } else { String error = gs.rawState.optString("error"); if (error.equals("vehicle unavailable")) v.wakeUp(); ThreadManager.get().sleep(10 * 1000); } } return Result.Failed; } private Result cacheBasics() { final int MaxTriesToStart = 10; Result madeContact = establishContact(); if (!madeContact.success) { // Try getting last values from previous run of the app logger.warning("Unable to contact vehicle after successful login"); GUIState gs = vtVehicle.lastSavedGS(); VehicleState vs = vtVehicle.lastSavedVS(); ChargeState cs = vtVehicle.lastSavedCS(); if (gs != null && vs != null && cs != null) { vtVehicle.chargeState.reset(cs); vtVehicle.vehicleState.reset(vs); vtVehicle.guiState.reset(gs); logger.info("Able to initial state from old values"); warnAboutNoCarAccess(madeContact.explanation.equals("mobile_access_disabled")); // TO DO: Force Sleep Mode! return Result.Succeeded; } else { logger.info("Unable to initial state from old values"); return madeContact; } } // As part of establishing contact with the car we cached the GUIState Vehicle v = vtVehicle.getVehicle(); VehicleState vs = v.queryVehicle(); ChargeState cs = v.queryCharge(); int tries = 0; while (!(vs.valid && cs.valid)) { if (tries++ > MaxTriesToStart) { return Result.Failed; } ThreadManager.get().sleep(5 * 1000); if (ThreadManager.get().shuttingDown()) return Result.Failed; if (!vs.valid) vs = v.queryVehicle(); if (!cs.valid) cs = v.queryCharge(); } vtVehicle.noteUpdatedState(vs); vtVehicle.noteUpdatedState(cs); return Result.Succeeded; } /*------------------------------------------------------------------------------ * * Private Utility Methods for Tab handling * *----------------------------------------------------------------------------*/ private void setTabsEnabled(boolean enabled) { for (Tab t : tabs) { t.setDisable(!enabled); } loginTab.setDisable(false); // The Login Tab is always enabled prefsTab.setDisable(false); // The Prefs Tab is always enabled } private void jumpToTab(final Tab tab) { Platform.runLater(new Runnable() { @Override public void run() { tabPane.getSelectionModel().select(tab); } }); } /** * Utility method that returns the BaseController object associated with * a given tab. It does this by extracting the userData object which each * BaseController sets to itself. * @param t The tab for which we want the BaseController * @return The BaseController */ private BaseController controllerFromTab(Tab t) { Object userData = t.getContent().getUserData(); return (userData instanceof BaseController) ? (BaseController) userData : null; } /*------------------------------------------------------------------------------ * * This section implements UI Actionhandlers for the menu items * *----------------------------------------------------------------------------*/ // File->Close @FXML void closeHandler(ActionEvent event) { Platform.exit(); } // File->Export * Data... private static final String[] statsColumns = new String[] { VTData.VoltageKey, VTData.CurrentKey, VTData.EstRangeKey, VTData.SOCKey, VTData.ROCKey, VTData.BatteryAmpsKey, VTData.SpeedKey, VTData.PowerKey, }; private static final String[] locColumns = new String[] { VTData.LatitudeKey, VTData.LongitudeKey, VTData.HeadingKey, VTData.SpeedKey, VTData.OdometerKey, VTData.PowerKey }; @FXML void exportHandler(ActionEvent event) { MenuItem mi = (MenuItem) event.getSource(); if (mi == exportStatsMenuItem) { exportStats(statsColumns); } else if (mi == exportLocMenuItem) { exportStats(locColumns); } else if (mi == exportAllMenuItem) { exportStats(VTData.schema.columnNames); } else if (mi == exportChargeMenuItem) { exportCycles("Charge"); } else if (mi == exportRestMenuItem) { exportCycles("Rest"); } else if (mi == this.vampireLossMenuItem) { showVampireLoss(); } } // Options->"Inactivity Mode" menu items @FXML void inactivityOptionsHandler(ActionEvent event) { if (event.getTarget() == allowSleepMenuItem) app.api.allowSleeping(); else app.api.stayAwake(); } // Help->Documentation @FXML private void helpHandler(ActionEvent event) { app.showDocument(DocumentationURL); } // Help->What's New @FXML private void whatsNewHandler(ActionEvent event) { app.showDocument(ReleaseNotesURL); } // Help->About @FXML private void aboutHandler(ActionEvent event) { Dialogs.showInformationDialog(app.stage, "Copyright (c) 2013, Joe Pasqua\n" + "Free for personal and non-commercial use.\n" + "Based on the great API detective work of many members\n" + "of teslamotorsclub.com. All Tesla imagery derives\n" + "from the official Tesla iPhone app.", App.productName() + " " + App.productVersion(), "About " + App.productName()); } // Help->Check for Updates @FXML private void updatesHandler(ActionEvent event) { if (!VersionUpdater.checkForNewerVersion(App.productVersion(), app.stage, app.getHostServices(), prefs.offerExperimental.get())) { Dialogs.showInformationDialog(app.stage, "You already have the latest release.", "Update Check Results", "Checking for Updates"); } } @FXML private void remoteStart(ActionEvent e) { final String[] unp = PasswordDialog.getCredentials(app.stage, "Authenticate", "Remote Start", false); if (unp == null) return; // User cancelled if (unp[1] == null || unp[1].isEmpty()) { Dialogs.showErrorDialog(app.stage, "You must enter a password"); return; } issuer.issueCommand(new Callable<Result>() { @Override public Result call() { return vtVehicle.getVehicle().remoteStart(unp[1]); } }, true, null, "Remote Start"); } // Options->Action_>{Honk,Flsh,Wakeup} @FXML private void honk(ActionEvent e) { issuer.issueCommand(new Callable<Result>() { @Override public Result call() { return vtVehicle.getVehicle().honk(); } }, true, null, "Honk"); } @FXML private void flash(ActionEvent e) { issuer.issueCommand(new Callable<Result>() { @Override public Result call() { return vtVehicle.getVehicle().flashLights(); } }, true, null, "Flash Lights"); } @FXML private void wakeup(ActionEvent e) { issuer.issueCommand(new Callable<Result>() { @Override public Result call() { return vtVehicle.getVehicle().wakeUp(); } }, true, null, "Wake up"); } /*------------------------------------------------------------------------------ * * Export Handling Methods * *----------------------------------------------------------------------------*/ private void exportCycles(String cycleType) { String initialDir = prefs.storage().get(App.LastExportDirKey, System.getProperty("user.home")); FileChooser fileChooser = new FileChooser(); fileChooser.setTitle("Export " + cycleType + " Data"); fileChooser.setInitialDirectory(new File(initialDir)); Stage stage = app.stage; File file = fileChooser.showSaveDialog(stage); if (file != null) { String enclosingDirectory = file.getParent(); if (enclosingDirectory != null) prefs.storage().put(App.LastExportDirKey, enclosingDirectory); Range<Long> exportPeriod = DateRangeDialog.getExportPeriod(stage); if (exportPeriod == null) return; boolean exported; if (cycleType.equals("Charge")) { exported = vtData.exportCharges(file, exportPeriod); } else { exported = vtData.exportRests(file, exportPeriod); } if (exported) { Dialogs.showInformationDialog(stage, "Your data has been exported", "Data Export Process", "Export Complete"); } else { Dialogs.showErrorDialog(stage, "Unable to save to: " + file, "Data Export Process", "Export Failed"); } } } private void exportStats(String[] columns) { String initialDir = prefs.storage().get(App.LastExportDirKey, System.getProperty("user.home")); FileChooser fileChooser = new FileChooser(); fileChooser.setTitle("Export Data"); fileChooser.setInitialDirectory(new File(initialDir)); File file = fileChooser.showSaveDialog(app.stage); if (file != null) { String enclosingDirectory = file.getParent(); if (enclosingDirectory != null) prefs.storage().put(App.LastExportDirKey, enclosingDirectory); Range<Long> exportPeriod = DateRangeDialog.getExportPeriod(app.stage); if (exportPeriod == null) return; if (vtData.export(file, exportPeriod, columns)) { Dialogs.showInformationDialog(app.stage, "Your data has been exported", "Data Export Process", "Export Complete"); } else { Dialogs.showErrorDialog(app.stage, "Unable to save to: " + file, "Data Export Process", "Export Failed"); } } } /*------------------------------------------------------------------------------ * * Other UI Handlers and utilities * *----------------------------------------------------------------------------*/ private void showVampireLoss() { Range<Long> exportPeriod = getExportPeriod(); if (exportPeriod != null) { List<RestCycle> rests = vtData.getRestCycles(exportPeriod); boolean useMiles = vtVehicle.unitType() == Utils.UnitType.Imperial; // Compute some stats and generate detail output long totalRestTime = 0; double totalLoss = 0; for (RestCycle r : rests) { totalRestTime += r.endTime - r.startTime; totalLoss += r.startRange - r.endRange; } VampireLossResults.show(app.stage, rests, useMiles ? "mi" : "km", totalLoss / hours(totalRestTime)); } } private double hours(long millis) { return ((double) (millis)) / (60 * 60 * 1000); } private void addSystemSpecificHandlers(final Stage theStage) { if (SystemUtils.IS_OS_MAC) { // Add a handler for Command-H theStage.getScene().getAccelerators() .put(new KeyCodeCombination(KeyCode.H, KeyCombination.SHORTCUT_DOWN), new Runnable() { @Override public void run() { theStage.setIconified(true); } }); } } private void refreshTitle() { Vehicle v = vtVehicle.getVehicle(); String carName = (v != null) ? v.getDisplayName() : null; String title = App.productName() + " " + App.productVersion(); if (carName != null) title = title + " for " + carName; if (app.api.isIdle()) { String time = String.format("%1$tH:%1$tM", new Date()); title = title + " [sleeping at " + time + "]"; } app.stage.setTitle(title); } private void setAppModeMenu() { if (app.api.allowingSleeping()) allowSleepMenuItem.setSelected(true); else stayAwakeMenuItem.setSelected(true); } private void logAppInfo() { logger.info(App.productName() + ": " + App.productVersion()); logger.info(String.format("Max memory: %4dmb", Runtime.getRuntime().maxMemory() / (1024 * 1024))); List<String> jvmArgs = Utils.getJVMArgs(); logger.info("JVM Arguments"); if (jvmArgs != null) { for (String arg : jvmArgs) { logger.info("Arg: " + arg); } } } private class WakeEarlyPredicate implements Utils.Predicate { private long lastEval = System.currentTimeMillis(); @Override public boolean eval() { try { if (app.api.mode.lastSet() > lastEval && app.api.stayingAwake()) return true; return ThreadManager.get().shuttingDown(); } finally { lastEval = System.currentTimeMillis(); } } } private class CollectNowPredicate implements VTData.TimeBasedPredicate { private long last = Long.MAX_VALUE; @Override public void setTime(long time) { last = time; } @Override public boolean eval() { return (app.api.isActive() && app.api.state.lastSet() > last); } } private class PassiveCollectionPredicate implements Utils.Predicate { @Override public boolean eval() { return (app.api.isIdle() && app.api.allowingSleeping()); } } /*------------------------------------------------------------------------------ * * Display various info and warning dialogs * *----------------------------------------------------------------------------*/ private boolean letItSleep() { WakeSleepDialog wsd = WakeSleepDialog.show(app.stage); return wsd.letItSleep(); } @FXML private void wakeButtonHandler(ActionEvent event) { forceWakeup.set(true); } private void warnAboutNoCarAccess(final boolean mobileDisabled) { Platform.runLater(new Runnable() { @Override public void run() { String message = "VisibleTesla is unable to access your car even though" + "the login succeeded.\n\n"; if (mobileDisabled) message = message + "Your Tesla has not been configured to allow mobile " + "access. You have to enable this on your car's touch" + "screen using Controls / Settings / Vehicle.\n\n"; message = message + "VisibleTesla will still launch, but will not" + "be able to control or monitor your car until access is restored."; Dialogs.showErrorDialog(app.stage, message, "Mobile access is not enabled", "Communication Problem"); } }); } private void exitWithMobileAccessError() { Platform.runLater(new Runnable() { @Override public void run() { Dialogs.showErrorDialog(app.stage, "Your Tesla has not been configured to allow mobile " + "access. You have to enable this on your car's touch" + "screen using Controls / Settings / Vehicle." + "\n\nChange that setting in your car, then relaunch VisibleTesla.", "Unable to communicate with your Tesla", "Communication Problem"); logger.log(Level.SEVERE, "Mobile access is not enabled - exiting."); Platform.exit(); } }); } private void exitWithCachingError() { Platform.runLater(new Runnable() { @Override public void run() { Dialogs.showErrorDialog(app.stage, "Failed to connect to your vehicle even after a successful " + "login. It may be in a deep sleep and can't be woken up.\n" + "\nPlease try to wake your Tesla and then try VisibleTesla again.", "Unable to communicate with your Tesla", "Communication Problem"); logger.severe("Can't communicate with vehicle - exiting."); Platform.exit(); } }); } private void showLockError() { Dialogs.showErrorDialog(app.stage, "There appears to be another copy of VisibleTesla\n" + "running on this computer and trying to talk\n" + "to the same car. That can cause problems and\n" + "is not allowed\n\n" + "VisibleTesla will close when you close this window.", "Multiple Copies of VisibleTesla", "Problem launching application"); } private void showDisclaimer() { boolean disclaimer = prefs.storage().getBoolean("V2_Disclaimer", false); if (!disclaimer) { Dialogs.showInformationDialog(app.stage, "Use this application at your own risk. The author\n" + "does not guarantee its proper functioning.\n" + "It is possible that use of this application may cause\n" + "unexpected damage for which nobody but you are\n" + "responsible. Use of this application can change the\n" + "settings on your car and may have negative\n" + "consequences such as (but not limited to):\n" + "unlocking the doors, opening the sun roof, or\n" + "reducing the available charge in the battery.", "Please Read Carefully", "Disclaimer"); Dialogs.showInformationDialog(app.stage, "Use of this application may result in Tesla Motors\n" + "restricting your access via your official Tesla mobile\n" + "phone application. The author takes no responsibilitity\n" + "for this outcome. By running this application you\n" + "acknowledge this and accept all responsibility for\n" + "any adverse outcomes.", "Please Read Carefully", "Disclaimer"); prefs.storage().putBoolean("V2_Disclaimer", true); } } private Range<Long> getExportPeriod() { NavigableMap<Long, Row> rows = vtData.getAllLoadedRows(); long timestamp = rows.firstKey(); Calendar start = Calendar.getInstance(); start.setTimeInMillis(timestamp); timestamp = rows.lastKey(); Calendar end = Calendar.getInstance(); end.setTimeInMillis(timestamp); Range<Long> exportPeriod = DateRangeDialog.getExportPeriod(app.stage, start, end); return exportPeriod; } }