How to handle java web start (jnlp) downloading progress in a preloader?
Asked Answered
G

2

28

Issue

I have a preloader for my application that handles the Application-specific initialization. Now I'm trying to extend this so that the preloader also shows the progress of the downloaded application JARs.


TL;DR

  • Why is the preloader not getting loaded during Phase 2, as this should handle the PreloaderFx::handleProgressNotification(); to track the downloading of the JARs I suppose?

  • Update 14 March 2016: Is using DownloadServiceListener the way to solve this? How to connect this to a JavaFX stage?


Documentation

According to Oracle, there are 4 phases when an application gets launched:

  • Phase 1: Initialization: Initialization of Java Runtime and an initial examination identifies components that must be loaded and executed before starting the application. During this phase, a splash screen is shown. Default this is this:

Java starts

  • Phase 2: Loading and preparation: The required resources are loaded from either the network or a disk cache, and validation procedures occur. All execution modes see the default or a custom preloader. During this phase, my custom preloader should be shown.

  • Phase 3: Application-specific initialization: The application is started, but it may need to load additional resources or perform other lengthy preparations before it becomes fully functional. At the moment, my custom preloader is shown:

preloader

  • Phase 4: Application execution: The application is displayed and is ready to use. In my case, a login window is shown and the user can proceed.

login


My Case

The first thing I notice, is that in Phase 2, the default JavaFX preloader handling the downloading of the application JARs is not showing. Because of this, the user gets the feeling the program did not start or terminated prematurely, making them to open the JNLP file multiple times. Once the JARs are downloaded, we enter Phase 3 and the preloader is shown.

However, I'd like my custom preloader to handle the downloading progress in the ProgressBar as well (Phase 2). I made everything as simple as possible to track down which events are happening during the start-up of my application. This is based on an example of Jewelsea and on Oracle examples:

Preloader:

public class PreloaderFX extends Preloader {

        Stage stage;
        //boolean noLoadingProgress = true;

        public static final String APPLICATION_ICON
            = "http://cdn1.iconfinder.com/data/icons/Copenhagen/PNG/32/people.png";
        public static final String SPLASH_IMAGE
            = "http://fxexperience.com/wp-content/uploads/2010/06/logo.png";

        private Pane splashLayout;
        private ProgressBar loadProgress;
        private Label progressText;
        private static final int SPLASH_WIDTH = 676;
        private static final int SPLASH_HEIGHT = 227;

        @Override
        public void init() {
            ImageView splash = new ImageView(new Image(
                SPLASH_IMAGE
            ));
            loadProgress = new ProgressBar();
            loadProgress.setPrefWidth(SPLASH_WIDTH - 20);
            progressText = new Label("Loading . . .");
            splashLayout = new VBox();
            splashLayout.getChildren().addAll(splash, loadProgress, progressText);
            progressText.setAlignment(Pos.CENTER);
            splashLayout.setStyle(
                "-fx-padding: 5; "
                + "-fx-background-color: white; "
                + "-fx-border-width:5; "
            );
            splashLayout.setEffect(new DropShadow());
        }

        @Override
        public void start(Stage stage) throws Exception {
            System.out.println("PreloaderFx::start();");

            //this.stage = new Stage(StageStyle.DECORATED);
            stage.setTitle("Title");
            stage.getIcons().add(new Image(APPLICATION_ICON));
            stage.initStyle(StageStyle.UNDECORATED);
            final Rectangle2D bounds = Screen.getPrimary().getBounds();
            stage.setScene(new Scene(splashLayout));
            stage.setX(bounds.getMinX() + bounds.getWidth() / 2 - SPLASH_WIDTH / 2);
            stage.setY(bounds.getMinY() + bounds.getHeight() / 2 - SPLASH_HEIGHT / 2);
            stage.show();

            this.stage = stage;
        }

        @Override
        public void handleProgressNotification(ProgressNotification pn) {
            System.out.println("PreloaderFx::handleProgressNotification(); progress = " + pn.getProgress());
            //application loading progress is rescaled to be first 50%
            //Even if there is nothing to load 0% and 100% events can be
            // delivered
            if (pn.getProgress() != 1.0 /*|| !noLoadingProgress*/) {
                loadProgress.setProgress(pn.getProgress() / 2);
                /*if (pn.getProgress() > 0) {
                noLoadingProgress = false;
                }*/
            }
        }

        @Override
        public void handleStateChangeNotification(StateChangeNotification evt) {
            //ignore, hide after application signals it is ready
            System.out.println("PreloaderFx::handleStateChangeNotification(); state = " + evt.getType());
        }

        @Override
        public void handleApplicationNotification(PreloaderNotification pn) {
            if (pn instanceof ProgressNotification) {
                //expect application to send us progress notifications 
                //with progress ranging from 0 to 1.0
                double v = ((ProgressNotification) pn).getProgress();
                System.out.println("PreloaderFx::handleApplicationNotification(); progress = " + v);
                //if (!noLoadingProgress) {
                //if we were receiving loading progress notifications 
                //then progress is already at 50%. 
                //Rescale application progress to start from 50%               
                v = 0.5 + v / 2;
                //}
                loadProgress.setProgress(v);
            } else if (pn instanceof StateChangeNotification) {
                System.out.println("PreloaderFx::handleApplicationNotification(); state = " + ((StateChangeNotification) pn).getType());
                //hide after get any state update from application
                stage.hide();
            }
        }
    }

Code that is being handled in Phase 3 is from the main application who interacts with the preloader, this is what is being seen in the progressbar:

public class MainApp extends Application {
    BooleanProperty ready = new SimpleBooleanProperty(false);

    public static void main(String[] args) throws Exception {
        launch(args);
    }

    @Override
    public void start(final Stage initStage) throws Exception {
        System.out.println("MainApp::start();");
        this.mainStage = initStage;

        longStart();

        ready.addListener((ObservableValue<? extends Boolean> ov, Boolean t, Boolean t1) -> {
            if (Boolean.TRUE.equals(t1)) {
                Platform.runLater(() -> {
                    System.out.println("MainApp::showMainStage();");
                    showMainStage();
                });
            }
        });   
    }

    private void longStart() {
        //simulate long init in background
        Task task = new Task<Void>() {
            @Override
            protected Void call() throws Exception {
                int max = 10;
                for (int i = 1; i <= max; i++) {
                    Thread.sleep(500);
                    System.out.println("longStart " + i);
                    // Send progress to preloader
                    notifyPreloader(new ProgressNotification(((double) i)/max)); //this moves the progress bar of the preloader
                }
                // After init is ready, the app is ready to be shown
                // Do this before hiding the preloader stage to prevent the 
                // app from exiting prematurely
                ready.setValue(Boolean.TRUE);

                notifyPreloader(new StateChangeNotification(
                    StateChangeNotification.Type.BEFORE_START));

                return null;
            }
        };
        new Thread(task).start();
    }

    private void showMainStage() {
        //showing the login window
    }
}

JNLP

<jnlp spec="1.0+" xmlns:jfx="http://javafx.com" codebase="<***>/preloadertest/jnlp" href="launch.jnlp">
    <information>
        ...
    </information>
    <resources>
        <j2se version="1.6+" href="http://java.sun.com/products/autodl/j2se" />


        ... //whole bunch of JARS

        <jar href="lib/preloader-1.1.1.jar" download="progress" />


    </resources>
    <security>
        <all-permissions/>
    </security>
    <applet-desc width="1024" height="768" main-class="com.javafx.main.NoJavaFXFallback" name="JavaFX Client">
        <param name="requiredFXVersion" value="8.0+"/>
    </applet-desc>
    <jfx:javafx-desc width="1024" height="768" main-class="GUI.MainApp" name="JavaFX Client" preloader-class="GUI.PreloaderFX" />
    <update check="background"/>
</jnlp>

Debugging

I closely watched the Java Console when launching the file (with Show logging enabled, Show tracing disabled) and noticed following things:

During Phase 2, nothing shows up in the Java Console (console closes after this phase)

During Phase 3, following output is generated (in a new console window):

PreloaderFx::start();
PreloaderFx::handleProgressNotification(); progress = 1.0
PreloaderFx::handleStateChangeNotification(); state = BEFORE_LOAD
PreloaderFx::handleStateChangeNotification(); state = BEFORE_INIT
PreloaderFx::handleStateChangeNotification(); state = BEFORE_START
MainApp::start();
MainApp::longstart();
longStart 1
PreloaderFx::handleApplicationNotification(); progress = 0.1
longStart 2
PreloaderFx::handleApplicationNotification(); progress = 0.2
longStart 3
PreloaderFx::handleApplicationNotification(); progress = 0.3
longStart 4
PreloaderFx::handleApplicationNotification(); progress = 0.4
longStart 5
PreloaderFx::handleApplicationNotification(); progress = 0.5
longStart 6
PreloaderFx::handleApplicationNotification(); progress = 0.6
longStart 7
PreloaderFx::handleApplicationNotification(); progress = 0.7
longStart 8
PreloaderFx::handleApplicationNotification(); progress = 0.8
longStart 9
PreloaderFx::handleApplicationNotification(); progress = 0.9
longStart 10
PreloaderFx::handleApplicationNotification(); progress = 1.0
MainApp::showMainStage();
PreloaderFx::handleApplicationNotification(); state = BEFORE_START

Updates 13 March 2016:

  • Adjusted the code so the stage passed in the method is used rather than creating a new one and commented out everything related to the noLoadingProgress boolean (suggested by nhylated)
  • Added some extra System.out.println() in the MainApp

Solution

Simple adding <jfx:javafx-runtime version="8.0+"/> to the JNLP file fixed it. With that line added, the preloader shows in Phase 2. I also took the liberty to change the j2se version="1.6+" to j2se version="1.8+" The result:

Preloader result

The first 50% is the handling of the JAR downloads. This is done by the handleProgressNotification() method. The second 50% is the actual initializing of the MainApp (longstart() which notifies the preloader), done by the handleApplicationNotification().

Graven answered 7/3, 2016 at 17:18 Comment(4)
It doesn't work for me on JRE8u144. What version are you using?Cytology
What is the behavior you are experiencing? At the moment, the preloader seems to only show when the downloading is complete for me. So basicly at the initializing 50% phase in the example above.Graven
Exactly. The preloader shows only after the download is complete.Cytology
@Cytology "The preloader shows only after the download is complete." If it used to work but does not any longer, it suggests a bug in the JRE. Are there any reports in the bug DB? If not, please raise one & get the manufacturer involved.Laundress
R
9

I have been fighting with this too, recently. I switched back to the (ugly) default preloader (as that one shows up nicely) until I find some more time to investigate this.

If you enable Java Webstart full tracing

"<JAVA_HOME>\bin\javaws.exe" -userConfig deployment.trace true
"<JAVA_HOME>\bin\javaws.exe" -userConfig deployment.trace.level all

you should see preloader messages which should give you some information about what is going on. In my case I could see a lot of messages like these

preloader: Added pending event 2: DownloadEvent[type=load,loaded=0, total=62791, percent=0]

indicating that the custom preloader hasn't been verified/started yet but download events were already coming in.

What happens if you switch <update check="background"/> to <update check="always"/>?

EDIT

This is my test JNLP. Seems like you're missing to specify the JavaFX runtime resource?

<?xml version="1.0" encoding="utf-8"?>
<jnlp spec="1.0+" xmlns:jfx="http://javafx.com" codebase="http://localhost:8080/HelloWorldFX" href="HelloWorldFX.jnlp">
  <information>
    <title>HelloWorldFX</title>
    <vendor>Unknown</vendor>
    <description>HelloWorldFX</description>
    <offline-allowed/>
  </information>
  <resources os="Windows">
        <jfx:javafx-runtime version="8.0+"/>
    </resources>
  <resources>
    <j2se version="1.8+" href="http://java.sun.com/products/autodl/j2se"/>
    <jar href="HelloWorldPreloader.jar" size="10774" download="progress" />
    <jar href="HelloWorldFX.jar" size="248884114" download="eager" main="true" />
  </resources>
  <jfx:javafx-desc  width="600" height="400" main-class="sample.Main"  name="HelloWorldFX"  preloader-class="HelloWorldPreloader"/>
  <update check="always"/>
</jnlp>
Repatriate answered 14/3, 2016 at 14:49 Comment(5)
Changing the <update check="background"/> to <update check="always"/> doesn't change anything sadly. Do you see the default JavaFX preloader? Because I don't for some reason...Graven
Yes, I do. But you will only see it if you don't specify a custom preloader class. I'm pretty sure something is wrong with your JNLP if you don't even see the default preloader. A full trace would help...Repatriate
I think we are getting somewhere, adding the line <jfx:javafx-runtime version="8.0+"/> to the resources does show my custom preloader in Phase 2 now and PreloaderFx::handleProgressNotification(); are getting called. However, the <jfx:javafx-runtime version="8.0+"/> should be obsolete since Java 8 according to this: bugs.openjdk.java.net/browse/JDK-8096671. I'll look into it more in depth this afternoon! :)Graven
works like a charm by adding the javafx-runtime version! Thanks a lot!Graven
Using <jfx:javafx-runtime version="8.0+"/> doesn't seem to work in latest versions of Java 8 :(Cytology
L
5

Note: I haven't tested or executed the code; my answer is primarily based on looking at the code. I haven't worked on JavaFX but I can grasp the code since I've worked on Swing and JNLP before.

The first thing I notice, is that in Phase 2, the default JavaFX preloader handling the downloading of the application JARs is not showing.

This seems to be because of the PreloaderFX.start(stage) method. The stage passed as the method's argument remains empty because the method constructs a new Stage() and adds children to it. If you add the child elements to the method's argument, it should display your custom preloader/progressbar during phase 2.

If the code still doesn't work as expected after this, I'd try debugging all the logic/code associated with the noLoadingProgress flag. Try commenting all that out (essentially taking noLoadingProgress out of the picture) and see if it fixes the issue.

Also, even though you've got this right in your code, see this answer - according to which all ProgressNotifications are handled by the handleApplicationNotification method.

Hope this helps!

Livi answered 13/3, 2016 at 4:33 Comment(1)
First of all, thanks for taking your time to look into this question. I edited the question based on your suggestions and my findings. The result is still the same after using the stage passed by the method and commenting out the noLoadingProgress boolean. I added some extra System.out.println() lines in the MainApp to check out when Phase 3 starts. I'm still convinced that handleApplicationNotification should only be used to handle notifyPreloader() actions from the MainApp. The search continues :)Graven

© 2022 - 2024 — McMap. All rights reserved.