// -*- 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 static com.google.appinventor.client.Ode.MESSAGES;

import java.util.logging.Logger;
import java.util.ArrayList;
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.Ode;
import com.google.appinventor.client.output.OdeLog;
import com.google.appinventor.client.OdeAsyncCallback;
import com.google.appinventor.client.boxes.PrivateUserProfileTabPanel;
import com.google.appinventor.client.utils.Uploader;
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.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.TextBox;
import com.google.gwt.user.client.ui.VerticalPanel;

/* profileGUI has:

  profileSingle
   mainContent -- like app Info
     userContentTitle
     userNameLabel
     userNameBox
     userLinkLabel
     userLinkBox
     userEmailFrequencyBox
     userEmailFrequencyPrefixLabel
     userEmailFrequencySuffixLabel

   appCardWrapper
     imageUploadBox
       imageUploadBoxInner
         userAvatar
         imageUploadPrompt
         upload

*/

/**
 * The profile page shows a single user's profile information
 *
 * It has different modes for public viewing or when user is editing privately
 *
 * @author [email protected] (Vincent Zhang)
 * @author [email protected] (Daniel Buraimo)
 */
public class ProfilePage extends Composite/* implements GalleryRequestListener*/ {
  private  List<GalleryApp> apps;
  private final List<GalleryApp> selectedApps;
  private static final int ZERO = 0;
  public static final int PRIVATE = 0;
  public static final int PUBLIC = 1;
  public static final int REQUEST_BYDEVELOPER = 7;
  private int appCatalogCounter = 0;
  private boolean appCatalogExhausted = false;
  public static final int NUMAPPSTOSHOW = 10;

  String userId = "-1";
  final int profileStatus;

  final FileUpload imageUpload = new FileUpload();
  // Create GUI wrappers and components

  private GalleryAppTab appCatalogTab;
  private final TabPanel appTabs;
  private final FlowPanel appCatalog;
  private final FlowPanel appCatalogContent;

  // The abstract top-level GUI container
  VerticalPanel profileGUI = new VerticalPanel();
  // The actual container that components go in
  VerticalPanel profileSingle = new VerticalPanel();
  // The main profile container, same as appDetails in GalleryPage
  FlowPanel mainContent = new FlowPanel();
  // The sidebar showing a list of apps by this author, same as GalleryPage
  private TabPanel sidebarTabs = new TabPanel();

  // Wrapper for primary profile content (image + userinfo)
  FlowPanel profilePrimaryWrapper = new FlowPanel();
  // Header in this case is basically image-related components
  FlowPanel profileHeader = new FlowPanel();
  FocusPanel profileHeaderWrapper = new FocusPanel();
  // Other basic user profile information
  FlowPanel profileInfo = new FlowPanel();

  FocusPanel appCardWrapper = new FocusPanel();
  FlowPanel imageUploadBox = new FlowPanel();
  FlowPanel imageUploadBoxInner = new FlowPanel();
  Image userAvatar = new Image();
  Label imageUploadPrompt = new Label();

  // the majorContentCard has a label and namebox
  Label userContentHeader = new Label();
  Label usernameLabel = new Label();
  Label userLinkLabel = new Label();
  Label userEmailDescriptionLabel = new Label();
  Label userEmailFrequencyPrefixLabel = new Label();
  Label userEmailFrequencySuffixLabel = new Label();
  Button editProfile = new Button(MESSAGES.buttonEditProfile());
  final TextBox userNameBox = new TextBox();
  final TextBox userLinkBox = new TextBox();
  final TextBox userEmailFrequencyBox = new TextBox();
  final Label userNameDisplay = new Label();
  Anchor userLinkDisplay = new Anchor();
  final Button profileSubmit = new Button(MESSAGES.buttonUpdateProfile());

  private static final Logger LOG = Logger.getLogger(ProfilePage.class.getName());
  private static final Ode ode = Ode.getInstance();

  GalleryClient gallery = GalleryClient.getInstance();
  GalleryGuiFactory galleryGF = new GalleryGuiFactory();

  /**
   * Creates a new ProfilePage, must take in parameters
   *
   * @param incomingUserId  the string ID of user that we are about to render
   * @param editStatus  the edit status (0 is private, 1 is public)
   *
   */
  public ProfilePage(final String incomingUserId, final int editStatus) {

    appCatalog = new FlowPanel();
    appCatalogContent = new FlowPanel();
    selectedApps = new ArrayList<GalleryApp>();
    appTabs = new TabPanel();

    // Replace the global variable
    if (incomingUserId.equalsIgnoreCase("-1")) {
      // this is user checking out own profile, thus we grab current user info
      // Get current user id
      final User currentUser = Ode.getInstance().getUser();
      userId = currentUser.getUserId();
    } else {
      // this is checking out an already existing user's profile...
      userId = incomingUserId;
    }
    profileStatus = editStatus;

    // If we're editing or updating, add input form for image
    if (editStatus == PRIVATE) {
    // This should only set up image after userId is returned above
    } else  { // we are just viewing this page so setup the image
      initReadOnlyImage();
    }

    if (editStatus == PRIVATE) {
      userContentHeader.setText(MESSAGES.labelEditYourProfile());
      usernameLabel.setText(MESSAGES.labelYourDisplayName());
      userLinkLabel.setText(MESSAGES.labelMoreInfoLink());
      userEmailDescriptionLabel.setText(MESSAGES.labelEmailDescription());
      userEmailFrequencyPrefixLabel.setText(MESSAGES.labelEmailFrequencyPrefix());
      userEmailFrequencySuffixLabel.setText(MESSAGES.labelEmailFrequencySuffix());
      editProfile.setVisible(false);

      profileSubmit.addClickHandler(new ClickHandler() {
        @Override
        public void onClick(ClickEvent event) {
          profileSubmit.setEnabled(false);
          if(!validEmailFrequency(userEmailFrequencyBox.getText())){
            Window.alert(MESSAGES.errorEmailFrequency());
            profileSubmit.setEnabled(true);
            return;
          }

          // Store the name value of user, modify database
          final OdeAsyncCallback<Void> userNameUpdateCallback = new OdeAsyncCallback<Void>(
              // failure message
              MESSAGES.galleryError()) {
                @Override
                public void onSuccess(Void arg0) {
                  profileSubmit.setEnabled(true);
                }
            };
           ode.getUserInfoService().storeUserName(userNameBox.getText(), userNameUpdateCallback);

          // Store the link value of user, modify database
          final OdeAsyncCallback<Void> userLinkUpdateCallback = new OdeAsyncCallback<Void>(
              // failure message
              MESSAGES.galleryError()) {
                @Override
                public void onSuccess(Void arg0) {
                }
            };
          if (userLinkBox.getText().isEmpty()) {
            Ode.getInstance().getUserInfoService().storeUserLink(
                "", userLinkUpdateCallback);
          } else {
            Ode.getInstance().getUserInfoService().storeUserLink(
                userLinkBox.getText(), userLinkUpdateCallback);
          }

          // Store the email notification frequency value of user, modofy database
          final OdeAsyncCallback<Void> userEmailFrequencyUpdateCallback = new OdeAsyncCallback<Void>(
              // failure message
              MESSAGES.galleryError()) {
                @Override
                public void onSuccess(Void arg0) {
                }
            };
            Ode.getInstance().getUserInfoService().storeUserEmailFrequency(
                Integer.valueOf(userEmailFrequencyBox.getText()), userEmailFrequencyUpdateCallback);

        }
      });

      profileInfo.add(userContentHeader);
      profileInfo.add(usernameLabel);
      profileInfo.add(userNameBox);
      profileInfo.add(userLinkLabel);
      profileInfo.add(userLinkBox);
      profileInfo.add(userEmailDescriptionLabel);
      profileInfo.add(userEmailFrequencyPrefixLabel);
      profileInfo.add(userEmailFrequencyBox);
      profileInfo.add(userEmailFrequencySuffixLabel);
      profileInfo.add(profileSubmit);

    } else {
      profileSingle.addStyleName("ode-Public");
      // USER PROFILE IN PUBLIC (NON-EDITABLE) STATE
      // Set up the user info stuff
      userLinkLabel.setText("More info:");
      profileInfo.add(userContentHeader);
      profileInfo.add(userLinkLabel);
      profileInfo.add(userLinkDisplay);
      profileInfo.add(editProfile);
    }

    // Add GUI layers in the "main content" container
    profileHeader.addStyleName("app-header"); //TODO: change a more contextual style name
    profilePrimaryWrapper.add(profileHeader); // profileImage
    profileInfo.addStyleName("app-info-container");
    profilePrimaryWrapper.add(profileInfo);
    profilePrimaryWrapper.addStyleName("clearfix");
    mainContent.add(profilePrimaryWrapper);

    // Add styling for user info detail components
    mainContent.addStyleName("gallery-container");
    mainContent.addStyleName("gallery-content-details");
    userContentHeader.addStyleName("app-title");
    usernameLabel.addStyleName("profile-textlabel");
    userNameBox.addStyleName("profile-textbox");
    userNameDisplay.addStyleName("profile-textdisplay");
    userLinkLabel.addStyleName("profile-textlabel");
    userLinkBox.addStyleName("profile-textbox");
    userLinkDisplay.addStyleName("profile-textdisplay");
    userEmailDescriptionLabel.addStyleName("profile-textlabel-emaildescription");
    userEmailFrequencyPrefixLabel.addStyleName("profile-textlabel");
    userEmailFrequencySuffixLabel.addStyleName("profile-textlabel");
    userEmailFrequencyBox.addStyleName("profile-textbox-small");
    editProfile.addStyleName("profile-submit");

    profileSubmit.addStyleName("profile-submit");
    imageUpload.addStyleName("app-image-upload");

    // Add sidebar
    if (editStatus == PUBLIC) {
      sidebarTabs.addStyleName("gallery-container");
      sidebarTabs.addStyleName("gallery-app-showcase");
    }
    // Setup top level containers
    // profileSingle is the actual container that components go in
    profileSingle.addStyleName("gallery-page-single");


    // Add containers to the top-tier GUI, initialize
    profileSingle.add(mainContent);
    if (editStatus == PUBLIC) {
      profileSingle.add(appTabs);
      profileSingle.add(sidebarTabs);
    }

    // profileGUI is just the abstract top-level GUI container
    profileGUI.add(profileSingle);
    profileGUI.addStyleName("ode-UserProfileWrapper");
    profileGUI.addStyleName("gallery");
    initWidget(profileGUI);

    // Retrieve other user info right after GUI is initialized
    final OdeAsyncCallback<User> userInformationCallback = new OdeAsyncCallback<User>(
        // failure message
        MESSAGES.galleryError()) {
          @Override
          public void onSuccess(User user) {
            // Set associate GUI components of public states
            // In this case it'll return the user of [userId]
            userContentHeader.setText(user.getUserName());
            makeValidLink(userLinkDisplay, user.getUserLink());
            userEmailFrequencyBox.setText(String.valueOf(user.getUserEmailFrequency()));
         }
    };
    if (editStatus == PRIVATE) {
      User currentUser = Ode.getInstance().getUser();
      // In this case it'll return the current user
      userId = currentUser.getUserId();
      userNameBox.setText(currentUser.getUserName());
      userLinkBox.setText(currentUser.getUserLink());
      userEmailFrequencyBox.setText(String.valueOf(currentUser.getUserEmailFrequency()));
    } else {
      // Public state
      Ode.getInstance().getUserInfoService().getUserInformationByUserId(userId, userInformationCallback);
      sidebarTabs.setVisible(false);
      appCatalogTab = new GalleryAppTab(appCatalog, appCatalogContent,userId);
      appTabs.add(appCatalog,"My Catalog");
      appTabs.selectTab(0);
      appTabs.addStyleName("gallery-app-tabs");
    }

    //TODO this callback should combine with previous ones. Leave it out for now
    final User user = Ode.getInstance().getUser();
    if(incomingUserId.equals(user.getUserId())){
      editProfile.setVisible(true);
      editProfile.addClickHandler(new ClickHandler() {
        @Override
        public void onClick(ClickEvent clickEvent) {
          ode.switchToPrivateUserProfileView();
          PrivateUserProfileTabPanel.getPrivateUserProfileTabPanel().selectTab(0);
        }
      });
    }else{
      editProfile.setVisible(false);
    }

  }


  /**
   * Helper method to validify a hyperlink
   * @param link    the GWT anchor object to validify
   * @param linktext    the actual http link that the anchor should point to
   */
  private void makeValidLink(Anchor link, String linktext) {
    if (linktext == null) {
      link.setText("N/A");
    } else {
      if (linktext.isEmpty()) {
        link.setText("N/A");
      } else {
        linktext = linktext.toLowerCase();
        // Validate link format, fill in http part
        if (!linktext.startsWith("http")) {
          linktext = "http://" + linktext;
        }
        link.setText(linktext);
        link.setHref(linktext);
        link.setTarget("_blank");
      }
    }
  }


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

    if(gallery.getGallerySettings() != null){
      updateUserImage(gallery.getUserImageURL(userId), imageUploadBoxInner);
    }
    imageUploadPrompt.addStyleName("app-image-uploadprompt");
    //imageUploadBoxInner.add(imageUploadPrompt);

    // Set the correct handler for servlet side capture
    imageUpload.setName(ServerLayout.UPLOAD_FILE_FORM_ELEMENT);
    imageUpload.addChangeHandler(new ChangeHandler (){
      public void onChange(ChangeEvent event) {
        uploadImage();
      }
    });
    imageUploadBoxInner.add(imageUpload);
    imageUploadBox.add(imageUploadBoxInner);
    profileHeaderWrapper.add(imageUploadBox);
    profileHeaderWrapper.addClickHandler(new ClickHandler() {
      @Override
      public void onClick(ClickEvent event) {
        // The correct way to trigger click event on FileUpload
        //imageUpload.getElement().<InputElement>cast().click();
      }
    });
    profileHeader.add(profileHeaderWrapper);
    
    Label uploadPrompt = new Label("Upload your profile image");
    uploadPrompt.addStyleName("primary-link-small");
    uploadPrompt.addClickHandler(new ClickHandler() {
      @Override
      public void onClick(ClickEvent event) {
        // The correct way to trigger click event on FileUpload
        imageUpload.getElement().<InputElement>cast().click();
      }
    });
    profileHeader.add(uploadPrompt);
  }


  /**
   * Helper method called by constructor to create the app image for display
   */
  private void initReadOnlyImage() {
    updateUserImage(gallery.getUserImageURL(userId), profileHeader);
  }


  /**
   * Main method to validify and upload the app image
   */
  private void uploadImage() {
    String uploadFilename = imageUpload.getFilename();
    if (!uploadFilename.isEmpty()) {
      String filename = makeValidFilename(uploadFilename);
      // Forge the request URL for gallery servlet
      String uploadUrl = GWT.getModuleBaseURL() + ServerLayout.GALLERY_SERVLET +
          "/user/" + userId + "/" + filename;
      Uploader.getInstance().upload(imageUpload, uploadUrl,
          new OdeAsyncCallback<UploadResponse>(MESSAGES.fileUploadError()) {
        @Override
        public void onSuccess(UploadResponse uploadResponse) {
          switch (uploadResponse.getStatus()) {
            case SUCCESS:
              ErrorReporter.hide();
              imageUploadBoxInner.clear();
              updateUserImage(gallery.getUserImageURL(userId), imageUploadBoxInner);
              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;
          }
        }
      });
    }
  }


  /**
   * 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 user's image
   * @param url  The URL of the image to show
   * @param container  The container that image widget resides
   */
  private void updateUserImage(final String url, Panel container) {
    userAvatar = new Image();
    //setUrl if the new URL is the same one as it was before; an easy workaround is
    //to make the URL unique so it forces the browser to reload
    userAvatar.setUrl(url + "?" + System.currentTimeMillis());
    userAvatar.addStyleName("app-image");
    if (profileStatus == PRIVATE) {
      //userAvatar.addStyleName("status-updating");
    }
    // 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
    userAvatar.addErrorHandler(new ErrorHandler() {
      public void onError(ErrorEvent event) {
        userAvatar.setResource(GalleryImages.get().androidIcon());
      }
    });
    container.add(userAvatar);

    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) {
            if (userAvatar != null) {
              userAvatar.setUrl(newUrl + "?" + System.currentTimeMillis());
            }
          }
        };
      Ode.getInstance().getGalleryService().getBlobServingUrl(url, callback);
    }
  }

  public void loadImage(){
    initImageComponents(userId);
  }

  private boolean validEmailFrequency(String emailFrequencyString){
    int emailFrequency = 0;
    try {
      emailFrequency = Integer.valueOf(emailFrequencyString);
      if(emailFrequency < 1) return false;
    } catch (NumberFormatException e) {
      return false;
    }

    return true;
  }

  /**
   * A wrapper class of tab, which provides help method to get/set UI components
   */
  private class GalleryAppTab{
    Label buttonNext;
    Label noResultsFound;
    Label keywordTotalResultsLabel;
    Label generalTotalResultsLabel;
    /**
     * @param container: the FlowPanel that this app tab will reside.
     * @param content: the sub-panel that contains the actual app content.
     * @apram incomingUserId: the user id
     */
    GalleryAppTab(FlowPanel container, FlowPanel content, final String incomingUserId){
      addGalleryAppTab(container, content,incomingUserId);
    }

    /**
     * @return Label buttonNext
     */
    public Label getButtonNext(){
      return buttonNext;
    }

    /**
     * @return Label noResultsFound
     */
    public Label getNoResultsFound(){
      return noResultsFound;
    }

    /**
     * @return Label keywordTotalResultsLabel
     */
    public Label getKeywordTotalResultsLabel(){
      return keywordTotalResultsLabel;
    }

    /**
     * Set keywordTotalResultsLabel's text to new text
     * @param keyword the search keyword
     * @param num number of results
     */
    public void setKeywordTotalResultsLabel(String keyword, int num){
       keywordTotalResultsLabel.setText(MESSAGES.gallerySearchResultsPrefix() + keyword + MESSAGES.gallerySearchResultsInfix() + num + MESSAGES.gallerySearchResultsSuffix());
    }

    /**
     * @return Label generalTotalResultsLabel
     */
    public Label getGeneralTotalResultsLabel(){
      return generalTotalResultsLabel;
    }

    /**
     * set generalTotalResultsLabel to new text
     * @param num number of results
     */
    public void setGeneralTotalResultsLabel(int num){
      generalTotalResultsLabel.setText(num + MESSAGES.gallerySearchResultsSuffix());
    }

    /**
     * Creates the GUI components for a regular app tab.
     * This method resides here because it needs access to global variables.
     * @param container: the FlowPanel that this app tab will reside.
     * @param content: the sub-panel that contains the actual app content.
     */
    private void addGalleryAppTab(FlowPanel container, FlowPanel content, final String incomingUserId) {
      // Search specific
      generalTotalResultsLabel = new Label();
      container.add(generalTotalResultsLabel);

      final OdeAsyncCallback<GalleryAppListResult> byAuthorCallback = new OdeAsyncCallback<GalleryAppListResult>(
        // failure message
        MESSAGES.galleryError()) {
        @Override
        public void onSuccess(GalleryAppListResult appsResult) {
          refreshApps(appsResult,false);
        }
      };
      Ode.getInstance().getGalleryService().getDeveloperApps(userId,appCatalogCounter ,NUMAPPSTOSHOW, byAuthorCallback);
      container.add(content);

      buttonNext = new Label();
      buttonNext.setText(MESSAGES.galleryMoreApps());
      buttonNext.addStyleName("active");

      FlowPanel next = new FlowPanel();
      next.add(buttonNext);
      next.addStyleName("gallery-nav-next");
      container.add(next);
      buttonNext.addClickHandler(new ClickHandler() {
        //  @Override
        public void onClick(ClickEvent event) {
           if (!appCatalogExhausted) {
                // If the next page still has apps to retrieve, do it
                appCatalogCounter += NUMAPPSTOSHOW;
                Ode.getInstance().getGalleryService().getDeveloperApps(userId,appCatalogCounter ,NUMAPPSTOSHOW, byAuthorCallback);
              }
        }
      });
    }
  }

  /**
   * Loads the proper tab GUI with gallery's app data.
   * @param apps: list of returned gallery apps from callback.
   */
  private void refreshApps(GalleryAppListResult appsResult, boolean refreshable) {
        appCatalogTab.setGeneralTotalResultsLabel(appsResult.getTotalCount());
        if (appsResult.getTotalCount() < NUMAPPSTOSHOW) {
          // That means there's not enough apps to show (reaches the end)
          appCatalogExhausted = true;
        } else {
          appCatalogExhausted = false;
        }
        galleryGF.generateHorizontalAppList(appsResult.getApps(), appCatalogContent, refreshable);
        if(appsResult.getTotalCount() < NUMAPPSTOSHOW || appCatalogCounter + NUMAPPSTOSHOW >= appsResult.getTotalCount()){
          appCatalogTab.getButtonNext().setVisible(false);
        }
  }

  /**
   * Gets the number of selected apps
   *
   * @return the number of selected apps
   */
  public int getNumSelectedApps() {
    return selectedApps.size();
  }

  /**
   * Returns the list of selected apps
   *
   * @return the selected apps
   */
  public List<GalleryApp> getSelectedApps() {
    return selectedApps;
  }
  /**
   * select specific tab index based on given index
   * @param index
   */
  public void setSelectTabIndex(int index){
    appTabs.selectTab(index);
  }
  /**
   * Process the results after retrieving GalleryAppListResult
   * @param appsResult GalleryAppList Result
   * @param refreshable whether or not clear container
   * @see GalleryRequestListener
   */
  public void onAppListRequestCompleted(GalleryAppListResult appsResult, boolean refreshable)
  {
    List<GalleryApp> apps = appsResult.getApps();
    if (apps != null)
      refreshApps(appsResult, refreshable);
    else
      OdeLog.log("apps was null");
  }
  /**
   * Process the results after retrieving list of GalleryComment
   * @see GalleryRequestListener
   */
  public void onCommentsRequestCompleted(List<GalleryComment> comments){

  }

  /**
   * Process the results after retrieving list of UserProject
   * @see GalleryRequestListener
   */
  public void onSourceLoadCompleted(UserProject projectInfo) {

  }
}