content: /*
   * Androzic - android navigation client that uses OziExplorer maps (ozf2, ozfx3).
   * Copyright (C) 2010-2013 Andrey Novikov <http://andreynovikov.info/>
   * 
   * This file is part of Androzic application.
   * 
   * Androzic 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.
   * 
   * Androzic 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 Androzic. If not, see <http://www.gnu.org/licenses/>.
   */
  
  package com.androzic.location;
  
  import java.io.File;
  import java.util.HashSet;
  import java.util.Iterator;
  import java.util.Set;
  
  import android.app.Notification;
  import android.app.NotificationManager;
  import android.app.PendingIntent;
  import android.content.ComponentName;
  import android.content.ContentValues;
  import android.content.Context;
  import android.content.Intent;
  import android.content.SharedPreferences;
  import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
  import android.database.Cursor;
  import android.database.SQLException;
  import android.database.sqlite.SQLiteDatabase;
  import android.database.sqlite.SQLiteException;
  import android.location.GpsSatellite;
  import android.location.GpsStatus;
  import android.location.GpsStatus.NmeaListener;
  import android.location.Location;
  import android.location.LocationListener;
  import android.location.LocationManager;
  import android.location.LocationProvider;
  import android.os.Binder;
  import android.os.Bundle;
  import android.os.Handler;
  import android.os.IBinder;
  import android.os.RemoteCallbackList;
  import android.os.RemoteException;
  import android.os.SystemClock;
  import android.preference.PreferenceManager;
  import android.support.v4.app.NotificationCompat;
  import android.util.Log;
  
  import com.androzic.Androzic;
  import com.androzic.R;
  import com.androzic.Splash;
  import com.androzic.data.Track;
  
  public class LocationService extends BaseLocationService implements LocationListener, NmeaListener, GpsStatus.Listener, OnSharedPreferenceChangeListener
  {
  	private static final String TAG = "Location";
  	private static final int NOTIFICATION_ID = 24161;
  	private static final boolean DEBUG_ERRORS = false;
  
  	/**
  	 * Intent action to enable locating
  	 */
  	public static final String ENABLE_LOCATIONS = "enableLocations";
  	/**
  	 * Intent action to disable locating
  	 */
  	public static final String DISABLE_LOCATIONS = "disableLocations";
  
  	public static final String ENABLE_TRACK = "enableTrack";
  	public static final String DISABLE_TRACK = "disableTrack";
  
  	public static final String BROADCAST_TRACKING_STATUS = "com.androzic.trackingStatusChanged";
  
  	private boolean locationsEnabled = false;
  	//FIXME Use this to permanently loose location
  	private int gpsLocationTimeout = 120000;
  	// Used for test purposes
  	private static final boolean enableMockLocations = false;
  	private Handler mockCallback = new Handler();
  
  	private LocationManager locationManager = null;
  
  	private int gpsStatus = GPS_OFF;
  
  	private float[] speed = new float[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
  	private float[] speedav = new float[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
  	private float[] speedavex = new float[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
  
  	private long lastLocationMillis = 0;
  	private long tics = 0;
  	private int pause = 1;
  
  	private Location lastKnownLocation = null;
  	private boolean isContinous = false;
  	private boolean justStarted = true;
  	private float smoothSpeed = 0.0f;
  	private float avgSpeed = 0.0f;
  	private float nmeaGeoidHeight = Float.NaN;
  	private float HDOP = Float.NaN;
  	private float VDOP = Float.NaN;
  
  	private SQLiteDatabase trackDB = null;
  	private boolean trackingEnabled = false;
  	private String errorMsg = "";
  	private long errorTime = 0;
  
  	private Location lastWritenLocation = null;
  	private Location lastLocation = null;
  	private double distanceFromLastWriting = 0;
  	private long timeFromLastWriting = 0;
  
  	private long minTime = 2000; // 2 seconds (default)
  	private long maxTime = 300000; // 5 minutes
  	private int minDistance = 3; // 3 meters (default)
  
  	private final Binder binder = new LocalBinder();
  	private final RemoteCallbackList<ILocationCallback> locationRemoteCallbacks = new RemoteCallbackList<ILocationCallback>();
  	private final Set<ILocationListener> locationCallbacks = new HashSet<ILocationListener>();
  	private final RemoteCallbackList<ITrackingCallback> trackingRemoteCallbacks = new RemoteCallbackList<ITrackingCallback>();
  	private final Set<ITrackingListener> trackingCallbacks = new HashSet<ITrackingListener>();
  
  	@Override
  	public void onCreate()
  	{
  		super.onCreate();
  
  		lastKnownLocation = new Location("unknown");
  
  		SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
  		// Location preferences
  		onSharedPreferenceChanged(sharedPreferences, getString(R.string.pref_loc_gpstimeout));
  		// Tracking preferences
  		onSharedPreferenceChanged(sharedPreferences, getString(R.string.pref_tracking_mintime));
  		onSharedPreferenceChanged(sharedPreferences, getString(R.string.pref_tracking_mindistance));
  
  		sharedPreferences.registerOnSharedPreferenceChangeListener(this);
  
  		Log.i(TAG, "Service started");
  	}
  
  	@Override
  	public int onStartCommand(Intent intent, int flags, int startId)
  	{
  		if (intent == null || intent.getAction() == null)
  			return 0;
  
  		if (intent.getAction().equals(ENABLE_LOCATIONS) && !locationsEnabled)
  		{
  			locationsEnabled = true;
  			connect();
  			sendBroadcast(new Intent(BROADCAST_LOCATING_STATUS));
  			if (trackingEnabled)
  			{
  				sendBroadcast(new Intent(BROADCAST_TRACKING_STATUS));
  			}
  		}
  		if (intent.getAction().equals(DISABLE_LOCATIONS) && locationsEnabled)
  		{
  			locationsEnabled = false;
  			disconnect();
  			updateProvider(LocationManager.GPS_PROVIDER, false);
  			sendBroadcast(new Intent(BROADCAST_LOCATING_STATUS));
  			if (trackingEnabled)
  			{
  				closeDatabase();
  				sendBroadcast(new Intent(BROADCAST_TRACKING_STATUS));
  			}
  		}
  		if (intent.getAction().equals(ENABLE_TRACK) && !trackingEnabled)
  		{
  			errorMsg = "";
  			errorTime = 0;
  			trackingEnabled = true;
  			isContinous = false;
  			openDatabase();
  			sendBroadcast(new Intent(BROADCAST_TRACKING_STATUS));
  		}
  		if (intent.getAction().equals(DISABLE_TRACK) && trackingEnabled)
  		{
  			trackingEnabled = false;
  			closeDatabase();
  			errorMsg = "";
  			errorTime = 0;
  			sendBroadcast(new Intent(BROADCAST_TRACKING_STATUS));
  		}
  		updateNotification();
  
  		return START_REDELIVER_INTENT | START_STICKY;
  	}
  
  	@Override
  	public void onDestroy()
  	{
  		super.onDestroy();
  		PreferenceManager.getDefaultSharedPreferences(this).unregisterOnSharedPreferenceChangeListener(this);
  		disconnect();
  		closeDatabase();
  		Log.i(TAG, "Service stopped");
  	}
  
  	private final ILocationRemoteService.Stub locationRemoteBinder = new ILocationRemoteService.Stub() {
  		public void registerCallback(ILocationCallback cb)
  		{
  			Log.i(TAG, "Register callback");
  			if (cb != null)
  				locationRemoteCallbacks.register(cb);
  		}
  
  		public void unregisterCallback(ILocationCallback cb)
  		{
  			if (cb != null)
  				locationRemoteCallbacks.unregister(cb);
  		}
  
  		public boolean isLocating()
  		{
  			return locationsEnabled;
  		}
  	};
  
  	private final ITrackingRemoteService.Stub trackingRemoteBinder = new ITrackingRemoteService.Stub() {
  		public void registerCallback(ITrackingCallback cb)
  		{
  			Log.i(TAG, "Register callback");
  			if (cb != null)
  				trackingRemoteCallbacks.register(cb);
  		}
  
  		public void unregisterCallback(ITrackingCallback cb)
  		{
  			if (cb != null)
  				trackingRemoteCallbacks.unregister(cb);
  		}
  	};
  
  	@Override
  	public IBinder onBind(Intent intent)
  	{
  		if (ANDROZIC_LOCATION_SERVICE.equals(intent.getAction()) || ILocationRemoteService.class.getName().equals(intent.getAction()))
  		{
  			return locationRemoteBinder;
  		}
  		if ("com.androzic.tracking".equals(intent.getAction()) || ITrackingRemoteService.class.getName().equals(intent.getAction()))
  		{
  			return trackingRemoteBinder;
  		}
  		else
  		{
  			return binder;
  		}
  	}
  
  	@Override
  	public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key)
  	{
  		if (getString(R.string.pref_loc_gpstimeout).equals(key))
  		{
  			gpsLocationTimeout = 1000 * sharedPreferences.getInt(key, getResources().getInteger(R.integer.def_loc_gpstimeout));
  		}
  		else if (getString(R.string.pref_tracking_mintime).equals(key))
  		{
  			try
  			{
  				minTime = Integer.parseInt(sharedPreferences.getString(key, "500"));
  			}
  			catch (NumberFormatException e)
  			{
  			}
  		}
  		else if (getString(R.string.pref_tracking_mindistance).equals(key))
  		{
  			try
  			{
  				minDistance = Integer.parseInt(sharedPreferences.getString(key, "5"));
  			}
  			catch (NumberFormatException e)
  			{
  			}
  		}
  		else if (getString(R.string.pref_folder_data).equals(key))
  		{
  			closeDatabase();
  			openDatabase();
  		}
  	}
  
  	private void connect()
  	{
  		locationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
  		if (locationManager != null)
  		{
  			lastLocationMillis = 0;
  			pause = 1;
  			isContinous = false;
  			justStarted = true;
  			smoothSpeed = 0.0f;
  			avgSpeed = 0.0f;
  			locationManager.addGpsStatusListener(this);
  			try
  			{
  				locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0, this);
  				locationManager.addNmeaListener(this);
  				Log.d(TAG, "Gps provider set");
  			}
  			catch (IllegalArgumentException e)
  			{
  				Log.d(TAG, "Cannot set gps provider, likely no gps on device");
  			}
  			startForeground(NOTIFICATION_ID, getNotification());
  		}
  		if (enableMockLocations)
  		{
  			updateProvider(LocationManager.GPS_PROVIDER, true);
  			mockCallback.post(sendMockLocation);
  		}
  	}
  
  	private void disconnect()
  	{
  		if (locationManager != null)
  		{
  			locationManager.removeNmeaListener(this);
  			locationManager.removeUpdates(this);
  			locationManager.removeGpsStatusListener(this);
  			locationManager = null;
  			stopForeground(true);
  		}
  		if (enableMockLocations)
  		{
  			mockCallback.removeCallbacks(sendMockLocation);
  			updateProvider(LocationManager.GPS_PROVIDER, false);
  		}
  	}
  
  	@SuppressWarnings("unused")
  	private Notification getNotification()
  	{
  		int msgId = R.string.notif_loc_started;
  		int ntfId = R.drawable.ic_stat_locating;
  		if (trackingEnabled)
  		{
  			msgId = R.string.notif_trk_started;
  			ntfId = R.drawable.ic_stat_tracking;
  		}
  		if (gpsStatus != LocationService.GPS_OK)
  		{
  			msgId = R.string.notif_loc_waiting;
  			ntfId = R.drawable.ic_stat_waiting;
  		}
  		if (gpsStatus == LocationService.GPS_OFF)
  		{
  			ntfId = R.drawable.ic_stat_off;
  		}
  		if (errorTime > 0)
  		{
  			msgId = R.string.notif_trk_failure;
  			ntfId = R.drawable.ic_stat_failure;
  		}
  
  		NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
  		builder.setWhen(errorTime);
  		builder.setSmallIcon(ntfId);
  		Intent intent = new Intent(Intent.ACTION_MAIN);
  		intent.addCategory(Intent.CATEGORY_LAUNCHER);
  		intent.setComponent(new ComponentName(getApplicationContext(), Splash.class));
  		intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
  		builder.setContentIntent(PendingIntent.getActivity(this, NOTIFICATION_ID, intent, 0));
  		builder.setContentTitle(getText(R.string.notif_loc_short));
  		builder.setGroup("androzic");
  		builder.setCategory(NotificationCompat.CATEGORY_SERVICE);
  		builder.setPriority(NotificationCompat.PRIORITY_LOW);
  		builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC);
  		builder.setColor(getResources().getColor(R.color.theme_accent_color));
  		if (errorTime > 0 && DEBUG_ERRORS)
  			builder.setContentText(errorMsg);
  		else
  			builder.setContentText(getText(msgId));
  		builder.setOngoing(true);
  
  		return builder.build();
  	}
  
  	private void updateNotification()
  	{
  		if (locationManager != null)
  		{
  			NotificationManager notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
  			notificationManager.notify(NOTIFICATION_ID, getNotification());
  		}
  	}
  
  	private void openDatabase()
  	{
  		Androzic application = Androzic.getApplication();
  		if (application.dataPath == null)
  		{
  			Log.e(TAG, "Data path is null");
  			errorMsg = "Data path is null";
  			errorTime = System.currentTimeMillis();
  			updateNotification();
  			return;
  		}
  		File dir = new File(application.dataPath);
  		if (!dir.exists() && !dir.mkdirs())
  		{
  			Log.e(TAG, "Failed to create data folder");
  			errorMsg = "Failed to create data folder";
  			errorTime = System.currentTimeMillis();
  			updateNotification();
  			return;
  		}
  		File path = new File(dir, "myTrack.db");
  		try
  		{
  			trackDB = SQLiteDatabase.openDatabase(path.getAbsolutePath(), null, SQLiteDatabase.OPEN_READWRITE | SQLiteDatabase.CREATE_IF_NECESSARY | SQLiteDatabase.NO_LOCALIZED_COLLATORS);
  			Cursor cursor = trackDB.rawQuery("SELECT DISTINCT tbl_name FROM sqlite_master WHERE tbl_name = 'track'", null);
  			if (cursor.getCount() == 0)
  			{
  				trackDB.execSQL("CREATE TABLE track (_id INTEGER PRIMARY KEY, latitude REAL, longitude REAL, code INTEGER, elevation REAL, speed REAL, track REAL, accuracy REAL, datetime INTEGER)");
  			}
  			cursor.close();
  		}
  		catch (SQLiteException e)
  		{
  			trackDB = null;
  			Log.e(TAG, "openDatabase", e);
  			errorMsg = "Failed to open DB";
  			errorTime = System.currentTimeMillis();
  			updateNotification();
  		}
  	}
  
  	private void closeDatabase()
  	{
  		if (trackDB != null)
  		{
  			trackDB.close();
  			trackDB = null;
  		}
  	}
  
  	public Track getTrack()
  	{
  		return getTrack(0);
  	}
  	
  	public Track getTrack(long limit)
  	{
  		if (trackDB == null)
  			openDatabase();
  		Track track = new Track();
  		if (trackDB == null)
  			return track;
  		String limitStr = limit > 0 ? " LIMIT " + limit : "";
  		Cursor cursor = trackDB.rawQuery("SELECT * FROM track ORDER BY _id DESC" + limitStr, null);
  		for (boolean hasItem = cursor.moveToLast(); hasItem; hasItem = cursor.moveToPrevious())
  		{
  			double latitude = cursor.getDouble(cursor.getColumnIndex("latitude"));
  			double longitude = cursor.getDouble(cursor.getColumnIndex("longitude"));
  			double elevation = cursor.getDouble(cursor.getColumnIndex("elevation"));
  			double speed = cursor.getDouble(cursor.getColumnIndex("speed"));
  			double bearing = cursor.getDouble(cursor.getColumnIndex("track"));
  			double accuracy = cursor.getDouble(cursor.getColumnIndex("accuracy"));
  			int code = cursor.getInt(cursor.getColumnIndex("code"));
  			long time = cursor.getLong(cursor.getColumnIndex("datetime"));
  			track.addPoint(code == 0, latitude, longitude, elevation, speed, bearing, accuracy, time);
  		}
  		cursor.close();
  		return track;
  	}
  
  	public Track getTrack(long start, long end)
  	{
  		if (trackDB == null)
  			openDatabase();
  		Track track = new Track();
  		if (trackDB == null)
  			return track;
  		Cursor cursor = trackDB.rawQuery("SELECT * FROM track WHERE datetime >= ? AND datetime <= ? ORDER BY _id DESC", new String[] {String.valueOf(start), String.valueOf(end)});
  		for (boolean hasItem = cursor.moveToLast(); hasItem; hasItem = cursor.moveToPrevious())
  		{
  			double latitude = cursor.getDouble(cursor.getColumnIndex("latitude"));
  			double longitude = cursor.getDouble(cursor.getColumnIndex("longitude"));
  			double elevation = cursor.getDouble(cursor.getColumnIndex("elevation"));
  			double speed = cursor.getDouble(cursor.getColumnIndex("speed"));
  			double bearing = cursor.getDouble(cursor.getColumnIndex("track"));
  			double accuracy = cursor.getDouble(cursor.getColumnIndex("accuracy"));
  			int code = cursor.getInt(cursor.getColumnIndex("code"));
  			long time = cursor.getLong(cursor.getColumnIndex("datetime"));
  			track.addPoint(code == 0, latitude, longitude, elevation, speed, bearing, accuracy, time);
  		}
  		cursor.close();
  		return track;
  	}
  
  	public long getTrackStartTime()
  	{
  		long res = Long.MIN_VALUE;
  		if (trackDB == null)
  			openDatabase();
  		if (trackDB == null)
  			return res;
  		Cursor cursor = trackDB.rawQuery("SELECT MIN(datetime) FROM track WHERE datetime > 0", null);
  		if (cursor.moveToFirst())
  			res = cursor.getLong(0);
  		cursor.close();
  		return res;
  	}
  
  	public long getTrackEndTime()
  	{
  		long res = Long.MAX_VALUE;
  		if (trackDB == null)
  			openDatabase();
  		if (trackDB == null)
  			return res;
  		Cursor cursor = trackDB.rawQuery("SELECT MAX(datetime) FROM track", null);
  		if (cursor.moveToFirst())
  			res = cursor.getLong(0);
  		cursor.close();
  		return res;
  	}
  
  	public void clearTrack()
  	{
  		if (trackDB == null)
  			openDatabase();
  		if (trackDB != null)
  			trackDB.execSQL("DELETE FROM track");
  	}
  
  	public void addPoint(boolean continous, double latitude, double longitude, double elevation, float speed, float bearing, float accuracy, long time)
  	{
  		if (trackDB == null)
  		{
  			openDatabase();
  			if (trackDB == null)
  				return;
  		}
  
  		ContentValues values = new ContentValues();
  		values.put("latitude", latitude);
  		values.put("longitude", longitude);
  		values.put("code", continous ? 0 : 1);
  		values.put("elevation", elevation);
  		values.put("speed", speed);
  		values.put("track", bearing);
  		values.put("accuracy", accuracy);
  		values.put("datetime", time);
  
  		try
  		{
  			trackDB.insertOrThrow("track", null, values);
  		}
  		catch (SQLException e)
  		{
  			Log.e(TAG, "addPoint", e);
  			errorMsg = e.getMessage();
  			errorTime = System.currentTimeMillis();
  			updateNotification();
  			closeDatabase();
  		}
  	}
  
  	private void writeLocation(final Location loc, final boolean continous)
  	{
  		Log.d(TAG, "Fix needs writing");
  		lastWritenLocation = loc;
  		distanceFromLastWriting = 0;
  		addPoint(continous, loc.getLatitude(), loc.getLongitude(), loc.getAltitude(), loc.getSpeed(), loc.getBearing(), loc.getAccuracy(), loc.getTime());
  
  		for (ITrackingListener callback : trackingCallbacks)
  		{
  			callback.onNewPoint(continous, loc.getLatitude(), loc.getLongitude(), loc.getAltitude(), loc.getSpeed(), loc.getBearing(), loc.getAccuracy(), loc.getTime());
  		}
  
  		final int n = trackingRemoteCallbacks.beginBroadcast();
  		for (int i = 0; i < n; i++)
  		{
  			final ITrackingCallback callback = trackingRemoteCallbacks.getBroadcastItem(i);
  			try
  			{
  				callback.onNewPoint(continous, loc.getLatitude(), loc.getLongitude(), loc.getAltitude(), loc.getSpeed(), loc.getBearing(), loc.getAccuracy(), loc.getTime());
  			}
  			catch (RemoteException e)
  			{
  				Log.e(TAG, "Point broadcast error", e);
  			}
  		}
  		trackingRemoteCallbacks.finishBroadcast();
  	}
  
  	private void writeTrack(Location loc, boolean continous, boolean geoid, float smoothspeed, float avgspeed)
  	{
  		boolean needsWrite = false;
  		if (lastLocation != null)
  		{
  			distanceFromLastWriting += loc.distanceTo(lastLocation);
  		}
  		if (lastWritenLocation != null)
  			timeFromLastWriting = loc.getTime() - lastWritenLocation.getTime();
  
  		if (lastLocation == null || lastWritenLocation == null || !continous || timeFromLastWriting > maxTime || distanceFromLastWriting > minDistance && timeFromLastWriting > minTime)
  		{
  			needsWrite = true;
  		}
  
  		lastLocation = loc;
  
  		if (needsWrite)
  			writeLocation(loc, continous);
  	}
  
  	private void tearTrack()
  	{
  		if (lastLocation != null && (lastWritenLocation == null || !lastLocation.toString().equals(lastWritenLocation.toString())))
  			writeLocation(lastLocation, isContinous);
  		isContinous = false;
  	}
  
  	private void updateLocation()
  	{
  		final Location location = lastKnownLocation;
  		final boolean continous = isContinous;
  		final boolean geoid = !Float.isNaN(nmeaGeoidHeight);
  		final float smoothspeed = smoothSpeed;
  		final float avgspeed = avgSpeed;
  
  		final Handler handler = new Handler();
  
  		if (trackingEnabled)
  		{
  			handler.post(new Runnable() {
  				@Override
  				public void run()
  				{
  					writeTrack(location, continous, geoid, smoothspeed, avgspeed);
  				}
  			});
  		}
  		for (final ILocationListener callback : locationCallbacks)
  		{
  			handler.post(new Runnable() {
  				@Override
  				public void run()
  				{
  					callback.onLocationChanged(location, continous, geoid, smoothspeed, avgspeed);
  				}
  			});
  		}
  		final int n = locationRemoteCallbacks.beginBroadcast();
  		for (int i = 0; i < n; i++)
  		{
  			final ILocationCallback callback = locationRemoteCallbacks.getBroadcastItem(i);
  			try
  			{
  				callback.onLocationChanged(location, continous, geoid, smoothspeed, avgspeed);
  			}
  			catch (RemoteException e)
  			{
  				Log.e(TAG, "Location broadcast error", e);
  			}
  		}
  		locationRemoteCallbacks.finishBroadcast();
  		Log.d(TAG, "Location dispatched: " + (locationCallbacks.size() + n));
  	}
  
  	private void updateLocation(final ILocationListener callback)
  	{
  		if (!"unknown".equals(lastKnownLocation.getProvider()))
  			callback.onLocationChanged(lastKnownLocation, isContinous, !Float.isNaN(nmeaGeoidHeight), smoothSpeed, avgSpeed);
  	}
  
  	private void updateProvider(final String provider, final boolean enabled)
  	{
  		if (LocationManager.GPS_PROVIDER.equals(provider))
  			updateNotification();
  		final Handler handler = new Handler();
  		for (final ILocationListener callback : locationCallbacks)
  		{
  			handler.post(new Runnable() {
  				@Override
  				public void run()
  				{
  					if (enabled)
  						callback.onProviderEnabled(provider);
  					else
  						callback.onProviderDisabled(provider);
  				}
  			});
  		}
  		final int n = locationRemoteCallbacks.beginBroadcast();
  		for (int i = 0; i < n; i++)
  		{
  			final ILocationCallback callback = locationRemoteCallbacks.getBroadcastItem(i);
  			try
  			{
  				if (enabled)
  					callback.onProviderEnabled(provider);
  				else
  					callback.onProviderDisabled(provider);
  			}
  			catch (RemoteException e)
  			{
  				Log.e(TAG, "Provider broadcast error", e);
  			}
  		}
  		locationRemoteCallbacks.finishBroadcast();
  		Log.d(TAG, "Provider status dispatched: " + (locationCallbacks.size() + n));
  	}
  
  	private void updateProvider(final ILocationListener callback)
  	{
  		if (gpsStatus == GPS_OFF)
  			callback.onProviderDisabled(LocationManager.GPS_PROVIDER);
  		else
  			callback.onProviderEnabled(LocationManager.GPS_PROVIDER);
  	}
  
  	private void updateGpsStatus(final int status, final int fsats, final int tsats)
  	{
  		gpsStatus = status;
  		updateNotification();
  		final Handler handler = new Handler();
  		for (final ILocationListener callback : locationCallbacks)
  		{
  			handler.post(new Runnable() {
  				@Override
  				public void run()
  				{
  					callback.onGpsStatusChanged(LocationManager.GPS_PROVIDER, status, fsats, tsats);
  				}
  			});
  		}
  		final int n = locationRemoteCallbacks.beginBroadcast();
  		for (int i = 0; i < n; i++)
  		{
  			final ILocationCallback callback = locationRemoteCallbacks.getBroadcastItem(i);
  			try
  			{
  				callback.onGpsStatusChanged(LocationManager.GPS_PROVIDER, status, fsats, tsats);
  			}
  			catch (RemoteException e)
  			{
  				Log.e(TAG, "Status broadcast error", e);
  			}
  		}
  		locationRemoteCallbacks.finishBroadcast();
  		Log.d(TAG, "GPS status dispatched: " + (locationCallbacks.size() + n));
  	}
  
  	@Override
  	public void onLocationChanged(final Location location)
  	{
  		if (enableMockLocations)
  			return;
  		
  		tics++;
  
  		boolean sendUpdate = false;
  
  		long time = SystemClock.elapsedRealtime();
  
  		// Log.i(TAG, "Location arrived: "+location.toString());
  
  		Log.d(TAG, "Fix arrived");
  
  		long prevLocationMillis = lastLocationMillis;
  		float prevSpeed = lastKnownLocation.getSpeed();
  		float prevTrack = lastKnownLocation.getBearing();
  
  		lastKnownLocation = location;
  
  		if (lastKnownLocation.getSpeed() == 0 && prevTrack != 0)
  		{
  			lastKnownLocation.setBearing(prevTrack);
  		}
  
  		lastLocationMillis = time;
  		sendUpdate = true;
  
  		if (!Float.isNaN(nmeaGeoidHeight))
  		{
  			lastKnownLocation.setAltitude(lastKnownLocation.getAltitude() + nmeaGeoidHeight);
  		}
  
  		if (justStarted)
  		{
  			justStarted = prevSpeed == 0;
  		}
  		else if (lastKnownLocation.getSpeed() > 0)
  		{
  			// filter speed outrages
  			double a = 2 * 9.8 * (lastLocationMillis - prevLocationMillis) / 1000;
  			if (Math.abs(lastKnownLocation.getSpeed() - prevSpeed) > a)
  				lastKnownLocation.setSpeed(prevSpeed);
  		}
  
  		// smooth speed
  		float smoothspeed = 0;
  		float curspeed = lastKnownLocation.getSpeed();
  		for (int i = speed.length - 1; i > 1; i--)
  		{
  			smoothspeed += speed[i];
  			speed[i] = speed[i - 1];
  		}
  		smoothspeed += speed[1];
  		if (speed[1] < speed[0] && speed[0] > curspeed)
  		{
  			speed[0] = (speed[1] + curspeed) / 2;
  		}
  		smoothspeed += speed[0];
  		speed[1] = speed[0];
  		lastKnownLocation.setSpeed(speed[1]);
  		speed[0] = curspeed;
  		if (speed[0] == 0 && speed[1] == 0)
  			smoothspeed = 0;
  		else
  			smoothspeed = smoothspeed / speed.length;
  
  		// average speed
  		float avspeed = 0;
  		for (int i = speedav.length - 1; i >= 0; i--)
  		{
  			avspeed += speedav[i];
  		}
  		avspeed = avspeed / speedav.length;
  		if (tics % pause == 0)
  		{
  			if (avspeed > 0)
  			{
  				float diff = curspeed / avspeed;
  				if (0.95 < diff && diff < 1.05)
  				{
  					for (int i = speedav.length - 1; i > 0; i--)
  					{
  						speedav[i] = speedav[i - 1];
  					}
  					speedav[0] = curspeed;
  				}
  			}
  			float fluct = 0;
  			for (int i = speedavex.length - 1; i > 0; i--)
  			{
  				fluct += speedavex[i] / curspeed;
  				speedavex[i] = speedavex[i - 1];
  			}
  			fluct += speedavex[0] / curspeed;
  			speedavex[0] = curspeed;
  			fluct = fluct / speedavex.length;
  			if (0.95 < fluct && fluct < 1.05)
  			{
  				for (int i = speedav.length - 1; i >= 0; i--)
  				{
  					speedav[i] = speedavex[i];
  				}
  				if (pause < 5)
  					pause++;
  			}
  		}
  
  		smoothSpeed = smoothspeed;
  		avgSpeed = avspeed;
  
  		if (sendUpdate)
  			updateLocation();
  
  		isContinous = true;
  	}
  
  	@Override
  	public void onNmeaReceived(long timestamp, String nmea)
  	{
  		if (nmea.indexOf('\n') == 0)
  			return;
  		if (nmea.indexOf('\n') > 0)
  		{
  			nmea = nmea.substring(0, nmea.indexOf('\n') - 1);
  		}
  		int len = nmea.length();
  		if (len < 9)
  		{
  			return;
  		}
  		if (nmea.charAt(len - 3) == '*')
  		{
  			nmea = nmea.substring(0, len - 3);
  		}
  		String[] tokens = nmea.split(",");
  		String sentenceId = tokens[0].length() > 5 ? tokens[0].substring(3, 6) : "";
  
  		try
  		{
  			if (sentenceId.equals("GGA") && tokens.length > 11)
  			{
  				// String time = tokens[1];
  				// String latitude = tokens[2];
  				// String latitudeHemi = tokens[3];
  				// String longitude = tokens[4];
  				// String longitudeHemi = tokens[5];
  				// String fixQuality = tokens[6];
  				// String numSatellites = tokens[7];
  				// String horizontalDilutionOfPrecision = tokens[8];
  				// String altitude = tokens[9];
  				// String altitudeUnits = tokens[10];
  				String heightOfGeoid = tokens[11];
  				if (!"".equals(heightOfGeoid))
  					nmeaGeoidHeight = Float.parseFloat(heightOfGeoid);
  				// String heightOfGeoidUnits = tokens[12];
  				// String timeSinceLastDgpsUpdate = tokens[13];
  			}
  			else if (sentenceId.equals("GSA") && tokens.length > 17)
  			{
  				// String selectionMode = tokens[1]; // m=manual, a=auto 2d/3d
  				// String mode = tokens[2]; // 1=no fix, 2=2d, 3=3d
  				@SuppressWarnings("unused")
  				String pdop = tokens[15];
  				String hdop = tokens[16];
  				String vdop = tokens[17];
  				if (!"".equals(hdop))
  					HDOP = Float.parseFloat(hdop);
  				if (!"".equals(vdop))
  					VDOP = Float.parseFloat(vdop);
  			}
  		}
  		catch (NumberFormatException e)
  		{
  			Log.e(TAG, "NFE", e);
  		}
  		catch (ArrayIndexOutOfBoundsException e)
  		{
  			Log.e(TAG, "AIOOBE", e);
  		}
  	}
  
  	@Override
  	public void onProviderDisabled(String provider)
  	{
  		updateProvider(provider, false);
  	}
  
  	@Override
  	public void onProviderEnabled(String provider)
  	{
  		updateProvider(provider, true);
  	}
  
  	@Override
  	public void onStatusChanged(String provider, int status, Bundle extras)
  	{
  		if (enableMockLocations)
  			return;
  
  		if (LocationManager.GPS_PROVIDER.equals(provider))
  		{
  			switch (status)
  			{
  				case LocationProvider.TEMPORARILY_UNAVAILABLE:
  				case LocationProvider.OUT_OF_SERVICE:
  					tearTrack();
  					updateNotification();
  					break;
  			}
  		}
  	}
  
  	@Override
  	public void onGpsStatusChanged(int event)
  	{
  		if (enableMockLocations)
  			return;
  
  		switch (event)
  		{
  			case GpsStatus.GPS_EVENT_STARTED:
  				updateProvider(LocationManager.GPS_PROVIDER, true);
  				updateGpsStatus(GPS_SEARCHING, 0, 0);
  				break;
  			case GpsStatus.GPS_EVENT_FIRST_FIX:
  				isContinous = false;
  				break;
  			case GpsStatus.GPS_EVENT_STOPPED:
  				tearTrack();
  				updateGpsStatus(GPS_OFF, 0, 0);
  				updateProvider(LocationManager.GPS_PROVIDER, false);
  				break;
  			case GpsStatus.GPS_EVENT_SATELLITE_STATUS:
  				if (locationManager == null)
  					return;
  				GpsStatus gpsStatus = locationManager.getGpsStatus(null);
  				Iterator<GpsSatellite> it = gpsStatus.getSatellites().iterator();
  				int tSats = 0;
  				int fSats = 0;
  				while (it.hasNext())
  				{
  					tSats++;
  					GpsSatellite sat = (GpsSatellite) it.next();
  					if (sat.usedInFix())
  						fSats++;
  				}
  				if (SystemClock.elapsedRealtime() - lastLocationMillis < 3000)
  				{
  					updateGpsStatus(GPS_OK, fSats, tSats);
  				}
  				else
  				{
  					tearTrack();
  					updateGpsStatus(GPS_SEARCHING, fSats, tSats);
  				}
  				break;
  		}
  	}
  
  	public class LocalBinder extends Binder implements ILocationService
  	{
  		@Override
  		public void registerLocationCallback(ILocationListener callback)
  		{
  			updateProvider(callback);
  			updateLocation(callback);
  			locationCallbacks.add(callback);
  		}
  
  		@Override
  		public void unregisterLocationCallback(ILocationListener callback)
  		{
  			locationCallbacks.remove(callback);
  		}
  
  		@Override
  		public void registerTrackingCallback(com.androzic.location.ITrackingListener callback)
  		{
  			trackingCallbacks.add(callback);
  		}
  
  		@Override
  		public void unregisterTrackingCallback(com.androzic.location.ITrackingListener callback)
  		{
  			trackingCallbacks.remove(callback);
  		}
  
  		@Override
  		public boolean isLocating()
  		{
  			return locationsEnabled;
  		}
  
  		@Override
  		public boolean isTracking()
  		{
  			return trackingEnabled;
  		}
  
  		@Override
  		public float getHDOP()
  		{
  			return HDOP;
  		}
  
  		@Override
  		public float getVDOP()
  		{
  			return VDOP;
  		}
  
  		@Override
  		public Track getTrack()
  		{
  			return LocationService.this.getTrack();
  		}
  
  		@Override
  		public Track getTrack(long start, long end)
  		{
  			return LocationService.this.getTrack(start, end);
  		}
  
  		@Override
  		public void clearTrack()
  		{
  			LocationService.this.clearTrack();
  		}
  
  		@Override
  		public long getTrackStartTime()
  		{
  			return LocationService.this.getTrackStartTime();
  		}
  
  		@Override
  		public long getTrackEndTime()
  		{
  			return LocationService.this.getTrackEndTime();
  		}
  	}
  	
  	/**
  	 * Mock location generator used for application testing. Locations are generated
  	 * by logic required for particular test.
  	 */
  	final private Runnable sendMockLocation = new Runnable() {
  		public void run()
  		{
  			mockCallback.postDelayed(this, 1000);
  
  			updateGpsStatus(GPS_OK, 5, 25);
  
  			lastKnownLocation.setProvider(LocationManager.GPS_PROVIDER);
  			lastKnownLocation.setTime(System.currentTimeMillis());
  			lastKnownLocation.setSpeed(20);
  			lastKnownLocation.setBearing(323);
  			lastKnownLocation.setAltitude(39);
  			lastKnownLocation.setLatitude(34.865792);
  			lastKnownLocation.setLongitude(32.351646);
  			lastKnownLocation.setBearing((System.currentTimeMillis() / 166) % 360);
  			//lastKnownLocation.setAltitude(169);
  			//lastKnownLocation.setLatitude(55.852527);
  			//lastKnownLocation.setLongitude(29.451150);
  			nmeaGeoidHeight = 0;
  			smoothSpeed = 19;
  			avgSpeed = 14;
  
  			updateLocation();
  			isContinous = true;
  		}
  	};
  }
  