diff --git a/src/main/java/io/bitsquare/app/BitsquareEnvironment.java b/src/main/java/io/bitsquare/app/BitsquareEnvironment.java index 13669fd88e..5c80aea540 100644 --- a/src/main/java/io/bitsquare/app/BitsquareEnvironment.java +++ b/src/main/java/io/bitsquare/app/BitsquareEnvironment.java @@ -45,7 +45,9 @@ public class BitsquareEnvironment extends StandardEnvironment { public static final String APP_VERSION_KEY = "app.version"; + // TODO what is the difference to APP_DATA_DIR ? public static final String USER_DATA_DIR_KEY = "user.data.dir"; + public static final String DEFAULT_USER_DATA_DIR = defaultUserDataDir(); public static final String APP_NAME_KEY = "app.name"; diff --git a/src/main/java/io/bitsquare/app/gui/BitsquareApp.java b/src/main/java/io/bitsquare/app/gui/BitsquareApp.java index fd2681d80c..a36d32bb40 100644 --- a/src/main/java/io/bitsquare/app/gui/BitsquareApp.java +++ b/src/main/java/io/bitsquare/app/gui/BitsquareApp.java @@ -49,12 +49,17 @@ import javafx.scene.image.*; import javafx.scene.input.*; import javafx.stage.Stage; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import org.springframework.core.env.Environment; import org.springframework.util.FileSystemUtils; import static io.bitsquare.app.BitsquareEnvironment.*; public class BitsquareApp extends Application { + private static final Logger log = LoggerFactory.getLogger(BitsquareAppMain.class); + private static Environment env; private BitsquareAppModule bitsquareAppModule; @@ -66,6 +71,9 @@ public class BitsquareApp extends Application { @Override public void start(Stage primaryStage) throws IOException { + // For some reason the JavaFX launch process results in us losing the thread context class loader: reset it. + Thread.currentThread().setContextClassLoader(BitsquareApp.class.getClassLoader()); + bitsquareAppModule = new BitsquareAppModule(env, primaryStage); injector = Guice.createInjector(bitsquareAppModule); injector.getInstance(InjectorViewFactory.class).setInjector(injector); @@ -139,8 +147,7 @@ public class BitsquareApp extends Application { else iconPath = "/images/task_bar_icon_linux.png"; - if (iconPath != null) - primaryStage.getIcons().add(new Image(getClass().getResourceAsStream(iconPath))); + primaryStage.getIcons().add(new Image(getClass().getResourceAsStream(iconPath))); // make the UI visible diff --git a/src/main/java/io/bitsquare/app/gui/BitsquareAppMain.java b/src/main/java/io/bitsquare/app/gui/BitsquareAppMain.java index 8d71b1ad30..a9e660c036 100644 --- a/src/main/java/io/bitsquare/app/gui/BitsquareAppMain.java +++ b/src/main/java/io/bitsquare/app/gui/BitsquareAppMain.java @@ -24,6 +24,12 @@ import io.bitsquare.network.BootstrapNodes; import io.bitsquare.network.Node; import io.bitsquare.util.joptsimple.EnumValueConverter; +import java.io.File; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.vinumeris.updatefx.UpdateFX; import joptsimple.OptionParser; import joptsimple.OptionSet; @@ -33,8 +39,17 @@ import static io.bitsquare.network.Node.*; import static java.util.Arrays.asList; public class BitsquareAppMain extends BitsquareExecutable { + private static final Logger log = LoggerFactory.getLogger(BitsquareAppMain.class); public static void main(String[] args) throws Exception { + // We don't want to do the whole arg parsing/setup here as that might easily change in update versions + // So we only handle the absolute minimum which is APP_NAME and USER_DATA_DIR + // TODO Not impl. yet, just use default for first testings + UpdateFX.bootstrap(BitsquareAppMain.class, new File(BitsquareEnvironment.DEFAULT_APP_DATA_DIR).toPath(), args); + } + + // That will be called from UpdateFX after updates are checked + public static void realMain(String[] args) throws Exception { new BitsquareAppMain().execute(args); } diff --git a/src/main/java/io/bitsquare/app/gui/BitsquareAppModule.java b/src/main/java/io/bitsquare/app/gui/BitsquareAppModule.java index 245e72572d..a3ed2b1739 100644 --- a/src/main/java/io/bitsquare/app/gui/BitsquareAppModule.java +++ b/src/main/java/io/bitsquare/app/gui/BitsquareAppModule.java @@ -61,6 +61,11 @@ class BitsquareAppModule extends BitsquareModule { bindConstant().annotatedWith(named(Persistence.PREFIX_KEY)).to(env.getRequiredProperty(Persistence.PREFIX_KEY)); bind(Persistence.class).asEagerSingleton(); + // TODO UpdateFXHelper needs Environment. Should we just expose the 2 properties needed? + bind(Environment.class).toInstance(env); + // for temp testing with mock + bind(UpdateProcess.class).to(MockUpdateProcess.class).asEagerSingleton(); + install(messageModule()); install(bitcoinModule()); install(cryptoModule()); diff --git a/src/main/java/io/bitsquare/app/gui/ExampleApp.java b/src/main/java/io/bitsquare/app/gui/ExampleApp.java new file mode 100644 index 0000000000..5bf508d61a --- /dev/null +++ b/src/main/java/io/bitsquare/app/gui/ExampleApp.java @@ -0,0 +1,154 @@ +/* + * This file is part of Bitsquare. + * + * Bitsquare is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bitsquare is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bitsquare. If not, see . + */ + +package io.bitsquare.app.gui; + +import org.bitcoinj.utils.BriefLogFormatter; + +import com.google.common.util.concurrent.Uninterruptibles; + +import java.io.IOException; + +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.logging.FileHandler; + +import javafx.application.Application; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.*; +import javafx.scene.control.*; +import javafx.scene.layout.*; +import javafx.stage.Stage; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.vinumeris.updatefx.AppDirectory; +import com.vinumeris.updatefx.Crypto; +import com.vinumeris.updatefx.UpdateFX; +import com.vinumeris.updatefx.UpdateSummary; +import com.vinumeris.updatefx.Updater; +import org.bouncycastle.math.ec.ECPoint; + +// TODO remove it after we have impl. UpdateFX. +// Let it here for reference and for easier test setup for the moment. +public class ExampleApp extends Application { + private static final Logger log = LoggerFactory.getLogger(ExampleApp.class); + public static int VERSION = 3; + + public static void main(String[] args) throws IOException { + // We want to store updates in our app dir so must init that here. + AppDirectory.initAppDir("UpdateFX Example App"); + setupLogging(); + // re-enter at realMain, but possibly running a newer version of the software i.e. after this point the + // rest of this code may be ignored. + UpdateFX.bootstrap(ExampleApp.class, AppDirectory.dir(), args); + } + + public static void realMain(String[] args) { + launch(args); + } + + private static java.util.logging.Logger logger; + + private static void setupLogging() throws IOException { + logger = java.util.logging.Logger.getLogger(""); + logger.getHandlers()[0].setFormatter(new BriefLogFormatter()); + FileHandler handler = new FileHandler(AppDirectory.dir().resolve("log.txt").toString(), true); + handler.setFormatter(new BriefLogFormatter()); + logger.addHandler(handler); + } + + @Override + public void start(Stage primaryStage) throws Exception { + // For some reason the JavaFX launch process results in us losing the thread context class loader: reset it. + Thread.currentThread().setContextClassLoader(ExampleApp.class.getClassLoader()); + // Must be done twice for the times when we come here via realMain. + AppDirectory.initAppDir("UpdateFX Example App"); + + log.info("Hello World! This is version " + VERSION); + + ProgressIndicator indicator = showGiantProgressWheel(primaryStage); + + List pubkeys = Crypto.decode("028B41BDDCDCAD97B6AE088FEECA16DC369353B717E13319370C729CB97D677A11", + // wallet_1 + "031E3D80F21A4D10D385A32ABEDC300DACBEDBC839FBA58376FBD5D791D806BA68"); // wallet + + Updater updater = new Updater("http://localhost:8000/", "ExampleApp/" + VERSION, VERSION, + AppDirectory.dir(), UpdateFX.findCodePath(ExampleApp.class), + pubkeys, 1) { + @Override + protected void updateProgress(long workDone, long max) { + super.updateProgress(workDone, max); + // Give UI a chance to show. + Uninterruptibles.sleepUninterruptibly(100, TimeUnit.MILLISECONDS); + } + }; + + indicator.progressProperty().bind(updater.progressProperty()); + + log.info("Checking for updates!"); + updater.setOnSucceeded(event -> { + try { + UpdateSummary summary = updater.get(); + if (summary.descriptions.size() > 0) { + log.info("One liner: {}", summary.descriptions.get(0).getOneLiner()); + log.info("{}", summary.descriptions.get(0).getDescription()); + } + if (summary.highestVersion > VERSION) { + log.info("Restarting to get version " + summary.highestVersion); + if (UpdateFX.getVersionPin(AppDirectory.dir()) == 0) + UpdateFX.restartApp(); + } + } catch (Throwable e) { + log.error("oops", e); + } + }); + updater.setOnFailed(event -> { + log.error("Update error: {}", updater.getException()); + updater.getException().printStackTrace(); + }); + + indicator.setOnMouseClicked(ev -> UpdateFX.restartApp()); + + new Thread(updater, "UpdateFX Thread").start(); + + primaryStage.show(); + } + + private ProgressIndicator showGiantProgressWheel(Stage stage) { + ProgressIndicator indicator = new ProgressIndicator(); + BorderPane borderPane = new BorderPane(indicator); + borderPane.setMinWidth(640); + borderPane.setMinHeight(480); + Button pinButton = new Button(); + pinButton.setText("Pin to version 1"); + pinButton.setOnAction(event -> { + UpdateFX.pinToVersion(AppDirectory.dir(), 1); + UpdateFX.restartApp(); + }); + HBox box = new HBox(new Label("Version " + VERSION), pinButton); + box.setSpacing(10); + box.setAlignment(Pos.CENTER_LEFT); + box.setPadding(new Insets(10)); + borderPane.setTop(box); + Scene scene = new Scene(borderPane); + stage.setScene(scene); + return indicator; + } +} diff --git a/src/main/java/io/bitsquare/app/gui/MockUpdateProcess.java b/src/main/java/io/bitsquare/app/gui/MockUpdateProcess.java new file mode 100644 index 0000000000..8314bcb5c3 --- /dev/null +++ b/src/main/java/io/bitsquare/app/gui/MockUpdateProcess.java @@ -0,0 +1,56 @@ +/* + * This file is part of Bitsquare. + * + * Bitsquare is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bitsquare is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bitsquare. If not, see . + */ + +package io.bitsquare.app.gui; + +import com.google.inject.Inject; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.springframework.core.env.Environment; + +public class MockUpdateProcess extends UpdateProcess { + private static final Logger log = LoggerFactory.getLogger(MockUpdateProcess.class); + + @Inject + public MockUpdateProcess(Environment environment) { + super(environment); + } + + @Override + protected void init(Environment environment) { + + /* timeoutTimer.stop(); + state.set(State.UPDATE_AVAILABLE);*/ + + state.set(State.UP_TO_DATE); + timeoutTimer.stop(); + process.onCompleted(); + + /* state.set(State.FAILURE); + errorMessage = "dummy exc."; + timeoutTimer.stop(); + process.onCompleted();*/ + + } + + @Override + public void restart() { + log.debug("restart requested"); + } +} diff --git a/src/main/java/io/bitsquare/app/gui/UpdateProcess.java b/src/main/java/io/bitsquare/app/gui/UpdateProcess.java new file mode 100644 index 0000000000..827157336b --- /dev/null +++ b/src/main/java/io/bitsquare/app/gui/UpdateProcess.java @@ -0,0 +1,176 @@ +/* + * This file is part of Bitsquare. + * + * Bitsquare is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bitsquare is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bitsquare. If not, see . + */ + +package io.bitsquare.app.gui; + +import io.bitsquare.app.BitsquareEnvironment; +import io.bitsquare.util.Utilities; + +import com.google.inject.Inject; + +import java.io.File; + +import java.nio.file.Path; + +import java.util.List; +import java.util.function.Function; + +import javafx.animation.AnimationTimer; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.value.ChangeListener; +import javafx.beans.value.ObservableValue; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.vinumeris.updatefx.Crypto; +import com.vinumeris.updatefx.UpdateFX; +import com.vinumeris.updatefx.UpdateSummary; +import com.vinumeris.updatefx.Updater; +import org.bouncycastle.math.ec.ECPoint; +import org.springframework.core.env.Environment; +import rx.Observable; +import rx.subjects.BehaviorSubject; +import rx.subjects.Subject; + +public class UpdateProcess { + private static final Logger log = LoggerFactory.getLogger(UpdateProcess.class); + + private static final int VERSION = 1; + private static final List UPDATE_SIGNING_KEYS = Crypto.decode( + "028B41BDDCDCAD97B6AE088FEECA16DC369353B717E13319370C729CB97D677A11", + "031E3D80F21A4D10D385A32ABEDC300DACBEDBC839FBA58376FBD5D791D806BA68" + ); + private static final int UPDATE_SIGNING_THRESHOLD = 1; + private static final String UPDATES_BASE_URL = "http://localhost:8000/"; + private static final Path ROOT_CLASS_PATH = UpdateFX.findCodePath(BitsquareAppMain.class); + + + public enum State { + CHECK_FOR_UPDATES, + UPDATE_AVAILABLE, + UP_TO_DATE, + FAILURE + } + + public final ObjectProperty state = new SimpleObjectProperty<>(State.CHECK_FOR_UPDATES); + + protected String errorMessage; + protected final Subject process = BehaviorSubject.create(); + protected final AnimationTimer timeoutTimer; + + @Inject + public UpdateProcess(Environment environment) { + // process.timeout() will cause an error state back but we dont want to break startup in case of an update + // timeout + timeoutTimer = Utilities.setTimeout(10000, new Function() { + @Override + public Void apply(AnimationTimer animationTimer) { + process.onCompleted(); + return null; + } + }); + timeoutTimer.start(); + + init(environment); + } + + public void restart() { + UpdateFX.restartApp(); + } + + public Observable getProcess() { + return process.asObservable(); + } + + public String getErrorMessage() { + return errorMessage; + } + + protected void init(Environment environment) { + log.info("version " + VERSION); + + String agent = environment.getProperty(BitsquareEnvironment.APP_NAME_KEY) + VERSION; + Path dataDirPath = new File(environment.getProperty(BitsquareEnvironment.APP_DATA_DIR_KEY)).toPath(); + Updater updater = new Updater(UPDATES_BASE_URL, agent, VERSION, dataDirPath, ROOT_CLASS_PATH, + UPDATE_SIGNING_KEYS, UPDATE_SIGNING_THRESHOLD) { + @Override + protected void updateProgress(long workDone, long max) { + log.debug("updateProgress " + workDone + "/" + max); + super.updateProgress(workDone, max); + } + }; + + updater.progressProperty().addListener(new ChangeListener() { + @Override + public void changed(ObservableValue observableValue, Number oldValue, Number newValue) { + log.trace("progressProperty newValue = " + newValue); + } + }); + + log.info("Checking for updates!"); + updater.setOnSucceeded(event -> { + try { + UpdateSummary summary = updater.get(); + if (summary.descriptions.size() > 0) { + log.info("One liner: {}", summary.descriptions.get(0).getOneLiner()); + log.info("{}", summary.descriptions.get(0).getDescription()); + } + if (summary.highestVersion > VERSION) { + state.set(State.UPDATE_AVAILABLE); + } + else if (summary.highestVersion == VERSION) { + state.set(State.UP_TO_DATE); + timeoutTimer.stop(); + process.onCompleted(); + } + + /* if (summary.highestVersion > VERSION) { + log.info("Restarting to get version " + summary.highestVersion); + if (UpdateFX.getVersionPin(dataDirPath) == 0) + UpdateFX.restartApp(); + }*/ + } catch (Throwable e) { + log.error("Exception at processing UpdateSummary: " + e.getMessage()); + + // we treat errors as update not as critical errors to prevent startup, + // so we use state.onCompleted() instead of state.onError() + errorMessage = "Exception at processing UpdateSummary: " + e.getMessage(); + state.set(State.FAILURE); + timeoutTimer.stop(); + process.onCompleted(); + } + }); + updater.setOnFailed(event -> { + log.error("Update failed: " + updater.getException()); + updater.getException().printStackTrace(); + + // we treat errors as update not as critical errors to prevent startup, + // so we use state.onCompleted() instead of state.onError() + errorMessage = "Update failed: " + updater.getException(); + state.set(State.FAILURE); + timeoutTimer.stop(); + process.onCompleted(); + }); + + Thread thread = new Thread(updater, "Online update check"); + thread.setDaemon(true); + thread.start(); + } + +} diff --git a/src/main/java/io/bitsquare/gui/main/MainView.java b/src/main/java/io/bitsquare/gui/main/MainView.java index 7c7acc45b8..2ba94dd112 100644 --- a/src/main/java/io/bitsquare/gui/main/MainView.java +++ b/src/main/java/io/bitsquare/gui/main/MainView.java @@ -152,7 +152,7 @@ public class MainView extends InitializableView { root.getChildren().addAll(baseApplicationContainer, splashScreen); - model.isReadyForMainScreen.addListener((ov, oldValue, newValue) -> { + model.showAppScreen.addListener((ov, oldValue, newValue) -> { if (newValue) { bankAccountComboBoxHolder.getChildren().setAll(createBankAccountComboBox()); @@ -184,15 +184,13 @@ public class MainView extends InitializableView { Pane notification = new Pane(); notification.relocate(30, 9); notification.setMouseTransparent(true); - notification.setVisible(model.numPendingTrades.get() > 0); notification.setEffect(new DropShadow(4, 1, 2, Color.GREY)); notification.getChildren().addAll(icon, numPendingTradesLabel); + notification.visibleProperty().bind(model.showPendingTradesNotification); portfolioButtonHolder.getChildren().add(notification); - model.numPendingTrades.addListener((ov, oldValue, newValue) -> { - notification.setVisible((int) newValue > 0); - - if ((int) newValue > 0) + model.showPendingTradesNotification.addListener((ov, oldValue, newValue) -> { + if (newValue) SystemNotification.openInfoNotification(title, "You got a new trade message."); }); } @@ -200,17 +198,17 @@ public class MainView extends InitializableView { private VBox createSplashScreen() { VBox vBox = new VBox(); vBox.setAlignment(Pos.CENTER); - vBox.setSpacing(10); + vBox.setSpacing(0); vBox.setId("splash"); ImageView logo = new ImageView(); logo.setId("image-splash-logo"); Label blockchainSyncLabel = new Label(); - blockchainSyncLabel.textProperty().bind(model.blockchainSyncState); + blockchainSyncLabel.textProperty().bind(model.blockchainSyncInfo); model.walletServiceErrorMsg.addListener((ov, oldValue, newValue) -> { blockchainSyncLabel.setId("splash-error-state-msg"); - Popups.openErrorPopup("Error", "An error occurred at startup. \n\nError message:\n" + + Popups.openErrorPopup("Error", "Connecting to the bitcoin network failed. \n\nReason: " + newValue); }); @@ -238,7 +236,7 @@ public class MainView extends InitializableView { HBox blockchainSyncBox = new HBox(); blockchainSyncBox.setSpacing(10); blockchainSyncBox.setAlignment(Pos.CENTER); - blockchainSyncBox.setPadding(new Insets(60, 0, 0, 0)); + blockchainSyncBox.setPadding(new Insets(40, 0, 0, 0)); blockchainSyncBox.setPrefHeight(50); blockchainSyncBox.getChildren().addAll(blockchainSyncLabel, blockchainSyncIndicator, blockchainSyncIcon, bitcoinNetworkLabel); @@ -247,20 +245,18 @@ public class MainView extends InitializableView { bootstrapStateLabel.setWrapText(true); bootstrapStateLabel.setMaxWidth(500); bootstrapStateLabel.setTextAlignment(TextAlignment.CENTER); - bootstrapStateLabel.textProperty().bind(model.bootstrapStateText); + bootstrapStateLabel.textProperty().bind(model.bootstrapInfo); ProgressIndicator bootstrapIndicator = new ProgressIndicator(); bootstrapIndicator.setMaxSize(24, 24); bootstrapIndicator.progressProperty().bind(model.bootstrapProgress); - model.bootstrapFailed.addListener((ov, oldValue, newValue) -> { - if (newValue) { - bootstrapStateLabel.setId("splash-error-state-msg"); - bootstrapIndicator.setVisible(false); + model.bootstrapErrorMsg.addListener((ov, oldValue, newValue) -> { + bootstrapStateLabel.setId("splash-error-state-msg"); + bootstrapIndicator.setVisible(false); - Popups.openErrorPopup("Error", "Connecting to the Bitsquare network failed. \n\nReason: " + - model.bootstrapErrorMsg.get()); - } + Popups.openErrorPopup("Error", "Connecting to the Bitsquare network failed. \n\nReason: " + + model.bootstrapErrorMsg.get()); }); ImageView bootstrapIcon = new ImageView(); @@ -279,11 +275,35 @@ public class MainView extends InitializableView { HBox bootstrapBox = new HBox(); bootstrapBox.setSpacing(10); bootstrapBox.setAlignment(Pos.CENTER); - bootstrapBox.setPadding(new Insets(10, 0, 0, 0)); bootstrapBox.setPrefHeight(50); bootstrapBox.getChildren().addAll(bootstrapStateLabel, bootstrapIndicator, bootstrapIcon); - vBox.getChildren().addAll(logo, blockchainSyncBox, bootstrapBox); + // software update + Label updateInfoLabel = new Label(); + updateInfoLabel.setTextAlignment(TextAlignment.RIGHT); + updateInfoLabel.textProperty().bind(model.updateInfo); + + Button restartButton = new Button("Restart"); + restartButton.setDefaultButton(true); + restartButton.visibleProperty().bind(model.showRestartButton); + restartButton.managedProperty().bind(model.showRestartButton); + restartButton.setOnAction(e -> model.restart()); + + ImageView updateIcon = new ImageView(); + updateIcon.setId(model.updateIconId.get()); + model.updateIconId.addListener((ov, oldValue, newValue) -> { + updateIcon.setId(newValue); + updateIcon.setVisible(true); + updateIcon.setManaged(true); + }); + + HBox updateBox = new HBox(); + updateBox.setSpacing(10); + updateBox.setAlignment(Pos.CENTER); + updateBox.setPrefHeight(20); + updateBox.getChildren().addAll(updateInfoLabel, restartButton, updateIcon); + + vBox.getChildren().addAll(logo, blockchainSyncBox, bootstrapBox, updateBox); return vBox; } diff --git a/src/main/java/io/bitsquare/gui/main/MainViewModel.java b/src/main/java/io/bitsquare/gui/main/MainViewModel.java index eb4e63d5fe..c68f2a3ab7 100644 --- a/src/main/java/io/bitsquare/gui/main/MainViewModel.java +++ b/src/main/java/io/bitsquare/gui/main/MainViewModel.java @@ -18,13 +18,13 @@ package io.bitsquare.gui.main; import io.bitsquare.account.AccountSettings; +import io.bitsquare.app.gui.UpdateProcess; import io.bitsquare.arbitrator.Arbitrator; import io.bitsquare.arbitrator.Reputation; import io.bitsquare.bank.BankAccount; import io.bitsquare.bank.BankAccountType; import io.bitsquare.btc.BitcoinNetwork; import io.bitsquare.btc.WalletService; -import io.bitsquare.gui.components.Popups; import io.bitsquare.gui.util.BSFormatter; import io.bitsquare.locale.CountryUtil; import io.bitsquare.locale.LanguageUtil; @@ -35,7 +35,6 @@ import io.bitsquare.trade.Trade; import io.bitsquare.trade.TradeManager; import io.bitsquare.user.User; import io.bitsquare.util.DSAKeyUtil; -import io.bitsquare.util.Utilities; import org.bitcoinj.core.Coin; import org.bitcoinj.core.ECKey; @@ -47,18 +46,16 @@ import java.util.ArrayList; import java.util.Currency; import java.util.List; import java.util.Locale; +import java.util.concurrent.TimeoutException; import viewfx.model.ViewModel; -import javafx.animation.AnimationTimer; import javafx.application.Platform; import javafx.beans.property.BooleanProperty; import javafx.beans.property.DoubleProperty; -import javafx.beans.property.IntegerProperty; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleDoubleProperty; -import javafx.beans.property.SimpleIntegerProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; @@ -75,106 +72,63 @@ import rx.Observable; class MainViewModel implements ViewModel { private static final Logger log = LoggerFactory.getLogger(MainViewModel.class); - final DoubleProperty networkSyncProgress = new SimpleDoubleProperty(-1); - final IntegerProperty numPendingTrades = new SimpleIntegerProperty(0); - final StringProperty numPendingTradesAsString = new SimpleStringProperty(); - final ObjectProperty bootstrapState = new SimpleObjectProperty<>(); - final StringProperty bootstrapStateText = new SimpleStringProperty(); - final ObjectProperty walletServiceException = new SimpleObjectProperty(); - - final StringProperty bankAccountsComboBoxPrompt = new SimpleStringProperty(); - final BooleanProperty bankAccountsComboBoxDisable = new SimpleBooleanProperty(); - - final StringProperty blockchainSyncState = new SimpleStringProperty("Initializing"); + // BTC network + final StringProperty blockchainSyncInfo = new SimpleStringProperty("Initializing"); final DoubleProperty blockchainSyncProgress = new SimpleDoubleProperty(-1); - final BooleanProperty blockchainSyncIndicatorVisible = new SimpleBooleanProperty(true); - final StringProperty blockchainSyncIconId = new SimpleStringProperty(); final StringProperty walletServiceErrorMsg = new SimpleStringProperty(); - final BooleanProperty isReadyForMainScreen = new SimpleBooleanProperty(); + final StringProperty blockchainSyncIconId = new SimpleStringProperty(); + // P2P network + final StringProperty bootstrapInfo = new SimpleStringProperty(); final DoubleProperty bootstrapProgress = new SimpleDoubleProperty(-1); - final BooleanProperty bootstrapFailed = new SimpleBooleanProperty(); final StringProperty bootstrapErrorMsg = new SimpleStringProperty(); final StringProperty bootstrapIconId = new SimpleStringProperty(); - final StringProperty featureNotImplementedWarning = new SimpleStringProperty(); + // software update + final StringProperty updateInfo = new SimpleStringProperty(); + final BooleanProperty showRestartButton = new SimpleBooleanProperty(false); + final StringProperty updateIconId = new SimpleStringProperty(); + + final StringProperty bankAccountsComboBoxPrompt = new SimpleStringProperty(); + final BooleanProperty bankAccountsComboBoxDisable = new SimpleBooleanProperty(); final ObjectProperty currentBankAccount = new SimpleObjectProperty<>(); + + final BooleanProperty showAppScreen = new SimpleBooleanProperty(); + final StringProperty featureNotImplementedWarning = new SimpleStringProperty(); + final StringProperty numPendingTradesAsString = new SimpleStringProperty(); + final BooleanProperty showPendingTradesNotification = new SimpleBooleanProperty(); + final String bitcoinNetworkAsString; private final User user; private final WalletService walletService; private final MessageService messageService; private final TradeManager tradeManager; + private UpdateProcess updateProcess; private final BSFormatter formatter; private Persistence persistence; private AccountSettings accountSettings; - private AnimationTimer bitcoinNetworkTimeout; @Inject public MainViewModel(User user, WalletService walletService, MessageService messageService, - TradeManager tradeManager, BitcoinNetwork bitcoinNetwork, BSFormatter formatter, - Persistence persistence, AccountSettings accountSettings) { + TradeManager tradeManager, BitcoinNetwork bitcoinNetwork, UpdateProcess updateProcess, + BSFormatter formatter, Persistence persistence, AccountSettings accountSettings) { this.user = user; this.walletService = walletService; this.messageService = messageService; this.tradeManager = tradeManager; + this.updateProcess = updateProcess; this.formatter = formatter; this.persistence = persistence; this.accountSettings = accountSettings; bitcoinNetworkAsString = bitcoinNetwork.toString(); + updateProcess.state.addListener((observableValue, oldValue, newValue) -> applyUpdateState(newValue)); + applyUpdateState(updateProcess.state.get()); + user.getCurrentBankAccount().addListener((observable, oldValue, newValue) -> persistence.write(user)); - currentBankAccount.bind(user.currentBankAccountProperty()); - - bootstrapState.addListener((ov, oldValue, newValue) -> { - if (newValue == BootstrapState.DISCOVERY_DIRECT_SUCCEEDED || - newValue == BootstrapState.DISCOVERY_MANUAL_PORT_FORWARDING_SUCCEEDED || - newValue == BootstrapState.DISCOVERY_AUTO_PORT_FORWARDING_SUCCEEDED || - newValue == BootstrapState.RELAY_SUCCEEDED) { - bootstrapStateText.set("Successfully connected to P2P network: " + newValue.getMessage()); - bootstrapProgress.set(1); - - if (newValue == BootstrapState.DISCOVERY_DIRECT_SUCCEEDED) - bootstrapIconId.set("image-connection-direct"); - else if (newValue == BootstrapState.DISCOVERY_MANUAL_PORT_FORWARDING_SUCCEEDED || - newValue == BootstrapState.DISCOVERY_AUTO_PORT_FORWARDING_SUCCEEDED) - bootstrapIconId.set("image-connection-nat"); - else if (newValue == BootstrapState.RELAY_SUCCEEDED) - bootstrapIconId.set("image-connection-relay"); - } - else if (newValue == BootstrapState.PEER_CREATION_FAILED || - newValue == BootstrapState.DISCOVERY_FAILED || - newValue == BootstrapState.DISCOVERY_AUTO_PORT_FORWARDING_FAILED || - newValue == BootstrapState.RELAY_FAILED) { - - bootstrapErrorMsg.set(newValue.getMessage()); - bootstrapStateText.set("Connecting to the Bitsquare network failed."); - bootstrapProgress.set(0); - bootstrapFailed.set(true); - } - else { - bootstrapStateText.set("Connecting to the Bitsquare network: " + newValue.getMessage()); - } - } - ); - - walletServiceException.addListener((ov, oldValue, newValue) -> { - blockchainSyncIndicatorVisible.set(false); - blockchainSyncState.set("Startup failed."); - walletServiceErrorMsg.set(((Throwable) newValue).getMessage()); - }); - - networkSyncProgress.addListener((ov, oldValue, newValue) -> { - setNetworkSyncProgress((double) newValue); - - if ((double) newValue >= 1) - blockchainSyncIconId.set("image-connection-synced"); - }); - setNetworkSyncProgress(networkSyncProgress.get()); - - user.getBankAccounts().addListener((ListChangeListener) change -> { bankAccountsComboBoxDisable.set(change.getList().isEmpty()); bankAccountsComboBoxPrompt.set(change.getList().isEmpty() ? "No accounts" : ""); @@ -182,57 +136,67 @@ class MainViewModel implements ViewModel { bankAccountsComboBoxDisable.set(user.getBankAccounts().isEmpty()); bankAccountsComboBoxPrompt.set(user.getBankAccounts().isEmpty() ? "No accounts" : ""); - tradeManager.featureNotImplementedWarningProperty().addListener((ov, oldValue, newValue) -> { if (oldValue == null && newValue != null) { featureNotImplementedWarning.set(newValue); - Popups.openWarningPopup(newValue); tradeManager.setFeatureNotImplementedWarning(null); } }); } - public void initBackend() { - bitcoinNetworkTimeout = Utilities.setTimeout(20000, animationTimer -> { - Platform.runLater(() -> { - networkSyncProgress.set(0); - blockchainSyncState.set("Connecting to the bitcoin network failed."); - Popups.openErrorPopup("Connecting to the bitcoin network failed", - "Please check your network connection.\n\n" + - "You must allow outgoing TCP connections to port 18333 for the bitcoin testnet.\n\n" + - "See https://github.com/bitsquare/bitsquare/wiki for instructions."); - }); - return null; - }); + public void restart() { + updateProcess.restart(); + } + public void initBackend() { + setBitcoinNetworkSyncProgress(-1); walletService.getDownloadProgress().subscribe( percentage -> Platform.runLater(() -> { if (percentage > 0) - networkSyncProgress.set(percentage / 100.0); + setBitcoinNetworkSyncProgress(percentage / 100.0); }), error -> log.error(error.toString()), - () -> Platform.runLater(() -> networkSyncProgress.set(1.0))); + () -> Platform.runLater(() -> setBitcoinNetworkSyncProgress(1.0))); Observable message = messageService.init(); message.publish(); message.subscribe( - state -> Platform.runLater(() -> bootstrapState.set(state)), - error -> log.error(error.toString()), + state -> Platform.runLater(() -> setBootstrapState(state)), + error -> Platform.runLater(() -> { + log.error(error.toString()); + bootstrapErrorMsg.set(error.getMessage()); + bootstrapInfo.set("Connecting to the Bitsquare network failed."); + bootstrapProgress.set(0); + + }), () -> log.trace("message completed")); Observable wallet = walletService.initialize(Platform::runLater); wallet.subscribe( next -> { + log.trace("wallet next"); }, - error -> Platform.runLater(() -> walletServiceException.set(error)), + error -> Platform.runLater(() -> { + log.trace("wallet error"); + setWalletServiceException(error); + }), () -> { log.trace("wallet completed"); - bitcoinNetworkTimeout.stop(); - bitcoinNetworkTimeout = null; }); - Observable backend = Observable.merge(message, wallet); - backend.subscribe( + Observable updateProcess = this.updateProcess.getProcess(); + updateProcess.subscribe(next -> { + log.trace("updateProcess next"); + }, + error -> { + log.trace("updateProcess error"); + }, + () -> { + log.trace("updateProcess completed"); + }); + + Observable backEnd = Observable.merge(message, wallet, updateProcess); + backEnd.subscribe( next -> { }, error -> log.error(error.toString()), @@ -246,7 +210,7 @@ class MainViewModel implements ViewModel { tradeManager.getPendingTrades().addListener( (MapChangeListener) change -> updateNumPendingTrades()); updateNumPendingTrades(); - isReadyForMainScreen.set(true); + showAppScreen.set(true); // For alpha version // uses messageService, so don't call it before backend is ready @@ -270,6 +234,77 @@ class MainViewModel implements ViewModel { } } + private void applyUpdateState(UpdateProcess.State state) { + switch (state) { + case CHECK_FOR_UPDATES: + updateInfo.set("Checking for updates..."); + updateIconId.set("image-update-in-progress"); + break; + case UPDATE_AVAILABLE: + updateInfo.set("New update available. Please restart!"); + updateIconId.set("image-update-available"); + showRestartButton.set(true); + break; + case UP_TO_DATE: + updateInfo.set("Software is up to date."); + updateIconId.set("image-update-up-to-date"); + break; + case FAILURE: + updateInfo.set(updateProcess.getErrorMessage()); + updateIconId.set("image-update-failed"); + break; + } + } + + private void setBootstrapState(BootstrapState state) { + switch (state) { + case DISCOVERY_DIRECT_SUCCEEDED: + bootstrapIconId.set("image-connection-direct"); + break; + case DISCOVERY_MANUAL_PORT_FORWARDING_SUCCEEDED: + case DISCOVERY_AUTO_PORT_FORWARDING_SUCCEEDED: + bootstrapIconId.set("image-connection-nat"); + break; + case RELAY_SUCCEEDED: + bootstrapIconId.set("image-connection-relay"); + break; + default: + bootstrapIconId.set(null); + break; + } + + switch (state) { + case DISCOVERY_DIRECT_SUCCEEDED: + case DISCOVERY_MANUAL_PORT_FORWARDING_SUCCEEDED: + case DISCOVERY_AUTO_PORT_FORWARDING_SUCCEEDED: + case RELAY_SUCCEEDED: + bootstrapInfo.set("Successfully connected to P2P network: " + state.getMessage()); + bootstrapProgress.set(1); + break; + default: + bootstrapInfo.set("Connecting to the Bitsquare network: " + state.getMessage()); + bootstrapProgress.set(-1); + break; + } + } + + private void setWalletServiceException(Throwable error) { + setBitcoinNetworkSyncProgress(0); + blockchainSyncInfo.set("Connecting to the bitcoin network failed."); + if (error instanceof TimeoutException) { + walletServiceErrorMsg.set("Please check your network connection.\n\n" + + "You must allow outgoing TCP connections to port 18333 for the bitcoin testnet.\n\n" + + "See https://github.com/bitsquare/bitsquare/wiki for instructions."); + } + else if (error.getMessage() != null) { + walletServiceErrorMsg.set(error.getMessage()); + } + else { + walletServiceErrorMsg.set(error.toString()); + } + } + + public StringConverter getBankAccountsConverter() { return new StringConverter() { @Override @@ -293,21 +328,24 @@ class MainViewModel implements ViewModel { } private void updateNumPendingTrades() { - numPendingTrades.set(tradeManager.getPendingTrades().size()); - if (numPendingTrades.get() > 0) - numPendingTradesAsString.set(String.valueOf(numPendingTrades.get())); + int numPendingTrades = tradeManager.getPendingTrades().size(); + if (numPendingTrades > 0) + numPendingTradesAsString.set(String.valueOf(numPendingTrades)); + showPendingTradesNotification.set(numPendingTrades > 0); } - private void setNetworkSyncProgress(double value) { + private void setBitcoinNetworkSyncProgress(double value) { blockchainSyncProgress.set(value); - if (value >= 1) - blockchainSyncState.set("Blockchain synchronization complete."); - else if (value > 0.0) - blockchainSyncState.set("Synchronizing blockchain: " + formatter.formatToPercent(value)); - else - blockchainSyncState.set("Connecting to the bitcoin network..."); - - blockchainSyncIndicatorVisible.set(value < 1); + if (value >= 1) { + blockchainSyncInfo.set("Blockchain synchronization complete."); + blockchainSyncIconId.set("image-connection-synced"); + } + else if (value > 0.0) { + blockchainSyncInfo.set("Synchronizing blockchain: " + formatter.formatToPercent(value)); + } + else { + blockchainSyncInfo.set("Connecting to the bitcoin network..."); + } } private void addMockArbitrator() { diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml index fe780b595e..a6f34b0b97 100644 --- a/src/main/resources/logback.xml +++ b/src/main/resources/logback.xml @@ -27,6 +27,7 @@ +