WeatherView.java

/*
 * Copyright © 2012 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.TXT for license information.
 */

package com.nokia.example.weatherapp.views;

import com.nokia.example.weatherapp.Main;
import com.nokia.example.weatherapp.ads.*;
import com.nokia.example.weatherapp.components.*;
import com.nokia.example.weatherapp.helpers.*;
import com.nokia.example.weatherapp.location.*;
import com.nokia.example.weatherapp.network.*;
import com.nokia.example.weatherapp.orientation.Orientation;
import com.nokia.example.weatherapp.resources.*;
import com.nokia.mid.ui.VirtualKeyboard;
import java.util.Calendar;

import javax.microedition.lcdui.game.GameCanvas;
import javax.microedition.lcdui.Graphics;
import javax.microedition.lcdui.Image;
import java.util.Timer;
import java.util.TimerTask;
import java.util.Vector;
import javax.microedition.io.ConnectionNotFoundException;
import javax.microedition.lcdui.AlertType;
import javax.microedition.lcdui.game.Sprite;

/**
 * View displaying the weather forecast
 */
public class WeatherView
        extends GameCanvas
        implements View, LocationListener, AdListener, Button.Listener {

    private static final int AD_DURATION = 60000;
    private static final boolean overlayBackBtn = detectCategoryBar();
    private boolean startup = true;
    private boolean closing = false;
    private boolean exitAdShown = false;
    private int day = 0;
    private int downX = -1; // Flick start x coordinate
    private Graphics g;
    private Resources r;
    private Visual v;
    private Timer timer;
    private Timer closingTimer = new Timer();
    private LoaderImage loaderImg;
    private Location currentLocation;
    private LocationFinder locationFinder;
    private Ad ad = null;
    private AdManager adManager;
    private Banner banner;
    private Vector forecasts = null;
    private Vector lines = new Vector();
    private Button prevBtn;
    private Button nextBtn;
    private Sprite windDirection;
    private Sprite background;
    private Calendar calendar = Calendar.getInstance();
    private String message = "";
    private String caption = "";

    public WeatherView() {
        super(false);
        this.setFullScreenMode(false);
        setTitle("WeatherApp");

        g = getGraphics();
        r = Resources.getInstance(getWidth(), getHeight());
        v = Visual.getInstance();
        initialize();
        if (Network.isAllowed()) {
            adManager = AdManager.getInstance(Main.getInstance());
            adManager.setListener(this);
            adManager.fetchBanner(AD_DURATION + 4000);
        }
        setMessage("Welcome to WeatherApp!");

        hideOpenKeypadCommand();
        Orientation.enableOrientations();
    }

    /**
     * Starts the location retrieval
     * @param interval
     */
    public synchronized void startOrUpdateLocationFinder() {
        if (Network.isAllowed() && !closing) {
            if (locationFinder == null) {
                locationFinder = LocationFinder.getLocationFinder();
            }

            if (locationFinder != null) {
                setMessage("Retrieving location...");
                locationFinder.setLocationListener(this);
                locationFinder.start(45);
            }
            else {
                ViewMaster.getInstance().openLocationsView();
            }
        }
        else {
            // App requires network access
            ViewMaster.getInstance().showAlert("Access required", "Network access is required. Please restart "
                    + "and allow network access.", AlertType.INFO);
            setMessage("Network access required.");
            loaderImg.hide();
        }
    }

    /**
     * Cancels location retrieval
     */
    public synchronized void stopLocationFinder() {
        if (locationFinder != null) {
            locationFinder.cancel();
        }
    }

    /**
     * Draws the forecasts and messages
     */
    public void render() {
        Weather w = null;
        Vector f = forecasts;

        if (f != null) {
            try {
                w = (Weather) forecasts.elementAt(day);
            }
            catch (Exception e) {
                // On rare occasions the day might be out of bounds during a weather update.
            }
        }

        g = getGraphics();
        g.setColor(v.COLOR_BACKGROUND);
        g.fillRect(0, 0, getWidth(), getHeight());
        background.paint(g);
        Image clouds = r.getCloudsBack();
        if (clouds != null) {
            g.drawImage(clouds, getWidth() / 2, getHeight(), Graphics.BOTTOM | Graphics.HCENTER);
        }

        if (w == null) {
            g.setFont(v.FONT_MEDIUM);
            g.setColor(v.COLOR_PRIMARY_TEXT);
            int anchorX = getWidth() / 2;
            int anchorY = getHeight() / 8 * 5;
            int length = lines.size();
            for (int i = 0; i < length; i++) {
                g.drawString((String) lines.elementAt(i), anchorX, anchorY, Graphics.TOP | Graphics.HCENTER);
                anchorY += g.getFont().getHeight();
            }
        }
        else {
            drawNaviHeader();
            WeatherLayout.layout(w, g, v, r, this);
        }

        banner.paint(g);
        loaderImg.paint(g);
    }

    /**
     * Draws the navigation header
     */
    private void drawNaviHeader() {
        g.setColor(v.COLOR_HEADER_TEXT);
        g.setFont(v.FONT_MEDIUM_BOLD);
        int anchor = Graphics.TOP | Graphics.HCENTER;
        int y = v.HEIGHT_HEADER / 2 - v.FONT_MEDIUM_BOLD.getHeight() / 2;
        g.drawString(caption, getWidth() / 2, y, anchor);
        nextBtn.paint(g);
        prevBtn.paint(g);
    }

    /**
     * Initializes weather view
     */
    private void initialize() {
        Image img = r.getLoader();
        loaderImg = new LoaderImage(img, getWidth() / 2, getHeight() / 15 * 7,
                                    img.getWidth() / 4, img.getHeight());

        nextBtn = new Button(r.getNextUp(), r.getNextDown());
        nextBtn.setPosition(getWidth() - nextBtn.getWidth(), 0);

        prevBtn = new Button(r.getPrevUp(), r.getPrevDown());
        prevBtn.setPosition(0, 0);

        nextBtn.addListener(this);
        prevBtn.addListener(this);

        img = r.getWindDirection();
        windDirection = new Sprite(img, img.getWidth() / 16, img.getHeight());
        windDirection.defineReferencePixel(img.getHeight(), img.getHeight());

        img = r.getBackground();
        background = new Sprite(img);
        updateBackground();

        // Initialize banner to prevent null pointer exceptions
        banner = new Banner(Image.createImage(1, 1), Image.createImage(1, 1), -1, -1);
    }

    /**
     * Checks the key events
     * @param keyCode
     */
    protected void keyPressed(int keyCode) {
        switch (getGameAction(keyCode)) {
            case GameCanvas.LEFT:
                prevBtn.select();
                break;
            case GameCanvas.RIGHT:
                nextBtn.select();
                break;
            case GameCanvas.UP:
                if (banner.isVisible()) {
                    banner.toggle();
                    ViewMaster.getInstance().showGoCommmand(banner.isSeleted());
                }
                break;
            case GameCanvas.DOWN:
                if (banner.isVisible()) {
                    banner.toggle();
                    ViewMaster.getInstance().showGoCommmand(banner.isSeleted());
                }
        }
    }

    /**
     * Checks the key events
     * @param keyCode
     */
    protected void keyReleased(int keyCode) {
        switch (getGameAction(keyCode)) {
            case GameCanvas.LEFT:
                prevBtn.deselect();
                setDay(day - 1);
                break;
            case GameCanvas.RIGHT:
                nextBtn.deselect();
                setDay(day + 1);
                break;
        }
    }

    /**
     * Changes day
     * @param indx
     */
    public void setDay(int indx) {
        if (forecasts != null) {
            if (indx >= 0 && indx < forecasts.size()) {
                Weather weather = (Weather) forecasts.elementAt(indx);
                day = indx;
                r.clearWeatherIcon();
                windDirection.setFrame((int) ((Integer.parseInt(weather.windDirectionDegrees) + 180) / 22.5 + 0.5) % 16);
            }
            setWeekday();
            updateButtons();
        }
    }

    public int getDay() {
        return day;
    }

    public Sprite getWinddirection() {
        return windDirection;
    }

    /**
     * Updates button visibilities
     */
    private void updateButtons() {
        if (day >= 0 && day < forecasts.size()) {
            prevBtn.setVisible(true);
            nextBtn.setVisible(true);
        }
        if (day == 0) {
            prevBtn.setVisible(false);
        }
        else if (day == forecasts.size() - 1) {
            nextBtn.setVisible(false);
        }
    }

    /**
     * Handles the location updates
     * @param location
     */
    public void locationUpdated(Location location) {
        if (location == null) {
            informLocationNotAvailable();
        }
        else {

            // Create new GET request
            new GetLocationsOperation(new GetLocationsOperation.Listener() {

                // Define callback for the response data
                public synchronized void locationsReceived(Vector locationsData) {
                    Vector locations = locationsData;
                    if (locations == null || locations.isEmpty()) {
                        informLocationNotAvailable();
                    }
                    else {
                        Location received = (Location) locations.elementAt(0);
                        Settings.setLocation(received);
                        setTitle(received.city);
                        updateForecasts();
                    }
                }
            }, location.latitude + "," + location.longitude).start();
        }
    }

    public void informLocationNotAvailable() {
        if (!closing) {
            loaderImg.hide();
            setMessage("Location not available.");
            ViewMaster.getInstance().openLocationsView();
        }
    }

    /**
     * Retrieves forecast for current location
     */
    public void updateForecasts() {
        stopLocationFinder();
        Location location = Settings.location;
        if (!location.city.equals("") && location != currentLocation) {
            currentLocation = location;
            setMessage("Updating forecast...");
            forecasts = null;
            loaderImg.show();

            // Create GET request for forecast
            new GetForecastsOperation(new GetForecastsOperation.Listener() {

                // Define callback for response data
                public synchronized void forecastsReceived(Vector forecastData) {
                    if (!closing) {
                        if (forecastData == null) {
                            setMessage("Could not connect to internet.");
                            ViewMaster.getInstance().showAlert("Network error", "Sorry, cannot connect to internet.",
                                                               AlertType.INFO);
                        }
                        else if (forecastData.isEmpty()) {
                            setMessage("Forecast not available.");
                            ViewMaster.getInstance().showAlert("No forecast",
                                                               "Sorry, unable to retrieve forecast data for current location.",
                                                               AlertType.ERROR);
                        }
                        else {
                            forecasts = forecastData;
                            setDay(0);
                        }
                        loaderImg.hide();
                    }
                }
            }, location.city + "," + location.country).start();
        }
    }

    public void activate() {
        Location location = Settings.location;
        if (!location.city.equals("")) {
            setTitle(Settings.location.city);
            updateForecasts();
        }
        else if (startup && !LocationFinder.supportsLocation()) {
            startup = false;
            loaderImg.hide();
            setMessage("Location unknown.");
            ViewMaster.getInstance().openLocationsView();
        }
        sizeChanged(getWidth(), getHeight());

        // Start the drawing loop
        timer = new Timer();
        timer.schedule(new TimerTask() {

            public void run() {
                render();
                flushGraphics();
            }
        }, 0, 200);
    }

    public void deactivate() {
        if (timer != null) {
            timer.cancel();
            timer = null;
        }
    }

    /**
     * Updates the visual theme according to the mode
     * @param mode
     */
    public void updateMode(int mode) {
        if (mode != Visual.getInstance().getMode()) {
            freeBackground();

            v = Visual.getInstance(mode);
            r.updateMode(mode);

            nextBtn.setReleasedImg(r.getNextUp());
            prevBtn.setReleasedImg(r.getPrevUp());
            Image img = r.getWindDirection();
            windDirection.setImage(img, img.getWidth() / 16, img.getHeight());

            img = r.getBackground();
            background.setImage(img, img.getWidth(), img.getHeight());
            updateBackground();
        }
    }

    /**
     * Handles the button clicks
     * @param button
     */
    public void clicked(Button button) {
        if (button == nextBtn) {
            setDay(day + 1);
        }
        else if (button == prevBtn) {
            setDay(day - 1);
        }
        else if (button == banner) {
            launchAdEndpoint();
        }
    }

    public void selected(Button button) {
    }

    public void deselected(Button button) {
        if (button == banner) {
            ViewMaster.getInstance().showGoCommmand(false);
        }
    }

    /**
     * Shows a full-screen ad before closing the application. Sets also a timer
     * for timeout just in case the ad request is taking too long.
     */
    public void showAdAndExit() {

        setMessage("Closing...");
        closing = true;
        forecasts = null;
        stopLocationFinder();
        banner.setVisible(false);
        loaderImg.show();
        if (adManager != null && Network.isAllowed()) {
            // In case ad retrieval takes too much time,
            // allow user to exit after 12 seconds
            closingTimer.schedule(new TimerTask() {

                public void run() {
                    Main.getInstance().exit();
                }
            }, 12000);
            // Release some memory for the fullscreen ad
            banner.setImage(Image.createRGBImage(new int[]{0x00000000}, 1, 1, true), 1, 1);
            r.clearClouds();

            adManager.showFullscreenAd(-1);
            exitAdShown = true;
        }
        else {
            Main.getInstance().exit();
        }
    }

    public boolean isClosing() {
        return closing;
    }

    /**
     * Launches ad url to browser. On older platforms the application needs to exit before launching
     */
    public void launchAdEndpoint() {
        try {
            Main midlet = Main.getInstance();
            if (midlet.platformRequest(ad.getUrl())) {
                midlet.exit();
            }
        }
        catch (ConnectionNotFoundException cnfe) {
        }
        catch (NullPointerException npe) {
        }
    }

    /**
     * Called when and ad has been returned from the Ad SDK
     * @param ad Contains a banner image and an endpoint url.
     * Ad is null, if the retrieval was unsuccesful.
     */
    public synchronized void bannerReceived(Ad ad) {
        if (!closing) {
            try {
                this.ad = ad;
                Image released = ad.getBanner();

                // Scale the banner for Series 40, unless it's the tiny screen
                if (System.getProperty("microedition.platform").indexOf("S60") == -1 && getWidth() > 128) {
                    float scaling = 1.0f;
                    /*
                     * Java Runtime 2.0 for Series 40 places the back button on top, of the
                     * banner, so the banner needs to be scaled smaller and anchored left
                     */
                    if (overlayBackBtn) {
                        scaling = 50 * 0.6f / released.getHeight();
                    }
                    else {
                        scaling = 50 * 0.74f / released.getHeight();
                    }
                    released = ImageHelper.scaleImage(released, scaling);
                }

                // Draw highlight to the banner
                Image pressed = Image.createImage(released.getWidth(), released.getHeight());
                Graphics bg = pressed.getGraphics();
                bg.setColor(0x76b8cc);
                bg.drawRect(0, 0, pressed.getWidth() - 1, pressed.getHeight() - 3);
                bg.setColor(0x000a42);
                bg.drawRect(1, 1, pressed.getWidth() - 3, pressed.getHeight() - 5);
                bg.drawRegion(released, 2, 2, pressed.getWidth() - 4, pressed.getHeight() - 6, Sprite.TRANS_NONE, 2, 2,
                              Graphics.LEFT | Graphics.TOP);

                banner = new Banner(released, pressed, getHeight(), getHeight() - released.getHeight());
                banner.addListener(this);
                if (overlayBackBtn) {
                    banner.setPosition(10, banner.getOrigin());
                }
                else {
                    banner.setPosition((getWidth() - banner.getWidth()) / 2, banner.getOrigin());
                }

                banner.show(AD_DURATION);
            }
            catch (Exception e) {
                // Received invalid banner
            }
        }
    }

    /*
     * InneractiveAdListener callbacks
     */
    public void inneractiveOnReceiveAd() {
        closingTimer.cancel();
    }

    public void inneractiveOnReceiveDefaultAd() {
        closingTimer.cancel();
    }

    public void inneractiveOnFailedToReceiveAd() {
        Main.getInstance().exit();
    }

    public void inneractiveOnClickAd() {
    }

    public void inneractiveOnSkipAd() {
        Main.getInstance().exit();
    }

    public void showNotify() {
        if (exitAdShown) {
            Main.getInstance().exit();
        }
    }

    /**
     * Handles the orientation switches
     * @param w
     * @param h
     */
    public void sizeChanged(int w, int h) {
        // Update the Graphics object and paint the background with the correct color
        // immediately after orientation change
        g = getGraphics();
        g.setColor(v.COLOR_BACKGROUND);
        g.fillRect(0, 0, getWidth(), getHeight());
        flushGraphics();

        updateBackground();

        loaderImg.setRefPixelPosition(w / 2, h / 15 * 7);
        nextBtn.setPosition(w - nextBtn.getWidth(), 0);
        prevBtn.setPosition(0, 0);
        setMessage(message);
        int offsetY = banner.getOrigin() - banner.getY();
        banner.setOrigin(getHeight());
        banner.setDestination(getHeight() - banner.getHeight() - getHeight() / 48);
        if(overlayBackBtn && getWidth() <= getHeight()) {
            banner.setPosition(10, banner.getOrigin() - offsetY);
        } else {
            banner.setPosition((getWidth() - banner.getWidth()) / 2, banner.getOrigin() - offsetY);
        }
    }

    protected void pointerPressed(int x, int y) {
        nextBtn.touchDown(x, y);
        prevBtn.touchDown(x, y);
        downX = x;
        banner.touchDown(x, y);
    }

    protected void pointerReleased(int x, int y) {
        nextBtn.touchUp(x, y);
        prevBtn.touchUp(x, y);
        banner.touchUp(x, y);
    }

    protected void pointerDragged(int x, int y) {
        int flickX = (int) (x - downX) / 60;
        if (downX > 0 && Math.abs(flickX) == 1) {
            setDay(day - flickX);
            downX = -1;
        }
    }

    /**
     * Sets the current shown week day
     */
    private void setWeekday() {
        int weekday = (calendar.get(Calendar.DAY_OF_WEEK) - 1 + day) % 7 + 1;
        String str = "";
        switch (weekday) {
            case Calendar.MONDAY:
                str = "Monday";
                break;
            case Calendar.TUESDAY:
                str = "Tuesday";
                break;
            case Calendar.WEDNESDAY:
                str = "Wednesday";
                break;
            case Calendar.THURSDAY:
                str = "Thursday";
                break;
            case Calendar.FRIDAY:
                str = "Friday";
                break;
            case Calendar.SATURDAY:
                str = "Saturday";
                break;
            case Calendar.SUNDAY:
                str = "Sunday";
                break;
        }
        if (day == 0) {
            str = "Today (" + str.substring(0, 3) + ")";
        }
        caption = str;
    }

    private void setMessage(String msg) {
        if (!closing) {
            message = msg;
            lines = TextWrapper.wrapTextToWidth(msg, getWidth() / 10 * 9, v.FONT_MEDIUM);
        }
    }

    /**
     * Releases biggest resources
     */
    private void freeBackground() {
        // Release reference to current background avoiding also null pointer exception
        Image img = Image.createRGBImage(new int[]{0x00000000}, 1, 1, true);
        background.setImage(img, 1, 1);
        r.clearBackground();
        System.gc();
    }

    private void hideOpenKeypadCommand() {
        try {
            String keypad = System.getProperty("com.nokia.keyboard.type");
            if (keypad.equals("None")) {
                // This is a full touch device
                VirtualKeyboard.hideOpenKeypadCommand(true);
            }
        }
        // If this fails, there's probably no need for hiding the command
        catch (NoClassDefFoundError ncdfe) {
        }
        catch (Exception e) {
        }
    }

    /**
     * Rotates and centers background according to screen orientation
     */
    private void updateBackground() {
        if (getWidth() > getHeight()) {
            background.setTransform(Sprite.TRANS_ROT90);
        }
        else {
            background.setTransform(Sprite.TRANS_NONE);
        }
        background.setPosition((getWidth() - background.getWidth()) / 2, (getHeight() - background.getHeight()) / 2);
    }

    /**
     * Check if CategoryBar class is present
     */
    private static boolean detectCategoryBar() {
        try {
            Class.forName("com.nokia.mid.ui.CategoryBar");
            return true;
        }
        catch (Exception e) {
            return false;
        }
    }
}