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.data.*; import envoy.data.User.UserStatus; import envoy.event.UserStatusChange; import envoy.exception.EnvoyException; import envoy.util.EnvoyLog; import envoy.client.data.*; import envoy.client.data.shortcuts.EnvoyShortcutConfig; import envoy.client.helper.ShutdownHelper; import envoy.client.net.Client; import envoy.client.ui.controller.LoginScene; import envoy.client.util.IconUtil; /** * Handles application startup. * * @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.2-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 { // Initialize config and logger 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 localDBFile = new File(config.getHomeDirectory(), config.getServer()); logger.info("Initializing LocalDB at " + localDBFile); localDB = new LocalDB(localDBFile); } 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")); // Configure global shortcuts EnvoyShortcutConfig.initializeEnvoyShortcuts(); // Create scene context final var sceneContext = new SceneContext(stage); context.setSceneContext(sceneContext); // Authenticate with token if present or load login scene 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 sceneContext.load(SceneInfo.LOGIN_SCENE); stage.show(); } /** * 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 originalStatus = localDB.getUser() == null ? UserStatus.ONLINE : localDB.getUser().getStatus(); try { client.performHandshake(credentials); if (client.isOnline()) { // Restore the original status as the server automatically returns status ONLINE client.getSender().setStatus(originalStatus); loadChatScene(); // Request an ID generator if none is present or the existing one is consumed if (!localDB.hasIDGenerator() || !localDB.getIDGenerator().hasNext()) client.requestIDGenerator(); 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 final var user = client.getSender(); localDB.setUser(user); // 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(); // Inform the server that this user has a different user status than expected if (!user.getStatus().equals(UserStatus.ONLINE)) client.send(new UserStatusChange(user)); } 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(SceneInfo.CHAT_SCENE); stage.centerOnScreen(); // Exit or minimize the stage when a close request occurs stage.setOnCloseRequest( e -> { ShutdownHelper.exit(); if (Settings.getInstance().isHideOnClose() && StatusTrayIcon.isSupported()) e.consume(); }); // Initialize status tray icon if (StatusTrayIcon.isSupported()) new StatusTrayIcon(stage).show(); // Start auto save thread localDB.initAutoSave(); } }