MainView.java

/**
 * Copyright (c) 2013 Nokia Corporation. All rights reserved.
 * Nokia and Nokia Connecting People are registered trademarks of Nokia Corporation. 
 * Oracle and Java are trademarks or registered trademarks of Oracle and/or its
 * affiliates. Other product and company names mentioned herein may be trademarks
 * or trade names of their respective owners. 
 * See license text file delivered with this project for more information.
 */

package com.nokia.example.statusshout.ui;

import java.io.IOException;

import javax.microedition.content.Invocation;
import javax.microedition.lcdui.Alert;
import javax.microedition.lcdui.AlertType;
import javax.microedition.lcdui.Canvas;
import javax.microedition.lcdui.Command;
import javax.microedition.lcdui.CommandListener;
import javax.microedition.lcdui.Display;
import javax.microedition.lcdui.Displayable;
import javax.microedition.lcdui.Gauge;
import javax.microedition.lcdui.Graphics;
import javax.microedition.lcdui.Image;
import javax.microedition.lcdui.TextField;
import javax.microedition.midlet.MIDlet;

import com.nokia.mid.ui.CategoryBar;
import com.nokia.mid.ui.ElementListener;
import com.nokia.mid.ui.FileSelect;
import com.nokia.mid.ui.FileSelectDetail;
import com.nokia.mid.ui.IconCommand;
import com.nokia.mid.ui.KeyboardVisibilityListener;
import com.nokia.mid.ui.TextEditor;
import com.nokia.mid.ui.TextEditorListener;
import com.nokia.mid.ui.VirtualKeyboard;

import com.nokia.example.statusshout.StatusShout;
import com.nokia.example.statusshout.animations.AnimationListener;
import com.nokia.example.statusshout.animations.IntAnimation;
import com.nokia.example.statusshout.engine.FacebookService;
import com.nokia.example.statusshout.engine.OAuthService;
import com.nokia.example.statusshout.engine.ShareApiManager;
import com.nokia.example.statusshout.engine.ShareListener;
import com.nokia.example.statusshout.engine.StatusShoutData;

/**
 * The main view of the application.
 */
public class MainView
    extends Canvas
    implements CommandListener,
               ElementListener,
               TextEditorListener,
               KeyboardVisibilityListener,
               Button.Listener,
               AnimationListener,
               ShareListener
{
    // Constants
    private static final String TAG = "MainView.";
    private static final String PHOTOS_FOLDER = System.getProperty("fileconn.dir.photos");
    private static final String BACKGROUND_IMAGE_URI = "/background.png";
    private static final String IMAGE_PLACEHOLDER_URI = "/image-placeholder.png";
    private static final String TEXT_BOX_PATTERN_IMAGE_URI = "/text-box-pattern.png";
    private static final String DELETE_BUTTON_IMAGE_URI = "/delete-button.png";
    private static final String FACEBOOK_ICON_URI = "/f-logo.png";
    private static final String SHARE_API_ICON_URI = "/share-icon.png";
    private static final String DISCARD_FACEBOOK_TOKEN_TEXT = "Discard Facebook token";
    private static final String HINT_TEXT = "Type your message here";
    private static final int BACKGROUND_COLOR = 0x000000;
    private static final int TEXT_COLOR = 0x464646;
    private static final int HINT_TEXT_COLOR = 0x717171;
    private static final int MARGIN = 5;
    private static final int IMAGE_WIDTH = 190;
    private static final int IMAGE_HEIGHT = 140;
    private static final int TEXT_FIELD_MAX_SIZE = 140;
    
    private static final int BUTTON_INDEX_ADD_IMAGE = 0;
    private static final int BUTTON_INDEX_DISCARD_IMAGE = 1;
    private static final int BUTTON_INDEX_LAST = 2;

    // Members
    private final Command exitAndBackCommand = new Command("Exit", Command.EXIT, 0x01);
    private final Button[] buttons = new Button[BUTTON_INDEX_LAST];
    private MIDlet midlet;
    private StatusShoutData appData;
    private FacebookService facebookService;
    private ShareApiManager shareApiManager;
    private AboutView aboutView;
    private CategoryBar categoryBar;
    private TextEditor textEditor;
    private IntAnimation animation;
    private IconCommand shareViaFacebookCommand;
    private IconCommand shareUsingShareApiCommand;
    private Command discardFacebookTokenCommand;
    private Command aboutCommand;
    private Image backgroundImage;
    private Image textBoxPatternImage;
    private Image selectedImage;
    private int yOffset; // For scrolling
    private int textAreaY;

    /**
     * Constructor.
     * @param midlet The application MIDlet instance.
     * @throws NullPointerException if MIDlet instance is null.
     */
    public MainView(MIDlet midlet) throws NullPointerException
    {
        super();
        
        if (midlet == null) {
            throw new NullPointerException("The MIDlet instance is null!");
        }
        
        this.midlet = midlet;
        appData = StatusShoutData.getInstance();
        facebookService = new FacebookService(midlet, this);
        shareApiManager = ShareApiManager.getInstance(midlet);
        shareApiManager.setListener(this);
        
        createImagesAndButtons();
        
        discardFacebookTokenCommand = new Command(DISCARD_FACEBOOK_TOKEN_TEXT, Command.ITEM, 0x04);
        aboutCommand = new Command("About", Command.ITEM, 0x06);
        
        addCommand(exitAndBackCommand);
        setCommandListener(this);
        
        // Create the text editor
        textEditor =
            TextEditor.createTextEditor("", TEXT_FIELD_MAX_SIZE,
                TextField.NON_PREDICTIVE, getWidth() - MARGIN * 2, 100);
        textEditor.setMultiline(true);
        textEditor.insert(HINT_TEXT, 0);
        textEditor.setForegroundColor(HINT_TEXT_COLOR);
        textEditor.setBackgroundColor(0x00000000);
        textEditor.setParent(this); // Canvas to draw on
        textEditor.setTouchEnabled(true);
        textEditor.setTextEditorListener(this);
        
        VirtualKeyboard.setVisibilityListener(this);
        
        animation = new IntAnimation();
        animation.setListener(this);
        
        restoreData();
    }

    /**
     * Creates the category bar and other commands which are placed in the menu.
     * If the category bar is already created, it is recreated if the intent is
     * to hide the share API icon command.
     * 
     * @param showShareApi If false, will hide the sharing option via share API.
     */
    public void setCategoryBar(boolean showShareApi) {
        if (categoryBar != null) {
            // Already created
            if (!showShareApi) {
                // Recreate without the share API icon command
                IconCommand[] iconCommands = new IconCommand[] { shareViaFacebookCommand };
                categoryBar = new CategoryBar(iconCommands, true);
                categoryBar.setMode(CategoryBar.ELEMENT_MODE_RELEASE_SELECTED);
                categoryBar.setVisibility(true);
                categoryBar.setElementListener(this);
            }
            
            return;
        }
        
        Image fbIcon = null;
        Image shareApiIcon = null;
        
        try {
            fbIcon = Image.createImage(FACEBOOK_ICON_URI);
            
            if (showShareApi) {
                shareApiIcon = Image.createImage(SHARE_API_ICON_URI);
            }
        }
        catch (IOException e) {
        }
        
        shareViaFacebookCommand = new IconCommand("Share via Facebook", fbIcon, null, Command.ITEM, 0x01);
        IconCommand[] iconCommands = null;
        
        if (showShareApi) {
            shareUsingShareApiCommand = new IconCommand("Other sharing options", shareApiIcon, null, Command.ITEM, 0x03);
            iconCommands = new IconCommand[] { shareViaFacebookCommand, shareUsingShareApiCommand };
        }
        else {
            iconCommands = new IconCommand[] { shareViaFacebookCommand };
        }
        
        categoryBar = new CategoryBar(iconCommands, true); 
        categoryBar.setMode(CategoryBar.ELEMENT_MODE_RELEASE_SELECTED);
        categoryBar.setVisibility(true);
        categoryBar.setElementListener(this);
    }

    /**
     * @see javax.microedition.lcdui.CommandListener#commandAction(
     * javax.microedition.lcdui.Command, javax.microedition.lcdui.Displayable)
     */
    public void commandAction(Command command, Displayable displayable) {
        if (command == exitAndBackCommand) {
            if (displayable == this) {
                ((StatusShout)midlet).quit();
            }
            else {
                ((StatusShout)midlet).getDisplay().setCurrent(this);
                categoryBar.setVisibility(true);
                aboutView = null;
            }
        }
        else if (command == discardFacebookTokenCommand) {
            appData.setFacebookToken(null);
            populateMenu();
        }
        else if (command == aboutCommand) {
            aboutView = new AboutView(midlet, backgroundImage);
            aboutView.addCommand(exitAndBackCommand);
            aboutView.setCommandListener(this);
            categoryBar.setVisibility(false);
            ((StatusShout)midlet).getDisplay().setCurrent(aboutView);
        }
    }

    /**
     * @see com.nokia.mid.ui.ElementListener#notifyElementSelected(com.nokia.mid.ui.CategoryBar, int)
     */
    public void notifyElementSelected(CategoryBar bar, int selectedIndex) {
        System.out.println(TAG + "notifyElementSelected(): " + selectedIndex);
        
        // Get the message
        String message = appData.getMessage();
        
        if ((message != null && message.equals(HINT_TEXT)) ||
                (message != null && message.length() == 0))
        {
            message = null;
        }
        
        // Get the image
        final FileSelectDetail imageDetails = appData.getSelectedImageDetails();
        
        String imageUrl = null;
        
        if (imageDetails != null
                && imageDetails.url != null
                && imageDetails.url.length() > 0)
        {
            imageUrl = imageDetails.url;
        }
        
        // Verify that we have something to share
        if (message == null && imageUrl == null) {
            showMessage("Add something to share.", AlertType.INFO);
            return;
        }
        
        switch (selectedIndex) {
            case 0: // Facebook
                facebookService.share(message, imageUrl);
                break;
            case 1: // Share API
                // Share API only supports sharing one item at a time i.e. you
                // cannot share both image and text at once. Thus, an image is
                // prioritised meaning that if an image is selected it is shared
                // and not the message even if one exists.
                if (imageUrl != null) {
                    shareApiManager.shareImage(imageDetails.url, imageDetails.mimeType);
                }
                else {
                    shareApiManager.shareText(message);
                }
                
                break;
        }
    }

    /**
     * @see com.nokia.mid.ui.KeyboardVisibilityListener#hideNotify(int)
     */
    public void hideNotify(int keyboardCategory) {
        if (textEditor.getContent().length() == 0) {
            textEditor.setForegroundColor(HINT_TEXT_COLOR);
            textEditor.setContent(HINT_TEXT);
        }
        
        textEditor.setFocus(false);
        animation.start(yOffset, 0, 400, IntAnimation.EASING_CURVE_INOUTQUAD);
    }

    /**
     * @see com.nokia.mid.ui.KeyboardVisibilityListener#showNotify(int)
     */
    public void showNotify(int keyboardCategory) {
        if (textEditor.getContent().equals(HINT_TEXT)) {
            textEditor.setContent("");
            textEditor.setCaret(0);
            textEditor.setForegroundColor(TEXT_COLOR);
        }
        
        animation.start(yOffset, 30 - textAreaY, 400, IntAnimation.EASING_CURVE_INOUTQUAD);
    }

    /**
     * @see com.nokia.mid.ui.TextEditorListener#inputAction(com.nokia.mid.ui.TextEditor, int)
     */
    public void inputAction(TextEditor textEditor, int actions) {
        appData.setMessage(textEditor.getContent());
    }

    /**
     * @see com.nokia.example.statusshout.ui.Button.Listener#onPressedChanged(
     * com.nokia.example.statusshout.ui.Button, boolean)
     */
    public void onPressedChanged(Button button, boolean pressed) {
        repaint();
    }

    /**
     * @see com.nokia.example.statusshout.ui.Button.Listener#onTapped(
     * com.nokia.example.statusshout.ui.Button)
     */
    public void onTapped(Button button) {
        System.out.println(TAG + "onTapped(): " + button);
        
        if (button == buttons[BUTTON_INDEX_ADD_IMAGE]) {
            selectImage();
        }
        else if (button == buttons[BUTTON_INDEX_DISCARD_IMAGE]) {
            appData.setSelectedImageDetails(null);
            selectedImage = null;
            buttons[BUTTON_INDEX_ADD_IMAGE].setIsDisabled(false);
            buttons[BUTTON_INDEX_DISCARD_IMAGE].setIsDisabled(true);
            repaint();
        }
    }

    /**
     * @see com.nokia.example.statusshout.animations.AnimationListener#onAnimatedValueChanged(int)
     */
    public void onAnimatedValueChanged(int value) {
        yOffset = value;
        repaint();
    }

    /**
     * @see com.nokia.example.statusshout.animations.AnimationListener#onAnimationStateChanged(int)
     */
    public void onAnimationStateChanged(int state) {
        if (state == IntAnimation.STATE_FINISHED) {
            if (textEditor.hasFocus()) {
                yOffset = 30 - textAreaY;
            }
            else {
                yOffset = 0;
            }
            
            repaint();
        }
    }

    /**
     * @see com.nokia.example.statusshout.engine.ShareListener#onSending()
     */
    public void onSending() {
        if (Display.getDisplay(midlet).getCurrent() != this) {
            Display.getDisplay(midlet).setCurrent(this);
        }
        
        Alert alert = new Alert("Sending", "Please wait...", null, AlertType.INFO);
        alert.setIndicator(new Gauge(null, false, Gauge.INDEFINITE, Gauge.CONTINUOUS_RUNNING));
        alert.setTimeout(Alert.FOREVER);
        alert.addCommand(new Command("OK", Command.OK, 0x01));
        
        try {
            Display.getDisplay(midlet).setCurrent(alert);
        }
        catch (Exception e) {
        }
    }

    /**
     * @see com.nokia.example.statusshout.engine.ShareListener#onSuccess(java.lang.String)
     */
    public void onSuccess(String message) {
        showMessage(message, AlertType.INFO);
        
        if (shareApiManager.getWasLaunchedAsSharingDestination()) {
            /*
             * This app was launched as a sharing destination, which means that
             * we can finish the invocation. Finishing the invocation with value
             * Invocation.OK will show up in the Fastlane UI.
             */
            shareApiManager.finishInvocation(Invocation.OK);
        }
    }

    /**
     * @see com.nokia.example.statusshout.engine.ShareListener#onError(java.lang.String)
     */
    public void onError(String errorMessage) {
        showMessage(errorMessage, AlertType.ERROR);
    }

    /**
     * @see com.nokia.example.statusshout.engine.ShareListener#onAuthenticated(
     * com.nokia.example.statusshout.engine.OAuthService)
     */
    public void onAuthenticated(OAuthService service) {
        populateMenu();
        
        if (Display.getDisplay(midlet).getCurrent() != this) {
            Display.getDisplay(midlet).setCurrent(this);
        }
    }

    /**
     * @see com.nokia.example.statusshout.engine.ShareListener#onLaunchedWithInvocation(
     * javax.microedition.content.Invocation)
     */
    public void onLaunchedWithInvocation(Invocation invocation) {
        if (categoryBar != null) {
            // Hide the Share API icon command
            setCategoryBar(false);
        }
    }

    /**
     * @see javax.microedition.lcdui.Canvas#pointerPressed(int, int)
     */
    protected void pointerPressed(int x, int y) {
        for (int i = 0; i < BUTTON_INDEX_LAST; ++i) {
            if (buttons[i].pointerPressed(x, y)) {
                return;
            }
        }
        
        if (textBoxPatternImage != null && !textEditor.hasFocus()
                && y > yOffset + textAreaY
                && y < yOffset + textAreaY + textBoxPatternImage.getHeight())
        {
            textEditor.setFocus(true);
        }
    }

    /**
     * @see javax.microedition.lcdui.Canvas#pointerReleased(int, int)
     */
    protected void pointerReleased(int x, int y) {
        for (int i = 0; i < BUTTON_INDEX_LAST; ++i) {
            if (buttons[i].pointerReleased(x, y)) {
                return;
            }
        }
    }

    /**
     * Sets the image.
     * @param imageUri The image URI.
     */
    public void setImage(final String imageUri) {
        System.out.println(TAG + "setImage(): " + imageUri);
        FileSelectDetail detail = new FileSelectDetail();
        detail.url = imageUri;
        appData.setSelectedImageDetails(detail);
        createImageThumbnail();
    }

    /**
     * @see javax.microedition.lcdui.Canvas#paint(javax.microedition.lcdui.Graphics)
     */
    protected void paint(Graphics graphics) {
        final int width = getWidth();
        final int height = getHeight();
        graphics.setColor(BACKGROUND_COLOR);
        graphics.fillRect(0, 0, width, height);
        
        if (backgroundImage != null) {
            graphics.drawImage(backgroundImage, 0, 0, Graphics.TOP | Graphics.LEFT);
        }
        
        int y = MARGIN * 2;
        
        if (selectedImage == null) {
            final Button button = buttons[BUTTON_INDEX_ADD_IMAGE]; 
            button.paint(graphics, button.getPositionX(), yOffset + button.getPositionY());
        }
        else {
            final int imageWidth = selectedImage.getWidth();
            final int frameWidth = imageWidth + MARGIN * 2;
            final int frameHeight = selectedImage.getHeight() + MARGIN * 2;
            
            
            graphics.setColor(0xf4f4f4);
            graphics.fillRect((width - frameWidth) / 2, yOffset + y, frameWidth, frameHeight);
            
            graphics.drawImage(selectedImage,
                    (width - imageWidth) / 2, yOffset + y + MARGIN,
                    Graphics.TOP | Graphics.LEFT);
            
            // Draw delete button
            final Button button = buttons[BUTTON_INDEX_DISCARD_IMAGE];
            button.paint(graphics, (width - imageWidth) / 2 + imageWidth - button.getWidth() / 2, button.getPositionY());
        }
        
        y += IMAGE_HEIGHT + MARGIN * 2;
        
        if (textBoxPatternImage != null) {
            textAreaY = y;
            graphics.drawImage(textBoxPatternImage, 0,
                    yOffset + y, Graphics.TOP | Graphics.LEFT);
            textEditor.setPosition(MARGIN, yOffset + y + MARGIN);
        }
        
        if (!textEditor.isVisible()) {
            textEditor.setVisible(true);
        }
    }

    /**
     * Restores the app state and settings.
     */
    private void restoreData() {
        final MainView mainView = this;
        
        new Thread() {
            public void run() {
                appData.load();
                createImageThumbnail();
                final String message = appData.getMessage();
                
                if (message != null) {
                    textEditor.setForegroundColor(TEXT_COLOR);
                    mainView.textEditor.setContent(message);
                }
                
                populateMenu();
                repaint();
            }
        }.start();
    }

    /**
     * Creates the larger image assets of the view. To be on the safe side this
     * is done in a separate thread so that we don't block the UI.
     */
    private void createImagesAndButtons() {
        final MainView mainView = this;
        
        new Thread() {
            public void run() {
                Image placeholderImageUnpressed = null;
                Image placeholderImagePressed = null;
                Image discardImageButtonImage = null;
                
                try {
                    backgroundImage = Image.createImage(BACKGROUND_IMAGE_URI);
                    placeholderImageUnpressed = Image.createImage(IMAGE_PLACEHOLDER_URI);
                    placeholderImageUnpressed = ImageUtils.pixelMixingScale(placeholderImageUnpressed, IMAGE_WIDTH, IMAGE_HEIGHT);
                    
                    // Highlight color is 0x29a7cc (RGB: 41, 167, 204)
                    placeholderImagePressed = ImageUtils.substractRgb(placeholderImageUnpressed, 214, 88, 51);
                    
                    textBoxPatternImage = ImageUtils.setAlpha(Image.createImage(TEXT_BOX_PATTERN_IMAGE_URI), 220);
                    discardImageButtonImage = Image.createImage(DELETE_BUTTON_IMAGE_URI);
                }
                catch (IOException e) {
                }
                
                Button addImageButton = new Button(placeholderImageUnpressed, placeholderImagePressed, mainView);
                addImageButton.setPosition((getWidth() - placeholderImageUnpressed.getWidth()) / 2, MARGIN * 2);
                buttons[0] = addImageButton;
                
                Button discardImageButton = new Button(discardImageButtonImage, null, mainView);
                discardImageButton.setPosition(0, MARGIN * 2 - discardImageButtonImage.getHeight() / 2);
                buttons[1] = discardImageButton;
                
                repaint();
            }
        }.start();
    }

    /**
     * Populates the menu items. If no tokens are stored, the corresponding
     * discard menu items are not included in the menu.
     */
    private void populateMenu() {
        try {
            removeCommand(discardFacebookTokenCommand);
            removeCommand(aboutCommand);
        }
        catch (Exception e) {
        }
        
        if (appData.getFacebookToken() != null) {
            addCommand(discardFacebookTokenCommand);
        }
        
        addCommand(aboutCommand);
    }

    /**
     * Launches FileSelect UI for image selection. Creates a thumbnail of the
     * selected image if a file was selected.
     */
    private void selectImage() {
        new Thread() {
            public void run() {
                FileSelectDetail[] fileSelectDetails = null;
                
                try {
                    fileSelectDetails = FileSelect.launch(PHOTOS_FOLDER,
                            FileSelect.MEDIA_TYPE_PICTURE, false);
                }
                catch (IllegalArgumentException e) {
                    e.printStackTrace();
                }
                catch (SecurityException e) {
                    return;
                }
                catch (IOException e) {
                    e.printStackTrace();
                }
                
                if (fileSelectDetails == null || fileSelectDetails[0] == null) {
                    // No file selected
                    return;
                }
                
                FileSelectDetail imageDetails = fileSelectDetails[0];
                appData.setSelectedImageDetails(imageDetails);
                
                System.out.println(TAG + "selectImage(): Image file details: "
                    + imageDetails.mimeType + ", "
                    + imageDetails.displayName + ", "
                    + imageDetails.url + ", "
                    + imageDetails.size);
                
                createImageThumbnail();
            }
        }.start();
    }

    /**
     * Creates and displays an image thumbnail of the selected image.
     */
    private void createImageThumbnail() {
        FileSelectDetail imageDetails = appData.getSelectedImageDetails();
        
        if (imageDetails == null || imageDetails.url == null) {
            return;
        }
        
        try {
            selectedImage = ImageUtils.loadImageFromPhone(imageDetails.url);
        }
        catch (SecurityException e) {
            appData.setSelectedImageDetails(null);
            return;
        }
        
        if (selectedImage == null) {
            System.out.println(TAG + "createImageThumbnail(): Failed to load the image!");
            return;
        }
        
        final int width = selectedImage.getWidth();
        final int height = selectedImage.getHeight();
        float ratio = 0;
        
        if (selectedImage.getWidth() > selectedImage.getHeight()) {
            // Landscape image
            ratio = (float)(IMAGE_WIDTH - MARGIN * 2) / width;
        }
        else {
            // Portrait image
            ratio = (float)(IMAGE_HEIGHT - MARGIN * 2) / height;
        }
        
        int newWidth = (int)(width * ratio);
        int newHeight = (int)(height * ratio);
        
        selectedImage = ImageUtils.pixelMixingScale(selectedImage, newWidth, newHeight);
        buttons[BUTTON_INDEX_ADD_IMAGE].setIsDisabled(true);
        final Button discardImageButton = buttons[BUTTON_INDEX_DISCARD_IMAGE];
        discardImageButton.setIsDisabled(false);
        discardImageButton.setPosition(
                (getWidth() - newWidth) / 2 + newWidth - discardImageButton.getWidth() / 2,
                discardImageButton.getPositionY());
        repaint();
    }

    /**
     * Shows an alert dialog with the given message.
     * @param message The message to show.
     * @param alertType The alert type.
     */
    private void showMessage(String message, AlertType alertType) {
        if (Display.getDisplay(midlet).getCurrent() != this) {
            Display.getDisplay(midlet).setCurrent(this);
        }
        
        Alert alert = new Alert(alertType == AlertType.ERROR ? "Error" : "");
        alert.setString(message);
        alert.addCommand(new Command("OK", Command.OK, 0x01));
        
        if (alertType == AlertType.ERROR) {
            alert.setTimeout(Alert.FOREVER);
        }
        else {
            alert.setTimeout(5000);
        }
        
        Display.getDisplay(midlet).setCurrent(alert);
    }
}