Android Open Source - LocationUpdate Send Service






From Project

Back to project page LocationUpdate.

License

The source code is released under:

Copyright (c) 2011 Stefan A. van der Meer Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to dea...

If you think the Android project LocationUpdate listed in this page is inappropriate, such as containing malicious code/tools or violating the copyright, please email info at java2s dot com, thanks.

Java Source Code

package com.meernet.LocationUpdate;
/*www  .  j  a va  2 s.c o m*/
import java.net.MalformedURLException;
import java.net.URL;

import android.app.AlarmManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.PowerManager;
import android.os.SystemClock;
import android.text.format.DateUtils;
import android.util.Log;

/**
 * SendService implements a service that is scheduled to run at regular intervals,
 * at which it requests the current location and transmits it to a server (if the
 * location is sufficiently different from the last transmitted coordinate).
 *
 * @author svdm
 *
 */
public class SendService extends Service implements LocationListener {
    static final String TAG                = "SendService";
    static final String UPDATE_ACTION_NAME = "com.meernet.LocationUpdate.PERFORM_UPDATE";
    static final String PREFS_NAME         = "LocationUpdatePrefs";

    /*
     * Setting defaults
     */
    static final long    WAKELOCK_TIME_DEFAULT     = 2 * 60 * 1000;
    static final boolean USE_WAKELOCK_DEFAULT      = false;
    static final String  LOCATION_PROVIDER_DEFAULT = LocationManager.NETWORK_PROVIDER;
    static final long    SEND_DELAY_DEFAULT        = 3 * 60;
    static final float   SEND_MIN_DISTANCE_DEFAULT = 100;
    static final int     SEND_MAX_ERROR_DEFAULT    = 10;
    static final long    TIMEOUT_DELAY_DEFAULT     = 5;
    static final boolean AUTOSTART_DEFAULT         = true;
    static final int     CONNECTION_TYPE_ANY       = -1;
    static final boolean SHOW_LOG_DEFAULT          = false;

    static final int     MAX_LOG_SIZE              = 25 * 50; // 25 lines of approx 50 chars

    /**
     * Last location successfully sent to the destination.
     */
    private Location lastLocation = null;

    /**
     * A wakelock might be necessary to keep the device active until the process completes.
     */
    private PowerManager.WakeLock wakeLock = null;

    /**
     * Asynchronous task that POSTs a coordinate to a URL.
     */
    private LocationSendTask httpSendTask = null;

    /**
     * Handler for a timed Runnable that puts a timeout on the location update listening.
     */
    private Handler timeoutHandler = new Handler();

    /**
     * Status update log that can be shown to users.
     */
    private String statusLog;

    /*
     * Settings
     */
    private URL     sendDestination     = null;
    private long    sendDelay           = SEND_DELAY_DEFAULT;
    private float   sendMinDistance     = SEND_MIN_DISTANCE_DEFAULT;
    private int     sendMaxError        = SEND_MAX_ERROR_DEFAULT;
    private boolean useWakeLock         = USE_WAKELOCK_DEFAULT;
    private boolean directLogging       = SHOW_LOG_DEFAULT;
    private String  locationProvider    = LOCATION_PROVIDER_DEFAULT;
    private long    timeoutDelay        = TIMEOUT_DELAY_DEFAULT;

    @Override
    public void onCreate() {
        super.onCreate();

        lastLocation = null;
    }

    /**
     * Set an alarm that will start this service again after the given delay.
     *
     * If autostart is disabled by user, the alarm will only be set if manualStart is true.
     *
     * @param context       Application context in which to schedule.
     * @param delay         Time in milliseconds after which the service should run.
     * @param manualStart   Must be true if this scheduling action was initiated by the user.
     * @return              Whether scheduling succeeded.
     */
    public static boolean schedule(Context context, long delay, boolean manualStart) {

        if ( !manualStart && !shouldAutoStart(context))
            return false;


        AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);

        // hardcode intent to start this service (again) and tell it to check if it should send
        Intent intent = new Intent(context, SendService.class);
        intent.setAction(UPDATE_ACTION_NAME); // identify as an alarm-sent intent

        long millis = SystemClock.elapsedRealtime() + delay;

        alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,
                         millis,
                         PendingIntent.getService(context, 0, intent, 0));

        Log.d(TAG, "Scheduled update with delay " + delay);

        return true;
    }

    /**
     * Check whether user has configured service to autostart/schedule or not.
     * @param context   Context from which to load preferences.
     * @return          True if service should autostart and schedule.
     */
    public static boolean shouldAutoStart(Context context) {
        SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, 0);
        return prefs.getBoolean("autostart", AUTOSTART_DEFAULT);
    }


    /**
     * Should be called when device has booted to schedule the first SendService run.
     */
    public static void onBoot(Context context, Intent intent) {
        SharedPreferences prefs = context.getSharedPreferences(SendService.PREFS_NAME, 0);

        long delay = Long.valueOf(prefs.getString("delay", Long.toString(SendService.SEND_DELAY_DEFAULT)));
        SendService.schedule(context, SendService.delayToMillis(delay), false);
    }

    /**
     * Acquires wake lock, instantiating first if necessary. If timeout is 0, no timeout is used.
     */
    private void acquireWakeLock(long timeout) {
        if ( !useWakeLock) { return; }

        if (wakeLock == null) {
            PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
            wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
        }

        if (timeout > 0) {
            // timeout wake locks are problematic in that they error if the lock has already been released (android bug?)
            // shouldn't use them if possible
            wakeLock.acquire(timeout);
            Log.d(TAG, "Acquired wake lock with timeout " + timeout);
        } else {
            wakeLock.acquire();
            Log.d(TAG, "Acquired wake lock with no timeout");
        }
    }

    private void acquireWakeLock() { acquireWakeLock(0); }

    /**
     * Release wake lock if it exists.
     */
    private void releaseWakeLock() {
        if ( !useWakeLock) { return; }

        if (wakeLock != null) {
            wakeLock.release();

            Log.d(TAG, "Released wake lock.");
        }
    }

    /**
     * Begin the process of acquiring a location fix. If successful, the
     * callback will start the coordinate sending process. If/when that
     * completes, the service will exit.
     *
     * Hence, calling this function sets in motion a longer term process.
     */
    private void beginAcquiringLocation() {
        //Log.d(TAG, "Beginning location acquisition");
        logStatus("Requesting location update.");

        // we don't want to be killed while handling data transmission in another thread
        // to guarantee we don't take an eternal lock even in case of bugs, set a timeout
        acquireWakeLock(WAKELOCK_TIME_DEFAULT);

        LocationManager locationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);

         // no significant time or distance minima, we'll decide if it's usable when we get a coord
        locationManager.requestLocationUpdates( locationProvider, 500, 0, this);

        // next action in process is in the callback upon receiving a location update

        // to prevent endless listening if no updates are ever received, we need to schedule a timeout
        timeoutHandler.removeCallbacks(timeoutTask);
        timeoutHandler.postDelayed(timeoutTask, timeoutDelay);
    }


    /**
     * The asynchronous transmission of a location to the server has completed or failed.
     *
     * Callback from the CoordinateSendTask that it completes.
     *
     * @param result                Whether the data was sent successfully.
     * @param message               Status message, may be null if task succeeded.
     * @param submittedLocation     The location that the task sent (or attempted to).
     */
    public void onSendTaskComplete(boolean result, String message, Location submittedLocation) {
        Log.d(TAG, "Send asynctask completed with result " + result + ": " + message);

        if (result) {
            lastLocation = submittedLocation;

            logStatus("Sent location to server.");

            stopSelf();
        } else {
            failedSend("Send task failed, error: " + message);
        }
    }

    /**
     * We were started by someone, which will often be an alarm.
     */
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {

        loadStoredState();

        if (UPDATE_ACTION_NAME.equals(intent.getAction())) {
            Log.d(TAG, "Service started");

            // we want to re-schedule no matter what our result
            schedule(this, sendDelay, false);

            beginAcquiringLocation();

            // will close ourselves when we are done
            return START_STICKY;
        } else {

            logStatus("Received unhandled intent: " + intent.getAction());
        }

        return START_NOT_STICKY;
    }

    /**
     * An issue occurred that causes the location acquisition and/or sending to fail.
     * Log the reason and kill this service so that cleanup can occur.
     *
     * @param reason
     */
    private void failedSend(String reason) {
        //Log.i(TAG, "Failed to acquire and send a coordinate: " + reason);

        logStatus(reason);

        stopSelf();
    }

    /**
     * Service is being killed, so perform all cleanup and save persistent state.
     */
    @Override
    public void onDestroy() {
        super.onDestroy();

        timeoutHandler.removeCallbacks(timeoutTask);

        LocationManager locationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
        locationManager.removeUpdates(this);

        saveState();

        releaseWakeLock();
    }

    /**
     * Quick'n'dirty log we can easily display in the LocationUpdate activity.
     * Very helpful when debugging.
     *
     * @param msg   Message to log.
     */
    public void logStatus(String msg) {

        msg = FormatStatusMessage(msg);

        StringBuilder b = new StringBuilder(statusLog.length() + msg.length());

        b.append(msg);
        b.append(statusLog);

        // Checking the char count is faster than counting the number of lines
        // and we don't really care about the *exact* line count, only that the
        // log does not balloon in size.
        if (statusLog.length() > MAX_LOG_SIZE) {
            // delete oldest line if log is too big
            int idx = b.lastIndexOf("\n");
            if (idx == -1) idx = 0;

            b.delete(idx, b.length());
        }

        statusLog = b.toString();

        Log.i(TAG, msg);

        // If we are showing the log in the app, write to storage immediately
        // so that the activity can detect the change and display it (if it's
        // running).
        if (directLogging) {
            SharedPreferences prefs = getSharedPreferences(PREFS_NAME, 0);
            prefs.edit().putString("log", statusLog).commit();
        }
        // Else only save when the service exits, along with the other data.
    }

    /**
     * Format a status message as:
     *   [date]: [msg]\n
     *
     * @param msg   Status message.
     * @return      Formatted status message.
     */
    private String FormatStatusMessage(String msg) {
        return String.format("%s: %s\n",
                DateUtils.formatDateTime(this, System.currentTimeMillis(),
                        DateUtils.FORMAT_SHOW_TIME +
                        DateUtils.FORMAT_SHOW_DATE +
                        DateUtils.FORMAT_24HOUR +
                        DateUtils.FORMAT_ABBREV_ALL +
                        DateUtils.FORMAT_NO_YEAR),
                msg);
    }

    /*
     * Save/load
     *
     */

    /**
     * Load persistent state (last sent location) and settings from preferences.
     */
    private void loadStoredState() {
        SharedPreferences prefs = getSharedPreferences(PREFS_NAME, 0);

        // load previously sent location, if any
        float lastLat  = prefs.getFloat("lastLat", 0);
        float lastLon  = prefs.getFloat("lastLon", 0);
        long  lastTime = prefs.getLong("lastTime", 0);
        if (lastLat != 0 && lastLon != 0) {
            // we don't want to overwrite newer information, though we typically won't have any
            if (lastLocation == null || lastLocation.getTime() < lastTime) {
                lastLocation = new Location("Restored");
                lastLocation.setLatitude(lastLat);
                lastLocation.setLongitude(lastLon);
                lastLocation.setTime(lastTime);
            }
        }

        // Certain preferences are set by user via ListPreference, which can only work with strings.
        // Working around this requires some ugly ceremony.
        sendDelay        = Long.valueOf(   prefs.getString("delay",        Long.toString(SEND_DELAY_DEFAULT)));
        timeoutDelay     = Long.valueOf(   prefs.getString("timeout",      Long.toString(TIMEOUT_DELAY_DEFAULT)));
        sendMinDistance  = Float.valueOf(  prefs.getString("minDistance", Float.toString(SEND_MIN_DISTANCE_DEFAULT)));
        sendMaxError     = Integer.valueOf(prefs.getString("maxError",  Integer.toString(SEND_MAX_ERROR_DEFAULT)));

        locationProvider = prefs.getString("locationProvider", LOCATION_PROVIDER_DEFAULT);
        useWakeLock      = prefs.getBoolean("useWakeLock", USE_WAKELOCK_DEFAULT);

        statusLog        = prefs.getString("log", "");
        directLogging    = prefs.getBoolean("showLog", SHOW_LOG_DEFAULT);

        sendDelay    = delayToMillis(sendDelay); // not stored in millis
        timeoutDelay = delayToMillis(timeoutDelay);

        try {
            sendDestination = new URL(prefs.getString("destination", ""));
        } catch (MalformedURLException e) {
            logStatus("Invalid destination URL set.");

            Log.e(TAG, "User set bad URL", e);
        }

        Log.d(TAG, "Loaded stored state.");
    }

    static long delayToMillis(long delay) { return delay * 60 * 1000; }

    /**
     * Store persistent state that we need next time the service runs.
     */
    private void saveState() {
        if (lastLocation != null) {
            SharedPreferences prefs = getSharedPreferences(PREFS_NAME, 0);
            Editor editor = prefs.edit();

            editor.putFloat( "lastLat",   (float) lastLocation.getLatitude());
            editor.putFloat( "lastLon",   (float) lastLocation.getLongitude());
            editor.putLong(  "lastTime",  lastLocation.getTime());

            editor.putString("log",       statusLog);

            editor.commit();

            // settings are not stored, they are never modified by this service

            Log.d(TAG, "Saved state.");
        }
    }

    /**
     * Returns whether the distance of the given location compared to
     * the last sent location warrants sending an update to the server.
     */
    private boolean shouldSend(Location newLocation) {
        // user-configured minimum distance must have been traveled
        return (lastLocation == null || lastLocation.distanceTo(newLocation) > sendMinDistance);
    }

    /**
     * Returns whether the location is of sufficient accuracy to pass the
     * user-set threshold.
     *
     * @param location      The Location to test.
     * @return              False if the location does not pass the error
     *                      threshold, else true.
     */
    private boolean passesErrorThreshold(Location location) {
        return !location.hasAccuracy() || location.getAccuracy() <= sendMaxError;
    }

    /**
     * Having listened for location updates, possibly rejecting some location
     * fixes of insufficient quality, handle the final location.
     *
     * Stop running listeners and timeouts, and determine whether the location
     * should be sent or not, followed by appropriate action.
     *
     * After this function is called, the service will either stop due to
     * failure, or wait for an async CoordinateSendTask to complete.
     *
     * @param location      The Location where the service believes the user
     *                      is now. May be sent to the destination server if it
     *                      is sufficiently different from earlier updates.
     */
    private void onFinalLocation(Location location) {
        acquireWakeLock();

        // Having received the final location of this service lifetime, we no
        // longer need to time out to prevent eternal location listening, so we
        // remove the timeout message.
        timeoutHandler.removeCallbacks(timeoutTask);

        // This location is either sufficient to send, or the service ends
        // hence no more updates are required.
        LocationManager locationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
        locationManager.removeUpdates(this);

        if (shouldSend(location)) {

            if (sendDestination != null) {
                httpSendTask = (LocationSendTask) new LocationSendTask(this, sendDestination).execute(location);
            } else {
                failedSend("No valid destination URL set.");
            }

        } else {
            failedSend(String.format("Location is %.1fm from previous update, not sending.", lastLocation.distanceTo(location)));
        }
    }


    /*
     * LocationListener impl
     */

    @Override
    public void onLocationChanged(Location location) {
        logStatus(String.format("Location received, accuracy %.1fm.", location.getAccuracy()));

        // If this location is inaccurate and is provided by GPS, then we will
        // wait for a better fix.
        if ( !passesErrorThreshold(location) && location.getProvider().equals(LocationManager.GPS_PROVIDER)) {
            return;
        }

        onFinalLocation(location);
    }

    public void onProviderDisabled(String provider) {
        // if for whatever reason our provider is turned off, we have nothing more to do
        // unless we have a running sendtask
        if (provider.equals(locationProvider) && httpSendTask == null) {
            failedSend("LocationProvider disabled.");
        }
    }

    public void onProviderEnabled(String provider) {}
    public void onStatusChanged(String arg0, int arg1, Bundle arg2) {}

    /*
     * Timeout handling
     */
    private Runnable timeoutTask = new Runnable() {
        public void run() {
            failedSend("Location updating timed out.");
        }
    };

    /**
     *  Service should be scheduled and started remotely, not bound to by activities.
     */
    @Override
    public IBinder onBind(Intent intent) { return null; }


}




Java Source Code List

com.meernet.LocationUpdate.BootReceiver.java
com.meernet.LocationUpdate.LocationSendTask.java
com.meernet.LocationUpdate.LocationUpdatePrefs.java
com.meernet.LocationUpdate.LocationUpdate.java
com.meernet.LocationUpdate.SendService.java