package org.multibit.hd.ui;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.google.common.util.concurrent.Uninterruptibles;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import org.multibit.commons.concurrent.SafeExecutors;
import org.multibit.hd.core.config.Configurations;
import org.multibit.hd.core.dto.WalletSummary;
import org.multibit.hd.core.events.CoreEvents;
import org.multibit.hd.core.events.ShutdownEvent;
import org.multibit.hd.core.logging.LoggingFactory;
import org.multibit.hd.core.managers.HttpsManager;
import org.multibit.hd.core.managers.InstallationManager;
import org.multibit.hd.core.managers.WalletManager;
import org.multibit.hd.core.services.CoreServices;
import org.multibit.hd.core.utils.OSUtils;
import org.multibit.hd.hardware.core.HardwareWalletService;
import org.multibit.hd.hardware.core.events.HardwareWalletEvents;
import org.multibit.hd.ui.audio.Sounds;
import org.multibit.hd.ui.controllers.HeaderController;
import org.multibit.hd.ui.controllers.MainController;
import org.multibit.hd.ui.events.controller.ControllerEvents;
import org.multibit.hd.ui.events.view.ViewEvents;
import org.multibit.hd.ui.platform.GenericApplicationFactory;
import org.multibit.hd.ui.platform.GenericApplicationSpecification;
import org.multibit.hd.ui.services.ExternalDataListeningService;
import org.multibit.hd.ui.views.MainView;
import org.multibit.hd.ui.views.themes.ThemeKey;
import org.multibit.hd.ui.views.themes.Themes;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.swing.*;
import javax.swing.plaf.nimbus.NimbusLookAndFeel;
import javax.swing.text.DefaultEditorKit;
import java.awt.*;
import java.awt.event.KeyEvent;
import java.io.File;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* <p>Main entry point to the application</p>
*/
public class MultiBitHD {
//////////////////////////// NO STATIC VARIABLES ALLOWED IN THIS CLASS! ////////////////////////
// See initialiseSystemProperties for explanation
// Special case logger that is not static
private final Logger log = LoggerFactory.getLogger(MultiBitHD.class);
private MainController mainController;
////////////////////////////////////////////////////////////////////////////////////////////////
/**
* <p>Main entry point to the application</p>
*
* @param args None specified
*/
public static void main(String[] args) throws Exception {
// These are called at the first line to avoid any other class loading
// interfering with them
initialiseSystemProperties();
// Hand over to an instance to simplify FEST tests
final MultiBitHD multiBitHD = new MultiBitHD();
if (!multiBitHD.start(args)) {
// Failed to start so issue a hard shutdown
CoreServices.shutdownNow(ShutdownEvent.ShutdownType.HARD);
} else {
// Initialise the UI views in the EDT, Nimbus etc
SwingUtilities.invokeLater(
new Runnable() {
@Override
public void run() {
multiBitHD.initialiseUIViews();
}
});
}
}
/**
* Initialise any system properties that need to be in place before any other
* classes are loaded
*/
private static void initialiseSystemProperties() {
// Fix for "TimSort" clipboard failure - https://github.com/keepkey/multibit-hd/issues/645
// Suggested by https://www.java.net/node/700601
// See also http://stackoverflow.com/a/26829874/396747 for more details on ordering at startup
// Verified that java.util.Arrays has not been loaded at this stage
System.setProperty("java.util.Arrays.useLegacyMergeSort", "true");
// Fix for Windows / Java 7 / VPN bug
System.setProperty("java.net.preferIPv4Stack", "true");
// Fix for version.txt not visible for Java 7
System.setProperty("jsse.enableSNIExtension", "true");
}
/**
* <p>Start this instance of MultiBit HD</p>
*
* @param args The command line arguments
*
* @return True if the application started successfully, false if a shutdown is required
*
* @throws Exception If something goes wrong
*/
public boolean start(String[] args) throws Exception {
// Start the logging factory (see later for instance) to get console logging up fast
LoggingFactory.bootstrap();
// Get the configuration fast (Bitcoin URI processing relies on it)
CoreServices.bootstrap();
// Analyse the command line
if (args != null && args.length > 0) {
// Show the command line arguments
for (int i = 0; i < args.length; i++) {
log.info("MultiBit launched with args[{}]: '{}'", i, args[i]);
}
} else {
// Provide empty arguments to avoid potential NPEs
log.info("No command line arguments");
args = new String[]{};
}
// Check for another instance as soon as possible
Optional<ExternalDataListeningService> externalDataListeningService = initialiseListeningService(args);
if (!externalDataListeningService.isPresent()) {
return false;
}
// Prepare the CA certs (run on a separate thread)
initialiseCaCerts();
// Start core services (logging, environment alerts, configuration, Bitcoin URI handling etc)
initialiseCore(args);
// Create controllers so that the generic app can access listeners
if (!initialiseUIControllers()) {
// Required to shut down
return false;
}
// Must be OK to be here
return true;
}
public void stop() {
log.debug("Stopping MultiBit HD");
mainController = null;
// Final purge in case anything gets missed
ViewEvents.unsubscribeAll();
ControllerEvents.unsubscribeAll();
CoreEvents.unsubscribeAll();
HardwareWalletEvents.unsubscribeAll();
}
/**
* @param args The command line arguments
*
* @return The external data listening service if it could be started successfully, absent implies another instance
*/
private Optional<ExternalDataListeningService> initialiseListeningService(String[] args) {
// Determine if another instance is running and shutdown if this is the case
ExternalDataListeningService externalDataListeningService = new ExternalDataListeningService(args);
if (externalDataListeningService.start()) {
return Optional.of(externalDataListeningService);
}
// Must have failed to be here
return Optional.absent();
}
/**
* <p>Initialise the CA certs</p>
*/
@SuppressFBWarnings({"DM_EXIT"})
private void initialiseCaCerts() throws Exception {
log.debug("Initialising CA certs...");
try {
// Execute the CA certificates download on a separate thread to avoid slowing
// the startup time
SafeExecutors.newSingleThreadExecutor("install-cacerts").submit(
new Runnable() {
@Override
public void run() {
HttpsManager.INSTANCE.installCACertificates(
InstallationManager.getOrCreateApplicationDataDirectory(),
InstallationManager.CA_CERTS_NAME,
null, // Use default host list
false // Do not force loading if they are already present
);
}
});
} catch (SecurityException se) {
log.error("Security exception: {} {}", se.getClass().getName(), se.getMessage());
}
}
/**
* <p>Initialise the UI controllers once all the core services are in place</p>
* <p>This creates the singleton controllers that respond to generic events</p>
* <p>At this stage none of the following will be running:</p>
* <ul>
* <li>Themes or views</li>
* <li>Wallet service</li>
* <li>Backup service</li>
* <li>Bitcoin network service</li>
* </ul>
*/
private boolean initialiseUIControllers() {
if (OSUtils.isWindowsXPOrEarlier()) {
log.error("Windows XP or earlier detected. Forcing shutdown.");
JOptionPane.showMessageDialog(
null, "This version of Windows is not supported for security reasons.\nPlease upgrade.", "Error",
JOptionPane.ERROR_MESSAGE);
return false;
}
// Including the other controllers avoids dangling references during a soft shutdown
mainController = new MainController(
new HeaderController()
);
// Start the hardware wallet support to allow credentials screen to be selected
mainController.handleHardwareWallets();
// Set the tooltip delay to be slightly longer
ToolTipManager.sharedInstance().setInitialDelay(1000);
// Must be OK to be here
return true;
}
/**
* <p>Initialise the UI once all the core services are in place</p>
* <p>This creates the singleton views and controllers that respond to configuration
* and theme changes</p>
* <p>At this stage none of the following will be running:</p>
* <ul>
* <li>Wallet service</li>
* <li>Backup service</li>
* <li>Bitcoin network service</li>
* </ul>
* <p>Once the UI renders, control passes to the <code>MainController</code> to
* respond to the wizard close event which will trigger ongoing initialisation.</p>
*/
@SuppressFBWarnings({"DM_EXIT"})
public MainView initialiseUIViews() {
log.debug("Initialising UI...");
Preconditions.checkNotNull(mainController, "'mainController' must be present. FEST will cause this if another instance is running.");
Preconditions.checkState(SwingUtilities.isEventDispatchThread(), "Must execute on EDT. Check calling environment.");
// Give MultiBit Hardware a chance to process any attached hardware wallet
// and for MainController to subsequently process the events
// The delay observed in reality and FEST tests ranges from 1400-2200ms and if
// not included results in wiped hardware wallets being missed on startup
log.debug("Starting the clock for hardware wallet initialisation");
long hardwareInitialisationTime = System.currentTimeMillis();
// Perform time consuming tasks to use the hardware initialisation time to best effect
// Prepare platform-specific integration (protocol handlers, quit events etc)
initialiseGenericApp();
// Pre-load sound library
Sounds.initialise();
try {
// Set look and feel (expect ~1000ms to perform this)
log.debug("Loading Nimbus LaF...");
UIManager.setLookAndFeel(new NimbusLookAndFeel() {
// Require configurable error feedback through beeps
@Override
public void provideErrorFeedback(Component component) {
if (Configurations.currentConfiguration.getSound().isAlertSound()) {
super.provideErrorFeedback(component);
}
}
});
} catch (UnsupportedLookAndFeelException e) {
try {
log.warn("Falling back to cross platform LaF...");
UIManager.setLookAndFeel(UIManager.getCrossPlatformLookAndFeelClassName());
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException e1) {
log.error("No look and feel available. MultiBit HD requires Java 7 or higher.", e1);
System.exit(-1);
}
}
log.debug("LaF loaded OK");
// This must be performed immediately after the LaF has been set
if (OSUtils.isMac()) {
log.debug("Applying OSX key bindings...");
// Ensure the correct name is displayed in the application menu
System.setProperty("com.apple.mrj.application.apple.menu.about.name", "multiBit HD");
// Ensure OSX key bindings are used for copy, paste etc
// Use the Nimbus keys and ensure this occurs before any component creation
addOSXKeyStrokes((InputMap) UIManager.get("EditorPane.focusInputMap"));
addOSXKeyStrokes((InputMap) UIManager.get("FormattedTextField.focusInputMap"));
addOSXKeyStrokes((InputMap) UIManager.get("PasswordField.focusInputMap"));
addOSXKeyStrokes((InputMap) UIManager.get("TextField.focusInputMap"));
addOSXKeyStrokes((InputMap) UIManager.get("TextPane.focusInputMap"));
addOSXKeyStrokes((InputMap) UIManager.get("TextArea.focusInputMap"));
}
// Ensure that we are using the configured theme (must be after key bindings)
log.debug("Switching theme...");
ThemeKey themeKey = ThemeKey.valueOf(Configurations.currentConfiguration.getAppearance().getCurrentTheme());
Themes.switchTheme(themeKey.theme());
log.debug("Building MainView...");
// Build a new MainView
final MainView mainView = new MainView();
mainController.setMainView(mainView);
log.debug("Checking for pre-existing wallets...");
// Check for any pre-existing wallets in the application directory
File applicationDataDirectory = InstallationManager.getOrCreateApplicationDataDirectory();
List<File> walletDirectories = WalletManager.findWalletDirectories(applicationDataDirectory);
List<WalletSummary> softWalletSummaries = WalletManager.getSoftWalletSummaries(Optional.of(Configurations.currentConfiguration.getLocale()));
// Check for fresh install
boolean noWallets = walletDirectories.isEmpty();
boolean noSoftWallets = softWalletSummaries.isEmpty();
// HardwareWalletService needs HARDWARE_INITIALISATION_TIME milliseconds to initialise so sleep the rest
conditionallySleep(hardwareInitialisationTime);
boolean deviceAttached = false;
boolean deviceWiped = false;
// Check hardware wallet situation after first initialisation (may not be present or ready)
Optional<HardwareWalletService> hardwareWalletService = CoreServices.useFirstReadyHardwareWalletService();
if (hardwareWalletService.isPresent()) {
// Hardware wallet must be attached to be present
deviceAttached = true;
if (!hardwareWalletService.get().isWalletPresent()) {
log.debug("Wiped hardware wallet detected");
// Must show the welcome wizard in hardware wallet mode
// regardless of wallet or licence situation
// MainController should have handled the events
deviceWiped = true;
}
}
// Determine if welcome wizard should show
boolean showWelcomeWizard = (deviceAttached && deviceWiped) // We have a wiped hardware wallet so need to initialise
|| (noSoftWallets && !deviceAttached) // No soft wallets and no hardware wallet so need to create/restore one
|| noWallets; // No wallets at all so need to create/restore one (either hard or soft)
if (showWelcomeWizard) {
log.debug("Showing the welcome wizard");
mainView.setShowExitingWelcomeWizard(true);
mainView.setShowExitingCredentialsWizard(false);
} else {
log.debug("Showing the credentials wizard");
mainView.setShowExitingCredentialsWizard(true);
mainView.setShowExitingWelcomeWizard(false);
}
// Provide a backdrop to the user and trigger the showing of the wizard
mainView.refresh(false);
log.debug("MainView is ready");
// See the MainController wizard hide event for the next stage
return mainView;
}
/**
* <p>Initialise the platform-specific services</p>
*/
private void initialiseGenericApp() {
GenericApplicationSpecification specification = new GenericApplicationSpecification();
specification.getOpenURIEventListeners().add(mainController);
specification.getOpenFilesEventListeners().add(mainController);
specification.getPreferencesEventListeners().add(mainController);
specification.getAboutEventListeners().add(mainController);
specification.getQuitEventListeners().add(mainController);
GenericApplicationFactory.INSTANCE.buildGenericApplication(specification);
}
/**
* <p>Initialise the core services</p>
*
* @param args The command line arguments
*/
private void initialiseCore(String[] args) {
log.debug("Initialising Core...");
// Start the core services
CoreServices.main(args);
}
/**
* <p>Apply OSX key strokes to input map for consistent UX</p>
*
* @param inputMap The input map for the Swing component
*/
private void addOSXKeyStrokes(InputMap inputMap) {
// Undo and redo require more complex handling
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_C, KeyEvent.ALT_DOWN_MASK), DefaultEditorKit.copyAction);
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_X, KeyEvent.META_DOWN_MASK), DefaultEditorKit.cutAction);
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_V, KeyEvent.META_DOWN_MASK), DefaultEditorKit.pasteAction);
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_A, KeyEvent.META_DOWN_MASK), DefaultEditorKit.selectAllAction);
}
/**
* Allow a delay of HARDWARE_INITIALISATION_TIME from the startTime
*
* @param startTime The reference time from which to measure the amount of sleep from
*/
private void conditionallySleep(long startTime) {
// Check if Trezor is required
if (!Configurations.currentConfiguration.isTrezor()) {
return;
}
final long HARDWARE_INITIALISATION_TIME = 2000; // milliseconds
long currentTime = System.currentTimeMillis();
long timeSpent = currentTime - startTime;
if (timeSpent < HARDWARE_INITIALISATION_TIME) {
long sleepFor = HARDWARE_INITIALISATION_TIME - timeSpent;
log.debug("Sleep for an extra {} milliseconds to allow hardware wallets to initialise", sleepFor);
Uninterruptibles.sleepUninterruptibly(sleepFor, TimeUnit.MILLISECONDS);
log.debug("Finished sleep");
} else {
log.debug("No need for extra sleep time to allow hardware wallets to initialise");
}
}
}