com.phonegap.geolocation.Geolocation.java Source code

Java tutorial

Introduction

Here is the source code for com.phonegap.geolocation.Geolocation.java

Source

/*
 * PhoneGap is available under *either* the terms of the modified BSD license *or* the
 * MIT License (2008). See http://opensource.org/licenses/alphabetical for full text.
 * 
 * Copyright (c) 2005-2010, Nitobi Software Inc.
 * Copyright (c) 2010, IBM Corporation
 */
package com.phonegap.geolocation;

import java.util.Date;
import java.util.Enumeration;
import java.util.Hashtable;

import javax.microedition.location.Criteria;
import javax.microedition.location.Location;
import javax.microedition.location.LocationException;
import javax.microedition.location.LocationProvider;

import net.rim.device.api.gps.BlackBerryCriteria;
import net.rim.device.api.gps.BlackBerryLocationProvider;
import net.rim.device.api.gps.GPSInfo;

import org.json.me.JSONArray;
import org.json.me.JSONException;
import org.json.me.JSONObject;

import com.phonegap.PhoneGapExtension;
import com.phonegap.api.Plugin;
import com.phonegap.api.PluginResult;
import com.phonegap.util.Logger;

public class Geolocation extends Plugin {

    /**
     * Possible actions.
     */
    protected static final int ACTION_WATCH = 0;
    protected static final int ACTION_CLEAR_WATCH = 1;
    protected static final int ACTION_GET_POSITION = 2;
    protected static final int ACTION_SHUTDOWN = 3;

    /**
     * Callback ID argument index.
     */
    protected static final int ARG_CALLBACK_ID = 0;

    /**
     * Minimum GPS accuracy (meters).
     */
    protected static final float MIN_GPS_ACCURACY = 10F; // meters

    /**
     * Hash of all the listeners created, keyed on callback ids.
     */
    protected final Hashtable geoListeners;

    /**
     * Constructor.
     */
    public Geolocation() {
        this.geoListeners = new Hashtable();
    }

    /**
     * Executes the specified geolocation action.
     *  
     * @param action 
     *      "getCurrentPosition" - Retrieves current location.
     *      "watchPosition"      - Establishes a location provider that is keyed on specified position options
     *                           and attaches a listener that notifies registered callbacks of location updates.
     *    "stop"               - Clears the watch identified by the watch ID that must be specified in args.
     *    "shutdown"           - Stops all listeners and resets all location providers.
     * @param callbackId callback managed by the plugin manager (ignored)
     * @param args contains the callback id and position options 
     */
    public PluginResult execute(String action, JSONArray args, String callbackId) {

        /*
         * The geolocation plugin bypasses the plugin callback framework for 
         * success callbacks because the current implementation of the framework 
         * deletes the callbacks after they have been called.  The geolocation 
         * listener callbacks need to continue listening for location changes, 
         * and are therefore managed separately from the plugin framework.  
         * 
         * This means the invoking script must pass the listener callback ID in 
         * the args parameter (along with the position options).  The callbackId
         * parameter (used by the plugin framework) is ignored.
         * 
         * The invoking script should still provide a failure callback so the 
         * plugin framework can handle general error reporting.
         */
        String listenerCallbackId;
        try {
            listenerCallbackId = args.getString(ARG_CALLBACK_ID);
        } catch (JSONException e) {
            return new PluginResult(PluginResult.Status.JSONEXCEPTION, "Callback ID argument is not valid.");
        }

        if (!GPSInfo.isGPSModeAvailable(GPSInfo.GPS_DEVICE_INTERNAL)) {
            return new PluginResult(GeolocationStatus.GPS_NOT_AVAILABLE);
        }

        PositionOptions options;
        switch (getAction(action)) {
        case ACTION_CLEAR_WATCH:
            clearWatch(listenerCallbackId);
            return null;

        case ACTION_WATCH:

            try {
                options = PositionOptions.fromJSONArray(args);
            } catch (NumberFormatException e) {
                return new PluginResult(PluginResult.Status.ILLEGAL_ARGUMENT_EXCEPTION,
                        "One of the position options is not a valid number.");
            } catch (JSONException e) {
                return new PluginResult(PluginResult.Status.JSONEXCEPTION,
                        "One of the position options is not valid JSON.");
            }

            this.watchPosition(listenerCallbackId, options);
            return null;

        case ACTION_GET_POSITION:

            try {
                options = PositionOptions.fromJSONArray(args);
            } catch (NumberFormatException e) {
                return new PluginResult(PluginResult.Status.ILLEGAL_ARGUMENT_EXCEPTION,
                        "One of the position options is not a valid number.");
            } catch (JSONException e) {
                return new PluginResult(PluginResult.Status.JSONEXCEPTION,
                        "One of the position options is not valid JSON.");
            }

            this.getCurrentPosition(listenerCallbackId, options);
            return null;

        case ACTION_SHUTDOWN:
            this.shutdown();
            return null;
        }

        return new PluginResult(PluginResult.Status.INVALIDACTION, "Geolocation: invalid action " + action);
    }

    /**
     * Checks if the provided location is valid.
     * @param location
     * @return true if the location is valid
     */
    protected boolean isLocationValid(Location location) {
        return location != null && location.isValid();
    }

    /**
     * Checks if the provided location is fresh or not.
     * @param po           position options containing maximum location age allowed
     * @param location     location object
     * @return true if the location is newer than maximum age allowed
     */
    protected boolean isLocationFresh(PositionOptions po, Location location) {
        return new Date().getTime() - location.getTimestamp() < po.maxAge;
    }

    /**
     * Checks if the accuracy of the location is high enough.
     * @param po           position options containing high accuracy flag
     * @param location     location object
     * @return true if the location accuracy is lower than MIN_GPS_ACCURACY
     */
    protected boolean isLocationAccurate(PositionOptions po, Location location) {
        return po.enableHighAccuracy
                && location.getQualifiedCoordinates().getHorizontalAccuracy() < MIN_GPS_ACCURACY;
    }

    /**
     * Retrieves a location provider with some criteria.
     * @param po position options
     */
    protected static LocationProvider getLocationProvider(PositionOptions po) {
        // configure criteria for location provider
        // Note: being too restrictive will make it less likely that one will be returned
        BlackBerryCriteria criteria = new BlackBerryCriteria();

        // can we get GPS info from the wifi network?
        if (GPSInfo.isGPSModeAvailable(GPSInfo.GPS_MODE_ASSIST))
            criteria.setMode(GPSInfo.GPS_MODE_ASSIST);
        // relies on device GPS receiver - not good indoors or if obstructed
        else if (GPSInfo.isGPSModeAvailable(GPSInfo.GPS_MODE_AUTONOMOUS))
            criteria.setMode(GPSInfo.GPS_MODE_AUTONOMOUS);

        criteria.setAltitudeRequired(true);

        // enable full power usage to increase location accuracy
        if (po.enableHighAccuracy) {
            criteria.setPreferredPowerConsumption(Criteria.POWER_USAGE_HIGH);
        }

        // Attempt to get a location provider
        BlackBerryLocationProvider provider;
        try {
            // Note: this could return an existing provider that meets above criteria
            provider = (BlackBerryLocationProvider) LocationProvider.getInstance(criteria);
        } catch (LocationException e) {
            // all LocationProviders are currently permanently unavailable :(
            provider = null;
        }

        return provider;
    }

    /**
     * Gets the current location, then creates a location listener to receive
     * updates. Registers the specified callback with the listener.
     * @param callbackId   callback to receive location updates
     * @param options      position options
     */
    protected void watchPosition(String callbackId, PositionOptions options) {

        // attempt to retrieve a location provider 
        LocationProvider provider = getLocationProvider(options);
        if (provider == null) {
            PhoneGapExtension.invokeErrorCallback(callbackId,
                    new GeolocationResult(GeolocationStatus.GPS_NOT_AVAILABLE));
            return;
        }

        // create a listener for location updates
        GeolocationListener listener;
        try {
            listener = new GeolocationListener(provider, callbackId, options);
        } catch (IllegalArgumentException e) {
            // if    interval < -1, or 
            // if    (interval != -1) and 
            //      (timeout > interval or maxAge > interval or 
            //         (timeout < 1 and timeout != -1) or 
            //         (maxAge < 1 and maxAge != -1)
            //      ) 
            PhoneGapExtension.invokeErrorCallback(callbackId,
                    new GeolocationResult(GeolocationStatus.GPS_ILLEGAL_ARGUMENT_EXCEPTION, e.getMessage()));
            return;
        }

        // store the listener
        addListener(callbackId, listener);
    }

    /**
     * Shuts down all location listeners.
     */
    protected synchronized void shutdown() {
        for (Enumeration listeners = this.geoListeners.elements(); listeners.hasMoreElements();) {
            GeolocationListener listener = (GeolocationListener) listeners.nextElement();
            listener.shutdown();
        }
        this.geoListeners.clear();
    }

    /**
     * Clears the watch for the specified callback id.
     * If no more watches exist for the location provider, it is shut down.
     * @param callbackId   identifer of the listener to shutdown
     */
    protected void clearWatch(String callbackId) {
        synchronized (this.geoListeners) {
            GeolocationListener listener = (GeolocationListener) this.geoListeners.get(callbackId);
            listener.shutdown();
            this.geoListeners.remove(callbackId);
        }
    }

    /**
     * Returns a PluginResult with status OK and a JSON object representing the coords
     * @param callbackId   callback to receive the the result
     * @param po           position options
     */
    protected void getCurrentPosition(String callbackId, PositionOptions options) {

        // Check the device for its last known location (may have come from 
        // another app on the device that has already requested a location).
        // If it is invalid, old, or inaccurate, attempt to get a new one.
        Location location = LocationProvider.getLastKnownLocation();
        if (!isLocationValid(location) || !isLocationFresh(options, location)
                || !isLocationAccurate(options, location)) {
            // attempt to retrieve a location provider 
            LocationProvider provider = getLocationProvider(options);
            if (provider == null) {
                PhoneGapExtension.invokeErrorCallback(callbackId,
                        new GeolocationResult(GeolocationStatus.GPS_NOT_AVAILABLE));
                return;
            }

            try {
                // convert timeout from millis
                int timeout = (options.timeout > 0) ? options.timeout / 1000 : -1;
                Logger.log(this.getClass().getName() + ": retrieving location with timeout=" + timeout);
                location = provider.getLocation(timeout);
            } catch (LocationException e) {
                Logger.log(this.getClass().getName() + ": " + e.getMessage());
                provider.reset();
                PhoneGapExtension.invokeErrorCallback(callbackId,
                        new GeolocationResult(GeolocationStatus.GPS_TIMEOUT));
                return;
            } catch (InterruptedException e) {
                Logger.log(this.getClass().getName() + ": " + e.getMessage());
                provider.reset();
                PhoneGapExtension.invokeErrorCallback(callbackId,
                        new GeolocationResult(GeolocationStatus.GPS_INTERUPTED_EXCEPTION));
                return;
            }
        }

        // send the location back
        sendLocation(callbackId, location);
    }

    /**
     * Converts the location to a geo position and sends result to JavaScript.
     * @param callbackId   callback to receive position
     * @param location     location to send
     */
    protected void sendLocation(String callbackId, Location location) {
        // convert the location to a JSON object and return it in the PluginResult
        JSONObject position = null;
        try {
            position = Position.fromLocation(location).toJSONObject();
        } catch (JSONException e) {
            PhoneGapExtension.invokeErrorCallback(callbackId, new GeolocationResult(
                    PluginResult.Status.JSONEXCEPTION, "Converting the location to a JSON object failed"));
            return;
        }

        // invoke the geolocation callback
        PhoneGapExtension.invokeSuccessCallback(callbackId,
                new GeolocationResult(GeolocationResult.Status.OK, position));
    }

    /**
     * Returns action to perform.
     * @param action 
     * @return action to perform
     */
    protected static int getAction(String action) {
        if ("watchPosition".equals(action))
            return ACTION_WATCH;
        if ("stop".equals(action))
            return ACTION_CLEAR_WATCH;
        if ("getCurrentPosition".equals(action))
            return ACTION_GET_POSITION;
        if ("shutdown".endsWith(action))
            return ACTION_SHUTDOWN;
        return -1;
    }

    /**
     * Adds a location listener.
     * @param callbackId    callback to receive listener updates
     * @param listener      location listener
     */
    protected synchronized void addListener(String callbackId, GeolocationListener listener) {
        this.geoListeners.put(callbackId, listener);
    }

    /**
     * Called when Plugin is destroyed. 
     */
    public void onDestroy() {
        this.shutdown();
    }
}