Implementation

For information about the design and functionality of the MIDlet, see section Design.

For information about the key aspects of implementing the MIDlet, see:

Basic UI structure

The MIDlet main view is basically a Canvas that draws forecasts for the selected day. All the settings views and location-related views use plain platform-styled LCDUI elements. View changing is handled by the ViewMaster class, which also takes care of storing views into a view stack. The view stack is used for the back navigation functionality.

Determining the device location

The available positioning methods are:

  • Assisted GPS

  • Standalone GPS

  • Offline cell ID

  • Online cell ID

  • WLAN

The LocationUtil extension of the Location API can be used to specify the desired positioning method. However, LocationUtil is only available on Series 40 devices with Java Runtime 1.0.0 for Series 40 or newer. Symbian devices and earlier Series 40 devices do not support LocationUtil. Note also that cell ID positioning can only be performed with the getLocation method, whereas GPS positioning can be implemented by using a LocationListener. In other words, to be able to listen for location changes when using the cell ID positioning approach, the getLocation method must be called frequently in a loop. However, if the MIDlet is not signed, calling getLocation in a loop causes an access prompt on every call. For more information about listening for location changes, see article Best practises for listening to location updates with Java ME in the Nokia Developer Wiki.

To ensure that the MIDlet runs on devices that do not support LocationUtil or the Location API, both APIs must be hidden from the code execution, so that they are not loaded automatically. This means that neither the com.nokia.mid.location package nor the javax.microedition.location package can be imported inside classes that need to run on any device. Therefore, classes that use either LocationUtil or the Location API must be isolated from the rest of the MIDlet. These classes can then be loaded at runtime using the Class.forName(<PACKAGE>.<CLASS_NAME>) method. This method throws an exception if a class is missing (for example, LocationUtil). However, the exception can be caught, necessary actions can be taken, and the MIDlet can try to use the next-best positioning method. For instructions on how to implement this approach, see article How to use an optional API in Java ME in the Nokia Developer Wiki.

Manual location search

The manual location search is used for finding cities when there is no automatic way to retrieve a location or when the user wants to know the weather forecast for a city other than the one they are currently located in. The manual location search works asynchronously and automatically sends an HTTP search request when the input in the text field changes:

    /**
     * Performs a location search, if text input changes.
     * @param item
     */
    public void itemStateChanged(Item item) {
        if (item instanceof TextField && !searchField.getString().equals(
                lastSearch)) {
            throttleSearch();
        }
    }

To limit the number of searches, the MIDlet implements a mechanism that delays the search by 1000 milliseconds. The timer is reset every time the user types in the text field. This is called throttling. The search additionally gives the user a "search as you type" kind of functionality. The search is implemented as follows:

    /**
     * Delays location search and cancels a possible pending search
     */
    public void throttleSearch() {
        if (throttle != null) {
            throttle.cancel();
        }

        throttle = new Timer();
        throttle.schedule(new TimerTask() {

            public void run() {
                searchLocations();
                cancel();
            }
        }, 1000);
    }

The search view consists of a Form, a TextField, and a set of CustomItems. CustomItems are used because a regular List cannot be appended to a Form other than a ChoiceGroup, which, in turn, can only contain check boxes and radio buttons.

Recent locations

All locations used by the MIDlet are stored in the recent locations view. Locations are saved in a RecordStore, so that they persist to the next time the user runs the MIDlet. The following code snippet shows how to save and load locations.

Note: On Symbian devices, the elements are stored in opposite order than on Series 40 devices.

    private static void saveLocations() {
        try {
            RecordStore.deleteRecordStore("locations"); // Clear data
        }
        catch (Exception e) { /* Nothing to delete */ }

        try {
            RecordStore rs = RecordStore.openRecordStore("locations", true);
            int count = recentLocations.size();
            for (int i = 0; i < count; i++) {
                ByteArrayOutputStream baos = new ByteArrayOutputStream();
                DataOutputStream dos = new DataOutputStream(baos);
                Location lctn = (Location) recentLocations.elementAt(i);
                dos.writeUTF(lctn.city);
                dos.writeUTF(lctn.country);
                byte[] b = baos.toByteArray();
                // Add it to the record store
                rs.addRecord(b, 0, b.length);
            }
            rs.closeRecordStore();
        }
        catch (RecordStoreException rse) {
            rse.printStackTrace();
        }
        catch (IOException ioe) {
            ioe.printStackTrace();
        }
    }

    // ...

    private static void loadLocations() {
        try {
            boolean symbian = System.getProperty("microedition.platform").indexOf("S60") > -1;
            RecordStore rs = RecordStore.openRecordStore("locations", true);
            RecordEnumeration re = rs.enumerateRecords(null, null, true);
            while (re.hasNextElement()) {
                int id = re.nextRecordId();
                ByteArrayInputStream bais = new ByteArrayInputStream(rs.getRecord(id));
                DataInputStream dis = new DataInputStream(bais);
                try {
                    Location location = new Location();
                    location.city = dis.readUTF();
                    location.country = dis.readUTF();
                    if (symbian) { // On Symbian the elements have been stored in opposite order compared to S40
                        recentLocations.addElement(location);
                    }
                    else {
                        recentLocations.insertElementAt(location, 0);
                    }
                }
                catch (EOFException eofe) {
                    eofe.printStackTrace();
                }
            }
            rs.closeRecordStore();
        }
        catch (RecordStoreException rse) {
            rse.printStackTrace();
        }
        catch (IOException ioe) {
            ioe.printStackTrace();
        }
    }

Requesting weather forecast data and city locations

You can request weather forecast data using World Weather Online's free Local Weather API with a simple GET request. The GET request must include the location for which you want to request the forecast data, the response format, the number of days for which you want to request the forecast data, and your API key. The location is defined as a city name and optionally country name, IP address, ZIP code, or geographic coordinates. The WeatherApp MIDlet uses both city and country names and latitude-longitude pairs for requesting forecast data, depending on whether the positioning is performed automatically or based on manual location search. The WeatherApp MIDlet uses the JSON format, but the forecast data is also available in XML and CSV formats. You can obtain the API key from ​World Weather Online. The web site also provides a free Local Weather API Request Builder for generating request URLs.

The following is an example of a weather forecast data request:

http://free.worldweatheronline.com/feed/weather.ashx?q=London,United+Kingdom&format=json&num_of_days=5&key=<YOUR API KEY>

World Weather Online also provides the City/Location Search API. The WeatherApp MIDlet uses the API for searching actual locations based on a city name or part of a city name. The free version of the API limits the maximum number of possible responses to three.

The following is an example of a city location request:

http://www.worldweatheronline.com/feed/search.ashx?query=London&format=JSON&num_of_results=3&key=<YOUR API KEY>

JSON parsing

JSON parsing is simple to implement with the org.json.me library. However, you need to keep a few things in mind. Firstly, JSON is parsed using JSONArrays and JSONObjects, so keep an eye on the square brackets to distinguish when a JSONArray needs to be used over a JSONObject. The following JSON is part of a weather forecast response from World Weather Online.

{
    "data": {
        "current_condition": [
            {
                "cloudcover": "75",
                "humidity": "87",
                "observation_time": "02:41 PM",
                "precipMM": "3.0",
                "pressure": "991",
                "temp_C": "8",
                "temp_F": "46",
                "visibility": "10",
                "weatherCode": "302",
                "weatherDesc": [
                    {
                        "value": "Moderate rain"
                    }
                ],
                "weatherIconUrl": [
                    {
                        "value": "http://www.worldweatheronline.com/images/wsymbols01_png_64/wsymbol_0018_cloudy_with_heavy_rain.png"
                    }
                ],
                "winddir16Point": "ESE",
                "winddirDegree": "120",
                "windspeedKmph": "24",
                "windspeedMiles": "15"
            }
        ],
        "request": [
            {
                "query": "London, United Kingdom",
                "type": "City"
            }
        ],
        "weather": [
            {
                "date": "2012-04-23",
                "precipMM": "6.3",
                "tempMaxC": "9",
                "tempMaxF": "48",
                "tempMinC": "4",
                "tempMinF": "38",
                "weatherCode": "266",
                "weatherDesc": [
                    {
                        "value": "Light drizzle"
                    }
                ],
                "weatherIconUrl": [
                    {
                        "value": "http://www.worldweatheronline.com/images/wsymbols01_png_64/wsymbol_0017_cloudy_with_light_rain.png"
                    }
                ],
                "winddir16Point": "ESE",
                "winddirDegree": "109",
                "winddirection": "ESE",
                "windspeedKmph": "22",
                "windspeedMiles": "14"
            },
            .
            .
            .
        ]
    }
}

Secondly, a response can contain required and optional keys. Required keys can be parsed using the getString method, which throws a JSONException if a required key cannot be found. The optString method comes handy when a specific key is optional in the response. The method does not interrupt the parsing by throwing a JSONException but returns a default value instead. The default value can be set through a method parameter. If the default value is not specified, the method returns an empty string. The methods in the following code snippet parse a forecast response and turn it into a collection of Weather objects. For more information, see the org.json reference.

    /**
     * Parses JSON response
     * @param response JSON response as string
     * @throws ParseError
     */
    public void parse(String response) throws ParseError {
        try {
            JSONObject obj = new JSONObject(response);
            if (obj.isNull("data")) {
                return;
            }
            JSONObject data = obj.getJSONObject("data");
            JSONArray currentCondition = data.getJSONArray("current_condition");
            forecasts.addElement(parseWeather(currentCondition.getJSONObject(0)));

            JSONArray upcomingConditions = data.getJSONArray("weather");
            int length = data.length();
            for (int i = 0; i < length; ++i) {
                forecasts.addElement(parseWeather(upcomingConditions.getJSONObject(i)));
            }
        }
        catch (Exception e) {
            throw new ParseError(e.getMessage());
        }
    }

    /**
     * Parses a single day from response
     * @param weather JSONObject containing weather data for one day
     * @return Populated Weather object
     * @throws JSONException
     */
    private Weather parseWeather(JSONObject weather) throws JSONException {
        Weather w = new Weather();
        w.humidity = weather.optString("humidity", "");
        w.temperatureC = weather.optString("temp_C", "");
        w.temperatureF = weather.optString("temp_F", "");
        w.minTempC = weather.optString("tempMinC", "");
        w.minTempF = weather.optString("tempMinF", "");
        w.maxTempC = weather.optString("tempMaxC", "");
        w.maxTempF = weather.optString("tempMaxF", "");
        w.windDirectionDegrees = weather.getString("winddirDegree");
        w.windDirectionPoints = weather.getString("winddir16Point");
        w.windSpeedKmph = weather.getString("windspeedKmph");
        w.windSpeedMph = weather.getString("windspeedMiles");

        JSONArray description = weather.getJSONArray("weatherDesc");
        w.description = description.getJSONObject(0).getString("value");

        JSONArray iconUrl = weather.getJSONArray("weatherIconUrl");
        w.iconUrl = iconUrl.getJSONObject(0).getString("value");
        return w;
    }

In-app advertising

The MIDlet employs in-app advertising in the form of banners and full-screen ads. In-app advertising allows you to monetize your application by showing ads provided by Inneractive. Ads are monetized through impressions and clicks. The click-through rate, which is the ratio between impressions and clicks, also affects the revenue.

The user must be able to click a banner. When the banner is clicked, an advertisement endpoint URL is launched inside a browser. On touch devices, it is easy to just tap the banner, but non-touch devices additionally require focus handling, which allows the user to first select the banner and then click it.

In the WeatherApp MIDlet, a full-screen ad is shown when the user is about to exit the MIDlet. In addition, banner ads are shown at the bottom of the screen. One banner is shown for 60 seconds and then changed to a new one with a sliding animation. Since the average usage time of the MIDlet is likely fairly short, the ads need to be updated quite frequently. Inneractive recommends using an interval of about 2-3 minutes.

It is recommended that you retrieve ads inside a worker thread, so that the main thread does not get blocked. The following code snippet shows how this is implemented in the WeatherApp MIDlet.

    private boolean running = false;
    private AdListener listener = null;
    private static MIDlet context;

    /**
     * Thread executing the ad retrieval
     */
    private class Worker
            extends Thread {

        private int task = 0;
        private long interval = -1;

        public synchronized void run() {
            running = true;
            while (running) {
                if (Network.isAllowed()) {
                    switch (task) {
                        case DISPLAY_FULL_AD:
                            if (listener == null) {
                                IADView.displayInterstitialAd(context);
                            }
                            else {
                                IADView.displayInterstitialAd(context, listener);
                            }
                            break;
                        case GET_BANNER_AD:
                            Ad ad = null;
                            try {
                                ad = new Ad(IADView.getBannerAdData(context));
                            }
                            catch (IllegalArgumentException iae) {
                            }
                            if (listener != null) {
                                listener.bannerReceived(ad);
                            }
                            break;
                    }
                }
                try {
                    if (interval < 0) {
                        task = IDLE;
                        wait();
                    }
                    else {
                        wait(interval);
                    }
                }
                catch (InterruptedException ex) {
                }
            }
        }

        public synchronized void doTask(int task, long interval) {
            if (context == null) {
                return;
            }
            this.task = task;
            this.interval = interval;
            notify();
        }
    }

Adding ads to the MIDlet requires some modifications to the layout, since there must be some extra space for the banners. To accommodate the banners, an additional layout was designed for landscape mode and smaller screen sizes. In addition, it must be noted that full-screen ads take up a lot of memory, so it is advisable to free as much memory as possible before showing an ad.

To implement in-app advertising, you need the Ad SDK, which can be downloaded from the Inneractive web site. The Ad SDK contains examples that demonstrate how to use it. The Ad SDK also contains an ad placement strategy guide and a step-by-step guide for adding in-app advertising to your application.

The Ad SDK requires that you specify some application attributes in the MIDlet JAD file, for example:

  • IA-X-appID (mandatory)

    The IA-X-appID attribute specifies the application ID. The ID is provided during the Inneractive registration process. It is used for directing ad revenue from impressions and clicks to you. IA-X-appID is the only mandatory attribute required by the Ad SDK. The WeatherApp MIDlet uses IA_GameTest as its application ID.

  • IA-X-distID (optional)

    The IA-X-distID attribute specifies the distribution ID and is used for validating ad requests, optimizing performance, and determining ad type. The value 551 must be used for banner ads and full screen ads.

  • IA-X-keywords (optional)

    The IA-X-keywords attribute is used for describing topics for ads, so that only ads relevant to the user's specific session are shown.

  • IA-X-gpsCoordinates and IA-X-location (optional)

    The IA-X-gpsCoordinates and IA-X-location attributes are used for ad localization.

For more information about these and other optional attributes, see the Ad SDK documentation.

Night and day modes

The MIDlet has different visual styles for night and day modes. The mode is selected according to the weather icon URL in the HTTP response, meaning that ultimately World Weather Online decides when day time ends and night begins.

Setting the UI orientation

The Series 40 full touch version of the MIDlet uses the Orientation API to detect display orientation changes and adjust its UI orientation accordingly. To adjust the UI orientation using the Orientation API, the Nokia-MIDlet-App-Orientation attribute must be declared with the value manual in the MIDlet JAD file:

Nokia-MIDlet-App-Orientation: manual

To ensure that the MIDlet runs on devices that do not support the Orientation API, the API, like the LocationUtil class and Location API, is wrapped so that it is isolated from the rest of the MIDlet and not loaded automatically when the MIDlet is run. The MIDlet uses the custom OrientationImpl class to implement the actual orientation support, and the custom Orientation class to load the implementation.

The OrientationImpl class uses the static Orientation.addOrientationListener method to register an OrientationListener, and the static Orientation.setAppOrientation method to set the UI orientation when the display orientation changes. The Orientation.setAppOrientation method is called from the mandatory displayOrientationChanged callback method, which every OrientationListener must implement, and which is called every time the display orientation changes. The following code snippets shows the implementation of the OrientationImpl class.

class OrientationImpl
        extends Orientation
        implements com.nokia.mid.ui.orientation.OrientationListener {

    OrientationImpl() {
        // Listen for orientation events
        com.nokia.mid.ui.orientation.Orientation.addOrientationListener(this);
    }

    /**
     * Deliver sizeChanged event to listener
     * @param newDisplayOrientation
     */
    public void displayOrientationChanged(int newDisplayOrientation) {
        com.nokia.mid.ui.orientation.Orientation.setAppOrientation(newDisplayOrientation);
    }
}

For instructions on how to implement the wrapper approach, see article How to use an optional API in Java ME in the Nokia Developer Wiki.

Adding an IconCommand

The Series 40 full touch version of the MIDlet uses the IconCommand class to map a custom "Add" command in the form of a plus icon to action button 1 in the header bar of the full touch UI. The IconCommand allows the user to add new locations to the recent locations view.

Figure: IconCommand as a plus icon in the header bar (action button 1)

To ensure that the MIDlet runs on devices that do not support IconCommands, the IconCommand implementation is wrapped inside a factory class that can be used to produce IconCommands when they are supported by the device. If the factory class returns an IconCommand, it is used to replace the default Command mapped to action button 1.

The MIDlet uses the abstract IconCommandFactory class to create IconCommands. The static IconCommandFactory.getIconCommand method returns the IconCommand instance. The method is static so that the IconCommandFactory class does not need to be explicitly instantiated by the caller. However, the IconCommandFactory class is instantiated in a static context, which allows the class to use the IconCommandFactoryImplementation.createIconCommand method internally to create the actual IconCommand instance returned by the IconCommandFactory.getIconCommand method. The IconCommandFactory class implements only one of the four possible constructors for IconCommand, but the other three could be easily implemented in the same manner.

The following code snippet shows the implementation of the IconCommandFactory class.

import javax.microedition.lcdui.Command;
import javax.microedition.lcdui.Image;

/**
 * Procudes icon commands, if the platform supports them.
 * IconCommand is supported from Java Runtime 2.0.0 for Series 40 onwards.
 */
public abstract class IconCommandFactory {

    private static IconCommandFactory implementation = null;

    /**
     * Try to instantiate IconCommandFactory
     */
    static {
        try {
            Class.forName("com.nokia.mid.ui.IconCommand");
            Class c = Class.forName("com.nokia.example.weatherapp.components.IconCommandFactoryImplementation");
            implementation = (IconCommandFactory) (c.newInstance());
        }
        catch (Exception e) {
            // Icon commands not supported
        }
    }

    protected IconCommandFactory() {
    }

    /*
     * Creates an IconCommand
     */
    public abstract Command createIconCommand(String label, Image upState, Image downState, int type, int priority);

    /**
     * Returns new IconCommand or null, if IconCommands are not supported
     */
    public static Command getIconCommand(String label, Image upState, Image downState, int type, int priority) {
        if (implementation == null) {
            return null;
        }
        return implementation.createIconCommand(label, upState, downState, type, priority);
    }

    public static boolean iconCommandsSupported() {
        return implementation != null ? true : false;
    }
}

/**
 * Creates an IconCommand. Hides the usage of IconCommand from the linker
 */
class IconCommandFactoryImplementation
        extends IconCommandFactory {

    protected IconCommandFactoryImplementation() {
    }

    public Command createIconCommand(String label, Image upState, Image downState, int type, int priority) {
        return new com.nokia.mid.ui.IconCommand(label, upState, downState, type, priority);
    }
}

The following code snippet shows how to create an IconCommand by calling the factory class, and use the IconCommand to replace the default Command.

        Resources r = Resources.getInstance(locationsView.getWidth(), locationsView.getHeight());
        if (IconCommandFactory.iconCommandsSupported()) {
            addCmd = IconCommandFactory.getIconCommand("Add", r.getAddIcon(), r.getAddIcon(), IconCommand.SCREEN, 1);
        }

        locationsView.addCommand(addCmd);

Concluding notes

While parsing JSON values is straightforward with the org.json.me library, retrieving the location is a bit more complicated. Extra prompts and long waiting times can irritate users, which is why it is important to evaluate the best method for each use case. Cell ID positioning is fast, but inaccurate, whereas GPS can be much more accurate, but can introduce longer delays. In addition, the application flow must be designed so that the application can flexibly use a different positioning method if the preferred one is unavailable on a particular device. Finally, the number of prompts must be kept to a minimum. Asking the same thing twice is already one time too many. This applies both to network access prompts and positioning prompts.

CustomItems provide a way to handle UI elements that are not possible to implement with LCDUI elements. However, there is a lot of variation between different platforms and platform versions in terms of how they implement CustomItems. For example, retaining the native platform look on a CustomItem can be difficult to accomplish.

Known issues

  • When mist, fog, or black clouds are forecast, the visual style always follows the day mode.

  • On some devices, network access is set to "ask always" by default. On Series 40 devices, the setting can be changed by scrolling down to the MIDlet in question and selecting Options > Application access. On Symbian devices, the setting can be change in the Application Manager.

  • In the search view, existing list items do not change size when changing orientation. This is because of a bug in the Symbian platform, which would turn the list items into white boxes instead.

  • On Symbian Belle devices, softkey labeling is not possible. In a list view, the right softkey can be either an "OK" tick mark or an options menu icon. For example, to prevent the Add command from turning into a tick mark in the locations view, the command type has been set to EXIT with a lower priority than the existing BACK button. This way the command appears under the options menu.