Back to project page LocationUpdate.
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.
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; } }