com.google.appinventor.client.youngandroid.CodeblocksManager.java Source code

Java tutorial

Introduction

Here is the source code for com.google.appinventor.client.youngandroid.CodeblocksManager.java

Source

// -*- mode: java; c-basic-offset: 2; -*-
// Copyright 2009-2011 Google, All Rights reserved
// Copyright 2011-2012 MIT, All rights reserved
// Released under the MIT License https://raw.github.com/mit-cml/app-inventor/master/mitlicense.txt

package com.google.appinventor.client.youngandroid;

import com.google.appinventor.client.DesignToolbar;
import com.google.appinventor.client.ErrorReporter;
import com.google.appinventor.client.Ode;
import static com.google.appinventor.client.Ode.MESSAGES;
import com.google.appinventor.client.OdeAsyncCallback;
import com.google.appinventor.client.editor.youngandroid.YaFormEditor;
import com.google.appinventor.client.jsonp.ConnectivityListener;
import com.google.appinventor.client.jsonp.JsonpConnection;
import com.google.appinventor.client.output.OdeLog;
import com.google.appinventor.client.properties.json.ClientJsonParser;
import com.google.appinventor.client.utils.Downloader;
import com.google.appinventor.client.utils.Urls;
import com.google.appinventor.common.youngandroid.YaHttpServerConstants;
import com.google.appinventor.shared.jsonp.JsonpConnectionInfo;
import com.google.appinventor.shared.rpc.ServerLayout;
import com.google.appinventor.shared.rpc.project.youngandroid.YoungAndroidFormNode;
import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwt.user.client.rpc.InvocationException;

import java.util.HashMap;
import java.util.Map;

/**
 * CodeblocksManager manages the client's connection to the Codeblocks JavaWebStart
 * process, and provides methods for communicating from the client to the
 * YaHttpServer (the HTTP server running with codeblocks)
 *
 * @author sharon@google.com (Sharon Perl)
 *
 */
@SuppressWarnings({ "ThrowableInstanceNeverThrown" })
public class CodeblocksManager implements ConnectivityListener {
    // Maintain only one Codeblocks process at a time.
    private CodeblocksConnection conn;
    private boolean unresponsiveConnection;

    private boolean startingCodeblocks;
    private boolean startCanceled;

    private final Map<Long, String> projectPaths = new HashMap<Long, String>();
    private YoungAndroidFormNode currentFormNode;
    private String currentProjectPath;

    private static final int CODEBLOCKS_CONNECTION_RETRY_DELAY_MILLIS = 5000;
    private static final long MAX_TIME_TO_CONNECT_MILLIS = 300000;

    /**
     * Returns the singleton codeblocks manager instance.
     *
     * @return  codeblocks manager instance
     */
    public static CodeblocksManager getCodeblocksManager() {
        return CodeblocksManagerInstanceHolder.INSTANCE;
    }

    private static class CodeblocksManagerInstanceHolder {
        private CodeblocksManagerInstanceHolder() {
        } // not to be instantiated

        private static final CodeblocksManager INSTANCE = new CodeblocksManager();
    }

    private CodeblocksManager() {
    }

    /**
     * Returns true if we can start codeblocks, false otherwise.
     */
    public boolean canStartCodeblocks() {
        // We can start codeblocks if there is a current project, we aren't already connected to
        // codeblocks, and we aren't already trying to start codeblocks.
        return Ode.getInstance().getCurrentYoungAndroidProjectId() != 0 && (conn == null || unresponsiveConnection)
                && !startingCodeblocks;
    }

    /**
     * Startup codeblocks. If codeblocks is already running, this method does
     * nothing.
     *
     * <p/>The work to start codeblocks and create a connection is done
     * asynchronously, and it may not have finished by the time the call returns.
     *
     * <p/>When codeblocks is up and we are connected, we will tell it to load the
     * source file corresponding to the current project/file at that time.
     */
    public void startCodeblocks() {
        if (canStartCodeblocks()) {
            if (unresponsiveConnection) {
                // We had a connection to codeblocks, but it is unresponsive.
                // Tell it to close (it might already be closed), and then abandon the
                // connection before starting up a new codeblocks instance.
                conn.removeConnectivityListener(this);
                conn.quit();
                conn = null;
                unresponsiveConnection = false;
            }

            startingCodeblocks = true;
            startCanceled = false;
            updateCodeblocksButton();

            // Before we download the jnlp file, clear the previously saved JSONP connection info.
            Ode.getInstance().getLaunchService().clearJsonpConnectionInfo(
                    YaHttpServerConstants.CODEBLOCKS_INFO_FILE_PREFIX, new AsyncCallback<Void>() {
                        @Override
                        public void onSuccess(Void result) {
                            if (startCanceled) {
                                startingCodeblocks = false;
                                updateCodeblocksButton();
                            } else {
                                downloadJnlpAndMakeConnection();
                            }
                        }

                        @Override
                        public void onFailure(Throwable caught) {
                            ErrorReporter.reportError(MESSAGES.startingCodeblocksFailed());
                            startingCodeblocks = false;
                            updateCodeblocksButton();
                        }
                    });
        }
    }

    private void downloadJnlpAndMakeConnection() {
        // fire up codeblocks via JavaWebStart. The download command goes from
        // the client to the ODE Server, which responds with the JNLP file
        // that causes the browser to start codeblocks via JavaWebStart.
        // We append a hash of the userid to the file name because we need the
        // jnlp file to be user-specific, while still be cacheable.
        String userId = Ode.getInstance().getUser().getUserId();
        Downloader.getInstance().download(ServerLayout.WEBSTART_JNLP_SERVLET_BASE
                + ServerLayout.WEBSTART_JNLP_PURPOSE_CODEBLOCKS + "/" + userId.hashCode());

        final long[] timeToStopTryingToConnect = { System.currentTimeMillis() + MAX_TIME_TO_CONNECT_MILLIS };

        // Get the connection info for the codeblocks HTTP server.
        Timer timer = new Timer() {
            @Override
            public void run() {
                Ode.getInstance().getLaunchService().retrieveJsonpConnectionInfo(
                        YaHttpServerConstants.CODEBLOCKS_INFO_FILE_PREFIX,
                        new AsyncCallback<JsonpConnectionInfo>() {
                            @Override
                            public void onSuccess(JsonpConnectionInfo connInfo) {
                                if (connInfo == null) {
                                    // The connection info has not been sent to the ODE server yet.
                                    boolean continueConnecting = true;
                                    if (System.currentTimeMillis() > timeToStopTryingToConnect[0]) {
                                        continueConnecting = askIfUserWantsToContinueTryingToConnect();
                                        if (continueConnecting) {
                                            timeToStopTryingToConnect[0] = System.currentTimeMillis()
                                                    + MAX_TIME_TO_CONNECT_MILLIS;
                                        }
                                    }
                                    // Try again, unless the user has canceled.
                                    if (startCanceled || !continueConnecting) {
                                        startingCodeblocks = false;
                                        updateCodeblocksButton();
                                    } else {
                                        schedule(CODEBLOCKS_CONNECTION_RETRY_DELAY_MILLIS); // schedule the timer
                                    }
                                } else {
                                    conn = new CodeblocksConnection(connInfo, new ClientJsonParser(),
                                            Urls.getEscapeQueryParameterFunction());
                                    conn.addConnectivityListener(CodeblocksManager.this);

                                    startingCodeblocks = false;
                                    updateCodeblocksButton();

                                    // The user may have switched projects/forms while codeblocks was starting.
                                    // We want the current project/form now.
                                    YaFormEditor formEditor = Ode.getInstance().getCurrentYoungAndroidFormEditor();
                                    if (formEditor != null) {
                                        YoungAndroidFormNode formNode = formEditor.getFormNode();
                                        loadPropertiesAndBlocks(formNode, null);
                                    }
                                }
                            }

                            @Override
                            public void onFailure(Throwable caught) {
                                ErrorReporter.reportError(MESSAGES.startingCodeblocksFailed());
                                startingCodeblocks = false;
                                updateCodeblocksButton();
                            }
                        });
            }
        };

        // Schedule the timer for 1 second to give some time for the download to occur.
        timer.schedule(CODEBLOCKS_CONNECTION_RETRY_DELAY_MILLIS);
    }

    private boolean askIfUserWantsToContinueTryingToConnect() {
        return Window.confirm(MESSAGES.continueTryingToConnect());
    }

    /**
     * Returns true if we can cancel codeblocks, false otherwise.
     */
    public boolean canCancelCodeblocks() {
        return startingCodeblocks && !startCanceled;
    }

    /**
     * Requests that we try to cancel the codeblocks launch.
     *
     * <p/>If the JNLP file for codeblocks has already been downloaded,
     * we may not be able to cancel.
     */
    public void cancelCodeblocks() {
        startCanceled = true;
        updateCodeblocksButton();
    }

    /*
     * Given a project id, retrieve and store the project path.
        
     * @param projectId the project id
     * @param callback a callback to call when the project path has been retrieved.
     */
    private void getProjectPath(final long projectId, final AsyncCallback<Void> callback) {

        // Get the project path from the ODE server.
        Ode.getInstance().getLaunchService().getWebStartProjectPath(projectId, new OdeAsyncCallback<String>() {
            @Override
            public void onSuccess(String projectPath) {
                /*
                 * Keep the project path for later use. We'd prefer not to get it multiple times both
                 * because it involves calls to the server and because we can get a different result each
                 * time (since the path is encrypted).
                 */
                projectPaths.put(projectId, projectPath);
                callback.onSuccess(null);
            }

            @Override
            public void onFailure(Throwable caught) {
                // Calling super.onFailure will cause the error to be reported.
                super.onFailure(caught);
                callback.onFailure(caught);
            }
        });
    }

    /*
     * Checks whether codeblocks is open and responsive, reports errors if
     * appropriate.
     * If this method returns true, the caller can proceed with sending a request
     * to codeblocks.
     * If this method returns false, the given callback will have been called and
     * the caller does not need to do so.
     *
     * @param codeblocksMustBeOpen true if codeblocks must be open
     * @param callback an optional callback to receive success or failure
     * @Return true if codeblocks is open and responsive, false otherwise
     */
    private boolean checkConnection(boolean codeblocksMustBeOpen, AsyncCallback<Void> callback) {
        if (conn == null) {
            if (codeblocksMustBeOpen) {
                // If codeblocks is not open, tell the user to open it.
                // The user can resolve the issue by opening
                // the codeblocks editor.
                ErrorReporter.reportInfo(MESSAGES.noCodeblocksConnection());
                if (callback != null) {
                    callback.onFailure(new InvocationException(MESSAGES.noCodeblocksConnection()));
                }
            } else {
                // It's ok if codeblocks is not open.
                if (callback != null) {
                    callback.onSuccess(null);
                }
            }
            return false;

        } else if (unresponsiveConnection) {
            // Codeblocks was open, but has been closed or is unresponsive.
            if (codeblocksMustBeOpen) {
                // If codeblocks has been closed or is unresponsive, report that.
                // The user can resolve this problem by restarting the
                // codeblocks editor.
                ErrorReporter.reportInfo(MESSAGES.codeblocksConnectionUnresponsive());
                if (callback != null) {
                    callback.onFailure(new InvocationException(MESSAGES.codeblocksConnectionUnresponsive()));
                }
            } else {
                // It's ok if codeblocks has been closed or is unresponsive.
                if (callback != null) {
                    callback.onSuccess(null);
                }
            }
            return false;
        }

        // Codeblocks is open and responsive.
        return true;
    }

    /**
     * Tell Codeblocks to reload the properties from the ODE server.
     *
     * @param callback an optional callback to pass along to the connection
     */
    public void reloadProperties(AsyncCallback<Void> callback) {
        if (checkConnection(false, callback)) {
            OdeLog.log("Telling Codeblocks to reload the properties");
            conn.reloadProperties(
                    createErrorReportingCallback(callback, MESSAGES.codeblocksFailedToReloadProperties()));
        }
    }

    /**
     * Tell Codeblocks to load the properties and blocks from the ODE server.
     *
     * @param formNode the YoungAndroidFormNode
     * @param callback an optional callback to pass along to the connection
     */
    public void loadPropertiesAndBlocks(final YoungAndroidFormNode formNode, final AsyncCallback<Void> callback) {
        if (checkConnection(false, callback)) {
            long projectId = formNode.getProjectId();
            final String projectPath = projectPaths.get(projectId);
            if (projectPath == null) {
                // We don't have the project path for the project.
                // Get the project path from the ODE server before telling codeblocks to load.
                getProjectPath(projectId, new AsyncCallback<Void>() {
                    @Override
                    public void onSuccess(Void result) {
                        loadPropertiesAndBlocks(formNode, callback);
                    }

                    @Override
                    public void onFailure(Throwable caught) {
                        // The error has already been reported in getProjectPath.
                        if (callback != null) {
                            callback.onFailure(caught);
                        }
                    }
                });

            } else {
                String formPath = projectPath + "/" + formNode.getFileId();
                String assetsPath = projectPath + "/" + YaHttpServerConstants.ASSETS_ZIPFILE;
                String projectName = Ode.getInstance().getProjectManager().getProject(projectId).getProjectName();
                OdeLog.log("Telling Codeblocks to load form:\n" + formPath);
                final AsyncCallback<Void> errorReportingCallback = createErrorReportingCallback(callback,
                        MESSAGES.codeblocksFailedToLoadPropertiesAndBlocks());
                conn.loadPropertiesAndBlocks(formPath, assetsPath, projectName, new AsyncCallback<Void>() {
                    @Override
                    public void onSuccess(Void result) {
                        currentFormNode = formNode;
                        currentProjectPath = projectPath;
                        errorReportingCallback.onSuccess(null);
                    }

                    @Override
                    public void onFailure(Throwable caught) {
                        errorReportingCallback.onFailure(caught);
                    }
                });
            }
        }
    }

    /**
     * Tell Codeblocks to generate its YAIL in order to show problems (like empty sockets) to the
     * user.
     */
    public void generateYail() {
        if (checkConnection(true, null)) {
            OdeLog.log("Telling Codeblocks to generate YAIL");
            conn.generateYail();
        }
    }

    /**
     * Tell Codeblocks to save codeblocks source.
     *
     * @param callback an optional callback to pass along to the connection
     */
    public void saveCodeblocksSource(AsyncCallback<Void> callback) {
        if (checkConnection(false, callback)) {
            OdeLog.log("Telling Codeblocks to save codeblocks source");
            conn.saveCodeblocksSource(
                    createErrorReportingCallback(callback, MESSAGES.codeblocksFailedToSaveBlocks()));
        }
    }

    /**
     * Tell Codeblocks to clear the workspace.
     *
     * @param callback an optional callback to pass along to the connection
     */
    public void clearCodeblocks(AsyncCallback<Void> callback) {
        if (checkConnection(false, callback)) {
            currentFormNode = null;
            OdeLog.log("Clearing blocks editor.");
            conn.clearCodeblocks(createErrorReportingCallback(callback, MESSAGES.clearCodeblocksError()));
        }
    }

    /**
     * Returns true if Codeblocks is open.
     */
    public boolean isCodeblocksOpen() {
        return conn != null;
    }

    /**
     * Returns true if Codeblocks is open and responsive.
     */
    public boolean isCodeblocksOpenAndResponsive() {
        return conn != null && !unresponsiveConnection;
    }

    /**
     * Returns the form node that is currently loaded in Codeblocks.
     */
    public YoungAndroidFormNode getCurrentFormNode() {
        return currentFormNode;
    }

    /**
     * Terminates codeblocks if it is running. Called from the
     * {@link com.google.appinventor.client.explorer.youngandroid.ProjectList}
     * when no projects are selected and when ODE is closing.
     */
    public void terminateCodeblocks() {
        if (conn != null) {
            conn.removeConnectivityListener(this);
            conn.quit();
            conn = null;
            unresponsiveConnection = false;
            updateCodeblocksButton();
        }
    }

    /**
     * Sync a single component property with codeblocks.
     *
     * @param componentName the name of the component
     * @param componentType the type of the component
     * @param propertyName the name of the property
     * @param propertyValue the value of the property
     * @param callback an optional callback to pass along to the connection
     */
    public void syncProperty(String componentName, String componentType, String propertyName, String propertyValue,
            AsyncCallback<Void> callback) {
        if (checkConnection(false, callback)) {
            OdeLog.log("Syncing property " + componentName + "." + propertyName);
            conn.syncProperty(componentName, componentType, propertyName, propertyValue,
                    createErrorReportingCallback(callback, MESSAGES.codeblocksFailedToSyncProperty()));
        }
    }

    /**
     * Tell codeblocks about a newly added asset in a project.
     *
     * @param assetFileId the file id of the asset
     * @param callback an optional callback to pass along to the connection
     *
     */
    public void addAsset(String assetFileId, AsyncCallback<Void> callback) {
        if (checkConnection(false, callback)) {
            String assetPath = currentProjectPath + "/" + assetFileId;
            OdeLog.log("Notifying codeblocks of asset: " + assetPath);
            conn.addAsset(assetPath, createErrorReportingCallback(callback, MESSAGES.codeblocksFailedToAddAsset()));
        }
    }

    /**
     * Tell codeblocks to install an application.
     *
     * @param apkFileId the file id of the apk
     * @param appName the application name
     * @param packageName the package name
     * @param callback an optional callback to pass along to the connection
     */
    public void installApplication(String apkFileId, String appName, String packageName,
            AsyncCallback<Void> callback) {
        if (checkConnection(true, callback)) {
            String apkPath = currentProjectPath + "/" + apkFileId;
            OdeLog.log("Telling Codeblocks to install application: " + appName + " with URL " + apkPath);
            conn.installApplication(apkPath, appName, packageName,
                    createErrorReportingCallback(callback, MESSAGES.codeblocksFailedToInstallApplication()));
        }
    }

    /**
     * Ask codeblocks whether a phone is connected.
     *
     * @param callback a callback to pass along to the connection
     */
    public void isPhoneConnected(final AsyncCallback<Boolean> callback) {
        AsyncCallback<Void> voidCallback = new AsyncCallback<Void>() {
            @Override
            public void onSuccess(Void result) {
                // This will never be called by checkConnection because we pass true to indicate that
                // codeblocks must be open.
            }

            @Override
            public void onFailure(Throwable caught) {
                callback.onFailure(caught);
            }
        };
        if (checkConnection(true, voidCallback)) {
            OdeLog.log("Checking if phone is connected.");
            conn.isPhoneConnected(
                    createErrorReportingBooleanCallback(callback, MESSAGES.codeblocksIsPhoneConnectedError()));
        }
    }

    private void updateCodeblocksButton() {
        DesignToolbar designToolbar = Ode.getInstance().getDesignToolbar();
        // If we are trying to start codeblocks, switch the open codeblocks button to a cancel button.
        // If codeblocks is open, change the button label to say that.
        designToolbar.updateCodeblocksButtonLabel(startingCodeblocks);
        // Enable/disable the button as appropriate.
        designToolbar.updateCodeblocksButton();
    }

    @Override
    public void onConnectivityStatusChange(JsonpConnection connection, boolean status) {
        if (!status) {
            // Codeblocks may have been terminated.
            OdeLog.wlog("Codeblocks is not responding.");
            unresponsiveConnection = true;
        } else {
            // Codeblocks is still alive!
            OdeLog.log("Codeblocks is still alive.");
            unresponsiveConnection = false;
        }
        updateCodeblocksButton();
    }

    /*
     * Creates an AsyncCallback<Void> that wraps the given optional callback.
     * If failure occurs, an appropriate error message will be reported prior to
     * calling onFailure.
     *
     * @param callback an optional callback to receive success or failure
     */
    private AsyncCallback<Void> createErrorReportingCallback(final AsyncCallback<Void> callback,
            final String errorMessage) {
        return new AsyncCallback<Void>() {
            @Override
            public void onSuccess(Void result) {
                if (callback != null) {
                    callback.onSuccess(null);
                }
            }

            @Override
            public void onFailure(Throwable caught) {
                ErrorReporter.reportError(errorMessage);
                if (callback != null) {
                    callback.onFailure(caught);
                }
            }
        };
    }

    /*
     * Creates an AsyncCallback<Boolean> that wraps the given optional callback.
     * If failure occurs, an appropriate error message will be reported prior to
     * calling onFailure.
     *
     * @param callback an optional callback to receive success or failure
     */
    private AsyncCallback<Boolean> createErrorReportingBooleanCallback(final AsyncCallback<Boolean> callback,
            final String errorMessage) {
        return new AsyncCallback<Boolean>() {
            @Override
            public void onSuccess(Boolean result) {
                if (callback != null) {
                    callback.onSuccess(result);
                }
            }

            @Override
            public void onFailure(Throwable caught) {
                ErrorReporter.reportError(errorMessage);
                if (callback != null) {
                    callback.onFailure(caught);
                }
            }
        };
    }
}