org.noroomattheinn.visibletesla.App.java Source code

Java tutorial

Introduction

Here is the source code for org.noroomattheinn.visibletesla.App.java

Source

/*
 * App.java - Copyright(c) 2013 Joe Pasqua
 * Provided under the MIT License. See the LICENSE file for details.
 * Created: Aug 30, 2013
 */
package org.noroomattheinn.visibletesla;

import com.sun.net.httpserver.BasicAuthenticator;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.List;
import java.util.Properties;
import javafx.application.Application;
import javafx.application.HostServices;
import javafx.application.Platform;
import javafx.event.EventHandler;
import javafx.scene.Node;
import javafx.scene.control.Tab;
import javafx.scene.input.InputEvent;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseEvent;
import javafx.stage.Stage;
import org.apache.commons.codec.digest.DigestUtils;
import org.noroomattheinn.tesla.Tesla;
import org.noroomattheinn.utils.PWUtils;
import org.noroomattheinn.utils.RestHelper;
import org.noroomattheinn.utils.ThreadManager;
import org.noroomattheinn.utils.TrackedObject;
import org.noroomattheinn.utils.Utils;

import static org.noroomattheinn.tesla.Tesla.logger;
import static org.noroomattheinn.utils.Utils.timeSince;

/**
 * App - Stores state about the app for use across the app. This is a singleton
 * that is created at app startup.
 *
 * @author Joe Pasqua <joe at NoRoomAtTheInn dot org>
 */
class App {
    /*------------------------------------------------------------------------------
     *
     * Constants and Enums
     * 
     *----------------------------------------------------------------------------*/

    private static final String AppPropertiesFile = "org/noroomattheinn/visibletesla/VT.properties";
    static final String LastExportDirKey = "APP_LAST_EXPORT_DIR";

    /*------------------------------------------------------------------------------
     *
     * Internal State
     * 
     *----------------------------------------------------------------------------*/

    private final File appFilesFolder;
    private final Prefs prefs;
    private final Application fxApp;
    private final PWUtils pwUtils = new PWUtils();
    private byte[] encPW, salt;
    private long lastEventTime;

    private static Properties appProperties = loadAppProperties(
            App.class.getClassLoader().getResourceAsStream(AppPropertiesFile));

    /*------------------------------------------------------------------------------
     *
     * Package-wide State
     * 
     *----------------------------------------------------------------------------*/

    final AppAPI api;
    final Tesla tesla;
    final Stage stage;
    final ProgressListener progressListener;
    final TrackedObject<String> schedulerActivity;

    /*==============================================================================
     * -------                                                               -------
     * -------         Package Internal Interface To This Class              ------- 
     * -------                                                               -------
     *============================================================================*/

    /**
     * Called once when the app starts in order to create the singleton. 
     * @param fxApp The JavaFX Application object
     * @param stage The JavaFX stage corresponding to our base window
     * @param prefs The application preferences
     * @return      The newly created singleton.
     */
    App(Application fxApp, Stage stage, final Prefs prefs) {
        this.fxApp = fxApp;
        this.stage = stage;
        this.prefs = prefs;
        this.schedulerActivity = new TrackedObject<>("");
        this.api = new AppAPI(schedulerActivity);
        this.lastEventTime = System.currentTimeMillis();

        api.mode.addTracker(new Runnable() {
            @Override
            public void run() {
                logger.finest("App Mode changed to " + api.mode.get());
                prefs.persist("InactivityMode", api.mode.get().name());
                if (api.mode.get() == AppAPI.Mode.StayAwake) {
                    api.setActive();
                }
            }
        });

        api.state.addTracker(new Runnable() {
            @Override
            public void run() {
                logger.finest("App State changed to " + api.state.get());
                if (api.state.get() == AppAPI.State.Active) {
                    logger.info("Resetting Idle start time to now");
                    lastEventTime = System.currentTimeMillis();
                }
            }
        });

        appFilesFolder = Utils.ensureAppFilesFolder(productName());
        Utils.setupLogger(appFilesFolder, "visibletesla", logger, prefs.getLogLevel());

        if (prefs.enableProxy.get()) {
            // Enable the proxy at the lowest level so all services use it
            RestHelper.setDefaultProxy(prefs.proxyHost.get(), prefs.proxyPort.get());
        }
        tesla = new Tesla();

        this.progressListener = new ProgressListener(prefs.submitAnonFailure, getAppID());

        internalizePW(prefs.authCode.get());
    }

    static String productName() {
        return appProperties.getProperty("APP_NAME");
    }

    static String productVersion() {
        return appProperties.getProperty("APP_VERSION");
    }

    void showDocument(String doc) {
        fxApp.getHostServices().showDocument(doc);
    }

    HostServices getHostServices() {
        return fxApp.getHostServices();
    }

    /**
     * Establish ourselves as the only running instance of the app for
     * a particular vehicle id.
     * @return  true is we got the lock
     *          false if another instance is already running
     */
    boolean lock(String vin) {
        return (Utils.obtainLock(vin + ".lck", appFilesFolder));
    }

    /**
     * Get the system folder in which app related files are to be stored.
     * @return  The folder in which app related files are to be stored
     */
    File appFileFolder() {
        return appFilesFolder;
    }

    final String getAppID() {
        String appID = "Unidentified";
        try {
            InetAddress ip = InetAddress.getLocalHost();
            NetworkInterface network = NetworkInterface.getByInetAddress(ip);
            if (network != null) {
                byte[] mac = network.getHardwareAddress();
                appID = DigestUtils.sha256Hex(mac);
            }
        } catch (UnknownHostException | SocketException e) {
            logger.warning("Unable to generate an AppID: " + e.getMessage());
        }
        return appID;
    }

    /**
     * Add a tracker to a TrackedObject, but ensure it will run on the
     * FX Application Thread.
     * @param t The tracked object
     * @param r The Runnable to execute on the FXApplicationThread
     */
    static void addTracker(TrackedObject t, final Runnable r) {
        t.addTracker(new Runnable() {
            @Override
            public void run() {
                Platform.runLater(r);
            }
        });
    }

    /**
     * Set the mode based on the value in the persistent store
     */
    void restoreMode() {
        String modeName = prefs.storage().get("InactivityMode", AppAPI.Mode.StayAwake.name());
        // Handle obsolete values or changed names
        switch (modeName) {
        case "Sleep":
            modeName = "AllowSleeping";
            break; // Name Changed
        case "Awake":
            modeName = "StayAwake";
            break; // Name Changed
        case "AllowDaydreaming":
            modeName = "Awake";
            break; // Obsolete
        case "Daydream":
            modeName = "Awake";
            break; // Obsolete
        }
        api.mode.set(AppAPI.Mode.valueOf(modeName));
    }

    /*------------------------------------------------------------------------------
     *
     * Support for authenticating to web services
     * 
     *----------------------------------------------------------------------------*/

    /**
     * Set the password used by the RESTServer. If no password is supplied, 
     * a random one will be chosen meaning there is effectively no access to
     * the server.
     * @param   pw  The new password
     * @return  An external representation of the salted password that can be
     *          stored safely in a data file.
     */
    final String setPW(String pw) {
        if (pw == null || pw.isEmpty()) { // Choose a random value!
            pw = String.valueOf(Math.floor(Math.random() * 100000));
        }
        salt = pwUtils.generateSalt();
        encPW = pwUtils.getEncryptedPassword(pw, salt);
        return pwUtils.externalRep(salt, encPW);
    }

    BasicAuthenticator authenticator = new BasicAuthenticator("VisibleTesla") {
        @Override
        public boolean checkCredentials(String user, String pwd) {
            if (!user.equals("VT"))
                return false;
            if (encPW == null || salt == null)
                return false;
            return pwUtils.authenticate(pwd, encPW, salt);
        }
    };

    /**
     * Initialize the password and salt from previously generated values.
     * @param externalForm  An external representation of the password and
     *                      salt that was previously returned by an invocation
     *                      of setPW()
     */
    private void internalizePW(String externalForm) {
        // Break down the external representation into the salt and password
        List<byte[]> internalForm = (new PWUtils()).internalRep(externalForm);
        salt = internalForm.get(0);
        encPW = internalForm.get(1);
    }

    /*------------------------------------------------------------------------------
     *
     * PRIVATE - Methods and classes to track activity
     * 
     *----------------------------------------------------------------------------*/

    /**
     * Begin watching for user inactivity (keyboard input, mouse movements, etc.)
     * on any of the specified Tabs.
     * @param tabs  Watch for user activity targeted to any of these tabs.
     */
    void watchForUserActivity(List<Tab> tabs) {
        for (Tab t : tabs) {
            Node n = t.getContent();
            n.addEventFilter(KeyEvent.ANY, new EventPassThrough());
            n.addEventFilter(MouseEvent.MOUSE_PRESSED, new EventPassThrough());
            n.addEventFilter(MouseEvent.MOUSE_RELEASED, new EventPassThrough());
        }
        ThreadManager.get().launch(new InactivityThread(60L * 1000L * prefs.idleThresholdInMinutes.get()),
                "Inactivity");
    }

    private class InactivityThread implements Runnable {
        long idleThreshold;

        InactivityThread(long threshold) {
            this.idleThreshold = threshold;
        }

        @Override
        public void run() {
            while (true) {
                ThreadManager.get().sleep(60 * 1000);
                if (ThreadManager.get().shuttingDown()) {
                    return;
                }
                if (timeSince(lastEventTime) > idleThreshold && api.allowingSleeping()) {
                    api.state.update(AppAPI.State.Idle);
                }
            }
        }
    }

    private class EventPassThrough implements EventHandler<InputEvent> {
        @Override
        public void handle(InputEvent ie) {
            lastEventTime = System.currentTimeMillis();
            api.state.update(AppAPI.State.Active);
        }
    }

    private static Properties loadAppProperties(InputStream is) {
        Properties p = new Properties();
        try {
            p.load(is);
        } catch (IOException ex) {
            logger.warning("Couldn't load app properties file: " + ex);
        }
        return p;
    }

}