/*------------------------------------------------------------------------------
** Ident: Sogeti Smart Mobile Solutions
** Author: rene
** Copyright: (c) Apr 24, 2011 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 nl.sogeti.android.gpstracker.logger;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Date;
import java.util.LinkedList;
import java.util.Queue;
import java.util.Timer;
import java.util.TimerTask;
import java.util.Vector;
import java.util.concurrent.Semaphore;
import nl.sogeti.android.gpstracker.R;
import nl.sogeti.android.gpstracker.db.GPStracking.Media;
import nl.sogeti.android.gpstracker.db.GPStracking.MetaData;
import nl.sogeti.android.gpstracker.db.GPStracking.Tracks;
import nl.sogeti.android.gpstracker.db.GPStracking.Waypoints;
import nl.sogeti.android.gpstracker.streaming.StreamUtils;
import nl.sogeti.android.gpstracker.util.Constants;
import nl.sogeti.android.gpstracker.viewer.LoggerMap;
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.database.Cursor;
import android.location.GpsSatellite;
import android.location.GpsStatus;
import android.location.GpsStatus.Listener;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.location.LocationProvider;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.media.RingtoneManager;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
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 1191 2012-01-01 11:49:47Z rcgroot $
* @author rene (c) Jan 22, 2009, Sogeti B.V.
*/
public class GPSLoggerService extends Service implements LocationListener
{
private static final float FINE_DISTANCE = 5F;
private static final long FINE_INTERVAL = 1000l;
private static final float FINE_ACCURACY = 20f;
private static final float NORMAL_DISTANCE = 10F;
private static final long NORMAL_INTERVAL = 15000l;
private static final float NORMAL_ACCURACY = 30f;
private static final float COARSE_DISTANCE = 25F;
private static final long COARSE_INTERVAL = 30000l;
private static final float COARSE_ACCURACY = 75f;
private static final float GLOBAL_DISTANCE = 500F;
private static final long GLOBAL_INTERVAL = 300000l;
private static final float GLOBAL_ACCURACY = 1000f;
/**
* <code>MAX_REASONABLE_SPEED</code> is about 324 kilometer per hour or 201
* mile per hour.
*/
private static final int MAX_REASONABLE_SPEED = 90;
/**
* <code>MAX_REASONABLE_ALTITUDECHANGE</code> between the last few waypoints
* and a new one the difference should be less then 200 meter.
*/
private static final int MAX_REASONABLE_ALTITUDECHANGE = 200;
private static final Boolean DEBUG = false;
private static final boolean VERBOSE = false;
private static final String TAG = "OGT.GPSLoggerService";
private static final String SERVICESTATE_DISTANCE = "SERVICESTATE_DISTANCE";
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 static final int ADDGPSSTATUSLISTENER = 0;
private static final int REQUEST_FINEGPS_LOCATIONUPDATES = 1;
private static final int REQUEST_NORMALGPS_LOCATIONUPDATES = 2;
private static final int REQUEST_COARSEGPS_LOCATIONUPDATES = 3;
private static final int REQUEST_GLOBALNETWORK_LOCATIONUPDATES = 4;
private static final int REQUEST_CUSTOMGPS_LOCATIONUPDATES = 5;
private static final int STOPLOOPER = 6;
private static final int GPSPROBLEM = 7;
private static final int LOGGING_UNAVAILABLE = R.string.service_connectiondisabled;
/**
* DUP from android.app.Service.START_STICKY
*/
private static final int START_STICKY = 1;
public static final String COMMAND = "nl.sogeti.android.gpstracker.extra.COMMAND";
public static final int EXTRA_COMMAND_START = 0;
public static final int EXTRA_COMMAND_PAUSE = 1;
public static final int EXTRA_COMMAND_RESUME = 2;
public static final int EXTRA_COMMAND_STOP = 3;
private LocationManager mLocationManager;
private NotificationManager mNoticationManager;
private PowerManager.WakeLock mWakeLock;
private Handler mHandler;
/**
* If speeds should be checked to sane values
*/
private boolean mSpeedSanityCheck;
/**
* If broadcasts of location about should be sent to stream location
*/
private boolean mStreamBroadcast;
private long mTrackId = -1;
private long mSegmentId = -1;
private long mWaypointId = -1;
private int mPrecision;
private int mLoggingState = Constants.STOPPED;
private boolean mStartNextSegment;
private String mSources;
private Location mPreviousLocation;
private float mDistance;
private Notification mNotification;
private Vector<Location> mWeakLocations;
private Queue<Double> mAltitudes;
/**
* <code>mAcceptableAccuracy</code> indicates the maximum acceptable accuracy
* of a waypoint in meters.
*/
private float mMaxAcceptableAccuracy = 20;
private int mSatellites = 0;
private boolean mShowingGpsDisabled;
/**
* Should the GPS Status monitor update the notification bar
*/
private boolean mStatusMonitor;
/**
* Time thread to runs tasks that check whether the GPS listener has received
* enough to consider the GPS system alive.
*/
private Timer mHeartbeatTimer;
/**
* Listens to changes in preference to precision and sanity checks
*/
private OnSharedPreferenceChangeListener mSharedPreferenceChangeListener = new OnSharedPreferenceChangeListener()
{
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key)
{
if (key.equals(Constants.PRECISION) || key.equals(Constants.LOGGING_DISTANCE) || key.equals(Constants.LOGGING_INTERVAL))
{
sendRequestLocationUpdatesMessage();
crashProtectState();
updateNotification();
broadCastLoggingState();
}
else if (key.equals(Constants.SPEEDSANITYCHECK))
{
mSpeedSanityCheck = sharedPreferences.getBoolean(Constants.SPEEDSANITYCHECK, true);
}
else if (key.equals(Constants.STATUS_MONITOR))
{
mLocationManager.removeGpsStatusListener(mStatusListener);
sendRequestStatusUpdateMessage();
updateNotification();
}
else if(key.equals(Constants.BROADCAST_STREAM) || key.equals("VOICEOVER_ENABLED") || key.equals("CUSTOMUPLOAD_ENABLED") )
{
if (key.equals(Constants.BROADCAST_STREAM))
{
mStreamBroadcast = sharedPreferences.getBoolean(Constants.BROADCAST_STREAM, false);
}
StreamUtils.shutdownStreams(GPSLoggerService.this);
if( !mStreamBroadcast )
{
StreamUtils.initStreams(GPSLoggerService.this);
}
}
}
};
public void onLocationChanged(Location location)
{
if (VERBOSE)
{
Log.v(TAG, "onLocationChanged( Location " + location + " )");
}
;
// Might be claiming GPS disabled but when we were paused this changed and this location proves so
if (mShowingGpsDisabled)
{
notifyOnEnabledProviderNotification(R.string.service_gpsenabled);
}
Location filteredLocation = locationFilter(location);
if (filteredLocation != null)
{
if (mStartNextSegment)
{
mStartNextSegment = false;
// Obey the start segment if the previous location is unknown or far away
if (mPreviousLocation == null || filteredLocation.distanceTo(mPreviousLocation) > 4 * mMaxAcceptableAccuracy)
{
startNewSegment();
}
}
else if( mPreviousLocation != null )
{
mDistance += mPreviousLocation.distanceTo(filteredLocation);
}
storeLocation(filteredLocation);
//Attempt to broadcast location
broadcastLocation(filteredLocation);
mPreviousLocation = location;
}
}
public void onProviderDisabled(String provider)
{
if (DEBUG)
{
Log.d(TAG, "onProviderDisabled( String " + provider + " )");
}
;
if (mPrecision != Constants.LOGGING_GLOBAL && provider.equals(LocationManager.GPS_PROVIDER))
{
notifyOnDisabledProvider(R.string.service_gpsdisabled);
}
else if (mPrecision == Constants.LOGGING_GLOBAL && provider.equals(LocationManager.NETWORK_PROVIDER))
{
notifyOnDisabledProvider(R.string.service_datadisabled);
}
}
public void onProviderEnabled(String provider)
{
if (DEBUG)
{
Log.d(TAG, "onProviderEnabled( String " + provider + " )");
}
;
if (mPrecision != Constants.LOGGING_GLOBAL && provider.equals(LocationManager.GPS_PROVIDER))
{
notifyOnEnabledProviderNotification(R.string.service_gpsenabled);
mStartNextSegment = true;
}
else if (mPrecision == Constants.LOGGING_GLOBAL && provider.equals(LocationManager.NETWORK_PROVIDER))
{
notifyOnEnabledProviderNotification(R.string.service_dataenabled);
}
}
public void onStatusChanged(String provider, int status, Bundle extras)
{
if (DEBUG)
{
Log.d(TAG, "onStatusChanged( String " + provider + ", int " + status + ", Bundle " + extras + " )");
}
;
if (status == LocationProvider.OUT_OF_SERVICE)
{
Log.e(TAG, String.format("Provider %s changed to status %d", provider, status));
}
}
/**
* Listens to GPS status changes
*/
private Listener mStatusListener = new GpsStatus.Listener()
{
public synchronized void onGpsStatusChanged(int event)
{
switch (event)
{
case GpsStatus.GPS_EVENT_SATELLITE_STATUS:
if (mStatusMonitor)
{
GpsStatus status = mLocationManager.getGpsStatus(null);
mSatellites = 0;
Iterable<GpsSatellite> list = status.getSatellites();
for (GpsSatellite satellite : list)
{
if (satellite.usedInFix())
{
mSatellites++;
}
}
updateNotification();
}
break;
case GpsStatus.GPS_EVENT_STOPPED:
break;
case GpsStatus.GPS_EVENT_STARTED:
break;
default:
break;
}
}
};
private IBinder mBinder = new IGPSLoggerServiceRemote.Stub()
{
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
{
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();
}
public void storeDerivedDataSource(String sourceName) throws RemoteException
{
GPSLoggerService.this.storeDerivedDataSource(sourceName);
}
public Location getLastWaypoint() throws RemoteException
{
return GPSLoggerService.this.getLastWaypoint();
}
public float getTrackedDistance() throws RemoteException
{
return GPSLoggerService.this.getTrackedDistance();
}
};
/**
* Task that will be run periodically during active logging to verify that
* the logging really happens and that the GPS hasn't silently stopped.
*/
private TimerTask mHeartbeat = null;
/**
* Task to determine if the GPS is alive
*/
class Heartbeat extends TimerTask
{
private String mProvider;
public Heartbeat(String provider)
{
mProvider = provider;
}
@Override
public void run()
{
if (isLogging())
{
// Collect the last location from the last logged location or a more recent from the last weak location
Location checkLocation = mPreviousLocation;
synchronized (mWeakLocations)
{
if (!mWeakLocations.isEmpty())
{
if (checkLocation == null)
{
checkLocation = mWeakLocations.lastElement();
}
else
{
Location weakLocation = mWeakLocations.lastElement();
checkLocation = weakLocation.getTime() > checkLocation.getTime() ? weakLocation : checkLocation;
}
}
}
// Is the last known GPS location something nearby we are not told?
Location managerLocation = mLocationManager.getLastKnownLocation(mProvider);
if (managerLocation != null && checkLocation != null)
{
if (checkLocation.distanceTo(managerLocation) < 2 * mMaxAcceptableAccuracy)
{
checkLocation = managerLocation.getTime() > checkLocation.getTime() ? managerLocation : checkLocation;
}
}
if (checkLocation == null || checkLocation.getTime() + mCheckPeriod < new Date().getTime())
{
Log.w(TAG, "GPS system failed to produce a location during logging: " + checkLocation);
mLoggingState = Constants.PAUSED;
resumeLogging();
if (mStatusMonitor)
{
soundGpsSignalAlarm();
}
}
}
}
};
/**
* Number of milliseconds that a functioning GPS system needs to provide a
* location. Calculated to be either 120 seconds or 4 times the requested
* period, whichever is larger.
*/
private long mCheckPeriod;
private float mBroadcastDistance;
private long mLastTimeBroadcast;
private class GPSLoggerServiceThread extends Thread
{
public Semaphore ready = new Semaphore(0);
GPSLoggerServiceThread()
{
this.setName("GPSLoggerServiceThread");
}
@Override
public void run()
{
Looper.prepare();
mHandler = new Handler()
{
@Override
public void handleMessage(Message msg)
{
_handleMessage(msg);
}
};
ready.release(); // Signal the looper and handler are created
Looper.loop();
}
}
/**
* 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();
if (DEBUG)
{
Log.d(TAG, "onCreate()");
}
;
GPSLoggerServiceThread looper = new GPSLoggerServiceThread();
looper.start();
try
{
looper.ready.acquire();
}
catch (InterruptedException e)
{
Log.e(TAG, "Interrupted during wait for the GPSLoggerServiceThread to start, prepare for trouble!", e);
}
mHeartbeatTimer = new Timer("heartbeat", true);
mWeakLocations = new Vector<Location>(3);
mAltitudes = new LinkedList<Double>();
mLoggingState = Constants.STOPPED;
mStartNextSegment = false;
mLocationManager = (LocationManager) this.getSystemService(Context.LOCATION_SERVICE);
mNoticationManager = (NotificationManager) this.getSystemService(Context.NOTIFICATION_SERVICE);
stopNotification();
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
mSpeedSanityCheck = sharedPreferences.getBoolean(Constants.SPEEDSANITYCHECK, true);
mStreamBroadcast = sharedPreferences.getBoolean(Constants.BROADCAST_STREAM, false);
boolean startImmidiatly = PreferenceManager.getDefaultSharedPreferences(this).getBoolean(Constants.LOGATSTARTUP, false);
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);
}
else
{
broadCastLoggingState();
}
}
/**
* This is the old onStart method that will be called on the pre-2.0
*
* @see android.app.Service#onStart(android.content.Intent, int) platform. On
* 2.0 or later we override onStartCommand() so this method will not be
* called.
*/
@Override
public void onStart(Intent intent, int startId)
{
handleCommand(intent);
}
@Override
public int onStartCommand(Intent intent, int flags, int startId)
{
handleCommand(intent);
// We want this service to continue running until it is explicitly
// stopped, so return sticky.
return START_STICKY;
}
private void handleCommand(Intent intent)
{
if (DEBUG)
{
Log.d(TAG, "handleCommand(Intent " + intent + ")");
}
;
if (intent != null && intent.hasExtra(COMMAND))
{
switch (intent.getIntExtra(COMMAND, -1))
{
case EXTRA_COMMAND_START:
startLogging();
break;
case EXTRA_COMMAND_PAUSE:
pauseLogging();
break;
case EXTRA_COMMAND_RESUME:
resumeLogging();
break;
case EXTRA_COMMAND_STOP:
stopLogging();
break;
default:
break;
}
}
}
/**
* (non-Javadoc)
*
* @see android.app.Service#onDestroy()
*/
@Override
public void onDestroy()
{
if (DEBUG)
{
Log.d(TAG, "onDestroy()");
}
;
super.onDestroy();
if (isLogging())
{
Log.w(TAG, "Destroyin an activly logging service");
}
mHeartbeatTimer.cancel();
mHeartbeatTimer.purge();
if (this.mWakeLock != null)
{
this.mWakeLock.release();
this.mWakeLock = null;
}
PreferenceManager.getDefaultSharedPreferences(this).unregisterOnSharedPreferenceChangeListener(this.mSharedPreferenceChangeListener);
mLocationManager.removeGpsStatusListener(mStatusListener);
stopListening();
mNoticationManager.cancel(R.layout.map);
Message msg = Message.obtain();
msg.what = STOPLOOPER;
mHandler.sendMessage(msg);
}
private void crashProtectState()
{
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
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.putFloat(SERVICESTATE_DISTANCE, mDistance);
editor.commit();
if (DEBUG)
{
Log.d(TAG, "crashProtectState()");
}
;
}
private synchronized void crashRestoreState()
{
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
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.");
startNotification();
mTrackId = preferences.getLong(SERVICESTATE_TRACKID, -1);
mSegmentId = preferences.getLong(SERVICESTATE_SEGMENTID, -1);
mPrecision = preferences.getInt(SERVICESTATE_PRECISION, -1);
mDistance = preferences.getFloat(SERVICESTATE_DISTANCE, 0F);
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 nl.sogeti.android.gpstracker.IGPSLoggerService#getLoggingState()
*/
protected boolean isLogging()
{
return this.mLoggingState == Constants.LOGGING;
}
/**
* Provides the cached last stored waypoint it current logging is active alse
* null.
*
* @return last waypoint location or null
*/
protected Location getLastWaypoint()
{
Location myLastWaypoint = null;
if (isLogging())
{
myLastWaypoint = mPreviousLocation;
}
return myLastWaypoint;
}
public float getTrackedDistance()
{
float distance = 0F;
if (isLogging())
{
distance = mDistance;
}
return distance;
}
protected boolean isMediaPrepared()
{
return !(mTrackId < 0 || mSegmentId < 0 || mWaypointId < 0);
}
/**
* (non-Javadoc)
*
* @see nl.sogeti.android.gpstracker.IGPSLoggerService#startLogging()
*/
public synchronized void startLogging()
{
if (DEBUG)
{
Log.d(TAG, "startLogging()");
}
;
if (this.mLoggingState == Constants.STOPPED)
{
startNewTrack();
sendRequestLocationUpdatesMessage();
sendRequestStatusUpdateMessage();
this.mLoggingState = Constants.LOGGING;
updateWakeLock();
startNotification();
crashProtectState();
broadCastLoggingState();
}
}
public synchronized void pauseLogging()
{
if (DEBUG)
{
Log.d(TAG, "pauseLogging()");
}
;
if (this.mLoggingState == Constants.LOGGING)
{
mLocationManager.removeGpsStatusListener(mStatusListener);
stopListening();
mLoggingState = Constants.PAUSED;
mPreviousLocation = null;
updateWakeLock();
updateNotification();
mSatellites = 0;
updateNotification();
crashProtectState();
broadCastLoggingState();
}
}
public synchronized void resumeLogging()
{
if (DEBUG)
{
Log.d(TAG, "resumeLogging()");
}
;
if (this.mLoggingState == Constants.PAUSED)
{
if (mPrecision != Constants.LOGGING_GLOBAL)
{
mStartNextSegment = true;
}
sendRequestLocationUpdatesMessage();
sendRequestStatusUpdateMessage();
this.mLoggingState = Constants.LOGGING;
updateWakeLock();
updateNotification();
crashProtectState();
broadCastLoggingState();
}
}
/**
* (non-Javadoc)
*
* @see nl.sogeti.android.gpstracker.IGPSLoggerService#stopLogging()
*/
public synchronized void stopLogging()
{
if (DEBUG)
{
Log.d(TAG, "stopLogging()");
}
;
mLoggingState = Constants.STOPPED;
crashProtectState();
updateWakeLock();
PreferenceManager.getDefaultSharedPreferences(this).unregisterOnSharedPreferenceChangeListener(this.mSharedPreferenceChangeListener);
mLocationManager.removeGpsStatusListener(mStatusListener);
stopListening();
stopNotification();
broadCastLoggingState();
}
private void startListening(String provider, long intervaltime, float distance)
{
mLocationManager.removeUpdates(this);
mLocationManager.requestLocationUpdates(provider, intervaltime, distance, this);
mCheckPeriod = Math.max(12 * intervaltime, 120 * 1000);
if (mHeartbeat != null)
{
mHeartbeat.cancel();
mHeartbeat = null;
}
mHeartbeat = new Heartbeat(provider);
mHeartbeatTimer.schedule(mHeartbeat, mCheckPeriod, mCheckPeriod);
}
private void stopListening()
{
if (mHeartbeat != null)
{
mHeartbeat.cancel();
mHeartbeat = null;
}
mLocationManager.removeUpdates(this);
}
/**
* (non-Javadoc)
*
* @see nl.sogeti.android.gpstracker.IGPSLoggerService#storeDerivedDataSource(java.lang.String)
*/
public void storeDerivedDataSource(String sourceName)
{
Uri trackMetaDataUri = Uri.withAppendedPath(Tracks.CONTENT_URI, mTrackId + "/metadata");
if (mTrackId >= 0)
{
if (mSources == null)
{
Cursor metaData = null;
String source = null;
try
{
metaData = this.getContentResolver().query(trackMetaDataUri, new String[] { MetaData.VALUE }, MetaData.KEY + " = ? ",
new String[] { Constants.DATASOURCES_KEY }, null);
if (metaData.moveToFirst())
{
source = metaData.getString(0);
}
}
finally
{
if (metaData != null)
{
metaData.close();
}
}
if (source != null)
{
mSources = source;
}
else
{
mSources = sourceName;
ContentValues args = new ContentValues();
args.put(MetaData.KEY, Constants.DATASOURCES_KEY);
args.put(MetaData.VALUE, mSources);
this.getContentResolver().insert(trackMetaDataUri, args);
}
}
if (!mSources.contains(sourceName))
{
mSources += "," + sourceName;
ContentValues args = new ContentValues();
args.put(MetaData.VALUE, mSources);
this.getContentResolver().update(trackMetaDataUri, args, MetaData.KEY + " = ? ", new String[] { Constants.DATASOURCES_KEY });
}
}
}
private void startNotification()
{
mNoticationManager.cancel(R.layout.map);
int icon = R.drawable.ic_maps_indicator_current_position;
CharSequence tickerText = getResources().getString(R.string.service_start);
long when = System.currentTimeMillis();
mNotification = new Notification(icon, tickerText, when);
mNotification.flags |= Notification.FLAG_ONGOING_EVENT;
updateNotification();
if (Build.VERSION.SDK_INT >= 5)
{
startForegroundReflected(R.layout.map, mNotification);
}
else
{
mNoticationManager.notify(R.layout.map, mNotification);
}
}
private void updateNotification()
{
CharSequence contentTitle = getResources().getString(R.string.app_name);
String precision = getResources().getStringArray(R.array.precision_choices)[mPrecision];
String state = getResources().getStringArray(R.array.state_choices)[mLoggingState - 1];
CharSequence contentText;
switch (mPrecision)
{
case (Constants.LOGGING_GLOBAL):
contentText = getResources().getString(R.string.service_networkstatus, state, precision);
break;
default:
if (mStatusMonitor)
{
contentText = getResources().getString(R.string.service_gpsstatus, state, precision, mSatellites);
}
else
{
contentText = getResources().getString(R.string.service_gpsnostatus, state, precision);
}
break;
}
Intent notificationIntent = new Intent(this, LoggerMap.class);
notificationIntent.setData(ContentUris.withAppendedId(Tracks.CONTENT_URI, mTrackId));
mNotification.contentIntent = PendingIntent.getActivity(this, 0, notificationIntent, Intent.FLAG_ACTIVITY_NEW_TASK);
mNotification.setLatestEventInfo(this, contentTitle, contentText, mNotification.contentIntent);
mNoticationManager.notify(R.layout.map, mNotification);
}
private void stopNotification()
{
if (Build.VERSION.SDK_INT >= 5)
{
stopForegroundReflected(true);
}
else
{
mNoticationManager.cancel(R.layout.map);
}
}
private void notifyOnEnabledProviderNotification(int resId)
{
mNoticationManager.cancel(LOGGING_UNAVAILABLE);
mShowingGpsDisabled = false;
CharSequence text = this.getString(resId);
Toast toast = Toast.makeText(this, text, Toast.LENGTH_LONG);
toast.show();
}
private void notifyOnPoorSignal(int resId)
{
int icon = R.drawable.ic_maps_indicator_current_position;
CharSequence tickerText = getResources().getString(resId);
long when = System.currentTimeMillis();
Notification signalNotification = new Notification(icon, tickerText, when);
CharSequence contentTitle = getResources().getString(R.string.app_name);
Intent notificationIntent = new Intent(this, LoggerMap.class);
PendingIntent contentIntent = PendingIntent.getActivity(this, 0, notificationIntent, Intent.FLAG_ACTIVITY_NEW_TASK);
signalNotification.setLatestEventInfo(this, contentTitle, tickerText, contentIntent);
signalNotification.flags |= Notification.FLAG_AUTO_CANCEL;
mNoticationManager.notify(resId, signalNotification);
}
private void notifyOnDisabledProvider(int resId)
{
int icon = R.drawable.ic_maps_indicator_current_position;
CharSequence tickerText = getResources().getString(resId);
long when = System.currentTimeMillis();
Notification gpsNotification = new Notification(icon, tickerText, when);
gpsNotification.flags |= Notification.FLAG_AUTO_CANCEL;
CharSequence contentTitle = getResources().getString(R.string.app_name);
CharSequence contentText = getResources().getString(resId);
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(LOGGING_UNAVAILABLE, gpsNotification);
mShowingGpsDisabled = true;
}
/**
* Send a system broadcast to notify a change in the logging or precision
*/
private void broadCastLoggingState()
{
Intent broadcast = new Intent(Constants.LOGGING_STATE_CHANGED_ACTION);
broadcast.putExtra(Constants.EXTRA_LOGGING_PRECISION, mPrecision);
broadcast.putExtra(Constants.EXTRA_LOGGING_STATE, mLoggingState);
this.getApplicationContext().sendBroadcast(broadcast);
if( isLogging() )
{
StreamUtils.initStreams(this);
}
else
{
StreamUtils.shutdownStreams(this);
}
}
private void sendRequestStatusUpdateMessage()
{
mStatusMonitor = PreferenceManager.getDefaultSharedPreferences(this).getBoolean(Constants.STATUS_MONITOR, false);
Message msg = Message.obtain();
msg.what = ADDGPSSTATUSLISTENER;
mHandler.sendMessage(msg);
}
private void sendRequestLocationUpdatesMessage()
{
stopListening();
mPrecision = new Integer(PreferenceManager.getDefaultSharedPreferences(this).getString(Constants.PRECISION, "2")).intValue();
Message msg = Message.obtain();
switch (mPrecision)
{
case (Constants.LOGGING_FINE): // Fine
msg.what = REQUEST_FINEGPS_LOCATIONUPDATES;
mHandler.sendMessage(msg);
break;
case (Constants.LOGGING_NORMAL): // Normal
msg.what = REQUEST_NORMALGPS_LOCATIONUPDATES;
mHandler.sendMessage(msg);
break;
case (Constants.LOGGING_COARSE): // Coarse
msg.what = REQUEST_COARSEGPS_LOCATIONUPDATES;
mHandler.sendMessage(msg);
break;
case (Constants.LOGGING_GLOBAL): // Global
msg.what = REQUEST_GLOBALNETWORK_LOCATIONUPDATES;
mHandler.sendMessage(msg);
break;
case (Constants.LOGGING_CUSTOM): // Global
msg.what = REQUEST_CUSTOMGPS_LOCATIONUPDATES;
mHandler.sendMessage(msg);
break;
default:
Log.e(TAG, "Unknown precision " + mPrecision);
break;
}
}
/**
* Message handler method to do the work off-loaded by mHandler to
* GPSLoggerServiceThread
*
* @param msg
*/
private void _handleMessage(Message msg)
{
if (DEBUG)
{
Log.d(TAG, "_handleMessage( Message " + msg + " )");
}
;
long intervaltime = 0;
float distance = 0;
switch (msg.what)
{
case ADDGPSSTATUSLISTENER:
this.mLocationManager.addGpsStatusListener(mStatusListener);
break;
case REQUEST_FINEGPS_LOCATIONUPDATES:
mMaxAcceptableAccuracy = FINE_ACCURACY;
intervaltime = FINE_INTERVAL;
distance = FINE_DISTANCE;
startListening(LocationManager.GPS_PROVIDER, intervaltime, distance);
break;
case REQUEST_NORMALGPS_LOCATIONUPDATES:
mMaxAcceptableAccuracy = NORMAL_ACCURACY;
intervaltime = NORMAL_INTERVAL;
distance = NORMAL_DISTANCE;
startListening(LocationManager.GPS_PROVIDER, intervaltime, distance);
break;
case REQUEST_COARSEGPS_LOCATIONUPDATES:
mMaxAcceptableAccuracy = COARSE_ACCURACY;
intervaltime = COARSE_INTERVAL;
distance = COARSE_DISTANCE;
startListening(LocationManager.GPS_PROVIDER, intervaltime, distance);
break;
case REQUEST_GLOBALNETWORK_LOCATIONUPDATES:
mMaxAcceptableAccuracy = GLOBAL_ACCURACY;
intervaltime = GLOBAL_INTERVAL;
distance = GLOBAL_DISTANCE;
startListening(LocationManager.NETWORK_PROVIDER, intervaltime, distance);
if (!isNetworkConnected())
{
notifyOnDisabledProvider(R.string.service_connectiondisabled);
}
break;
case REQUEST_CUSTOMGPS_LOCATIONUPDATES:
intervaltime = 60 * 1000 * new Long(PreferenceManager.getDefaultSharedPreferences(this).getString(Constants.LOGGING_INTERVAL, "15000"));
distance = new Float(PreferenceManager.getDefaultSharedPreferences(this).getString(Constants.LOGGING_DISTANCE, "10"));
mMaxAcceptableAccuracy = Math.max(10f, Math.min(distance, 50f));
startListening(LocationManager.GPS_PROVIDER, intervaltime, distance);
break;
case STOPLOOPER:
mLocationManager.removeGpsStatusListener(mStatusListener);
stopListening();
Looper.myLooper().quit();
break;
case GPSPROBLEM:
notifyOnPoorSignal(R.string.service_gpsproblem);
break;
}
}
private void updateWakeLock()
{
if (this.mLoggingState == Constants.LOGGING)
{
PreferenceManager.getDefaultSharedPreferences(this).registerOnSharedPreferenceChangeListener(mSharedPreferenceChangeListener);
PowerManager pm = (PowerManager) this.getSystemService(Context.POWER_SERVICE);
if (this.mWakeLock != null)
{
this.mWakeLock.release();
this.mWakeLock = null;
}
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)
{
// Do no include log wrong 0.0 lat 0.0 long, skip to next value in while-loop
if (proposedLocation != null && (proposedLocation.getLatitude() == 0.0d || proposedLocation.getLongitude() == 0.0d))
{
Log.w(TAG, "A wrong location was received, 0.0 latitude and 0.0 longitude... ");
proposedLocation = null;
}
// Do not log a waypoint which is more inaccurate then is configured to be acceptable
if (proposedLocation != null && proposedLocation.getAccuracy() > mMaxAcceptableAccuracy)
{
Log.w(TAG, String.format("A weak location was received, lots of inaccuracy... (%f is more than max %f)", proposedLocation.getAccuracy(),
mMaxAcceptableAccuracy));
proposedLocation = addBadLocation(proposedLocation);
}
// Do not log a waypoint which might be on any side of the previous waypoint
if (proposedLocation != null && mPreviousLocation != null && proposedLocation.getAccuracy() > mPreviousLocation.distanceTo(proposedLocation))
{
Log.w(TAG,
String.format("A weak location was received, not quite clear from the previous waypoint... (%f more then max %f)",
proposedLocation.getAccuracy(), mPreviousLocation.distanceTo(proposedLocation)));
proposedLocation = addBadLocation(proposedLocation);
}
// Speed checks, check if the proposed location could be reached from the previous one in sane speed
// Common to jump on network logging and sometimes jumps on Samsung Galaxy S type of devices
if (mSpeedSanityCheck && proposedLocation != null && 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;
float speed = meters / seconds;
if (speed > MAX_REASONABLE_SPEED)
{
Log.w(TAG, "A strange location was received, a really high speed of " + speed + " m/s, prob wrong...");
proposedLocation = addBadLocation(proposedLocation);
// Might be a messed up Samsung Galaxy S GPS, reset the logging
if (speed > 2 * MAX_REASONABLE_SPEED && mPrecision != Constants.LOGGING_GLOBAL)
{
Log.w(TAG, "A strange location was received on GPS, reset the GPS listeners");
stopListening();
mLocationManager.removeGpsStatusListener(mStatusListener);
mLocationManager = (LocationManager) this.getSystemService(Context.LOCATION_SERVICE);
sendRequestStatusUpdateMessage();
sendRequestLocationUpdatesMessage();
}
}
}
// Remove speed if not sane
if (mSpeedSanityCheck && proposedLocation != null && proposedLocation.getSpeed() > MAX_REASONABLE_SPEED)
{
Log.w(TAG, "A strange speed, a really high speed, prob wrong...");
proposedLocation.removeSpeed();
}
// Remove altitude if not sane
if (mSpeedSanityCheck && proposedLocation != null && proposedLocation.hasAltitude())
{
if (!addSaneAltitude(proposedLocation.getAltitude()))
{
Log.w(TAG, "A strange altitude, a really big difference, prob wrong...");
proposedLocation.removeAltitude();
}
}
// Older bad locations will not be needed
if (proposedLocation != null)
{
mWeakLocations.clear();
}
return proposedLocation;
}
/**
* Store a bad location, when to many bad locations are stored the the
* storage is cleared and the least bad one is returned
*
* @param location bad location
* @return null when the bad location is stored or the least bad one if the
* storage was full
*/
private Location addBadLocation(Location location)
{
mWeakLocations.add(location);
if (mWeakLocations.size() < 3)
{
location = null;
}
else
{
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;
}
}
}
synchronized (mWeakLocations)
{
mWeakLocations.clear();
}
location = best;
}
return location;
}
/**
* Builds a bit of knowledge about altitudes to expect and return if the
* added value is deemed sane.
*
* @param altitude
* @return whether the altitude is considered sane
*/
private boolean addSaneAltitude(double altitude)
{
boolean sane = true;
double avg = 0;
int elements = 0;
// Even insane altitude shifts increases alter perception
mAltitudes.add(altitude);
if (mAltitudes.size() > 3)
{
mAltitudes.poll();
}
for (Double alt : mAltitudes)
{
avg += alt;
elements++;
}
avg = avg / elements;
sane = Math.abs(altitude - avg) < MAX_REASONABLE_ALTITUDECHANGE;
return sane;
}
/**
* Trigged by events that start a new track
*/
private void startNewTrack()
{
mDistance = 0;
Uri newTrack = this.getContentResolver().insert(Tracks.CONTENT_URI, new ContentValues(0));
mTrackId = new Long(newTrack.getLastPathSegment()).longValue();
startNewSegment();
}
/**
* Trigged by events that start a new segment
*/
private void startNewSegment()
{
this.mPreviousLocation = null;
Uri newSegment = this.getContentResolver().insert(Uri.withAppendedPath(Tracks.CONTENT_URI, mTrackId + "/segments"), new ContentValues(0));
mSegmentId = new Long(newSegment.getLastPathSegment()).longValue();
crashProtectState();
}
protected void storeMediaUri(Uri 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());
this.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()));
}
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()));
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 = this.getContentResolver().insert(waypointInsertUri, args);
mWaypointId = Long.parseLong(inserted.getLastPathSegment());
}
/**
* Consult broadcast options and execute broadcast if necessary
*
* @param location
*/
public void broadcastLocation(Location location)
{
Intent intent = new Intent(Constants.STREAMBROADCAST);
if (mStreamBroadcast)
{
final long minDistance = (long) PreferenceManager.getDefaultSharedPreferences(this).getFloat("streambroadcast_distance_meter", 5000F);
final long minTime = 60000 * Long.parseLong(PreferenceManager.getDefaultSharedPreferences(this).getString("streambroadcast_time", "1"));
final long nowTime = location.getTime();
if (mPreviousLocation != null)
{
mBroadcastDistance += location.distanceTo(mPreviousLocation);
}
if (mLastTimeBroadcast == 0)
{
mLastTimeBroadcast = nowTime;
}
long passedTime = (nowTime - mLastTimeBroadcast);
intent.putExtra(Constants.EXTRA_DISTANCE, (int) mBroadcastDistance);
intent.putExtra(Constants.EXTRA_TIME, (int) passedTime/60000);
intent.putExtra(Constants.EXTRA_LOCATION, location);
intent.putExtra(Constants.EXTRA_TRACK, ContentUris.withAppendedId(Tracks.CONTENT_URI, mTrackId));
boolean distanceBroadcast = minDistance > 0 && mBroadcastDistance >= minDistance;
boolean timeBroadcast = minTime > 0 && passedTime >= minTime;
if (distanceBroadcast || timeBroadcast)
{
if (distanceBroadcast)
{
mBroadcastDistance = 0;
}
if (timeBroadcast)
{
mLastTimeBroadcast = nowTime;
}
this.sendBroadcast(intent, "android.permission.ACCESS_FINE_LOCATION");
}
}
}
private boolean isNetworkConnected()
{
ConnectivityManager connMgr = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo info = connMgr.getActiveNetworkInfo();
return (info != null && info.isConnected());
}
private void soundGpsSignalAlarm()
{
Uri alert = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM);
if (alert == null)
{
alert = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
if (alert == null)
{
alert = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE);
}
}
MediaPlayer mMediaPlayer = new MediaPlayer();
try
{
mMediaPlayer.setDataSource(GPSLoggerService.this, alert);
final AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
if (audioManager.getStreamVolume(AudioManager.STREAM_ALARM) != 0)
{
mMediaPlayer.setAudioStreamType(AudioManager.STREAM_ALARM);
mMediaPlayer.setLooping(false);
mMediaPlayer.prepare();
mMediaPlayer.start();
}
}
catch (IllegalArgumentException e)
{
Log.e(TAG, "Problem setting data source for mediaplayer", e);
}
catch (SecurityException e)
{
Log.e(TAG, "Problem setting data source for mediaplayer", e);
}
catch (IllegalStateException e)
{
Log.e(TAG, "Problem with mediaplayer", e);
}
catch (IOException e)
{
Log.e(TAG, "Problem with mediaplayer", e);
}
Message msg = Message.obtain();
msg.what = GPSPROBLEM;
mHandler.sendMessage(msg);
}
@SuppressWarnings("rawtypes")
private void startForegroundReflected(int id, Notification notification)
{
Method mStartForeground;
Class[] mStartForegroundSignature = new Class[] { int.class, Notification.class };
Object[] mStartForegroundArgs = new Object[2];
mStartForegroundArgs[0] = Integer.valueOf(id);
mStartForegroundArgs[1] = notification;
try
{
mStartForeground = getClass().getMethod("startForeground", mStartForegroundSignature);
mStartForeground.invoke(this, mStartForegroundArgs);
}
catch (NoSuchMethodException e)
{
Log.e(TAG, "Failed starting foreground notification using reflection", e);
}
catch (IllegalArgumentException e)
{
Log.e(TAG, "Failed starting foreground notification using reflection", e);
}
catch (IllegalAccessException e)
{
Log.e(TAG, "Failed starting foreground notification using reflection", e);
}
catch (InvocationTargetException e)
{
Log.e(TAG, "Failed starting foreground notification using reflection", e);
}
}
@SuppressWarnings("rawtypes")
private void stopForegroundReflected(boolean b)
{
Class[] mStopForegroundSignature = new Class[] { boolean.class };
Method mStopForeground;
Object[] mStopForegroundArgs = new Object[1];
mStopForegroundArgs[0] = Boolean.TRUE;
try
{
mStopForeground = getClass().getMethod("stopForeground", mStopForegroundSignature);
mStopForeground.invoke(this, mStopForegroundArgs);
}
catch (NoSuchMethodException e)
{
Log.e(TAG, "Failed stopping foreground notification using reflection", e);
}
catch (IllegalArgumentException e)
{
Log.e(TAG, "Failed stopping foreground notification using reflection", e);
}
catch (IllegalAccessException e)
{
Log.e(TAG, "Failed stopping foreground notification using reflection", e);
}
catch (InvocationTargetException e)
{
Log.e(TAG, "Failed stopping foreground notification using reflection", e);
}
}
}