/*------------------------------------------------------------------------------
** Ident: Innovation en Inspiration > Google Android
** Author: rene
** Copyright: (c) Jan 22, 2009 Sogeti Nederland B.V. All Rights Reserved.
**------------------------------------------------------------------------------
** Sogeti Nederland B.V. | No part of this file may be reproduced
** Distributed Software Engineering | or transmitted in any form or by any
** Lange Dreef 17 | means, electronic or mechanical, for the
** 4131 NJ Vianen | purpose, without the express written
** The Netherlands | permission of the copyright holder.
*------------------------------------------------------------------------------
*
* This file is part of OpenGPSTracker.
*
* OpenGPSTracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* OpenGPSTracker is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with OpenGPSTracker. If not, see <http://www.gnu.org/licenses/>.
*
*/
package edu.stanford.cs.sujogger.logger;
import java.util.Vector;
import edu.stanford.cs.sujogger.R;
import edu.stanford.cs.sujogger.db.GPStracking.Media;
import edu.stanford.cs.sujogger.db.GPStracking.Tracks;
import edu.stanford.cs.sujogger.db.GPStracking.Waypoints;
import edu.stanford.cs.sujogger.util.Constants;
import edu.stanford.cs.sujogger.viewer.LoggerMap;
import edu.stanford.cs.sujogger.logger.IGPSLoggerServiceRemote;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.location.GpsSatellite;
import android.location.GpsStatus;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.location.GpsStatus.Listener;
import android.net.Uri;
import android.os.Bundle;
import android.os.IBinder;
import android.os.PowerManager;
import android.os.RemoteException;
import android.preference.PreferenceManager;
import android.util.Log;
import android.widget.Toast;
/**
* A system service as controlling the background logging of gps locations.
*
* @version $Id: GPSLoggerService.java 461 2010-03-19 14:15:19Z rcgroot $
* @author rene (c) Jan 22, 2009, Sogeti B.V.
*/
public class GPSLoggerService extends Service {
private static final int MAX_REASONABLE_SPEED = 15; // in m/s
private static final String TAG = "OGT.GPSLoggerService";
private static final int LOGGING_FINE = 0;
private static final int LOGGING_NORMAL = 1;
private static final int LOGGING_COARSE = 2;
private static final int LOGGING_GLOBAL = 3;
private static final String SERVICESTATE_STATE = "SERVICESTATE_STATE";
private static final String SERVICESTATE_PRECISION = "SERVICESTATE_PRECISION";
private static final String SERVICESTATE_SEGMENTID = "SERVICESTATE_SEGMENTID";
private static final String SERVICESTATE_TRACKID = "SERVICESTATE_TRACKID";
private Context mContext;
private LocationManager mLocationManager;
private NotificationManager mNoticationManager;
private PowerManager.WakeLock mWakeLock;
private boolean mSpeedSanityCheck;
private long mTrackId = -1;
private long mSegmentId = -1;
private long mWaypointId = -1;
private int mPrecision;
private int mLoggingState = Constants.STOPPED;
private boolean mStartNextSegment;
private Location mPreviousLocation;
private Notification mNotification;
private Vector<Location> mWeakLocations;
/**
* <code>mAcceptableAccuracy</code> indicates the maximum acceptable
* accuracy of a waypoint in meters.
*/
private float mMaxAcceptableAccuracy = 20;
private int mSatellites = 0;
private OnSharedPreferenceChangeListener mSharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() {
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
if (key.equals(Constants.PRECISION)) {
requestLocationUpdates();
setupNotification();
}
// TODO: eliminated preference
// else if( key.equals( Constants.SPEEDSANITYCHECK ) )
// {
// mSpeedSanityCheck = sharedPreferences.getBoolean(
// Constants.SPEEDSANITYCHECK, true );
// }
}
};
private LocationListener mLocationListener = new LocationListener() {
public void onLocationChanged(Location location) {
location = locationFilter(location);
if (location != null) {
if (mStartNextSegment) {
mStartNextSegment = false;
startNewSegment();
}
storeLocation(location);
}
}
public void onProviderDisabled(String provider) {
disabledProviderNotification();
}
public void onProviderEnabled(String provider) {
enabledProviderNotification();
if (mPrecision != LOGGING_GLOBAL) {
mStartNextSegment = true;
}
}
public void onStatusChanged(String provider, int status, Bundle extras) {
Log.w(TAG, String.format("Provider %s changed to status %d", provider, status));
}
};
private Listener mStatusListener = new GpsStatus.Listener() {
public synchronized void onGpsStatusChanged(int event) {
switch (event) {
case GpsStatus.GPS_EVENT_SATELLITE_STATUS:
GpsStatus status = mLocationManager.getGpsStatus(null);
mSatellites = 0;
Iterable<GpsSatellite> list = status.getSatellites();
for (GpsSatellite satellite : list) {
if (satellite.usedInFix()) {
mSatellites++;
}
}
updateNotification();
break;
default:
break;
}
}
};
private IBinder mBinder = new IGPSLoggerServiceRemote.Stub() {
public long isLogging() throws RemoteException {
if ((GPSLoggerService.this.mLoggingState == Constants.LOGGING || GPSLoggerService.this.mLoggingState == Constants.PAUSED)
&& GPSLoggerService.this.mTrackId != -1) {
return GPSLoggerService.this.mTrackId;
}
return -1;
}
public int loggingState() throws RemoteException {
return mLoggingState;
}
public long startLogging() throws RemoteException {
GPSLoggerService.this.startLogging();
return mTrackId;
}
public void pauseLogging() throws RemoteException {
GPSLoggerService.this.pauseLogging();
}
public long resumeLogging() throws RemoteException {
GPSLoggerService.this.resumeLogging();
return mSegmentId;
}
public void stopLogging() throws RemoteException {
Log.d("GPSRemote", "Stop logging");
GPSLoggerService.this.stopLogging();
}
public Uri storeMediaUri(Uri mediaUri) throws RemoteException {
GPSLoggerService.this.storeMediaUri(mediaUri);
return null;
}
public boolean isMediaPrepared() throws RemoteException {
return GPSLoggerService.this.isMediaPrepared();
}
};
/**
* Called by the system when the service is first created. Do not call this
* method directly. Be sure to call super.onCreate().
*/
@Override
public void onCreate() {
super.onCreate();
mWeakLocations = new Vector<Location>(3);
mLoggingState = Constants.STOPPED;
mStartNextSegment = false;
mContext = getApplicationContext();
mLocationManager = (LocationManager) this.mContext
.getSystemService(Context.LOCATION_SERVICE);
mNoticationManager = (NotificationManager) this.mContext
.getSystemService(Context.NOTIFICATION_SERVICE);
mNoticationManager.cancel(R.layout.map);
mSpeedSanityCheck = true;
boolean startImmidiatly = PreferenceManager.getDefaultSharedPreferences(this.mContext)
.getBoolean(Constants.LOGATSTARTUP, false);
// Log.d( TAG, "Commence logging at startup:"+startImmidiatly );
crashRestoreState();
if (startImmidiatly && mLoggingState == Constants.STOPPED) {
startLogging();
ContentValues values = new ContentValues();
values.put(Tracks.NAME, "Recorded at startup");
getContentResolver().update(ContentUris.withAppendedId(Tracks.CONTENT_URI, mTrackId),
values, null, null);
}
}
/**
* (non-Javadoc)
*
* @see android.app.Service#onDestroy()
*/
@Override
public void onDestroy() {
stopLogging();
super.onDestroy();
}
private void crashProtectState() {
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(mContext);
Editor editor = preferences.edit();
editor.putLong(SERVICESTATE_TRACKID, mTrackId);
editor.putLong(SERVICESTATE_SEGMENTID, mSegmentId);
editor.putInt(SERVICESTATE_PRECISION, mPrecision);
editor.putInt(SERVICESTATE_STATE, mLoggingState);
editor.commit();
}
private void crashRestoreState() {
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(mContext);
long previousState = preferences.getInt(SERVICESTATE_STATE, Constants.STOPPED);
if (previousState == Constants.LOGGING || previousState == Constants.PAUSED) {
Log.w(TAG, "Recovering from a crash or kill and restoring state.");
setupNotification();
mTrackId = preferences.getLong(SERVICESTATE_TRACKID, -1);
mSegmentId = preferences.getLong(SERVICESTATE_SEGMENTID, -1);
mPrecision = preferences.getInt(SERVICESTATE_PRECISION, -1);
if (previousState == Constants.LOGGING) {
mLoggingState = Constants.PAUSED;
resumeLogging();
}
else if (previousState == Constants.PAUSED) {
mLoggingState = Constants.LOGGING;
pauseLogging();
}
}
}
/**
* (non-Javadoc)
*
* @see android.app.Service#onBind(android.content.Intent)
*/
@Override
public IBinder onBind(Intent intent) {
return this.mBinder;
}
/**
* (non-Javadoc)
*
* @see edu.stanford.cs.sujogger.IGPSLoggerService#getLoggingState()
*/
protected boolean isLogging() {
return this.mLoggingState == Constants.LOGGING;
}
protected boolean isMediaPrepared() {
return !(mTrackId < 0 || mSegmentId < 0 || mWaypointId < 0);
}
/**
* (non-Javadoc)
*
* @see edu.stanford.cs.sujogger.IGPSLoggerService#startLogging()
*/
public synchronized long startLogging() {
startNewTrack();
requestLocationUpdates();
this.mLocationManager.addGpsStatusListener(this.mStatusListener);
this.mLoggingState = Constants.LOGGING;
updateWakeLock();
setupNotification();
crashProtectState();
return mTrackId;
}
public synchronized void pauseLogging() {
if (this.mLoggingState == Constants.LOGGING) {
this.mLocationManager.removeGpsStatusListener(this.mStatusListener);
this.mLocationManager.removeUpdates(this.mLocationListener);
this.mLoggingState = Constants.PAUSED;
this.mPreviousLocation = null;
updateWakeLock();
updateNotification();
crashProtectState();
}
}
public synchronized void resumeLogging() {
if (this.mLoggingState == Constants.PAUSED) {
if (mPrecision != LOGGING_GLOBAL) {
mStartNextSegment = true;
}
requestLocationUpdates();
this.mLocationManager.addGpsStatusListener(this.mStatusListener);
this.mLoggingState = Constants.LOGGING;
updateWakeLock();
updateNotification();
crashProtectState();
}
}
/**
* (non-Javadoc)
*
* @see edu.stanford.cs.sujogger.IGPSLoggerService#stopLogging()
*/
public synchronized void stopLogging() {
Log.d(TAG, "Stop logging");
PreferenceManager.getDefaultSharedPreferences(this.mContext)
.unregisterOnSharedPreferenceChangeListener(this.mSharedPreferenceChangeListener);
this.mLocationManager.removeGpsStatusListener(this.mStatusListener);
this.mLocationManager.removeUpdates(this.mLocationListener);
this.mLoggingState = Constants.STOPPED;
updateWakeLock();
mNoticationManager.cancel(R.layout.map);
crashProtectState();
Log.d(TAG, "Stop logging done");
}
private void setupNotification() {
mNoticationManager.cancel(R.layout.map);
int icon = R.drawable.ic_maps_indicator_current_position;
CharSequence tickerText = this.getResources().getString(R.string.service_start);
long when = System.currentTimeMillis();
mNotification = new Notification(icon, tickerText, when);
mNotification.flags |= Notification.FLAG_ONGOING_EVENT;
updateNotification();
}
private void updateNotification() {
CharSequence contentTitle = this.getResources().getString(R.string.app_name);
String precision = this.getResources().getStringArray(R.array.precision_choices)[mPrecision];
String state = this.getResources().getStringArray(R.array.state_choices)[mLoggingState - 1];
CharSequence contentText;
switch (mPrecision) {
case (LOGGING_GLOBAL):
contentText = this.getResources().getString(R.string.service_networkstatus, state,
precision);
break;
default:
contentText = this.getResources().getString(R.string.service_gpsstatus, state,
precision, mSatellites);
break;
}
Intent notificationIntent = new Intent(this, LoggerMap.class);
notificationIntent.setData(ContentUris.withAppendedId(Tracks.CONTENT_URI, mTrackId));
PendingIntent contentIntent = PendingIntent.getActivity(this, 0, notificationIntent,
Intent.FLAG_ACTIVITY_NEW_TASK);
mNotification.setLatestEventInfo(this, contentTitle, contentText, contentIntent);
mNoticationManager.notify(R.layout.map, mNotification);
}
private void enabledProviderNotification() {
mNoticationManager.cancel(R.id.icon);
CharSequence text = mContext.getString(R.string.service_gpsenabled);
Toast toast = Toast.makeText(mContext.getApplicationContext(), text, Toast.LENGTH_LONG);
toast.show();
}
private void disabledProviderNotification() {
int icon = R.drawable.ic_maps_indicator_current_position;
CharSequence tickerText = this.getResources().getString(R.string.service_gpsdisabled);
long when = System.currentTimeMillis();
Notification gpsNotification = new Notification(icon, tickerText, when);
gpsNotification.flags |= Notification.FLAG_AUTO_CANCEL;
CharSequence contentTitle = this.getResources().getString(R.string.app_name);
CharSequence contentText = this.getResources().getString(R.string.service_gpsdisabled);
Intent notificationIntent = new Intent(this, LoggerMap.class);
notificationIntent.setData(ContentUris.withAppendedId(Tracks.CONTENT_URI, mTrackId));
PendingIntent contentIntent = PendingIntent.getActivity(this, 0, notificationIntent,
Intent.FLAG_ACTIVITY_NEW_TASK);
gpsNotification.setLatestEventInfo(this, contentTitle, contentText, contentIntent);
mNoticationManager.notify(R.id.icon, gpsNotification);
}
private void requestLocationUpdates() {
this.mLocationManager.removeUpdates(this.mLocationListener);
mPrecision = new Integer(PreferenceManager.getDefaultSharedPreferences(this.mContext)
.getString(Constants.PRECISION, "1")).intValue();
// Log.d( TAG, "requestLocationUpdates to precision "+precision );
switch (mPrecision) {
case (LOGGING_FINE): // Fine
mMaxAcceptableAccuracy = 10f;
mLocationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 1000l, 5F,
this.mLocationListener);
break;
case (LOGGING_NORMAL): // Normal
mMaxAcceptableAccuracy = 20f;
mLocationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 15000l, 10F,
this.mLocationListener);
break;
case (LOGGING_COARSE): // Coarse
mMaxAcceptableAccuracy = 50f;
mLocationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 30000l, 25F,
this.mLocationListener);
break;
case (LOGGING_GLOBAL): // Global
mMaxAcceptableAccuracy = 1000f;
mLocationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, 300000l,
500F, this.mLocationListener);
break;
default:
Log.e(TAG, "Unknown precision " + mPrecision);
break;
}
}
private void updateWakeLock() {
if (this.mLoggingState == Constants.LOGGING) {
PreferenceManager.getDefaultSharedPreferences(this.mContext)
.registerOnSharedPreferenceChangeListener(mSharedPreferenceChangeListener);
PowerManager pm = (PowerManager) this.mContext.getSystemService(Context.POWER_SERVICE);
this.mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
this.mWakeLock.acquire();
}
else {
if (this.mWakeLock != null) {
this.mWakeLock.release();
this.mWakeLock = null;
}
}
}
/**
* Some GPS waypoints received are of to low a quality for tracking use.
* Here we filter those out.
*
* @param proposedLocation
* @return either the (cleaned) original or null when unacceptable
*/
public Location locationFilter(Location proposedLocation) {
boolean hasAccuracy = proposedLocation.hasAccuracy();
float accuracy = proposedLocation.getAccuracy();
// Do not log a waypoint which is more inaccurate then is configured to
// be acceptable
if (hasAccuracy && accuracy > mMaxAcceptableAccuracy) {
Log.w(TAG, String.format(
"A weak location was recieved, lots of inaccuracy... (%f more then max %f)",
accuracy, mMaxAcceptableAccuracy));
mWeakLocations.add(proposedLocation);
if (mWeakLocations.size() < 3) {
return null;
}
else {
return collectLeastBad();
}
}
// Do not log a waypoint which might be on any side of the previous
// waypoint
if (hasAccuracy && mPreviousLocation != null
&& accuracy > mPreviousLocation.distanceTo(proposedLocation)) {
Log
.w(
TAG,
String
.format(
"A weak location was recieved, not quite clear from the previous waypoint... (%f more then max %f)",
accuracy, mPreviousLocation
.distanceTo(proposedLocation)));
mWeakLocations.add(proposedLocation);
if (mWeakLocations.size() < 3) {
return null;
}
else {
return collectLeastBad();
}
}
if (mPrecision == LOGGING_GLOBAL && this.mPreviousLocation != null) {
// To avoid near instant teleportation on network location or
// glitches cause continent hopping
float meters = proposedLocation.distanceTo(mPreviousLocation);
long seconds = (proposedLocation.getTime() - mPreviousLocation.getTime()) / 1000L;
if (meters / seconds > MAX_REASONABLE_SPEED) {
// To fast a location change to be likely
return null;
}
else {
mWeakLocations.clear();
return proposedLocation;
}
}
else {
// The log we have appears fine enough, just to remove the speed if
// it is weird (common on at least G1 GPS)
if (mSpeedSanityCheck && proposedLocation.hasSpeed()
&& proposedLocation.getSpeed() > MAX_REASONABLE_SPEED) {
Log.w(TAG, "A strange location was recieved, a really high speed, prob wrong...");
proposedLocation.removeSpeed();
}
mWeakLocations.clear();
return proposedLocation;
}
}
private Location collectLeastBad() {
Location best = mWeakLocations.lastElement();
for (Location whimp : mWeakLocations) {
if (whimp.hasAccuracy() && best.hasAccuracy()
&& whimp.getAccuracy() < best.getAccuracy()) {
best = whimp;
}
else {
if (whimp.hasAccuracy() && !best.hasAccuracy()) {
best = whimp;
}
}
}
mWeakLocations.clear();
return best;
}
/**
* Trigged by events that start a new track
*/
private void startNewTrack() {
Uri newTrack = this.mContext.getContentResolver().insert(Tracks.CONTENT_URI, null);
mTrackId = new Long(newTrack.getLastPathSegment()).longValue();
startNewSegment();
}
/**
* Trigged by events that start a new segment
*/
private void startNewSegment() {
this.mPreviousLocation = null;
Uri newSegment = this.mContext.getContentResolver().insert(
Uri.withAppendedPath(Tracks.CONTENT_URI, mTrackId + "/segments"), null);
mSegmentId = new Long(newSegment.getLastPathSegment()).longValue();
}
protected void storeMediaUri(Uri mediaUri) {
// Log.d( TAG, "Retrieved MediaUri to store on track: "+mediaUri );
if (isMediaPrepared()) {
Uri mediaInsertUri = Uri.withAppendedPath(Tracks.CONTENT_URI, mTrackId + "/segments/"
+ mSegmentId + "/waypoints/" + mWaypointId + "/media");
ContentValues args = new ContentValues();
args.put(Media.URI, mediaUri.toString());
mContext.getContentResolver().insert(mediaInsertUri, args);
}
else {
Log.e(TAG, "No logging done under which to store the track");
}
}
/**
* Use the ContentResolver mechanism to store a received location
*
* @param location
*/
public void storeLocation(Location location) {
if (!isLogging()) {
Log.e(TAG, String.format("Not logging but storing location %s, prepare to fail",
location.toString()));
}
mPreviousLocation = location;
ContentValues args = new ContentValues();
args.put(Waypoints.LATITUDE, new Double(location.getLatitude()));
args.put(Waypoints.LONGITUDE, new Double(location.getLongitude()));
args.put(Waypoints.SPEED, new Float(location.getSpeed()));
args.put(Waypoints.TIME, new Long(System.currentTimeMillis()));
// Log.d( TAG, "Location based time sent to ContentProvider"+
// DateFormat.getInstance().format(new Date( args.getAsLong(
// Waypoints.TIME ) ) ) );
if (location.hasAccuracy()) {
args.put(Waypoints.ACCURACY, new Float(location.getAccuracy()));
}
if (location.hasAltitude()) {
args.put(Waypoints.ALTITUDE, new Double(location.getAltitude()));
}
if (location.hasBearing()) {
args.put(Waypoints.BEARING, new Float(location.getBearing()));
}
Uri waypointInsertUri = Uri.withAppendedPath(Tracks.CONTENT_URI, mTrackId + "/segments/"
+ mSegmentId + "/waypoints");
Uri inserted = mContext.getContentResolver().insert(waypointInsertUri, args);
mWaypointId = Long.parseLong(inserted.getLastPathSegment());
}
}