FileDocCategorySizeDatePackage
GraphicalInstaller.javaAPI DocJ2ME MIDP 2.060981Thu Nov 07 12:02:24 GMT 2002com.sun.midp.dev

GraphicalInstaller.java

/*
 * @(#)GraphicalInstaller.java	1.22 02/09/12 @(#)
 *
 * Copyright (c) 2001-2002 Sun Microsystems, Inc.  All rights reserved.
 * PROPRIETARY/CONFIDENTIAL
 * Use is subject to license terms.
 */

package com.sun.midp.dev;

import java.io.*;

import java.util.*;

import javax.microedition.io.*;

import javax.microedition.lcdui.*;

import javax.microedition.midlet.*;

import javax.microedition.rms.*;

import com.sun.midp.io.j2me.storage.*;

import com.sun.midp.lcdui.Resource;

import com.sun.midp.midlet.*;

import com.sun.midp.midletsuite.*;

import com.sun.midp.security.*;

/**
 * The Graphical MIDlet suite manager.
 * <p>
 * Let the user install a suite from a list of suites
 * obtained using an HTML URL given by the user. This list is derived by
 * extracting the links with hrefs that are in quotes and end with ".jad" from
 * the HTML page. An href in an extracted link is assumed to be an absolute
 * URL for a MIDP application descriptor.
 */
public class GraphicalInstaller extends MIDlet implements CommandListener {

    /** Standard timeout for alerts. */
    static final int ALERT_TIMEOUT = 1250;
    /** settings database */
    static final String SETTINGS_STORE = "settings";
    /** record id of selected midlet */
    static final int URL_RECORD_ID = 1;
    /** record is of the last installed midlet */
    static final int SELECTED_MIDLET_RECORD_ID = 2;


    /** So the static method can know if there is a color display. */
    static boolean colorDisplay;

    /** The installer that is being used to install or update a suite. */
    private Installer installer;
    /** Display for this MIDlet. */
    private Display display;    
    /** Contains the default URL for the install list. */
    private String defaultInstallListUrl = "http://";
    /** Contains the URL the user typed in. */
    private TextBox urlTextBox;
    /** Form obtain a password and a username. */
    private Form passwordForm;
    /** Contains the username for installing. */
    private TextField usernameField;
    /** Contains the password for installing. */
    private TextField passwordField;
    /** Background installer that holds state for the current install. */
    private BackgroundInstaller backgroundInstaller;
    /** Displays the progress of the install. */
    private Form progressForm;
    /** Gauge for progress form index. */
    private int progressGaugeIndex;
    /** URL for progress form index. */
    private int progressUrlIndex;
    /** Keeps track of when the display last changed, in millseconds. */
    private long lastDisplayChange;
    /** What to display to the user when the current action is cancelled. */
    private String cancelledMessage;
    /** What to display to the user when the current action is finishing. */
    private String finishingMessage;
    /** Displays a list of suites to install to the user. */
    private List installListBox;
    /** Contains a list of suites to install. */
    private Vector installList;

    /** Command object for "Back" command in the URL form. */
    private Command endCmd = new Command(Resource.getString("Back"), 
                                         Command.BACK, 1);
    /** Command object for URL screen to go and discover available suites. */
    private Command discoverCmd =
        new Command(Resource.getString("Go"), Command.SCREEN, 1);
    /** Command object for URL screen to save the URL for suites. */
    private Command saveCmd =
        new Command(Resource.getString("Save"), Command.SCREEN, 2);

    /** Command object for "Back" command in the suite list form. */
    private Command backCmd = new Command(Resource.getString("Back"), 
                                           Command.BACK, 1);
    /** Command object for "Install" command in the suite list form . */
    private Command installCmd = new Command(
        Resource.getString("Install"), Command.ITEM, 1);

    /** Command object for "Stop" command for progress form. */
    private Command stopCmd = new Command(Resource.getString("Stop"), 
                                           Command.STOP, 1);

    /** Command object for "Cancel" command for the confirm form. */
    private Command cancelCmd = new Command(
                                           Resource.getString("Cancel"), 
                                           Command.CANCEL, 1);
    /** Command object for "Install" command for the confirm download form. */
    private Command continueCmd =
        new Command(Resource.getString("Install"), Command.OK, 1);
    /** Command object for "Next" command for password form. */
    private Command nextCmd = new Command(Resource.getString("Next"), 
                                           Command.OK, 1);
    /** Command object for "continue" command for warning form. */
    private Command okCmd = new Command(Resource.getString("Continue"), 
                                           Command.OK, 1);
    /** Command object for "OK" command for exception form. */
    private Command exceptionCmd = new Command(Resource.getString("OK"), 
                                           Command.OK, 1);
    /** Command object for "Yes" command for keep RMS form. */
    private Command keepRMSCmd = new Command(Resource.getString("Yes"), 
                                           Command.OK, 1);
    /** Command object for "No" command for keep RMS form. */
    private Command removeRMSCmd = new Command(Resource.getString("No"), 
                                           Command.CANCEL, 1);

    /**
     * Gets an icon from storage.
     *
     * @param iconName name without a path
     * @return icon image
     */
    static Image getIconFromStorage(String iconName) {
        String iconFilename;
        RandomAccessStream stream;
        byte[] rawPng;

        iconFilename = File.getStorageRoot() + iconName;
        stream = new RandomAccessStream();
        try { 
            stream.connect(iconFilename, Connector.READ);
            rawPng = new byte[stream.getSizeOf()];
            stream.readBytes(rawPng, 0, rawPng.length);
            stream.disconnect();
            return Image.createImage(rawPng, 0, rawPng.length);
        } catch (java.io.IOException noImage) {
        }

        return null;
    }

    /**
     * Translate an InvalidJadException into a message for the user.
     *
     * @param exception exception to translate
     * @param name name of the MIDlet suite to insert into the message
     * @param vendor vendor of the MIDlet suite to insert into the message,
     *        can be null
     * @param version version of the MIDlet suite to insert into the message,
     *        can be null
     * @param jadUrl URL of a JAD, can be null
     *
     * @return message to display to the user
     */
    private static String translateJadException(
            InvalidJadException exception, String name, String vendor,
            String version, String jadUrl) {
        String[] values = {name, vendor, version, jadUrl,
                           exception.getExtraData()};
        String key;

        switch (exception.getReason()) {
        case InvalidJadException.OLD_VERSION:
            key = "An OLDER version of %1 has been found. " +
                  "Continuing will replace the version installed on your " +
                  "phone.\n\n" +
                  "Currently installed: %5\n" +
                  "Version found: %3\n" +
                  "Vendor: %2\n" +
                  "Website: %4\n\n" +
                  "Would you like to continue?";
            break;

        case InvalidJadException.ALREADY_INSTALLED:
            key = "This version of %1 is already installed on your phone. " +
                  "Continuing will replace the version installed on your " +
                  "phone.\n\n" +
                  "Currently installed: %5\n" +
                  "Version found: %3\n" +
                  "Vendor: %2\n" +
                  "Website: %4\n\n" +
                  "Would you like to continue?";
            break;

        case InvalidJadException.NEW_VERSION:
            key = "A NEWER version of %1 is has been found. " +
                  "Continuing will replace the version installed on your " +
                  "phone.\n\n" +
                  "Currently installed: %5\n" +
                  "Latest available: %3\n" +
                  "Vendor: %2\n" +
                  "Website: %4\n\n" +
                  "Would you like to continue?";
            break;

        case InvalidJadException.JAD_SERVER_NOT_FOUND:
        case InvalidJadException.JAD_NOT_FOUND:
        case InvalidJadException.INVALID_JAD_URL:
            key = "%1 cannot be found at this URL. " +
                  "Contact the application provider for more information.";
            break;

        case InvalidJadException.INVALID_JAD_TYPE:
            key = "The application file (.jad) for %1 does not appear to " +
                  "be the correct type. Contact the application provider " +
                  "for more information.";
            break;

        case InvalidJadException.MISSING_PROVIDER_CERT:
        case InvalidJadException.MISSING_SUITE_NAME:
        case InvalidJadException.MISSING_VENDOR:
        case InvalidJadException.MISSING_VERSION:
        case InvalidJadException.MISSING_JAR_URL:
        case InvalidJadException.MISSING_JAR_SIZE:
            key = "%1 cannot be installed because critical information is " +
                  "missing from the application file (.jad).";
            break;

        case InvalidJadException.MISSING_CONFIGURATION:
        case InvalidJadException.MISSING_PROFILE:
            key = "%1 cannot be installed because critical information is " +
                  "missing from the application file (.jar).";
            break;

        case InvalidJadException.INVALID_KEY:
        case InvalidJadException.INVALID_VALUE:
        case InvalidJadException.INVALID_VERSION:
        case InvalidJadException.PUSH_FORMAT_FAILURE:
        case InvalidJadException.PUSH_CLASS_FAILURE:
            key = "%1 cannot be installed because critical " + 
                  "information is not formatted correctly or is invalid. " +
                  "Contact your application provider to correct this " +
                  "situation.";
            break;

        case InvalidJadException.DEVICE_INCOMPATIBLE:
            key = "%1 is not designed to work with this device and cannot " +
                  "be installed.";
            break;

        case InvalidJadException.JAD_MOVED:
            key = "The new version of %1 is not from the same provider " +
                  "as the old version. The download URLs do not match. " +
                  "Do you want to install the new version?" +
                  "\n\nOld URL: %5\nNew URL: %4";
            break;

        case InvalidJadException.INSUFFICIENT_STORAGE:
            key = "There is not enough room to install %1 (%5K is needed " +
                  "for installation.). Try removing other items to free " +
                  "up space.";
            break;

        case InvalidJadException.JAR_SERVER_NOT_FOUND:
        case InvalidJadException.JAR_NOT_FOUND:
        case InvalidJadException.INVALID_JAR_URL:
            key = "The application file (.jar) for %1 cannot be found at " +
                  "its URL. Contact the application provider for more " +
                  "information.";
            break;

        case InvalidJadException.INVALID_JAR_TYPE:
            key = "The application file (.jar) for %1 does not appear to " +
                  "be the correct type. Contact the application provider " +
                  "for more information.";
            break;

        case InvalidJadException.SUITE_NAME_MISMATCH:
        case InvalidJadException.VERSION_MISMATCH:
        case InvalidJadException.VENDOR_MISMATCH:
        case InvalidJadException.JAR_SIZE_MISMATCH:
        case InvalidJadException.ATTRIBUTE_MISMATCH:
            key = "%1 cannot be installed because critical information " +
                  "between the website and the application file does not " +
                  "match.";
            break;

        case InvalidJadException.CORRUPT_JAR:
            key = "The application file (.jar) for %1 appears to be " +
                  "corrupt. Contact the application provider " +
                  "for more information.";
            break;

        case InvalidJadException.CANNOT_AUTH:
            key = "The website has requested to authenticate the user " +
                  "in way that this device does not support.";
            break;

        case InvalidJadException.CORRUPT_PROVIDER_CERT:
        case InvalidJadException.INVALID_PROVIDER_CERT:
        case InvalidJadException.CORRUPT_SIGNATURE:
        case InvalidJadException.INVALID_SIGNATURE:
        case InvalidJadException.UNSUPPORTED_CERT:
            key = "%1 cannnot be installed because the application files " +
                  "cannot verified. Contact your application " +
                  "provider to correct this situation.";
            break;

        case InvalidJadException.UNKNOWN_CA:
            key = "%1 cannot be installed. The system does recognize who " +
                  "is trying to authorize the application. Contact your " +
                  "service provider to correct this situation. \n\n%5";
            break;

        case InvalidJadException.EXPIRED_PROVIDER_CERT:
            key = "%1 cannot be installed. The trusted certificate used to " +
                  "authorize the application has expired. Contact your " +
                  "application provider to correct this situation.";
            break;

        case InvalidJadException.EXPIRED_CA_KEY:
            key = "%1 cannot be installed. The public key used for " +
                  "authorization has expired. Contact your service " +
                  "provider to correct this situation.";
            break;

        case InvalidJadException.AUTHORIZATION_FAILURE:
            key = "%1 cannot be installed because it does not have " +
                  "permission to the operation it requires.";
            break;

        case InvalidJadException.PUSH_DUP_FAILURE:
            key = "%1 cannot be installed. %1 is requires the use of a " +
                  "particular network resource to listen for network " +
                  "information. This resource is in use by another " + 
                  "application. Try removing the other application and " +
                  "re-installing.";
            break;

        case InvalidJadException.PUSH_PROTO_FAILURE:
            key = "%1 cannot be installed. %1 is requires the use of a " +
                  "particular network resource to listen for network " +
                  "information. This network resource is not supported on " +
                  "this device.";
            break;

        case InvalidJadException.TRUSTED_OVERWRITE_FAILURE:
            key = "The new version of %1, cannot be installed. " +
                  "The old version of %1 is authorized by %5. " +
                  "The new version is not authorized. " +
                  "Authorized applications cannot be replaced by " +
                  "unauthorized applications.";
            break;

        default:
            return exception.getMessage();
        }

        return Resource.getString(key, values);
    }

    /**
     * Create and initialize a new graphical installer MIDlet.
     * The Display is retreived and the list of MIDlet will be retrived or
     * update a currently installed suite.
     */
    GraphicalInstaller() {
        String storageName;

        installer = Installer.getInstaller();
        display = Display.getDisplay(this);
        colorDisplay = display.isColor();

        initSettings();
        restoreSettings();
        
        storageName = getAppProperty("storageName");
        if (storageName != null) {
            updateSuite(storageName);
            return;
        }

        // get the URL of a list of suites to install
        getUrl();
    }

    /**
     * Start.
     */
    public void startApp() {
    }

    /**
     * Pause; there are no resources that need to be released.
     */
    public void pauseApp() {
    }

    /**
     * Destroy cleans up.
     *
     * @param unconditional is ignored; this object always
     * destroys itself when requested.
     */
    public void destroyApp(boolean unconditional) {
        if (installer != null) {
            installer.stopInstalling();
        }

        /* The backgroundInstaller could be waiting for the user. */
        cancelBackgroundInstall();
    }

    /**
     * Respond to a command issued on any Screen.
     *
     * @param c command activiated by the user
     * @param s the Displayable the command was on.
     */
    public void commandAction(Command c, Displayable s) {
        if (c == discoverCmd) {
            // user wants to discover the suites that can be installed
            discoverSuitesToInstall(urlTextBox.getString());
        } else if (s == installListBox &&
                  (c == List.SELECT_COMMAND || c == installCmd)) {
            installSuite(installListBox.getSelectedIndex());
        } else if (c == backCmd) {
            display.setCurrent(urlTextBox);
        } else if (c == nextCmd) {
            // the user has entered a username and password
            resumeInstallWithPassword();
        } else if (c == okCmd) {
            resumeInstallAfterWarning();
        } else if (c == continueCmd) {
            startJarDownload();
        } else if (c == saveCmd) {
            saveURLSetting();
        } else if (c == keepRMSCmd) {
            setKeepRMSAnswer(true);
        } else if (c == removeRMSCmd) {
            setKeepRMSAnswer(false);
        } else if (c == stopCmd) {
            if (installer != null) {
                /*
                 * BackgroundInstaller may be displaying
                 * the "Finishing" message
                 *
                 * also we need to prevent the BackgroundInstaller from
                 * re-displaying the list before the cancelled message is
                 * displayed
                 */
                synchronized (this) {
                    if (installer.stopInstalling()) {
                        displayCancelledMessage(cancelledMessage);
                    }
                }
            } else {
                // goto back to the manager midlet
                notifyDestroyed();
            }
        } else if (c == cancelCmd) {
            displayCancelledMessage(cancelledMessage);
            cancelBackgroundInstall();
        } else if (c == endCmd || c == Alert.DISMISS_COMMAND) {
            // goto back to the manager midlet
            notifyDestroyed();
        }
    }

    /**
     * Get the settings the Manager saved for the user.
     */
    private void restoreSettings() {
        ByteArrayInputStream bas;
        DataInputStream dis;
        byte[] data;
        RecordStore settings = null;
        
        try {
            settings = RecordStore.openRecordStore(SETTINGS_STORE, false);

            data = settings.getRecord(1);
            if (data != null) {
                bas = new ByteArrayInputStream(data);
                dis = new DataInputStream(bas);
                defaultInstallListUrl = dis.readUTF();
            }

        } catch (RecordStoreException e) {
            // ignore
        } catch (IOException e) {
            // ignore
        } finally {
            if (settings != null) {
                try {
                    settings.closeRecordStore();
                } catch (RecordStoreException e) {
                    // ignore
                }
            }
        }
    }

    /**
     * Initialize the settings database if it doesn't exist. This may create
     * two entries. The first will be for the download url, the second will
     * be for storing the storagename of the currently selected midlet
     */
    static void initSettings() {
        try {
            RecordStore settings = RecordStore.
                                   openRecordStore(SETTINGS_STORE, true);

            if (settings.getNumRecords() == 0) {
                // space for a URL
                settings.addRecord(null, 0, 0);

                // space for current MIDlet Suite name
                settings.addRecord(null, 0, 0);
            }

            settings.closeRecordStore();

        } catch (Exception e) {}
    }

   
    /**
     * Save the settings the user entered.
     *
     * @param url the url to save
     * @param curMidlet the storagename of the currently selected midlet
     * @return the Exception that may have been thrown, or null
     */
    static Exception saveSettings(String url, String curMidlet) {

        Exception ret = null;

        try {
            String temp;
            ByteArrayOutputStream bas;
            DataOutputStream dos;
            byte[] data;
            RecordStore settings;

            bas = new ByteArrayOutputStream();
            dos = new DataOutputStream(bas);
            settings = RecordStore.openRecordStore(SETTINGS_STORE, false);

            if (url != null) {
                dos.writeUTF(url);
                data = bas.toByteArray();
                settings.setRecord(URL_RECORD_ID, data, 0, data.length);
            }

            if (curMidlet != null) {
                bas.reset();

                dos.writeUTF(curMidlet);
                data = bas.toByteArray();
                settings.setRecord(SELECTED_MIDLET_RECORD_ID,
                                   data, 0, data.length);
            }

            settings.closeRecordStore();
            dos.close();
        } catch (Exception e) {
            ret = e;
        }

        return ret;
    }

    /**
     * Save the URL setting the user entered in to the urlTextBox.
     */
    private void saveURLSetting() {
        String temp;
        Exception ex;

        temp = urlTextBox.getString();

        ex = saveSettings(temp, null);
        if (ex != null) {
            displayException(Resource.getString("Exception"), ex.toString());
            return;
        }

        defaultInstallListUrl = temp;

        displaySuccessMessage(Resource.getString("Saved!"));
    }

    /**
     * Update a suite.
     *
     * @param storageName storage name of the suite to update
     */
    private void updateSuite(String storageName) {
        MIDletSuite midletSuite = installer.getMIDletSuite(storageName);
        MIDletInfo midletInfo;
        String name;

        if (midletSuite.getNumberOfMIDlets() == 1) {
            midletInfo = new MIDletInfo(midletSuite.getProperty("MIDlet-1"));
            name = midletInfo.name;
        } else {
            name = midletSuite.getProperty(Installer.SUITE_NAME_PROP);
        }

        cancelledMessage = Resource.getString("Update cancelled.");
        finishingMessage = Resource.getString("Finishing update.");
        installSuiteCommon("Updating", name,
            midletSuite.getDownloadUrl(),
            name + Resource.getString(" was successfully updated"),
            true);
    }

    /**
     * Let the user select a suite to install. The suites that are listed
     * are the links on a web page that end with .jad.
     *
     * @param url where to get the list of suites to install.
     */
    private void discoverSuitesToInstall(String url) {
        new Thread(new BackgroundInstallListGetter(this, url)).start();
    }

    /**
     * Install a suite.
     *
     * @param selectedSuite index into the installList
     */
    private void installSuite(int selectedSuite) {
        SuiteDownloadInfo suite;

        suite = (SuiteDownloadInfo)installList.elementAt(selectedSuite);
        cancelledMessage = Resource.getString("Installation cancelled.");
        finishingMessage = Resource.getString("Finishing installation.");
        installSuiteCommon("Installing", suite.label, suite.url,
            suite.label + Resource.getString(" was successfully installed"),
            false);
    }

    /**
     * Common helper method to install or update a suite.
     *
     * @param action action to put in the form's title
     * @param name name to in the form's title
     * @param url URL of a JAD
     * @param successMessage message to display to user upon success
     * @param updateFlag if true the current suite is being updated
     */
    private void installSuiteCommon(String action, String name, String url,
            String successMessage, boolean updateFlag) {
        try {
            createProgressForm(action, name, url, 0, "Connecting...");
            backgroundInstaller = new BackgroundInstaller(this, url, name,
                                      successMessage, updateFlag);
            new Thread(backgroundInstaller).start();
        } catch (Exception ex) {
            StringBuffer sb = new StringBuffer();

            sb.append(name);
            sb.append("\n");
            sb.append(Resource.getString("Error"));
            sb.append(": ");
            sb.append(ex.toString());
            displayException(Resource.getString("Cannot access: "), 
                            sb.toString());
        }
    }

    /**
     * Create and display the progress form to the user with the stop action.
     *
     * @param action action to put in the form's title
     * @param name name to in the form's title
     * @param url URL of a JAD
     * @param size 0 if unknown, else size of object to download in K bytes
     * @param gaugeLabel label for progress gauge
     */
    private void createProgressForm(String action, String name,
                                    String url, int size, String gaugeLabel) {
        Form installForm;

        // display the JAR progress form
        installForm = displayProgressForm(action, name, url, size,
                                            gaugeLabel);
        installForm.addCommand(stopCmd);
        installForm.setCommandListener(this);
    }

    /**
     * Display the connecting form to the user, let call set actions.
     *
     * @param action action to put in the form's title
     * @param name name to in the form's title
     * @param url URL of a JAD
     * @param size 0 if unknown, else size of object to download in K bytes
     * @param gaugeLabel label for progress gauge
     *
     * @return displayed form
     */
    private Form displayProgressForm(String action, String name,
            String url, int size, String gaugeLabel) {
        Gauge progressGauge;
        StringItem urlItem;

        progressForm = new Form(null);

        progressForm.setTitle(Resource.getString(action) + " " + name);

        if (size <= 0) {
            progressGauge = new Gauge(Resource.getString(gaugeLabel), 
				      false, Gauge.INDEFINITE,
				      Gauge.CONTINUOUS_RUNNING);
        } else {
            progressGauge = new Gauge(Resource.getString(gaugeLabel), 
				      false, size, 0);
        }

        progressGaugeIndex = progressForm.append(progressGauge);

        if (url == null) {
            urlItem = new StringItem("", "");
        } else {
            urlItem =
                new StringItem(Resource.getString("Website") + ": ", url);
        }

        progressUrlIndex = progressForm.append(urlItem);

        display.setCurrent(progressForm);
        lastDisplayChange = System.currentTimeMillis();

        return progressForm;
    }

    /** Cancel an install (if there is one) waiting for user input. */
    private void cancelBackgroundInstall() {
        if (backgroundInstaller != null) {
            backgroundInstaller.continueInstall = false;

            synchronized (backgroundInstaller) {
                backgroundInstaller.notify();
            }
        }
    }

    /**
     * Ask the user for the URL.
     */
    private void getUrl() {
        try {
            if (urlTextBox == null) {
                urlTextBox = new TextBox(Resource.getString(
                                         "Enter a website to Install From:"),
                                         defaultInstallListUrl, 1024,
                                         TextField.ANY);
                urlTextBox.addCommand(endCmd);
                urlTextBox.addCommand(saveCmd);
                urlTextBox.addCommand(discoverCmd);
                urlTextBox.setCommandListener(this);
            }

            display.setCurrent(urlTextBox);
        } catch (Exception ex) {
            displayException(Resource.getString("Exception"), 
                             ex.toString());
        }
    }

    /**
     * Update the status form.
     *
     * @param status current status of the install.
     * @param state current state of the install.
     */
    private void updateStatus(int status, InstallState state) {
        if (status == Installer.DOWNLOADING_JAD) {
            updateProgressForm("", 0,
               "Downloading the application description file...");
            return;
        }

        if (status == Installer.DOWNLOADING_JAR) {
            updateProgressForm(state.getJarUrl(), state.getJarSize(),
                               "Downloading the application file...");
            return;
        }

        if (status == Installer.DOWNLOADED_1K_OF_JAR &&
                state.getJarSize() > 0) {
            Gauge progressGauge = (Gauge)progressForm.get(progressGaugeIndex);
            progressGauge.setValue(progressGauge.getValue() + 1);
            return;
        }

        if (status == Installer.VERIFYING_SUITE) {
            updateProgressForm(null, 0, "Verifying the application...");
            return;
        }

        if (status == Installer.STORING_SUITE) {
            displaySuccessMessage(finishingMessage);
            return;
        }
    }

    /**
     * Update URL and gauge of the progress form.
     *
     * @param url new URL, null to remove, "" to not change
     * @param size 0 if unknown, else size of object to download in K bytes
     * @param gaugeLabel label for progress gauge
     */
    private void updateProgressForm(String url, int size, String gaugeLabel) {
        Gauge oldProgressGauge;
        Gauge progressGauge;
        StringItem urlItem;

        // We need to prevent "flashing" on fast development platforms.
        while (System.currentTimeMillis() - lastDisplayChange < ALERT_TIMEOUT);

        if (size <= 0) {
            progressGauge = new Gauge(Resource.getString(gaugeLabel), 
				      false, Gauge.INDEFINITE, 
				      Gauge.CONTINUOUS_RUNNING);
        } else {
            progressGauge = new Gauge(Resource.getString(gaugeLabel), 
				      false, size, 0);
        }

        oldProgressGauge = (Gauge)progressForm.get(progressGaugeIndex);
        progressForm.set(progressGaugeIndex, progressGauge);

        // this ends the background thread of gauge.
        oldProgressGauge.setValue(Gauge.CONTINUOUS_IDLE);

        if (url == null) {
            urlItem = new StringItem("", "");
            progressForm.set(progressUrlIndex, urlItem);
        } else if (url.length() != 0) {
            urlItem =
                new StringItem(Resource.getString("Website") + ": ", url);
            progressForm.set(progressUrlIndex, urlItem);
        }

        lastDisplayChange = System.currentTimeMillis();
    }

    /**
     * Give the user a chance to act on warning during an installation.
     *
     * @param name name of the MIDlet suite to insert into the message
     * @param vendor vendor of the MIDlet suite to insert into the message,
     *        can be null
     * @param version version of the MIDlet suite to insert into the message,
     *        can be null
     * @param jadUrl URL of a JAD, can be null
     * @param e last exception from the installer
     */
    private void warnUser(String name, String vendor, String version,
                          String jadUrl, InvalidJadException e) {
        Form warningForm;

        warningForm = new Form(null);
        warningForm.setTitle(Resource.getString("Warning"));
        warningForm.append(translateJadException(e, name, vendor, version,
                                                 jadUrl)); 
        warningForm.addCommand(cancelCmd);
        warningForm.addCommand(okCmd);
        warningForm.setCommandListener(this);
        display.setCurrent(warningForm);
    }

    /**
     * Resume the install after a the user overrides a warning.
     */
    private void resumeInstallAfterWarning() {
        // redisplay the progress form
        display.setCurrent(progressForm);

        backgroundInstaller.continueInstall = true;
        synchronized (backgroundInstaller) {
            backgroundInstaller.notify();
        }
    }

    /**
     * Ask for a username and password.
     */
    private void getUsernameAndPassword() {
        getUsernameAndPasswordCommon("");
    }

    /**
     * Ask for proxy username and password.
     */
    private void getProxyUsernameAndPassword() {
        getUsernameAndPasswordCommon("Firewall Authentication");
    }

    /**
     * Ask a username and password.
     *
     * @param title title of the password form
     */
    private void getUsernameAndPasswordCommon(String title) {
        if (passwordForm == null) {
            passwordForm = new Form(null);

            usernameField = new TextField(
                            Resource.getString("Please Enter ID"), null, 40,
                            TextField.ANY);
            passwordForm.append(usernameField);

            passwordField = new TextField(
                            Resource.getString("Password"), null, 40,
                            TextField.PASSWORD);
            passwordForm.append(passwordField);
            passwordForm.addCommand(cancelCmd);
            passwordForm.addCommand(nextCmd);
            passwordForm.setCommandListener(this);
        }

        passwordForm.setTitle(Resource.getString(title));
        passwordField.setString("");
        display.setCurrent(passwordForm);
    }

    /**
     * Resume the install of the suite with a password and username.
     */
    private void resumeInstallWithPassword() {
        String username;
        String password;


        username = usernameField.getString();
        password = passwordField.getString();
        if (username == null || username.length() == 0) {
            Alert a = new Alert(Resource.getString("Error"), 
                             Resource.getString(
                             "The ID has not been entered."),
                             null, AlertType.ERROR);
            a.setTimeout(ALERT_TIMEOUT);
            display.setCurrent(a, passwordForm);
            return;
        }

        if (password == null || password.length() == 0) {
            Alert a = new Alert(Resource.getString("Error"), 
                                Resource.getString(
                                "The password has not been entered."),
                                null, AlertType.ERROR);
            a.setTimeout(ALERT_TIMEOUT);
            display.setCurrent(a, passwordForm);
            return;
        }

        // redisplay the progress form
        display.setCurrent(progressForm);

        if (backgroundInstaller.proxyAuth) {
            backgroundInstaller.installState.setProxyUsername(username);
            backgroundInstaller.installState.setProxyPassword(password);
        } else {
            backgroundInstaller.installState.setUsername(username);
            backgroundInstaller.installState.setPassword(password);
        }

        backgroundInstaller.continueInstall = true;
        synchronized (backgroundInstaller) {
            backgroundInstaller.notify();
        }
    }

    /**
     * Confirm the JAR download with the user.
     *
     * @param state current state of the install.
     */
    private void displayDownloadConfirmation(InstallState state) {
        Form infoForm;
        StringItem item;
        String name;
        String desc;
        StringBuffer label = new StringBuffer(40);
        StringBuffer value = new StringBuffer(40);
        String[] values = new String[1];

        name = state.getAppProperty(Installer.SUITE_NAME_PROP);

        try {
            infoForm = new Form(null);

            infoForm.setTitle(Resource.getString("Confirmation"));

            values[0] = name;
            item = new StringItem(null, Resource.getString(
                "Are you sure you want to install \"%1\"?", values));
            item.setLayout(Item.LAYOUT_NEWLINE_AFTER | Item.LAYOUT_2);
            infoForm.append(item);

            // round up the size to a Kilobyte
            label.append(Resource.getString("Size"));
            label.append(": ");
            value.setLength(0);
            value.append(state.getJarSize());
            value.append(" K");
            item = new StringItem(label.toString(), value.toString());
            item.setLayout(Item.LAYOUT_NEWLINE_AFTER | Item.LAYOUT_2);
            infoForm.append(item);

            label.setLength(0);
            label.append(Resource.getString("Version"));
            label.append(": ");
            value.setLength(0);
            item = new StringItem(label.toString(),
                       state.getAppProperty(Installer.VERSION_PROP));
            item.setLayout(Item.LAYOUT_NEWLINE_AFTER | Item.LAYOUT_2);
            infoForm.append(item);

            label.setLength(0);
            label.append(Resource.getString("Vendor"));
            label.append(": ");
            item = new StringItem(label.toString(),
                      state.getAppProperty(Installer.VENDOR_PROP));
            item.setLayout(Item.LAYOUT_NEWLINE_AFTER | Item.LAYOUT_2);
            infoForm.append(item);

            desc = state.getAppProperty(Installer.DESC_PROP);
            if (desc != null) {
                label.setLength(0);
                label.append(Resource.getString("Description"));
                label.append(": ");
                item = new StringItem(label.toString(), desc);
                item.setLayout(Item.LAYOUT_NEWLINE_AFTER | Item.LAYOUT_2);
                infoForm.append(item);
            }

            label.setLength(0);
            label.append(Resource.getString("Website"));
            label.append(": ");
            infoForm.append(new StringItem(label.toString(),
                                           state.getJarUrl()));

            infoForm.addCommand(continueCmd);
            infoForm.addCommand(cancelCmd);
            infoForm.setCommandListener(this);

            // We need to prevent "flashing" on fast development platforms.
            while (System.currentTimeMillis() - lastDisplayChange <
                   ALERT_TIMEOUT);

            display.setCurrent(infoForm);
        } catch (Exception ex) {
            StringBuffer sb = new StringBuffer();
            
            sb.append(name);
            sb.append("\n");
            sb.append(Resource.getString("Exception"));
            sb.append(": ");
            sb.append(ex.toString());
            displayException(Resource.getString("Cannot access: "), 
                                sb.toString());
        }
    }

    /**
     * Ask the user during an update if they want to keep the old RMS data.
     *
     * @param state current state of the install.
     */
    private void displayKeepRMSForm(InstallState state) {
        Form infoForm;
        String name;
        String desc;
        StringBuffer label = new StringBuffer(40);
        StringBuffer value = new StringBuffer(40);
        String[] values = new String[1];

        name = state.getAppProperty(Installer.SUITE_NAME_PROP);

        try {
            infoForm = new Form(null);

            infoForm.setTitle(Resource.getString("Confirmation"));

            values[0] = name;
            value.append(Resource.getString("Do you want the new version " +
                "of %1 to be able to use the information stored by the " +
                "old version of %1?", values));
            infoForm.append(value.toString());

            infoForm.addCommand(keepRMSCmd);
            infoForm.addCommand(removeRMSCmd);
            infoForm.setCommandListener(this);

            // We need to prevent "flashing" on fast development platforms.
            while (System.currentTimeMillis() - lastDisplayChange <
                   ALERT_TIMEOUT);

            display.setCurrent(infoForm);
        } catch (Exception ex) {
            StringBuffer sb = new StringBuffer();
            
            sb.append(name);
            sb.append("\n");
            sb.append(Resource.getString("Exception"));
            sb.append(": ");
            sb.append(ex.toString());
            displayException(Resource.getString("Cannot access: "), 
                                sb.toString());
        }
    }

    /**
     * Resume the install to start the JAR download.
     */
    private void startJarDownload() {
        updateProgressForm(backgroundInstaller.url, 0, "Connecting...");

        // redisplay the progress form
        display.setCurrent(progressForm);

        backgroundInstaller.continueInstall = true;
        synchronized (backgroundInstaller) {
            backgroundInstaller.notify();
        }
    }

    /** Confirm the JAR only download with the user. */
    private void displayJarOnlyDownloadConfirmation() {
        Form infoForm;
        StringItem item;
        StringBuffer label = new StringBuffer(40);
        StringBuffer value = new StringBuffer(40);
        String[] values = new String[1];

        try {
            infoForm = new Form(null);

            infoForm.setTitle(Resource.getString("Confirmation"));

            values[0] = backgroundInstaller.name;
            item = new StringItem(null, Resource.getString(
                       "Are you sure you want to install \"%1\"?", values));
            item.setLayout(Item.LAYOUT_NEWLINE_AFTER | Item.LAYOUT_2);
            infoForm.append(item);

            label.append(Resource.getString("Website"));
            label.append(": ");
            item = new StringItem(label.toString(), backgroundInstaller.url);
            item.setLayout(Item.LAYOUT_NEWLINE_AFTER | Item.LAYOUT_2);
            infoForm.append(item);

            value.append(" \n");
            value.append(Resource.getString("There is no further " +
                "information available for the application."));
            infoForm.append(new StringItem(null, value.toString()));

            infoForm.addCommand(continueCmd);
            infoForm.addCommand(cancelCmd);
            infoForm.setCommandListener(this);

            // We need to prevent "flashing" on fast development platforms.
            while (System.currentTimeMillis() - lastDisplayChange <
                   ALERT_TIMEOUT);

            display.setCurrent(infoForm);
        } catch (Exception ex) {
            StringBuffer sb = new StringBuffer();
            
            sb.append(backgroundInstaller.name);
            sb.append("\n");
            sb.append(Resource.getString("Exception"));
            sb.append(": ");
            sb.append(ex.toString());
            displayException(Resource.getString("Cannot access: "), 
                             sb.toString());
        }
    }

    /**
     * Tell the background installer to keep the RMS data.
     *
     * @param keepRMS set to true to mean the user answered yes
     */
    private void setKeepRMSAnswer(boolean keepRMS) {
        // redisplay the progress form
        display.setCurrent(progressForm);

        // We need to prevent "flashing" on fast development platforms.
        while (System.currentTimeMillis() - lastDisplayChange <
               ALERT_TIMEOUT);

        backgroundInstaller.continueInstall = keepRMS;
        synchronized (backgroundInstaller) {
            backgroundInstaller.notify();
        }
    }

    /**
     * Alert the user that an action was successful.
     *
     * @param successMessage message to display to user
     */
    private void displaySuccessMessage(String successMessage) {
        Image icon;
        Alert successAlert;

        icon = getIconFromStorage(
                       (colorDisplay ? "_dukeok8.png" : "_dukeok2.png"));

        successAlert = new Alert(null, successMessage, icon, null);

        successAlert.setTimeout(ALERT_TIMEOUT);

        // We need to prevent "flashing" on fast development platforms.
        while (System.currentTimeMillis() - lastDisplayChange <
               ALERT_TIMEOUT);

        lastDisplayChange = System.currentTimeMillis();
        display.setCurrent(successAlert);
    }

    /**
     * Alert the user that an action was canceled. The backgroundInstaller
     * will hide the message.
     * @param message message to display to user
     */
    private void displayCancelledMessage(String message) {
        Form form;
        Image icon;

        form = new Form(null);

        icon = getIconFromStorage(
                       (colorDisplay ? "_ack_8.png" : "_ack_2.png"));
        form.append(new ImageItem(null, icon, ImageItem.LAYOUT_CENTER +
                                     ImageItem.LAYOUT_NEWLINE_BEFORE +
                                     ImageItem.LAYOUT_NEWLINE_AFTER, null));

        form.append(message);

        display.setCurrent(form);
        lastDisplayChange = System.currentTimeMillis();
    }

    /**
     * Display an exception to the user, with a done command.
     *
     * @param title exception form's title
     * @param message exception message
     */
    private void displayException(String title, String message) {
        Alert a = new Alert(title, message, null, AlertType.ERROR);

        a.setTimeout(Alert.FOREVER);
        a.setCommandListener(this);

        display.setCurrent(a);
    }

    /** A class to get the install list in a background thread. */
    private class BackgroundInstallListGetter implements Runnable {
        /** Parent installer. */
        private GraphicalInstaller parent;
        /** URL of the list. */
        private String url;

        /**
         * Construct a BackgroundInstallListGetter.
         *
         * @param theParent parent installer of this object
         * @param theUrl where to get the list of suites to install.
         */
        private BackgroundInstallListGetter(GraphicalInstaller theParent,
                                            String theUrl) {
            parent = theParent;
            url = theUrl;
        }

        /**
         * Get the list of suites for the user to install.
         * The suites that are listed
         * are the links on a web page that end with .jad.
         */
        public void run() {
            StreamConnection conn = null;
            InputStreamReader in = null;
            String errorMessage;
            long startTime;

            startTime = System.currentTimeMillis();

            try {
                parent.displayProgressForm("Getting Install List",
                                             "", url, 0, "Connecting...");
                conn = (StreamConnection)Connector.open(url, Connector.READ);
                in = new InputStreamReader(conn.openInputStream());
                try {
                    parent.updateProgressForm("", 0, "Downloading...");
                    parent.installList =
                        SuiteDownloadInfo.getDownloadInfoFromPage(in);
                    
                    if (parent.installList.size() > 0) {
                        parent.installListBox = new List(Resource.getString(
                                                "Select one to install:"),
                                                Choice.IMPLICIT);

                        // Add each suite
                        for (int i = 0; i < parent.installList.size(); i++) {
                            SuiteDownloadInfo suite =
                                (SuiteDownloadInfo)installList.elementAt(i);
                            parent.installListBox.append(suite.label,
                                                         (Image)null);
                        }

                        parent.installListBox.addCommand(backCmd);
                        parent.installListBox.addCommand(installCmd);
                        parent.installListBox.setCommandListener(parent);

                        /*
                         * We need to prevent "flashing" on fast development
                         * platforms. 
                         */
                        while (System.currentTimeMillis() -
                            parent.lastDisplayChange < 
                            parent.ALERT_TIMEOUT);

                        parent.display.setCurrent(parent.installListBox);
                        return;
                    }

                    errorMessage = Resource.getString(
                        "No MIDlet Suites found. " +
                        "Check the URL to make sure it is correct.");
                } catch (IllegalArgumentException ex) {
                    errorMessage = Resource.getString(
                        "The URL is not formatted correctly.");
                } catch (Exception ex) {
                    errorMessage = ex.getMessage();
                }
            } catch (Exception ex) {
                errorMessage = Resource.getString(
                    "The connection failed. Please check the website " +
                    "URL and try again.");
            } finally {
                if (parent.progressForm != null) {
                    // end the background thread of progress gauge.
                    Gauge progressGauge = (Gauge)parent.progressForm.get(
                                          parent.progressGaugeIndex);
                    progressGauge.setValue(Gauge.CONTINUOUS_IDLE);
                }

                try {
                    conn.close();
                    in.close();
                } catch (Exception e) {
                    // ignore
                }
            }

            Alert a = new Alert(Resource.getString("Error"), 
                                errorMessage, null, AlertType.ERROR);
            a.setTimeout(Alert.FOREVER);
            parent.display.setCurrent(a, parent.urlTextBox);
        }
    }

    /** A class to install a suite in a background thread. */
    private class BackgroundInstaller implements Runnable, InstallListener {
        /** Parent installer. */
        private GraphicalInstaller parent;
        /** URL to install from. */
        private String url; 
        /** Name of MIDlet suite. */
        private String name;
        /**
         * Message for the user after the current install completes
         * successfully.
         */
        private String successMessage;
        /** Flag to update the current suite. */
        private boolean update;
        /** State of the install. */
        InstallState installState;
        /** Signals that the user wants the install to continue. */
        boolean continueInstall;
        /** Signals that the suite only has JAR, no JAD. */
        boolean jarOnly;
        /** Signals that a proxyAuth is needed. */
        boolean proxyAuth;

        /**
         * Construct a BackgroundInstaller.
         *
         * @param theParent parent installer of this object
         * @param theJadUrl where to get the JAD.
         * @param theName name of the MIDlet suite
         * @param theSuccessMessage message to display to user upon success
         * @param updateFlag if true the current suite should be
         *                      overwritten without asking the user.
         */
        private BackgroundInstaller(GraphicalInstaller theParent,
                String theJadUrl, String theName, String theSuccessMessage,
                boolean updateFlag) {
            parent = theParent;
            url = theJadUrl;
            name = theName;
            successMessage = theSuccessMessage;
            update = updateFlag;
        }

        /**
         * Run the installer.
         */
        public void run() {
            try {
                String lastInstalledMIDletName;

                if (jarOnly) {
                    lastInstalledMIDletName = 
                        parent.installer.installJar(url, false, false, this);
                } else {
                    lastInstalledMIDletName =  
                        parent.installer.installJad(url, false, false, this);
                }

                // Let the manager know what suite was installed
                GraphicalInstaller.saveSettings(null, lastInstalledMIDletName);

                parent.displaySuccessMessage(successMessage);

                /*
                 * We need to prevent "flashing" on fast development
                 * platforms.
                 */
                while ((System.currentTimeMillis() -
                        parent.lastDisplayChange) < parent.ALERT_TIMEOUT);

                parent.notifyDestroyed();
                return;
            } catch (Throwable ex) {
                String title;
                String msg;

                if (parent.installer != null &&
                        parent.installer.wasStopped()) {
                    displayListAfterCancelMessage();
                    return;
                }

                if (ex instanceof InvalidJadException) {
                    InvalidJadException ije = (InvalidJadException)ex;

                    if (ije.getReason() ==
                            InvalidJadException.INVALID_JAD_TYPE) {
                        // media type of JAD was wrong, it could be a JAR
                        String mediaType = (String)ije.getExtraData();

                        if (Installer.JAR_MT_1.equals(mediaType) ||
                                Installer.JAR_MT_2.equals(mediaType)) {
                            // re-run as a JAR only install
                            if (confirmJarOnlyDownload()) {
                                jarOnly = true;
                                installState = null;
                                run();
                                return;
                            }

                            displayListAfterCancelMessage();
                            return;
                        }
                    }

                    msg = translateJadException((InvalidJadException)ex,
                                                name, null, null, url);
                } else if (ex instanceof IOException) {
                    msg = Resource.getString(
                          "The connection dropped and the installation " +
                          "did not complete. Please try installing again.");
                } else {
                    msg = ex.getMessage();
                }

                title = Resource.getString("Install Error");
                if (parent.installListBox == null) {
                    // go back to the app list
                    displayException(title, msg); 
                    return;
                }

                Alert a = new Alert(title, msg, null, AlertType.ERROR);
                a.setTimeout(Alert.FOREVER);
                parent.display.setCurrent(a, parent.installListBox);
            } finally {
                if (parent.progressForm != null) {
                    // end the background thread of progress gauge.
                    Gauge progressGauge = (Gauge)parent.progressForm.get(
                                          parent.progressGaugeIndex);
                    progressGauge.setValue(Gauge.CONTINUOUS_IDLE);
                }
            }
        }

        /**
         * Called with the current state of the install so the user can be
         * asked to override the warning. Calls the parent to display the
         * warning to the user and then waits on the state object for
         * user's response.
         *
         * @param state current state of the install.
         *
         * @return true if the user wants to continue,
         *         false to stop the install
         */
        public boolean warnUser(InstallState state) {
            installState = state;

            InvalidJadException e = installState.getLastException();

            switch (e.getReason()) {
            case InvalidJadException.UNAUTHORIZED:
                proxyAuth = false;
                parent.getUsernameAndPassword();
                break;

            case InvalidJadException.PROXY_AUTH:
                proxyAuth = true;
                parent.getProxyUsernameAndPassword();
                break;

            case InvalidJadException.OLD_VERSION:
            case InvalidJadException.ALREADY_INSTALLED:
            case InvalidJadException.NEW_VERSION:
                // this is now an update
                update = true;

                // fall through
            default:
                parent.warnUser(name,
                    state.getAppProperty(Installer.VENDOR_PROP),
                    state.getAppProperty(Installer.VERSION_PROP),
                    url, e);
            }

            return waitForUser();
        }

        /**
         * Called with the current state of the install so the user can be
         * asked to confirm the jar download.
         * If false is returned, the an I/O exception thrown and
         * {@link Installer#wasStopped()} will return true if called.
         *
         * @param state current state of the install.
         *
         * @return true if the user wants to continue, false to stop the
         *         install
         */
        public boolean confirmJarDownload(InstallState state) {
            if (update) {
                // this an update, no need to confirm.
                return true;
            }

            installState = state;

            url = state.getJarUrl();

            parent.displayDownloadConfirmation(state);
            return waitForUser();
        }

        /**
         * Called with the current state of the install so the user can be
         * asked to confirm if the RMS data should be kept for new version of
         * an updated suite.
         *
         * @param state current state of the install.
         *
         * @return true if the user wants to keep the RMS data for the next
         * suite
         */
        public boolean keepRMS(InstallState state) {
            installState = state;

            parent.displayKeepRMSForm(state);
            return waitForUser();
        }

        /**
         * Called with the current state of the install so the user can be
         * asked to confirm the jar only download.
         *
         * @return true if the user wants to continue, false to stop the
         *         install
         */
        private boolean confirmJarOnlyDownload() {
            if (update) {
                // this an update, no need to confirm.
                return true;
            }

            parent.displayJarOnlyDownloadConfirmation();
            return waitForUser();
        }

        /**
         * Wait for the user to respond to current dialog.
         *
         * @return true if the user wants to continue, false to stop the
         *         install
         */
        private boolean waitForUser() {    
            boolean temp;

            synchronized (this) {
                try {
                    this.wait();
                } catch (InterruptedException ie) {
                    // ignore
                }
            }

            installState = null;

            temp = continueInstall;
            continueInstall = false;

            return temp;
        }

        /**
         * Called with the current status of the install.
         * Changes the status alert box text based on the status.
         *
         * @param status current status of the install.
         * @param state current state of the install.
         */
        public void updateStatus(int status, InstallState state) {
            parent.updateStatus(status, state);
        }

        /**
         * Wait for the cancel message to be displayed to prevent flashing
         * and then display the list of suites.
         */
        private void displayListAfterCancelMessage() {
            // wait for the parent to display "cancelled"
            synchronized (parent) {
                /*
                 * We need to prevent "flashing" on fast
                 * development platforms.
                 */
                while ((System.currentTimeMillis() -
                        parent.lastDisplayChange) < parent.ALERT_TIMEOUT);

                if (parent.installListBox == null) {
                    // go back to app list
                    parent.notifyDestroyed();
                    return;
                }

                parent.display.setCurrent(parent.installListBox);
            }
        }
    }
}