com.google.appinventor.client.explorer.youngandroid.GalleryPage.java Source code

Java tutorial

Introduction

Here is the source code for com.google.appinventor.client.explorer.youngandroid.GalleryPage.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 Apache License, Version 2.0
// http://www.apache.org/licenses/LICENSE-2.0

package com.google.appinventor.client.explorer.youngandroid;

import java.util.Date;
import java.util.List;

import com.google.appinventor.client.ErrorReporter;
import com.google.appinventor.client.GalleryClient;
import com.google.appinventor.client.GalleryGuiFactory;
import com.google.appinventor.client.GalleryRequestListener;
import com.google.appinventor.client.Ode;
import com.google.appinventor.client.OdeAsyncCallback;
import com.google.appinventor.client.OdeMessages;
import com.google.appinventor.client.explorer.project.Project;
import com.google.appinventor.client.output.OdeLog;
import com.google.appinventor.client.utils.Uploader;
import com.google.appinventor.client.wizards.youngandroid.RemixedYoungAndroidProjectWizard;
import com.google.appinventor.shared.rpc.ServerLayout;
import com.google.appinventor.shared.rpc.UploadResponse;
import com.google.appinventor.shared.rpc.project.GalleryApp;
import com.google.appinventor.shared.rpc.project.GalleryAppListResult;
import com.google.appinventor.shared.rpc.project.GalleryComment;
import com.google.appinventor.shared.rpc.project.UserProject;
import com.google.appinventor.shared.rpc.user.User;
import com.google.gwt.core.client.GWT;
import com.google.gwt.dom.client.InputElement;
import com.google.gwt.event.dom.client.ChangeEvent;
import com.google.gwt.event.dom.client.ChangeHandler;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.dom.client.ErrorEvent;
import com.google.gwt.event.dom.client.ErrorHandler;
import com.google.gwt.event.logical.shared.ValueChangeEvent;
import com.google.gwt.event.logical.shared.ValueChangeHandler;
import com.google.gwt.i18n.client.DateTimeFormat;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.ui.Anchor;
import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.FileUpload;
import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.FocusPanel;
import com.google.gwt.user.client.ui.HTML;
import com.google.gwt.user.client.ui.Image;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.Panel;
import com.google.gwt.user.client.ui.TabPanel;
import com.google.gwt.user.client.ui.TextArea;
import com.google.gwt.user.client.ui.TextBox;
import com.google.gwt.user.client.ui.VerticalPanel;

/**
 * The gallery page shows a single app from the gallery
 *
 * It has different modes for public viewing or when user is publishing for first time
 * or updating a previously published app
 *
 * @author wolberd@gmail.com (Dave Wolber)
 * @author vincentaths@gmail.com (Vincent Zhang)
 */
public class GalleryPage extends Composite implements GalleryRequestListener {
    public static final OdeMessages MESSAGES = GWT.create(OdeMessages.class);
    final Ode ode = Ode.getInstance();

    GalleryClient gallery = null;
    GalleryGuiFactory galleryGF = new GalleryGuiFactory();
    GalleryApp app = null;
    String projectName = null;
    Project project;

    private final String HOLLOW_HEART_ICON_URL = "/images/numLikeHollow.png";
    private final String RED_HEART_ICON_URL = "/images/numLike.png";
    private final String DOWNLOAD_ICON_URL = "/images/numDownload.png";
    private final String NUM_VIEW_ICON_URL = "/images/numView.png";
    private final String NUM_COMMENT_ICON_URL = "/images/numComment.png";
    private boolean imageUploaded = false;

    private VerticalPanel panel; // the main panel
    private FlowPanel galleryGUI;
    private FlowPanel appSingle;
    private FlowPanel appsByAuthor;
    private FlowPanel appsByTags;
    private FlowPanel appsRemixes;
    private FlowPanel appDetails;
    private FlowPanel appHeader;
    private FlowPanel appInfo;
    private FlowPanel appAction;
    private FlowPanel appAuthor;
    private FlowPanel appMeta;
    private FlowPanel appDates;
    private FlowPanel appPrimaryWrapper;
    private FlowPanel appSecondaryWrapper;
    private TabPanel appActionTabs;
    private TabPanel sidebarTabs;
    private FlowPanel appDescPanel;
    private FlowPanel appReportPanel;
    private FlowPanel appSharePanel;
    private FlowPanel appComments;
    private FlowPanel appCommentsList;
    private FlowPanel returnToGallery;
    //private String tagSelected;

    public static final int VIEWAPP = 0;
    public static final int NEWAPP = 1;
    public static final int UPDATEAPP = 2;
    private int editStatus;
    private static final int MIN_DESC_LENGTH = 40;

    /* Publish & edit state components */
    private FlowPanel imageUploadBox;
    private Label imageUploadPrompt;
    private Image image;
    private FileUpload upload;
    private FlowPanel imageUploadBoxInner;
    private FocusPanel wrapper;
    private Label appCreated;
    private Label appChanged;
    private TextArea titleText;
    private TextArea desc;
    private TextArea moreInfoText;
    private TextArea creditText;
    private FlowPanel descBox;
    private FlowPanel titleBox;
    private Label likeCount;
    private Button actionButton;
    private Button removeButton;
    private Button editButton;
    private Button cancelButton;

    private HTML ccLicenseRef;

    /* Here is the organization of this page:
    panel
     galleryGUI
      appSingle
       appDetails
        appClear
          appHeader
    wrapper (focus panel)
     imageUploadBox (flow)
      imageUploadBoxInner (flow)
       imageUploadPRompt (label)
       upload
       image (this is put in dynamically)
    appAction (button)
         appInfo
           title or titlebox
           devName
           appMeta
           appDates
           desc/descbox
        appComments
       appsByDev
        
      divider
    */

    /**
     * Creates a new GalleryPage, must take in parameters
     * @param app GalleryApp
     * @param editStatus edit status
     */
    public GalleryPage(final GalleryApp app, final int editStatus) {
        // Get a reference to the Gallery Client which handles the communication to
        // server to get gallery data
        gallery = GalleryClient.getInstance();
        gallery.addListener(this);

        // We are either publishing a new app, updating, or just reading. If we are publishing
        //   a new app, app has some partial info to be published. Otherwise, it has all
        //   the info for the already published app
        this.app = app;
        this.editStatus = editStatus;
        initComponents();

        // App header - image
        appHeader.addStyleName("app-header");
        // If we're editing or updating, add input form for image
        if (newOrUpdateApp()) {
            initImageComponents();
        } else { // we are just viewing this page so setup the image
            initReadOnlyImage();
        }

        // Now let's add the button for publishing, updating, or trying
        appHeader.add(appAction);
        initActionButton();
        if (editStatus == NEWAPP) {
            initCancelButton();
            /* Add Creative Commons Publishing Reference */
            appAction.add(ccLicenseRef);
        }
        if (editStatus == UPDATEAPP) {
            initRemoveButton();
            initCancelButton();
            /* Add Creative Commons Updating Reference */
            appAction.add(ccLicenseRef);
        }

        // App details - app title
        appInfo.add(titleBox);
        initAppTitle(titleBox);

        // App details - app author info
        appInfo.add(appAuthor);
        initAppAuthor(appAuthor);

        // Not showing in new app becaus it doesn't have these info
        // App details - meta
        if (!newOrUpdateApp()) {
            appInfo.add(appMeta);
            initAppStats(appMeta);
        }

        // App details - dates
        appInfo.add(appDates);
        initAppMeta(appDates);

        // App details - app description
        appInfo.add(descBox);
        initAppDesc(descBox, appDescPanel);
        /**
         * TODO: I may need to change the code logic here. appDescPanel is actually
         * not added to [appInfo], instead in public state appDescPanel will be
         * added into the [appActionTabs] (not showing in editable states) as a sub
         * tab. So it may not be the best idea to modify appDescPanel in a method
         * that resides in [appInfo]'s code block. - Vincent, 03/28/2014
         */

        // Pass app components to App Detail container
        appInfo.addStyleName("app-info-container");
        appPrimaryWrapper.add(appHeader);
        appPrimaryWrapper.add(appInfo);
        appPrimaryWrapper.addStyleName("clearfix");
        appDetails.add(appPrimaryWrapper);

        // If app is in its public state, add action tabs
        if (!newOrUpdateApp()) {
            // Add a divider
            HTML dividerPrimary = new HTML("<div class='section-divider'></div>");
            appDetails.add(dividerPrimary);
            // Initialize action tabs
            initActionTabs();
            // Initialize app share
            initAppShare();
            // Initialize app action features
            initReportSection();

            // We are not showing comments at initial launch, Such sadness :'[
            /*
            HTML dividerSecondary = new HTML("<div class='section-divider'></div>");
            appDetails.add(dividerSecondary);
            initAppComments();
            */

            // Add sidebar stuff, only in public state
            // By default, load the first tag's apps
            gallery.GetAppsByDeveloper(0, 5, app.getDeveloperId());
        }

        // Add to appSingle
        appSingle.add(appDetails);
        appDetails.addStyleName("gallery-container");
        appDetails.addStyleName("gallery-app-details");

        if (!newOrUpdateApp()) {
            appSingle.add(sidebarTabs);
            sidebarTabs.addStyleName("gallery-container");
            sidebarTabs.addStyleName("gallery-app-showcase");
        }

        // Add everything to top-level containers
        galleryGUI.add(appSingle);
        appSingle.addStyleName("gallery-app-single");
        panel.add(galleryGUI);
        galleryGUI.addStyleName("gallery");
        initWidget(panel);
    }

    /**
    * Helper method called by constructor to initialize ui components
    */
    private void initComponents() {
        // Initialize UI
        panel = new VerticalPanel();
        panel.setWidth("100%");
        galleryGUI = new FlowPanel();
        appSingle = new FlowPanel();
        appDetails = new FlowPanel();
        appHeader = new FlowPanel();
        appInfo = new FlowPanel();
        appAction = new FlowPanel();
        appAuthor = new FlowPanel();
        appMeta = new FlowPanel();
        appDates = new FlowPanel();
        appPrimaryWrapper = new FlowPanel();
        appSecondaryWrapper = new FlowPanel();
        appDescPanel = new FlowPanel();
        appReportPanel = new FlowPanel();
        appSharePanel = new FlowPanel();
        appActionTabs = new TabPanel();
        sidebarTabs = new TabPanel();
        appComments = new FlowPanel();
        appCommentsList = new FlowPanel();
        appsByAuthor = new FlowPanel();
        appsByTags = new FlowPanel();
        appsRemixes = new FlowPanel();
        returnToGallery = new FlowPanel();
        //    tagSelected = "";

        appCreated = new Label();
        appChanged = new Label();
        descBox = new FlowPanel();
        titleBox = new FlowPanel();
        desc = new TextArea();
        titleText = new TextArea();
        moreInfoText = new TextArea();
        creditText = new TextArea();
        ccLicenseRef = new HTML(MESSAGES.galleryCcLicenseRef());
        ccLicenseRef.addStyleName("app-action-html");
    }

    /**
     * Helper method to check if the app is in its "editable" state
     * If the app is in its "public" state it should return false
     */
    private boolean newOrUpdateApp() {
        if ((editStatus == NEWAPP) || (editStatus == UPDATEAPP))
            return true;
        else
            return false;
    }

    /**
     * Helper method called by constructor to initialize image upload components
     */
    private void initImageComponents() {
        imageUploadBox = new FlowPanel();
        imageUploadBox.addStyleName("app-image-uploadbox");
        imageUploadBox.addStyleName("gallery-editbox");
        imageUploadBoxInner = new FlowPanel();
        imageUploadPrompt = new Label("Upload your project image!");
        imageUploadPrompt.addStyleName("gallery-editprompt");

        updateAppImage(gallery.getCloudImageURL(app.getGalleryAppId()), imageUploadBoxInner);
        image.addStyleName("status-updating");
        imageUploadPrompt.addStyleName("app-image-uploadprompt");
        imageUploadBoxInner.add(imageUploadPrompt);

        upload = new FileUpload();
        upload.addStyleName("app-image-upload");
        upload.getElement().setAttribute("accept", "image/*");
        // Set the correct handler for servlet side capture
        upload.setName(ServerLayout.UPLOAD_FILE_FORM_ELEMENT);
        upload.addChangeHandler(new ChangeHandler() {
            public void onChange(ChangeEvent event) {
                uploadImage();
            }
        });
        imageUploadBoxInner.add(upload);
        imageUploadBox.add(imageUploadBoxInner);
        wrapper = new FocusPanel();
        wrapper.add(imageUploadBox);
        wrapper.addClickHandler(new ClickHandler() {
            @Override
            public void onClick(ClickEvent event) {
                // The correct way to trigger click event on FileUpload
                upload.getElement().<InputElement>cast().click();
            }
        });
        appHeader.add(wrapper);
    }

    /**
     * Helper method called by constructor to create the app image for display
     */
    private void initReadOnlyImage() {
        updateAppImage(gallery.getCloudImageURL(app.getGalleryAppId()), appHeader);
    }

    /**
     * Main method to validify and upload the app image
     */
    private void uploadImage() {
        String uploadFilename = upload.getFilename();
        if (!uploadFilename.isEmpty()) {
            // Grab and validify the filename
            final String filename = makeValidFilename(uploadFilename);
            // Forge the request URL for gallery servlet
            // we used to send the gallery id to the servlet, now the project id as
            // the servlet just stores image temporarily before publish
            /* String uploadUrl = GWT.getModuleBaseURL() + ServerLayout.GALLERY_SERVLET +
                "/apps/" + String.valueOf(app.getGalleryAppId()) + "/"+ filename; */
            // send the project id as the id, to store image temporarily until published
            String uploadUrl = GWT.getModuleBaseURL() + ServerLayout.GALLERY_SERVLET + "/apps/"
                    + String.valueOf(app.getProjectId()) + "/" + filename;
            Uploader.getInstance().upload(upload, uploadUrl,
                    new OdeAsyncCallback<UploadResponse>(MESSAGES.fileUploadError()) {
                        @Override
                        public void onSuccess(UploadResponse uploadResponse) {
                            switch (uploadResponse.getStatus()) {
                            case SUCCESS:
                                // Update the app image preview after a success upload
                                imageUploadBoxInner.clear();
                                // updateAppImage(app.getCloudImageURL(), imageUploadBoxInner);
                                updateAppImage(gallery.getProjectImageURL(app.getProjectId()), imageUploadBoxInner);
                                imageUploaded = true;
                                ErrorReporter.hide();
                                break;
                            case FILE_TOO_LARGE:
                                // The user can resolve the problem by uploading a smaller file.
                                ErrorReporter.reportInfo(MESSAGES.fileTooLargeError());
                                break;
                            default:
                                ErrorReporter.reportError(MESSAGES.fileUploadError());
                                break;
                            }
                        }
                    });
        } else {
            if (editStatus == NEWAPP) {
                Window.alert(MESSAGES.noFileSelected());
            }
        }
    }

    /**
     * Helper method to validify file name, used in uploadImage()
     * @param uploadFilename  The full filename of the file
     */
    private String makeValidFilename(String uploadFilename) {
        // Strip leading path off filename.
        // We need to support both Unix ('/') and Windows ('\\') separators.
        String filename = uploadFilename
                .substring(Math.max(uploadFilename.lastIndexOf('/'), uploadFilename.lastIndexOf('\\')) + 1);
        // We need to strip out whitespace from the filename.
        filename = filename.replaceAll("\\s", "");
        return filename;
    }

    /**
     * Helper method to update the app image
     * @param url  The URL of the image to show
     * @param container  The container that image widget resides
     */
    private void updateAppImage(String url, final Panel container) {
        image = new Image();
        image.addStyleName("app-image");
        image.setUrl(url);
        // if the user has provided a gallery app image, we'll load it. But if not
        // the error will occur and we'll load default image
        image.addErrorHandler(new ErrorHandler() {
            public void onError(ErrorEvent event) {
                image.setUrl(GalleryApp.DEFAULTGALLERYIMAGE);
            }
        });
        container.add(image);

        if (gallery.getSystemEnvironment() != null
                && gallery.getSystemEnvironment().toString().equals("Development")) {
            final OdeAsyncCallback<String> callback = new OdeAsyncCallback<String>(
                    // failure message
                    MESSAGES.galleryError()) {
                @Override
                public void onSuccess(String newUrl) {
                    image.setUrl(newUrl + "?" + System.currentTimeMillis());
                }
            };
            Ode.getInstance().getGalleryService().getBlobServingUrl(url, callback);
        }
    }

    /**
     * Helper method called by constructor to create the app's main action button
     */
    private void initActionButton() {
        if (editStatus == NEWAPP)
            initPublishButton();
        else if (editStatus == UPDATEAPP)
            initUpdateButton();
        else { // Public view state
            initTryitButton();
            initEdititButton();
        }
    }

    /**
     * Helper method called by constructor to initialize the app's title section
     * @param container   The container that title resides
     */
    private void initAppTitle(Panel container) {
        if (newOrUpdateApp()) {
            // GUI for editable title container
            if (editStatus == NEWAPP) {
                // If it's new app, give a textual hint telling user this is title
                titleText.setText(app.getTitle());
            } else if (editStatus == UPDATEAPP) {
                // If it's not new, just set whatever's in the data field already
                titleText.setText(app.getTitle());
            }
            titleText.addValueChangeHandler(new ValueChangeHandler<String>() {
                @Override
                public void onValueChange(ValueChangeEvent<String> event) {
                    app.setTitle(titleText.getText());
                }
            });
            titleText.addStyleName("app-desc-textarea");
            container.add(titleText);
            container.addStyleName("app-title-container");
        } else {
            Label title = new Label(app.getTitle());
            title.addStyleName("app-title");
            container.add(title);
        }
    }

    /**
     * Helper method called by constructor to initialize the author's info
     * @param container   The container that author's info resides
     */
    private void initAppAuthor(Panel container) {

        // Add author's image - not when creating a new app
        if (editStatus != NEWAPP) {
            final Image authorAvatar = new Image();
            authorAvatar.addStyleName("app-userimage");
            authorAvatar.setUrl(gallery.getUserImageURL(app.getDeveloperId()));
            // If the user has provided a gallery app image, we'll load it. But if not
            // the error will occur and we'll load default image
            authorAvatar.addErrorHandler(new ErrorHandler() {
                public void onError(ErrorEvent event) {
                    authorAvatar.setUrl(GalleryApp.DEFAULTUSERIMAGE);
                }
            });
            authorAvatar.addClickHandler(new ClickHandler() {
                public void onClick(ClickEvent event) {
                    Ode.getInstance().switchToUserProfileView(app.getDeveloperId(), 1 /* 1 for public view */ );
                }
            });
            appInfo.add(authorAvatar);
        }

        // Add author's name
        final Label authorName = new Label();
        if (editStatus == NEWAPP) {
            // App doesn't have author info yet, grab current user info
            final User currentUser = Ode.getInstance().getUser();
            authorName.setText(currentUser.getUserName());
        } else {
            authorName.setText(app.getDeveloperName());
            authorName.addClickHandler(new ClickHandler() {
                public void onClick(ClickEvent event) {
                    Ode.getInstance().switchToUserProfileView(app.getDeveloperId(), 1 /* 1 for public view*/ );
                }
            });
        }
        authorName.addStyleName("app-username");
        authorName.addStyleName("app-subtitle");
        appInfo.add(authorName);
    }

    /**
     * Helper method called by constructor to initialize the app's stats fields
     * @param container   The container that stats fields reside
     */
    private void initAppStats(Panel container) {
        // Images for stats data
        Image numDownloads = new Image();
        numDownloads.setUrl(DOWNLOAD_ICON_URL);
        Image numLikes = new Image();
        numLikes.setUrl(HOLLOW_HEART_ICON_URL);

        // Add stats data
        container.addStyleName("app-stats");
        container.add(numDownloads);
        container.add(new Label(Integer.toString(app.getDownloads())));
        // Adds dynamic like
        initLikeSection(container);
        // Adds dynamic feature
        initFeatureSection(container);
        // Adds dynamic tutorial
        initTutorialSection(container);
        // Adds dynamic salvage
        initSalvageSection(container);

        // We are not using views and comments at initial launch
        /*
        Image numViews = new Image();
        numViews.setUrl(NUM_VIEW_ICON_URL);
        Image numComments = new Image();
        numComments.setUrl(NUM_COMMENT_ICON_URL);
        container.add(numViews);
        container.add(new Label(Integer.toString(app.getViews())));
        container.add(numComments);
        container.add(new Label(Integer.toString(app.getComments())));
        */
    }

    /**
     * Helper method called by constructor to initialize the app's meta fields
     * @param container   The container that date fields reside
     */
    private void initAppMeta(Panel container) {
        Date createdDate = new Date();
        Date changedDate = new Date();
        if (editStatus == NEWAPP) {
        } else {
            createdDate = new Date(app.getCreationDate());
            changedDate = new Date(app.getUpdateDate());
        }
        DateTimeFormat dateFormat = DateTimeFormat.getFormat("yyyy/MM/dd");

        Label appCreatedLabel = new Label(MESSAGES.galleryCreatedDateLabel());
        appCreatedLabel.addStyleName("app-meta-label");
        container.add(appCreatedLabel);
        appCreated.setText(dateFormat.format(createdDate));
        container.add(appCreated);

        Label appChangedLabel = new Label(MESSAGES.galleryChangedDateLabel());
        appChangedLabel.addStyleName("app-meta-label");
        container.add(appChangedLabel);
        appChanged.setText(dateFormat.format(changedDate));
        container.add(appChanged);

        if (newOrUpdateApp()) {
            // GUI for editable title container
            // Set the placeholders of textarea
            moreInfoText.getElement().setPropertyString("placeholder", MESSAGES.galleryMoreInfoHint());
            creditText.getElement().setPropertyString("placeholder", MESSAGES.galleryCreditHint());

            if (editStatus == NEWAPP) {
                // If it's a new app, it will show the placeholder hint
            } else if (editStatus == UPDATEAPP) {
                // If it's not new, just set whatever's in the data field already
                moreInfoText.setText(app.getMoreInfo());
                creditText.setText(app.getCredit());
            }

            moreInfoText.addValueChangeHandler(new ValueChangeHandler<String>() {
                @Override
                public void onValueChange(ValueChangeEvent<String> event) {
                    app.setMoreInfo(moreInfoText.getText());
                }
            });
            creditText.addValueChangeHandler(new ValueChangeHandler<String>() {
                @Override
                public void onValueChange(ValueChangeEvent<String> event) {
                    app.setCredit(creditText.getText());
                }
            });

            moreInfoText.addStyleName("app-desc-textarea");
            creditText.addStyleName("app-desc-textarea");
            container.add(moreInfoText);
            container.add(creditText);

        } else { // Public app view
            String linktext = makeValidLink(app.getMoreInfo());
            if (linktext != null) {
                Label moreInfoLabel = new Label(MESSAGES.galleryMoreInfoLabel());
                moreInfoLabel.addStyleName("app-meta-label");
                container.add(moreInfoLabel);

                Anchor userLinkDisplay = new Anchor();
                userLinkDisplay.setText(linktext);
                userLinkDisplay.setHref(linktext);
                userLinkDisplay.setTarget("_blank");
                container.add(userLinkDisplay);
            }
            //"remixed from" field
            container.add(initRemixFromButton());

            //"credits" field
            if (app.getCredit() != null && app.getCredit().length() > 0) {
                Label creditLabel = new Label(MESSAGES.galleryCreditLabel());
                creditLabel.addStyleName("app-meta-label");
                container.add(creditLabel);

                Label creditText = new Label(app.getCredit());
                container.add(creditText);
            }
        }

        container.addStyleName("app-meta");
    }

    /**
     * Helper method to validify a hyperlink
     * @param linktext    the actual http link that the anchor should point to
     * @return linktext a valid http link or null.
     */
    private String makeValidLink(String linktext) {
        if (linktext == null) {
            return null;
        } else {
            if (linktext.isEmpty()) {
                return null;
            } else {
                // Validate link format, fill in http part
                if (!linktext.toLowerCase().startsWith("http")) {
                    linktext = "http://" + linktext;
                }
                return linktext;
            }
        }
    }

    /**
     * Helper method called by constructor to initialize the app's description
     * @param c1   The container that description resides (editable state)
     * @param c2   The container that description resides (public state)
     */
    private void initAppDesc(Panel c1, Panel c2) {
        desc.getElement().setPropertyString("placeholder", MESSAGES.galleryDescriptionHint());
        if (newOrUpdateApp()) {
            desc.addValueChangeHandler(new ValueChangeHandler<String>() {
                @Override
                public void onValueChange(ValueChangeEvent<String> event) {
                    app.setDescription(desc.getText());
                }
            });
            if (editStatus == UPDATEAPP) {
                desc.setText(app.getDescription());
            }
            desc.addStyleName("app-desc-textarea");
            c1.add(desc);
        } else {
            Label description = new Label(app.getDescription());
            c2.add(description);
            c2.addStyleName("app-description");
        }
    }

    /**
     * Helper method called by constructor to initialize the app's comment area
     */
    private void initAppComments() {
        // App details - comments
        appDetails.add(appComments);
        appComments.addStyleName("app-comments-wrapper");
        Label commentsHeader = new Label("Comments and Reviews");
        commentsHeader.addStyleName("app-comments-header");
        appComments.add(commentsHeader);
        final TextArea commentTextArea = new TextArea();
        commentTextArea.addStyleName("app-comments-textarea");
        appComments.add(commentTextArea);
        Button commentSubmit = new Button("Submit my comment");
        commentSubmit.addStyleName("app-comments-submit");
        commentSubmit.addClickHandler(new ClickHandler() {
            @Override
            public void onClick(ClickEvent event) {
                final OdeAsyncCallback<Long> commentPublishCallback = new OdeAsyncCallback<Long>(
                        // failure message
                        MESSAGES.galleryError()) {
                    @Override
                    public void onSuccess(Long date) {
                        // get the new comment list so gui updates
                        //   note: we might modify the call to publishComment so it returns
                        //   the list instead, this would save one server call
                        gallery.GetComments(app.getGalleryAppId(), 0, 100);
                    }
                };
                Ode.getInstance().getGalleryService().publishComment(app.getGalleryAppId(),
                        commentTextArea.getText(), commentPublishCallback);
            }
        });
        appComments.add(commentSubmit);

        // Add list of comments
        gallery.GetComments(app.getGalleryAppId(), 0, 100);
        appComments.add(appCommentsList);
        appCommentsList.addStyleName("app-comments");

    }

    /**
     * Helper method called by constructor to initialize the app action tabs
     */
    private void initActionTabs() {
        // Add a bunch of tabs for executable actions regarding the app
        appSecondaryWrapper.addStyleName("clearfix");
        appSecondaryWrapper.add(appActionTabs);
        appActionTabs.addStyleName("app-actions");
        appActionTabs.add(appDescPanel, "Description");
        appActionTabs.add(appSharePanel, "Share");
        appActionTabs.add(appReportPanel, "Report");
        appActionTabs.selectTab(0);
        appActionTabs.addStyleName("app-actions-tabs");
        appDetails.add(appSecondaryWrapper);
        // Return to Gallery link
        Label returnLabel = new Label("Back to Gallery");
        returnLabel.addClickHandler(new ClickHandler() {
            @Override
            public void onClick(ClickEvent clickEvent) {
                ode.switchToGalleryView();
            }
        });
        returnToGallery.add(returnLabel);
        returnToGallery.addStyleName("gallery-nav-return");
        returnToGallery.addStyleName("primary-link");
        appSecondaryWrapper.add(returnToGallery); //
    }

    /**
     * Helper method called by constructor to initialize the remix button
     */
    private FlowPanel initRemixFromButton() {
        FlowPanel container = new FlowPanel();
        final Label remixedFrom = new Label(MESSAGES.galleryRemixedFrom());
        remixedFrom.addStyleName("app-meta-label");
        final Label parentApp = new Label();
        //gwt-Label use fixed width which will case border-underline-dot
        //be longer than text link.
        //gwt-Label-auto use auto width
        parentApp.removeStyleName("gwt-Label");
        parentApp.addStyleName("gwt-Label-auto");
        parentApp.addStyleName("primary-link");
        container.add(remixedFrom);
        container.add(parentApp);
        remixedFrom.setVisible(false);
        parentApp.setVisible(false);

        final Result<GalleryApp> attributionGalleryApp = new Result<GalleryApp>();
        final OdeAsyncCallback<Long> remixedFromCallback = new OdeAsyncCallback<Long>(
                // failure message
                MESSAGES.galleryError()) {
            @Override
            public void onSuccess(final Long attributionId) {
                if (attributionId != UserProject.FROMSCRATCH) {
                    remixedFrom.setVisible(true);
                    parentApp.setVisible(true);
                    final OdeAsyncCallback<GalleryApp> callback = new OdeAsyncCallback<GalleryApp>(
                            // failure message
                            MESSAGES.galleryError()) {
                        @Override
                        public void onSuccess(GalleryApp AppRemixedFrom) {
                            parentApp.setText(AppRemixedFrom.getTitle());
                            attributionGalleryApp.t = AppRemixedFrom;
                        }
                    };
                    Ode.getInstance().getGalleryService().getApp(attributionId, callback);
                } else {
                    attributionGalleryApp.t = null;
                }
            }
        };
        Ode.getInstance().getGalleryService().remixedFrom(app.getGalleryAppId(), remixedFromCallback);

        parentApp.addClickHandler(new ClickHandler() {
            public void onClick(ClickEvent event) {
                if (attributionGalleryApp.t == null) {
                } else {
                    Ode.getInstance().switchToGalleryAppView(attributionGalleryApp.t, GalleryPage.VIEWAPP);
                }
            }
        });

        final OdeAsyncCallback<List<GalleryApp>> callback = new OdeAsyncCallback<List<GalleryApp>>(
                // failure message
                MESSAGES.galleryError()) {
            @Override
            public void onSuccess(final List<GalleryApp> apps) {
                if (apps.size() != 0) {
                    // Display remixes at the sidebar on the same page
                    galleryGF.generateSidebar(apps, sidebarTabs, appsRemixes, "Remixes",
                            MESSAGES.galleryAppsRemixesSidebar() + app.getTitle(), false, false);
                }
            }
        };
        Ode.getInstance().getGalleryService().remixedTo(app.getGalleryAppId(), callback);

        return container;
    }

    /**
     * Helper method called by constructor to initialize the report section
     */
    private void initReportSection() {
        final HTML reportPrompt = new HTML();
        reportPrompt.setHTML(MESSAGES.galleryReportPrompt());
        reportPrompt.addStyleName("primary-prompt");
        final TextArea reportText = new TextArea();
        reportText.addStyleName("action-textarea");
        final Button submitReport = new Button(MESSAGES.galleryReportButton());
        submitReport.addStyleName("action-button");
        appReportPanel.add(reportPrompt);
        appReportPanel.add(reportText);
        appReportPanel.add(submitReport);

        final OdeAsyncCallback<Boolean> isReportdByUserCallback = new OdeAsyncCallback<Boolean>(
                // failure message
                MESSAGES.galleryError()) {
            @Override
            public void onSuccess(Boolean isAlreadyReported) {
                if (isAlreadyReported) { //already reported, cannot report again
                    reportPrompt.setHTML(MESSAGES.galleryAlreadyReportedPrompt());
                    reportText.setVisible(false);
                    submitReport.setVisible(false);
                    submitReport.setEnabled(false);
                } else {
                    submitReport.addClickHandler(new ClickHandler() {
                        public void onClick(ClickEvent event) {
                            final OdeAsyncCallback<Long> reportClickCallback = new OdeAsyncCallback<Long>(
                                    // failure message
                                    MESSAGES.galleryError()) {
                                @Override
                                public void onSuccess(Long id) {
                                    reportPrompt.setHTML(MESSAGES.galleryReportCompletionPrompt());
                                    reportText.setVisible(false);
                                    submitReport.setVisible(false);
                                    submitReport.setEnabled(false);
                                }
                            };
                            Ode.getInstance().getGalleryService().addAppReport(app, reportText.getText(),
                                    reportClickCallback);
                        }
                    });
                }
            }
        };
        Ode.getInstance().getGalleryService().isReportedByUser(app.getGalleryAppId(), isReportdByUserCallback);
    }

    /**
     * Helper method called by constructor to initialize the report section
     */
    private void initAppShare() {
        final HTML sharePrompt = new HTML();
        sharePrompt.setHTML(MESSAGES.gallerySharePrompt());
        sharePrompt.addStyleName("primary-prompt");
        final TextBox urlText = new TextBox();
        urlText.addStyleName("action-textbox");
        urlText.setText(Window.Location.getHost() + MESSAGES.galleryGalleryIdAction() + app.getGalleryAppId());
        urlText.addClickHandler(new ClickHandler() {
            @Override
            public void onClick(ClickEvent event) {
                urlText.selectAll();
            }
        });
        appSharePanel.add(sharePrompt);
        appSharePanel.add(urlText);
    }

    /**
     * Helper method called by constructor to initialize the like section
     * @param container   The container that like label & image reside
     */
    private void initLikeSection(Panel container) { //TODO: Update the location of this button
        final Image likeButton = new Image();
        likeButton.setUrl(HOLLOW_HEART_ICON_URL);
        container.add(likeButton);
        likeCount = new Label(MESSAGES.galleryEmptyText());
        container.add(likeCount);
        final Label likePrompt = new Label(MESSAGES.galleryEmptyText());
        likePrompt.addStyleName("primary-link");
        container.add(likePrompt);
        likePrompt.addClickHandler(new ClickHandler() {
            public void onClick(ClickEvent event) {
                final OdeAsyncCallback<Integer> changeLikeCallback = new OdeAsyncCallback<Integer>(
                        // failure message
                        MESSAGES.galleryError()) {
                    @Override
                    public void onSuccess(Integer num) {
                        // TODO: deal with/discuss server data sync later; now is updating locally.
                        final OdeAsyncCallback<Boolean> checkCallback = new OdeAsyncCallback<Boolean>(
                                MESSAGES.galleryError()) {
                            @Override
                            public void onSuccess(Boolean b) {
                                //email will be send automatically if condition matches (in ObjectifyGalleryStorageIo)
                            }
                        };
                        Ode.getInstance().getGalleryService().checkIfSendAppStats(app.getDeveloperId(),
                                app.getGalleryAppId(), gallery.getGallerySettings().getAdminEmail(),
                                Window.Location.getHost(), checkCallback);
                    }
                };
                final OdeAsyncCallback<Boolean> isLikedByUserCallback = new OdeAsyncCallback<Boolean>(
                        // failure message
                        MESSAGES.galleryError()) {
                    @Override
                    public void onSuccess(Boolean bool) {
                        if (bool) { // If the app is already liked before, and user clicks again, that means unlike
                            Ode.getInstance().getGalleryService().decreaseLikes(app.getGalleryAppId(),
                                    changeLikeCallback);
                            likePrompt.setText(MESSAGES.galleryAppsLike());
                            // Old code
                            likeCount.setText(String.valueOf(Integer.valueOf(likeCount.getText()) - 1));
                            likeButton.setUrl(HOLLOW_HEART_ICON_URL); // Unliked
                        } else {
                            // If the app is not yet liked, and user clicks like, that means add a like
                            Ode.getInstance().getGalleryService().increaseLikes(app.getGalleryAppId(),
                                    changeLikeCallback);
                            likePrompt.setText(MESSAGES.galleryAppsAlreadyLike());
                            // Old code
                            likeCount.setText(String.valueOf(Integer.valueOf(likeCount.getText()) + 1));
                            likeButton.setUrl(RED_HEART_ICON_URL); // Liked
                        }
                    }
                };
                Ode.getInstance().getGalleryService().isLikedByUser(app.getGalleryAppId(), isLikedByUserCallback); // This happens when user click on like, we need to check if it's already liked
            }
        });

        final OdeAsyncCallback<Integer> likeNumCallback = new OdeAsyncCallback<Integer>(
                // failure message
                MESSAGES.galleryError()) {
            @Override
            public void onSuccess(Integer num) {
                likeCount.setText(String.valueOf(num));
            }
        };
        Ode.getInstance().getGalleryService().getNumLikes(app.getGalleryAppId(), likeNumCallback);

        final OdeAsyncCallback<Boolean> isLikedCallback = new OdeAsyncCallback<Boolean>(
                // failure message
                MESSAGES.galleryError()) {
            @Override
            public void onSuccess(Boolean bool) {
                if (!bool) {
                    likePrompt.setText(MESSAGES.galleryAppsLike());
                    likeButton.setUrl(HOLLOW_HEART_ICON_URL);//unliked
                } else {
                    likePrompt.setText(MESSAGES.galleryAppsAlreadyLike());
                    likeButton.setUrl(RED_HEART_ICON_URL);//liked
                }
            }
        };
        Ode.getInstance().getGalleryService().isLikedByUser(app.getGalleryAppId(), isLikedCallback);
    }

    /**
     * Helper method called by constructor to initialize the salvage section
     * @param container   The container that salvage label reside
     */
    private void initSalvageSection(Panel container) { //TODO: Update the location of this button
        if (!canSalvage()) { // Permitted to salvage?
            return;
        }

        final Label salvagePrompt = new Label("salvage");
        salvagePrompt.addStyleName("primary-link");
        container.add(salvagePrompt);

        salvagePrompt.addClickHandler(new ClickHandler() {
            public void onClick(ClickEvent event) {
                final OdeAsyncCallback<Void> callback = new OdeAsyncCallback<Void>(
                        // failure message
                        MESSAGES.galleryError()) {
                    @Override
                    public void onSuccess(Void bool) {
                        salvagePrompt.setText("done");
                    }
                };
                Ode.getInstance().getGalleryService().salvageGalleryApp(app.getGalleryAppId(), callback);
            }
        });
    }

    /**
     * Helper method called by constructor to initialize the feature section
     * @param container   The container that feature label reside
     */
    private void initFeatureSection(Panel container) { //TODO: Update the location of this button
        final User currentUser = Ode.getInstance().getUser();
        if (currentUser.getType() != User.MODERATOR) { //not admin
            return;
        }

        final Label featurePrompt = new Label(MESSAGES.galleryEmptyText());
        featurePrompt.addStyleName("primary-link");
        container.add(featurePrompt);

        final OdeAsyncCallback<Boolean> isFeaturedCallback = new OdeAsyncCallback<Boolean>(
                // failure message
                MESSAGES.galleryError()) {
            @Override
            public void onSuccess(Boolean bool) {
                if (bool) { // If the app is already featured before, the prompt should show as unfeatured
                    featurePrompt.setText(MESSAGES.galleryUnfeaturedText());
                } else { // otherwise show as featured
                    featurePrompt.setText(MESSAGES.galleryFeaturedText());
                }
            }
        };
        Ode.getInstance().getGalleryService().isFeatured(app.getGalleryAppId(), isFeaturedCallback); // This happens when user click on like, we need to check if it's already liked

        featurePrompt.addClickHandler(new ClickHandler() {
            public void onClick(ClickEvent event) {
                final OdeAsyncCallback<Boolean> markFeaturedCallback = new OdeAsyncCallback<Boolean>(
                        // failure message
                        MESSAGES.galleryError()) {
                    @Override
                    public void onSuccess(Boolean bool) {
                        if (bool) { // If the app is already featured, the prompt should show as unfeatured
                            featurePrompt.setText(MESSAGES.galleryUnfeaturedText());
                        } else { // otherwise show as featured
                            featurePrompt.setText(MESSAGES.galleryFeaturedText());
                        }
                        //update gallery list
                        gallery.appWasChanged();
                    }
                };
                Ode.getInstance().getGalleryService().markAppAsFeatured(app.getGalleryAppId(),
                        markFeaturedCallback);
            }
        });
    }

    /**
     * Helper method called by constructor to initialize the tutorial section
     * @param container   The container that feature label reside
     */
    private void initTutorialSection(Panel container) { //TODO: Update the location of this button
        final User currentUser = Ode.getInstance().getUser();
        if (currentUser.getType() != User.MODERATOR) { //not admin
            return;
        }

        final Label tutorialPrompt = new Label(MESSAGES.galleryEmptyText());
        tutorialPrompt.addStyleName("primary-link");
        container.add(tutorialPrompt);

        final OdeAsyncCallback<Boolean> isTutorialCallback = new OdeAsyncCallback<Boolean>(
                // failure message
                MESSAGES.galleryError()) {
            @Override
            public void onSuccess(Boolean bool) {
                if (bool) { // If the app is already featured before, the prompt should show as unfeatured
                    tutorialPrompt.setText(MESSAGES.galleryUntutorialText());
                } else { // otherwise show as featured
                    tutorialPrompt.setText(MESSAGES.galleryTutorialText());
                }
            }
        };
        Ode.getInstance().getGalleryService().isTutorial(app.getGalleryAppId(), isTutorialCallback); // This happens when user click on like, we need to check if it's already liked

        tutorialPrompt.addClickHandler(new ClickHandler() {
            public void onClick(ClickEvent event) {
                final OdeAsyncCallback<Boolean> markTutorialCallback = new OdeAsyncCallback<Boolean>(
                        // failure message
                        MESSAGES.galleryError()) {
                    @Override
                    public void onSuccess(Boolean bool) {
                        if (bool) { // If the app is already featured, the prompt should show as unfeatured
                            tutorialPrompt.setText(MESSAGES.galleryUntutorialText());
                        } else { // otherwise show as featured
                            tutorialPrompt.setText(MESSAGES.galleryTutorialText());
                        }
                        //update gallery list
                        gallery.appWasChanged();
                    }
                };
                Ode.getInstance().getGalleryService().markAppAsTutorial(app.getGalleryAppId(),
                        markTutorialCallback);
            }
        });
    }

    /**
     * Helper method called by constructor to initialize the edit it button
     * Only seen by app owner.
     */
    private void initEdititButton() {
        final User currentUser = Ode.getInstance().getUser();
        if (app.getDeveloperId().equals(currentUser.getUserId())) {
            editButton = new Button(MESSAGES.galleryEditText());
            editButton.addClickHandler(new ClickHandler() {
                // Open up source file if clicked the action button
                public void onClick(ClickEvent event) {
                    editButton.setEnabled(false);
                    Ode.getInstance().switchToGalleryAppView(app, GalleryPage.UPDATEAPP);
                }
            });
            editButton.addStyleName("app-action-button");
            appAction.add(editButton);
        }
    }

    /**
     * Helper method called by constructor to initialize the try it button
     */
    private void initTryitButton() {
        actionButton = new Button(MESSAGES.galleryOpenText());
        actionButton.addClickHandler(new ClickHandler() {
            // Open up source file if clicked the action button
            public void onClick(ClickEvent event) {
                actionButton.setEnabled(false);
                /*
                 *  open a popup window that will prompt to ask user to enter
                 *  a new project name(if "new name" is not valid, user may need to
                 *  enter again). After that, "loadSourceFil" and "appWasDownloaded"
                 *  will be called.
                 */
                new RemixedYoungAndroidProjectWizard(app, actionButton).center();
            }
        });
        actionButton.addStyleName("app-action-button");
        appAction.add(actionButton);
    }

    /**
     * Helper method called by constructor to initialize the publish button
     */
    private void initPublishButton() {
        actionButton = new Button(MESSAGES.galleryPublishText());
        actionButton.addClickHandler(new ClickHandler() {
            public void onClick(ClickEvent event) {
                if (!checkIfReadyToPublishOrUpdateApp(app)) {
                    return;
                }
                actionButton.setEnabled(false);
                actionButton.setText(MESSAGES.galleryAppPublishing());
                final OdeAsyncCallback<GalleryApp> callback = new OdeAsyncCallback<GalleryApp>(
                        MESSAGES.galleryError()) {
                    @Override
                    // When publish or update call returns
                    public void onSuccess(final GalleryApp gApp) {
                        // we only set the projectId to the gallery app if new app. If we
                        // are updating its already set
                        final OdeAsyncCallback<Void> projectCallback = new OdeAsyncCallback<Void>(
                                MESSAGES.galleryError()) {
                            @Override
                            public void onSuccess(Void result) {
                                // this is called after published and after we've set the gallery id
                                // tell the project list to change project's button to "Update"
                                Ode.getInstance().getProjectManager().publishProject(app.getProjectId(),
                                        gApp.getGalleryAppId());
                                Ode.getInstance().switchToGalleryAppView(gApp, GalleryPage.VIEWAPP);
                                // above was app, switched to gApp which is the newly published thing
                                final OdeAsyncCallback<Long> attributionCallback = new OdeAsyncCallback<Long>(
                                        MESSAGES.galleryError()) {
                                    @Override
                                    public void onSuccess(Long result) {
                                    }
                                };
                                Ode.getInstance().getGalleryService().saveAttribution(gApp.getGalleryAppId(),
                                        app.getProjectAttributionId(), attributionCallback);
                            }//end of projectCallback#onSuccess

                            @Override
                            public void onFailure(Throwable caught) {
                                super.onFailure(caught);
                                actionButton.setEnabled(true);
                                actionButton.setText(MESSAGES.galleryPublishText());
                            }
                        };//end of projectCallback
                        Ode.getInstance().getProjectService().setGalleryId(gApp.getProjectId(),
                                gApp.getGalleryAppId(), projectCallback);
                        // we need to update the app object for this gallery page
                        gallery.appWasChanged();
                    }//end of callback#onSuccess

                    @Override
                    public void onFailure(Throwable caught) {
                        Window.alert(MESSAGES.galleryNoExtensionsPlease());
                        actionButton.setEnabled(true);
                        actionButton.setText(MESSAGES.galleryPublishText());
                    }
                };
                // call publish with the default app data...
                Ode.getInstance().getGalleryService().publishApp(app.getProjectId(), app.getTitle(),
                        app.getProjectName(), app.getDescription(), app.getMoreInfo(), app.getCredit(), callback);
            }
        });
        actionButton.addStyleName("app-action-button");
        appAction.add(actionButton);
    }

    /**
     * Helper method called by constructor to initialize the publish button
     */
    private void initUpdateButton() {
        actionButton = new Button(MESSAGES.galleryUpdateText());
        actionButton.addClickHandler(new ClickHandler() {
            public void onClick(ClickEvent event) {
                if (!checkIfReadyToPublishOrUpdateApp(app)) {
                    return;
                }
                actionButton.setEnabled(false);
                actionButton.setText(MESSAGES.galleryAppUpdating());
                final OdeAsyncCallback<Void> updateSourceCallback = new OdeAsyncCallback<Void>(
                        MESSAGES.galleryError()) {
                    @Override
                    public void onSuccess(Void result) {
                        gallery.appWasChanged(); // to update the gallery list and page
                        Ode.getInstance().switchToGalleryAppView(app, GalleryPage.VIEWAPP);
                    }

                    @Override
                    public void onFailure(Throwable caught) {
                        Window.alert(MESSAGES.galleryNoExtensionsPlease());
                        actionButton.setEnabled(true);
                        actionButton.setText(MESSAGES.galleryUpdateText());
                    }
                };
                Ode.getInstance().getGalleryService().updateApp(app, imageUploaded, updateSourceCallback);
            }
        });
        actionButton.addStyleName("app-action-button");
        appAction.add(actionButton);
    }

    /**
     * check if it is ready to publish or update GalleryApp
     * 1.The minimum length of Desc must be at least MIN_DESC_LENGTH
     * 2.User must upload an image first, in order to publish GaleryApp
     * @param app
     * @return
     */
    private boolean checkIfReadyToPublishOrUpdateApp(GalleryApp app) {
        if (app.getDescription().length() < MIN_DESC_LENGTH) {
            Window.alert(MESSAGES.galleryNotEnoughDescriptionMessage());
            return false;
        }
        if (!imageUploaded && editStatus == NEWAPP) {
            /*we only need to check the image on the publish status*/
            Window.alert(MESSAGES.galleryNoScreenShotMessage());
            return false;
        }
        return true;
    }

    /**
     * Helper method called by constructor to initialize the remove button
     */
    private void initRemoveButton() {
        removeButton = new Button(MESSAGES.galleryRemoveText());
        removeButton.addClickHandler(new ClickHandler() {
            public void onClick(ClickEvent event) {
                //popup confrim dialog
                if (!Window.confirm(MESSAGES.galleryRemoveConfirmText())) {
                    return;
                }
                removeButton.setEnabled(false);
                removeButton.setText(MESSAGES.galleryAppRemoving());
                ;
                final OdeAsyncCallback<Void> callback = new OdeAsyncCallback<Void>(MESSAGES.galleryDeleteError()) {
                    @Override
                    public void onSuccess(Void result) {
                        // once we have deleted, set the project id back to not published
                        final OdeAsyncCallback<Void> projectCallback = new OdeAsyncCallback<Void>(
                                MESSAGES.gallerySetProjectIdError()) {
                            @Override
                            public void onSuccess(Void result) {
                                // this is called after deleted and after we've set the galleryid
                                Ode.getInstance().getProjectManager().UnpublishProject(app.getProjectId());
                                Ode.getInstance().switchToProjectsView();
                            }

                            @Override
                            public void onFailure(Throwable caught) {
                                super.onFailure(caught);
                                removeButton.setEnabled(true);
                                removeButton.setText(MESSAGES.galleryRemoveText());
                            }
                        };
                        GalleryClient client = GalleryClient.getInstance();
                        client.appWasChanged(); // tell views to update
                        Ode.getInstance().getProjectService().setGalleryId(app.getProjectId(),
                                UserProject.NOTPUBLISHED, projectCallback);
                    }

                    @Override
                    public void onFailure(Throwable caught) {
                        super.onFailure(caught);
                        removeButton.setEnabled(true);
                        removeButton.setText(MESSAGES.galleryRemoveText());
                    }
                };
                Ode.getInstance().getGalleryService().deleteApp(app.getGalleryAppId(), callback);
            }
        });
        removeButton.addStyleName("app-action-button");
        appAction.add(removeButton);
    }

    /**
     * Helper method called by constructor to initialize the cancel button
     */
    private void initCancelButton() {
        cancelButton = new Button(MESSAGES.galleryCancelText());
        cancelButton.addClickHandler(new ClickHandler() {
            @Override
            public void onClick(ClickEvent event) {
                if (editStatus == NEWAPP) {
                    Ode.getInstance().switchToProjectsView();
                } else if (editStatus == UPDATEAPP) {
                    Ode.getInstance().switchToGalleryAppView(app, GalleryPage.VIEWAPP);
                }
            }
        });
        cancelButton.addStyleName("app-action-button");
        appAction.add(cancelButton);
    }

    /**
     * Loads the proper tab GUI with gallery's app data.
     * @param apps: list of returned gallery apps from callback.
     * @param requestId: determines the specific type of app data.
     */
    private void refreshApps(GalleryAppListResult appResults, int requestId, boolean refreshable) {
        switch (requestId) {
        case GalleryClient.REQUEST_BYDEVELOPER:
            galleryGF.generateSidebar(appResults.getApps(), sidebarTabs, appsByAuthor,
                    MESSAGES.galleryByAuthorText(), MESSAGES.galleryAppsByAuthorSidebar()
                            + MESSAGES.gallerySingleSpaceText() + app.getDeveloperName(),
                    true, true);
            break;
        //      case GalleryClient.REQUEST_BYTAG: /* We are not implementing tags at initial launch */
        //        String tagTitle = "Tagged with " + tagSelected;
        //        galleryGF.generateSidebar(apps, appsByTags, tagTitle, true);
        //        break;
        }
    }

    /**
     * When the gallery client gets some apps it fires this callback for
     * gallery page to listen to
     */
    @Override
    public void onAppListRequestCompleted(GalleryAppListResult appResults, int requestId, boolean refreshable) {
        if (appResults != null && appResults.getApps() != null)
            refreshApps(appResults, requestId, refreshable);
        else
            OdeLog.log("apps was null");
    }

    /**
     * When the gallery client gets some comments it fires this callback for
     * gallery page to listen to
     */
    @Override
    public void onCommentsRequestCompleted(List<GalleryComment> comments) {
        galleryGF.generateAppPageComments(comments, appCommentsList);
        if (comments == null)
            OdeLog.log("comment list was null");
    }

    @Override
    public void onSourceLoadCompleted(UserProject projectInfo) {

    }

    /**
     * Routine to determine if this user can salvage likes on a Gallery App
     * Verifies that they are a Gallery Moderator AND a site Admin.
     *
     * @return boolean true if permitted
     */
    private boolean canSalvage() {
        User currentUser = Ode.getInstance().getUser();
        if ((currentUser.getType() == User.MODERATOR) && currentUser.getIsAdmin()) {
            return true;
        } else {
            return false;
        }
    }

    /**
     * Create a final object of this class to hold a modifiable result value that
     * can be used in a method of an inner class
     */
    private class Result<T> {
        T t;
    }

    /**
     * Creates a new null GalleryPage.
     * This is only used for init in GalleryAppBox.java, do not use this normally
     *
     */
    public GalleryPage() {

    }
}