package envoy.client.ui; import java.io.*; import java.time.Instant; import java.util.concurrent.TimeoutException; import java.util.logging.*; import javafx.application.Application; import javafx.scene.control.Alert; import javafx.scene.control.Alert.AlertType; import javafx.stage.Stage; import envoy.client.data.*; import envoy.client.event.EnvoyCloseEvent; import envoy.client.net.Client; import envoy.client.ui.SceneContext.SceneInfo; import envoy.client.ui.controller.LoginScene; import envoy.data.*; import envoy.data.User.UserStatus; import envoy.event.*; import envoy.exception.EnvoyException; import envoy.util.EnvoyLog; import dev.kske.eventbus.EventBus; /** * Handles application startup and shutdown. *

* Project: envoy-client
* File: Startup.java
* Created: 26.03.2020
* * @author Kai S. K. Engelbart * @author Maximilian Käfer * @since Envoy Client v0.1-beta */ public final class Startup extends Application { /** * The version of this client. Used to verify compatibility with the server. * * @since Envoy Client v0.1-beta */ public static final String VERSION = "0.1-beta"; private static LocalDB localDB; private static final Context context = Context.getInstance(); private static final Client client = context.getClient(); private static final ClientConfig config = ClientConfig.getInstance(); private static final Logger logger = EnvoyLog.getLogger(Startup.class); /** * Loads the configuration, initializes the client and the local database and * delegates the rest of the startup process to {@link LoginScene}. * * @since Envoy Client v0.1-beta */ @Override public void start(Stage stage) throws Exception { try { config.loadAll(Startup.class, "client.properties", getParameters().getRaw().toArray(new String[0])); EnvoyLog.initialize(config); } catch (final IllegalStateException e) { new Alert(AlertType.ERROR, "Error loading configuration values:\n" + e); logger.log(Level.SEVERE, "Error loading configuration values: ", e); System.exit(1); } logger.log(Level.INFO, "Envoy starting..."); // Initialize the local database try { final var localDBDir = new File(config.getHomeDirectory(), config.getLocalDB().getPath()); logger.info("Initializing LocalDB at " + localDBDir); localDB = new LocalDB(localDBDir); } catch (IOException | EnvoyException e) { logger.log(Level.SEVERE, "Could not initialize local database: ", e); new Alert(AlertType.ERROR, "Could not initialize local database!\n" + e).showAndWait(); System.exit(1); return; } // Prepare handshake context.setLocalDB(localDB); // Configure stage stage.setTitle("Envoy"); stage.getIcons().add(IconUtil.loadIcon("envoy_logo")); // Create scene context final var sceneContext = new SceneContext(stage); context.setSceneContext(sceneContext); // Authenticate with token if present if (localDB.getAuthToken() != null) { logger.info("Attempting authentication with token..."); localDB.loadUserData(); if (!performHandshake( LoginCredentials.loginWithToken(localDB.getUser().getName(), localDB.getAuthToken(), VERSION, localDB.getLastSync()))) sceneContext.load(SceneInfo.LOGIN_SCENE); } else // Load login scene sceneContext.load(SceneInfo.LOGIN_SCENE); } /** * Tries to perform a Handshake with the server. * * @param credentials the credentials to use for the handshake * @return whether the handshake was successful or offline mode could be entered * @since Envoy Client v0.2-beta */ public static boolean performHandshake(LoginCredentials credentials) { final var cacheMap = new CacheMap(); cacheMap.put(Message.class, new Cache()); cacheMap.put(GroupMessage.class, new Cache()); cacheMap.put(MessageStatusChange.class, new Cache()); cacheMap.put(GroupMessageStatusChange.class, new Cache()); try { final var client = context.getClient(); client.performHandshake(credentials, cacheMap); if (client.isOnline()) { loadChatScene(); client.initReceiver(localDB, cacheMap); return true; } else return false; } catch (IOException | InterruptedException | TimeoutException e) { logger.log(Level.INFO, "Could not connect to server. Entering offline mode..."); return attemptOfflineMode(credentials.getIdentifier()); } } /** * Attempts to load {@link envoy.client.ui.controller.ChatScene} in offline mode * for a given user. * * @param identifier the identifier of the user - currently his username * @return whether the offline mode could be entered * @since Envoy Client v0.2-beta */ public static boolean attemptOfflineMode(String identifier) { try { // Try entering offline mode final User clientUser = localDB.getUsers().get(identifier); if (clientUser == null) throw new EnvoyException("Could not enter offline mode: user name unknown"); client.setSender(clientUser); loadChatScene(); return true; } catch (final Exception e) { new Alert(AlertType.ERROR, "Client error: " + e).showAndWait(); logger.log(Level.SEVERE, "Offline mode could not be loaded: ", e); System.exit(1); return false; } } /** * Loads the last known time a user has been online. * * @param identifier the identifier of this user - currently his name * @return the last {@code Instant} at which he has been online * @since Envoy Client v0.2-beta */ public static Instant loadLastSync(String identifier) { try { localDB.setUser(localDB.getUsers().get(identifier)); localDB.loadUserData(); } catch (final Exception e) { // User storage empty, wrong user name etc. -> default lastSync } return localDB.getLastSync(); } private static void loadChatScene() { // Set client user in local database localDB.setUser(client.getSender()); // Initialize chats in local database try { localDB.loadUserData(); } catch (final FileNotFoundException e) { // The local database file has not yet been created, probably first login } catch (final Exception e) { new Alert(AlertType.ERROR, "Error while loading local database: " + e + "\nChats will not be stored locally.").showAndWait(); logger.log(Level.WARNING, "Could not load local database: ", e); } context.initWriteProxy(); if (client.isOnline()) context.getWriteProxy().flushCache(); else // Set all contacts to offline mode localDB.getChats() .stream() .map(Chat::getRecipient) .filter(User.class::isInstance) .map(User.class::cast) .forEach(u -> u.setStatus(UserStatus.OFFLINE)); final var stage = context.getStage(); // Pop LoginScene if present if (!context.getSceneContext().isEmpty()) context.getSceneContext().pop(); // Load ChatScene stage.setMinHeight(400); stage.setMinWidth(843); context.getSceneContext().load(SceneContext.SceneInfo.CHAT_SCENE); stage.centerOnScreen(); if (StatusTrayIcon.isSupported()) { // Configure hide on close stage.setOnCloseRequest(e -> { if (Settings.getInstance().isHideOnClose()) { stage.setIconified(true); e.consume(); } else EventBus.getInstance().dispatch(new EnvoyCloseEvent()); }); // Initialize status tray icon final var trayIcon = new StatusTrayIcon(stage); Settings.getInstance().getItems().get("hideOnClose").setChangeHandler(c -> { if ((Boolean) c) trayIcon.show(); else trayIcon.hide(); }); } } }