com.androzic.location.LocationService.java Source code

Java tutorial

Introduction

Here is the source code for com.androzic.location.LocationService.java

Source

/*
 * 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 android.widget.Toast;

import com.androzic.Androzic;
import com.androzic.gp.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;
    private boolean useNetwork = true;
    private int gpsLocationTimeout = 120000;

    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_usenetwork));
        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);
            updateProvider(LocationManager.NETWORK_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_usenetwork).equals(key)) {
            useNetwork = sharedPreferences.getBoolean(key, getResources().getBoolean(R.bool.def_loc_usenetwork));
        } else 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);
            if (useNetwork) {
                try {
                    locationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, 0, 0, this);
                    Log.d(TAG, "Network provider set");
                } catch (IllegalArgumentException e) {
                    Toast.makeText(this, getString(R.string.err_no_network_provider), Toast.LENGTH_LONG).show();
                }
            }
            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());
        }
    }

    private void disconnect() {
        if (locationManager != null) {
            locationManager.removeNmeaListener(this);
            locationManager.removeUpdates(this);
            locationManager.removeGpsStatusListener(this);
            locationManager = null;
            stopForeground(true);
        }
    }

    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);
        PendingIntent contentIntent = PendingIntent.getActivity(this, NOTIFICATION_ID, intent, 0);
        builder.setContentIntent(contentIntent);
        builder.setContentTitle(getText(R.string.notif_loc_short));
        if (errorTime > 0 && DEBUG_ERRORS)
            builder.setContentText(errorMsg);
        else
            builder.setContentText(getText(msgId));
        builder.setOngoing(true);

        Notification notification = builder.getNotification();
        return notification;
    }

    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 || !isContinous || timeFromLastWriting > maxTime
                || distanceFromLastWriting > minDistance && timeFromLastWriting > minTime) {
            needsWrite = true;
        }

        lastLocation = loc;

        if (needsWrite) {
            writeLocation(loc, isContinous);
            isContinous = 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) {
        tics++;

        boolean fromGps = false;
        boolean sendUpdate = false;

        long time = SystemClock.elapsedRealtime();

        // Log.i(TAG, "Location arrived: "+location.toString());

        if (LocationManager.NETWORK_PROVIDER.equals(location.getProvider())) {
            if (useNetwork && (gpsStatus == GPS_OFF
                    || (gpsStatus == GPS_SEARCHING && time > lastLocationMillis + gpsLocationTimeout))) {
                Log.d(TAG, "New location");
                lastKnownLocation = location;
                lastLocationMillis = time;
                isContinous = false;
                sendUpdate = true;
            } else {
                return;
            }
        } else {
            fromGps = true;

            Log.d(TAG, "Fix arrived");

            long prevLocationMillis = lastLocationMillis;
            float prevSpeed = lastKnownLocation.getSpeed();

            lastKnownLocation = location;
            lastLocationMillis = time;
            sendUpdate = true;

            if (!Float.isNaN(nmeaGeoidHeight)) {
                location.setAltitude(location.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;
        }

        /*
         * lastKnownLocation.setSpeed(20); lastKnownLocation.setBearing(55);
         * lastKnownLocation.setAltitude(169);
         * lastKnownLocation.setLatitude(55.852527);
         * lastKnownLocation.setLongitude(29.451150);
         */

        if (sendUpdate)
            updateLocation();

        isContinous = fromGps;
    }

    @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 (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) {
        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();
        }
    }
}