org.spontaneous.trackservice.RemoteService.java Source code

Java tutorial

Introduction

Here is the source code for org.spontaneous.trackservice.RemoteService.java

Source

/*
 * Copyright (C) 2007 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.spontaneous.trackservice;

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 org.spontaneous.R;
import org.spontaneous.activities.CurrentActivityActivity;
import org.spontaneous.activities.model.GeoPoint;
import org.spontaneous.activities.model.SegmentModel;
import org.spontaneous.activities.model.TrackModel;
import org.spontaneous.activities.util.CustomExceptionHandler;
import org.spontaneous.core.TrackingUtil;
import org.spontaneous.db.GPSTracking.Segments;
import org.spontaneous.db.GPSTracking.SegmentsColumns;
import org.spontaneous.db.GPSTracking.Tracks;
import org.spontaneous.db.GPSTracking.TracksColumns;
import org.spontaneous.db.GPSTracking.Waypoints;
import org.spontaneous.trackservice.util.TrackingServiceConstants;
import org.spontaneous.utility.Constants;

import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.media.RingtoneManager;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.PowerManager;
import android.os.RemoteCallbackList;
import android.os.RemoteException;
import android.os.SystemClock;
import android.preference.PreferenceManager;
import android.provider.BaseColumns;
import android.renderscript.ScriptGroup.Binding;
import android.support.v4.app.NotificationCompat;
import android.util.Log;
import android.widget.Toast;
// Need the following import to get access to the app resources, since this
// class is in a sub-package.

//import com.google.android.gms.internal.nl;

/**
 * This is an example of implementing an application service that runs in a different process than the application.
 * Because it can be in another process, we must use IPC to interact with it. The {@link Controller} and {@link Binding}
 * classes show how to interact with the service.
 *
 * <p>
 * Note that most applications <strong>do not</strong> need to deal with the complexity shown here. If your application
 * simply has a service running in its own process, the {@link LocalService} sample shows a much simpler way to interact
 * with it.
 */
public class RemoteService extends Service implements LocationListener {

    private static final Boolean DEBUG = false;

    private static final boolean VERBOSE = false;

    private static final String TAG = "RemoteService";

    private long mTrackId = -1;

    private long mSegmentId = -1;

    private long mWaypointId = -1;

    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 LocationManager mLocationManager;

    private NotificationManager mNoticationManager;

    private PowerManager.WakeLock mWakeLock;

    private int mLoggingState = TrackingServiceConstants.STOPPED;

    private int mPrecision;

    private boolean mShowingGpsDisabled;

    private WayPointModel mWayPointModel;
    private TrackModel mTrackData;

    private boolean mStartNextSegment;

    private Location mPreviousLocation;

    private float mDistance;

    private float mTotalDistance;

    private Notification mNotification;

    private Vector<Location> mWeakLocations;

    private Queue<Double> mAltitudes;

    private long mLastTimeBroadcast;

    /**
     * If speeds should be checked to sane values
     */
    private boolean mSpeedSanityCheck;

    /**
     * Time thread to runs tasks that check whether the GPS listener has received enough to consider the GPS system alive.
     */
    private Timer mHeartbeatTimer;

    /**
     * <code>mAcceptableAccuracy</code> indicates the maximum acceptable accuracy of a waypoint in meters.
     */
    private float mMaxAcceptableAccuracy = 20;

    private int mSatellites = 0;

    /**
     * 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;

    /**
     * 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) {

            this.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 = RemoteService.this.mPreviousLocation;
                synchronized (RemoteService.this.mWeakLocations) {
                    if (!RemoteService.this.mWeakLocations.isEmpty()) {
                        if (checkLocation == null) {
                            checkLocation = RemoteService.this.mWeakLocations.lastElement();
                        } else {
                            Location weakLocation = RemoteService.this.mWeakLocations.lastElement();
                            checkLocation = weakLocation.getTime() > checkLocation.getTime() ? weakLocation
                                    : checkLocation;
                        }
                    }
                }
                // Is the last known GPS location something nearby we are not told?
                Location managerLocation = RemoteService.this.mLocationManager.getLastKnownLocation(this.mProvider);
                if (managerLocation != null && checkLocation != null) {
                    if (checkLocation.distanceTo(managerLocation) < 2 * RemoteService.this.mMaxAcceptableAccuracy) {
                        checkLocation = managerLocation.getTime() > checkLocation.getTime() ? managerLocation
                                : checkLocation;
                    }
                }

                if (checkLocation == null
                        || checkLocation.getTime() + RemoteService.this.mCheckPeriod < new Date().getTime()) {
                    Log.w(TAG, "GPS system failed to produce a location during logging: " + checkLocation);
                    RemoteService.this.mLoggingState = TrackingServiceConstants.PAUSED;
                    resumeLogging(-1l);

                    // if (mStatusMonitor) {
                    // soundGpsSignalAlarm();
                    // }

                }
            }
        }
    };

    /**
     * This is a list of callbacks that have been registered with the service. Note that this is package scoped (instead
     * of private) so that it can be accessed more efficiently from inner classes.
     */
    private final RemoteCallbackList<IRemoteServiceCallback> mCallbacks = new RemoteCallbackList<IRemoteServiceCallback>();

    private int mValue = 0;

    // private NotificationManager mNM;

    @Override
    public void onCreate() {

        // mNM = (NotificationManager)getSystemService(NOTIFICATION_SERVICE);

        // Get start location if available

        registerExceptionHandler();

        // Display a notification about us starting.
        showNotification();

        this.mHeartbeatTimer = new Timer("heartbeat", true);

        this.mWeakLocations = new Vector<Location>(3);
        this.mAltitudes = new LinkedList<Double>();
        this.mLoggingState = TrackingServiceConstants.STOPPED;
        this.mStartNextSegment = false;
        this.mLocationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);

        this.mNoticationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
        // stopNotification();

        SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
        this.mSpeedSanityCheck = sharedPreferences.getBoolean(TrackingServiceConstants.SPEEDSANITYCHECK, true);
        // mStreamBroadcast = sharedPreferences.getBoolean(Constants.BROADCAST_STREAM, false);
        // boolean startImmidiatly = PreferenceManager.getDefaultSharedPreferences(this).getBoolean(Constants.LOGATSTARTUP,
        // false);

        // While this service is running, it will continually increment a
        // number. Send the first message that is used to perform the
        // increment.
        this.mHandler.sendEmptyMessage(REPORT_MSG);
    }

    @Override
    public void onDestroy() {

        // Cancel the persistent notification.
        this.mNoticationManager.cancel(0);
        // Tell the user we stopped.
        Toast.makeText(this, R.string.remote_service_stopped, Toast.LENGTH_SHORT).show();
        // Unregister all callbacks.
        this.mCallbacks.kill();
        // Remove the next pending message to increment the counter, stopping
        // the increment loop.
        this.mHandler.removeMessages(REPORT_MSG);

        updateWakeLock();

        stopListening();

    }

    // BEGIN_INCLUDE(exposing_a_service)
    @Override
    public IBinder onBind(Intent intent) {

        // Select the interface to return. If your service only implements
        // a single interface, you can just return it here without checking
        // the Intent.
        try {
            Log.i(TAG, IRemoteService.class.getName().toString());
            Log.i(TAG, intent.getComponent().getClassName());
            if (Class.forName(RemoteService.class.getName()).toString()
                    .equals(intent.getComponent().getClassName())) {
                return this.mBinder;
            }
        } catch (ClassNotFoundException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        return this.mBinder;
    }

    @Override
    public boolean onUnbind(Intent intent) {

        stopListening();
        return super.onUnbind(intent);
    }

    /**
     * The IRemoteInterface is defined through IDL
     */
    private final IRemoteService.Stub mBinder = new IRemoteService.Stub() {

        @Override
        public void registerCallback(IRemoteServiceCallback cb) {

            if (cb != null)
                RemoteService.this.mCallbacks.register(cb);
        }

        @Override
        public void unregisterCallback(IRemoteServiceCallback cb) {

            if (cb != null)
                RemoteService.this.mCallbacks.unregister(cb);
        }

        @Override
        public int loggingState() throws RemoteException {

            return RemoteService.this.mLoggingState;
        }

        @Override
        public long startLogging(Location startLocation) throws RemoteException {

            RemoteService.this.startLogging(startLocation);
            return RemoteService.this.mTrackId;
        }

        @Override
        public void pauseLogging() throws RemoteException {

            RemoteService.this.pauseLogging();
        }

        @Override
        public long resumeLogging(long trackId) throws RemoteException {

            RemoteService.this.resumeLogging(trackId);
            return RemoteService.this.mSegmentId;
        }

        @Override
        public void stopLogging() throws RemoteException {

            RemoteService.this.mLoggingState = TrackingServiceConstants.STOPPED;
            stopListening();
            updateWakeLock();
            updateSegment();
            updateTrack();
        }
    };

    // END_INCLUDE(exposing_a_service)
    @Override
    public void onTaskRemoved(Intent rootIntent) {

        Toast.makeText(this, "Task removed: " + rootIntent, Toast.LENGTH_LONG).show();
    }

    private static final int REPORT_MSG = 1;

    private static final int LOCATION_MSG = 2;

    /**
     * Our Handler used to execute operations on the main thread. This is used to schedule increments of our value.
     */
    private final Handler mHandler = new Handler() {

        @Override
        public void handleMessage(Message msg) {

            switch (msg.what) {
            // It is time to bump the value!
            case REPORT_MSG: {
                // Up it goes.
                int value = ++RemoteService.this.mValue;
                // Broadcast to all clients the new value.
                final int N = RemoteService.this.mCallbacks.beginBroadcast();
                for (int i = 0; i < N; i++) {
                    try {
                        RemoteService.this.mCallbacks.getBroadcastItem(i).valueChanged(value);
                    } catch (RemoteException e) {
                        // The RemoteCallbackList will take care of removing
                        // the dead object for us.
                    }
                }
                RemoteService.this.mCallbacks.finishBroadcast();
                // Repeat every 1 second. LOCATION_MSG
                // sendMessageDelayed(obtainMessage(REPORT_MSG), 1*1000);
            }
            case LOCATION_MSG: {
                // Broadcast to all clients the new location.
                final int N = RemoteService.this.mCallbacks.beginBroadcast();
                for (int i = 0; i < N; i++) {
                    try {
                        Location location = (Location) msg.getData()
                                .getParcelable(TrackingServiceConstants.EXTRA_LOCATION);

                        GeoPoint geoPoint = new GeoPoint();
                        geoPoint.setLatitude(location.getLatitude());
                        geoPoint.setLongitude(location.getLongitude());
                        geoPoint.setDistance(RemoteService.this.mDistance);
                        geoPoint.setSpeed(location.getSpeed());
                        RemoteService.this.mWayPointModel.setTotalDistance(RemoteService.this.mTotalDistance);
                        RemoteService.this.mWayPointModel.setTrackId(Long.valueOf(RemoteService.this.mTrackId));
                        RemoteService.this.mWayPointModel.setSegmentId(Long.valueOf(RemoteService.this.mSegmentId));
                        RemoteService.this.mWayPointModel
                                .setWayPointId(Long.valueOf(RemoteService.this.mWaypointId));
                        RemoteService.this.mWayPointModel.setGeopoint(geoPoint);
                        RemoteService.this.mCallbacks.getBroadcastItem(i)
                                .locationChanged(RemoteService.this.mWayPointModel);
                    } catch (RemoteException e) {
                        // The RemoteCallbackList will take care of removing
                        // the dead object for us.
                    }
                }
                RemoteService.this.mCallbacks.finishBroadcast();
            }
                break;
            default:
                super.handleMessage(msg);
            }
        }
    };

    /**
     * Show a notification while this service is running.
     */
    private void showNotification() {

        // In this sample, we'll use the same text for the ticker and the expanded notification
        CharSequence text = getText(R.string.remote_service_label);

        // define sound URI, the sound to be played when there's a notification
        Uri soundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);

        // Set the icon, scrolling text and timestamp
        // Notification notification = new Notification(R.drawable.stat_sample, text,
        // System.currentTimeMillis());

        // The PendingIntent to launch our activity if the user selects this notification

        Intent resumeIntent = new Intent(this, CurrentActivityActivity.class);
        resumeIntent.putExtra(TrackingServiceConstants.TRACK_ID, this.mTrackId);
        resumeIntent.putExtra(TrackingServiceConstants.SEGMENT_ID, this.mSegmentId);

        PendingIntent contentIntent = PendingIntent.getActivity(this, 0, resumeIntent,
                PendingIntent.FLAG_UPDATE_CURRENT);
        // this is it, we'll build the notification!
        // in the addAction method, if you don't want any icon, just set the first param to 0
        Notification mNotification = new NotificationCompat.Builder(this).setContentTitle(text)
                .setContentText(getText(R.string.remote_service_started))
                .setSmallIcon(R.drawable.ic_process_launcher).setContentIntent(contentIntent).setSound(soundUri)
                .addAction(R.drawable.ic_process_launcher, "View", contentIntent)
                // .addAction(0, "Remind", contentIntent)
                .build();

        this.mNoticationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);

        // If you want to hide the notification after it was selected, do the code below
        // myNotification.flags |= Notification.FLAG_AUTO_CANCEL;

        // mNoticationManager.notify(0, mNotification);

        // Notification notification = new Notification(R.drawable.ic_process_launcher, text,
        // System.currentTimeMillis());
        //
        // long [] vibrate = {0,100,200,300};
        // notification.vibrate = vibrate;
        //
        // // Set the info for the views that show in the notification panel.
        // notification.setLatestEventInfo(this, getText(R.string.remote_service_label),
        // text, contentIntent);
        // Send the notification.
        // We use a string id because it is a unique number. We use it later to cancel.
        this.mNoticationManager.notify(0, mNotification);
    }

    @Override
    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 (this.mShowingGpsDisabled) {
            notifyOnEnabledProviderNotification(R.string.service_gpsenabled);
        }

        if (isLogging()) {
            Location filteredLocation = locationFilter(location);
            if (filteredLocation != null) {
                if (this.mStartNextSegment) {
                    this.mStartNextSegment = false;
                    startNewSegment();
                    // Obey the start segment if the previous location is unknown or far away
                    // if (mPreviousLocation == null || filteredLocation.distanceTo(mPreviousLocation) > 4 *
                    // mMaxAcceptableAccuracy) {
                    // startNewSegment();
                    // }
                } else if (this.mPreviousLocation != null) {
                    this.mDistance = this.mPreviousLocation.distanceTo(filteredLocation);
                    this.mTotalDistance += this.mDistance;
                }

                storeLocation(filteredLocation);
                updateTrack();
                broadcastLocation(filteredLocation);
                this.mPreviousLocation = filteredLocation;
            }
        }

    }

    public synchronized void startLogging(Location startLocation) {

        if (DEBUG) {
            Log.d(TAG, "startLogging()");
        }

        Long userId = getUserId();
        if (this.mLoggingState == TrackingServiceConstants.STOPPED && userId != null) {

            startNewTrack(startLocation, userId);
            mTrackData = readTrackAndSegmentsById(this.mTrackId);

            // sendRequestLocationUpdatesMessage();
            // sendRequestStatusUpdateMessage();
            this.mLoggingState = TrackingServiceConstants.LOGGING;
            updateWakeLock();
            // startNotification();
            // crashProtectState();
            // broadCastLoggingState();

            this.mMaxAcceptableAccuracy = COARSE_ACCURACY;
            long intervaltime = FINE_INTERVAL;
            float distance = FINE_DISTANCE;
            startListening(LocationManager.GPS_PROVIDER, intervaltime, distance);
        }
    }

    private Long getUserId() {
        Long userId = null;
        SharedPreferences sharedPrefs = getSharedPreferences(Constants.PREFERENCES, MODE_PRIVATE);
        if (sharedPrefs != null) {
            userId = sharedPrefs.getLong(Constants.PREF_USERID, -1L);
        }
        return userId;
    }

    public synchronized void pauseLogging() {

        this.mLoggingState = TrackingServiceConstants.PAUSED;
        updateSegment();
        updateTrack();
    }

    public synchronized void resumeLogging(Long trackId) {

        if (DEBUG) {
            Log.d(TAG, "resumeLogging()");
        }

        if (trackId > 0) {
            mTrackId = trackId;
            mTrackData = readTrackAndSegmentsById(this.mTrackId);
            this.mTotalDistance = mTrackData.getTotalDistance();

            this.mMaxAcceptableAccuracy = COARSE_ACCURACY;
            long intervaltime = FINE_INTERVAL;
            float distance = FINE_DISTANCE;
            startListening(LocationManager.GPS_PROVIDER, intervaltime, distance);
        }

        if (this.mLoggingState == TrackingServiceConstants.PAUSED
                || this.mLoggingState == TrackingServiceConstants.STOPPED) {

            if (this.mPrecision != TrackingServiceConstants.LOGGING_GLOBAL) {
                this.mStartNextSegment = true;
            }
            // sendRequestLocationUpdatesMessage();
            // sendRequestStatusUpdateMessage();

            this.mLoggingState = TrackingServiceConstants.LOGGING;
            updateWakeLock();
            // updateNotification();
            // crashProtectState();
            // broadCastLoggingState();
        }
    }

    private void startListening(String provider, long intervaltime, float distance) {

        this.mLocationManager.removeUpdates(this);
        this.mLocationManager.requestLocationUpdates(provider, intervaltime, distance, this);
        this.mWayPointModel = new WayPointModel();
        this.mWayPointModel.setStartTime(SystemClock.currentThreadTimeMillis());

        this.mCheckPeriod = Math.max(12 * intervaltime, 120 * 1000);
        if (this.mHeartbeat != null) {
            this.mHeartbeat.cancel();
            this.mHeartbeat = null;
        }
        this.mHeartbeat = new Heartbeat(provider);
        this.mHeartbeatTimer.schedule(this.mHeartbeat, this.mCheckPeriod, this.mCheckPeriod);
    }

    private void stopListening() {

        if (this.mHeartbeat != null) {
            this.mHeartbeat.cancel();
            this.mHeartbeat = null;
        }
        this.mLocationManager.removeUpdates(this);
    }

    /**
     * (non-Javadoc)
     *
     * @see nl.sogeti.android.gpstracker.IGPSLoggerService#getLoggingState()
     */
    protected boolean isLogging() {

        return this.mLoggingState == TrackingServiceConstants.LOGGING;
    }

    /**
     * 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
     */
    // TODO: Diese Methode auslagern und auch in StartFragment verwenden
    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() > this.mMaxAcceptableAccuracy) {
            Log.w(TAG, String.format("A weak location was received, lots of inaccuracy... (%f is more then max %f)",
                    proposedLocation.getAccuracy(), this.mMaxAcceptableAccuracy));
            proposedLocation = addBadLocation(proposedLocation);
        }

        // Do not log a waypoint which might be on any side of the previous waypoint
        if (proposedLocation != null && this.mPreviousLocation != null
                && proposedLocation.getAccuracy() > this.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(), this.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 (this.mSpeedSanityCheck && proposedLocation != null && this.mPreviousLocation != null) {
            // To avoid near instant teleportation on network location or glitches cause continent hopping
            float meters = proposedLocation.distanceTo(this.mPreviousLocation);
            long seconds = (proposedLocation.getTime() - this.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
                        && this.mPrecision != TrackingServiceConstants.LOGGING_GLOBAL) {
                    Log.w(TAG, "A strange location was received on GPS, reset the GPS listeners");
                    stopListening();
                    // mLocationManager.removeGpsStatusListener(mStatusListener);
                    this.mLocationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
                    // sendRequestStatusUpdateMessage();
                    // sendRequestLocationUpdatesMessage();
                }
            }
        }

        // Remove speed if not sane
        if (this.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 (this.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) {
            this.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) {

        this.mWeakLocations.add(location);
        if (this.mWeakLocations.size() < 3) {
            location = null;
        } else {
            Location best = this.mWeakLocations.lastElement();
            for (Location whimp : this.mWeakLocations) {
                if (whimp.hasAccuracy() && best.hasAccuracy() && whimp.getAccuracy() < best.getAccuracy()) {
                    best = whimp;
                } else {
                    if (whimp.hasAccuracy() && !best.hasAccuracy()) {
                        best = whimp;
                    }
                }
            }
            synchronized (this.mWeakLocations) {
                this.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
        this.mAltitudes.add(altitude);
        if (this.mAltitudes.size() > 3) {
            this.mAltitudes.poll();
        }
        for (Double alt : this.mAltitudes) {
            avg += alt;
            elements++;
        }
        avg = avg / elements;
        sane = Math.abs(altitude - avg) < MAX_REASONABLE_ALTITUDECHANGE;

        return sane;
    }

    /**
     * Consult broadcast options and execute broadcast if necessary
     *
     * @param location
     */
    public void broadcastLocation(Location location) {

        final long nowTime = location.getTime();
        if (this.mLastTimeBroadcast == 0) {
            this.mLastTimeBroadcast = nowTime;
        }
        long passedTime = (nowTime - this.mLastTimeBroadcast);

        Message msg = this.mHandler.obtainMessage(REPORT_MSG);
        Bundle data = new Bundle();
        data.putLong(TrackingServiceConstants.TRACK_ID, this.mTrackId);
        data.putLong(TrackingServiceConstants.SEGMENT_ID, this.mSegmentId);
        data.putLong(TrackingServiceConstants.WAYPOINT_ID, this.mWaypointId);
        data.putLong(TrackingServiceConstants.EXTRA_TIME, passedTime);
        data.putParcelable(TrackingServiceConstants.EXTRA_LOCATION, location);
        data.putFloat(TrackingServiceConstants.EXTRA_DISTANCE, this.mDistance);
        data.putFloat(TrackingServiceConstants.TOTAL_DISTANCE, this.mTotalDistance);
        data.putFloat(TrackingServiceConstants.EXTRA_SPEED, location.getSpeed());
        msg.setData(data);
        this.mHandler.sendMessage(msg);

    }

    private void notifyOnEnabledProviderNotification(int resId) {

        this.mNoticationManager.cancel(R.string.service_connectiondisabled);
        this.mShowingGpsDisabled = false;
        CharSequence text = this.getString(resId);
        Toast toast = Toast.makeText(this, text, Toast.LENGTH_LONG);
        toast.show();
    }

    /**
     * Use the ContentResolver mechanism to store a received location
     *
     * @param location
     */
    private 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, Double.valueOf(location.getLatitude()));
        args.put(Waypoints.LONGITUDE, Double.valueOf(location.getLongitude()));
        args.put(Waypoints.SPEED, Float.valueOf(location.getSpeed()));
        args.put(Waypoints.TIME, Long.valueOf(System.currentTimeMillis()));
        args.put(Waypoints.DISTANCE, this.mDistance);

        if (location.hasAccuracy()) {
            args.put(Waypoints.ACCURACY, Float.valueOf(location.getAccuracy()));
        }
        if (location.hasAltitude()) {
            args.put(Waypoints.ALTITUDE, Double.valueOf(location.getAltitude()));

        }
        if (location.hasBearing()) {
            args.put(Waypoints.BEARING, Float.valueOf(location.getBearing()));
        }

        Uri waypointInsertUri = Uri.withAppendedPath(Tracks.CONTENT_URI,
                this.mTrackId + "/segments/" + this.mSegmentId + "/waypoints");
        Uri inserted = getContentResolver().insert(waypointInsertUri, args);
        this.mWaypointId = Long.parseLong(inserted.getLastPathSegment());
    }

    /**
     * Update Track 
     * Store the current total distance of the track. 
     * Store the current total duration of the track.
     */
    private void updateTrack() {

        // Get current total duration
        TrackModel trackModel = readTrackAndSegmentsById(this.mTrackId);

        ContentValues args = new ContentValues();
        args.put(TracksColumns.TOTAL_DISTANCE, this.mTotalDistance);
        args.put(TracksColumns.TOTAL_DURATION, TrackingUtil.computeTotalDuration(trackModel));

        Uri trackUpdateUri = Uri.withAppendedPath(Tracks.CONTENT_URI, String.valueOf(this.mTrackId));
        getContentResolver().update(trackUpdateUri, args, null, null);

        this.mTrackData = readTrackAndSegmentsById(this.mTrackId);
    }

    /**
     * Trigged by events that start a new track
     */
    private void startNewTrack(Location startLocation, Long userId) {

        this.mTotalDistance = 0;
        this.mDistance = 0;
        ContentValues contentValues = new ContentValues();
        contentValues.put(Tracks.USER_ID, userId);
        Uri newTrack = getContentResolver().insert(Tracks.CONTENT_URI, contentValues);
        this.mTrackId = Long.valueOf(newTrack.getLastPathSegment()).longValue();

        startNewSegment();
        if (startLocation != null) {
            storeLocation(startLocation);
        }
    }

    /**
     * Trigged by events that start a new segment
     */
    private void startNewSegment() {

        this.mDistance = 0;
        this.mPreviousLocation = null;

        Uri newSegment = getContentResolver().insert(
                Uri.withAppendedPath(Tracks.CONTENT_URI, this.mTrackId + "/segments"), new ContentValues(0));
        this.mSegmentId = Long.valueOf(newSegment.getLastPathSegment()).longValue();
        // crashProtectState();
    }

    private void updateSegment() {

        if (this.mSegmentId > 0L) {
            ContentValues args = new ContentValues();
            args.put(SegmentsColumns.END_TIME, System.currentTimeMillis());

            Uri segmentUpdateUri = Uri.withAppendedPath(Tracks.CONTENT_URI,
                    this.mTrackId + "/segments/" + this.mSegmentId);
            getContentResolver().update(segmentUpdateUri, args, null, null);
        }
    }

    private void updateWakeLock() {

        if (this.mLoggingState == TrackingServiceConstants.LOGGING) {
            // PreferenceManager.getDefaultSharedPreferences(this).registerOnSharedPreferenceChangeListener(mSharedPreferenceChangeListener);

            PowerManager pm = (PowerManager) 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;
            }
        }
    }

    public TrackModel readTrackAndSegmentsById(Long trackId) {

        TrackModel trackModel = null;

        if (trackId != null) {
            String[] mTrackColumns = { Tracks._ID, Tracks.NAME, Tracks.TOTAL_DISTANCE, Tracks.TOTAL_DURATION,
                    Tracks.USER_ID, Tracks.CREATION_TIME };

            // Read track
            Uri trackReadUri = Uri.withAppendedPath(Tracks.CONTENT_URI, String.valueOf(trackId));
            Cursor mCursor = getContentResolver().query(trackReadUri, mTrackColumns, null, null,
                    Tracks.CREATION_TIME);
            if (mCursor != null && mCursor.moveToNext()) {

                trackModel = new TrackModel(Long.valueOf(mCursor.getString(mCursor.getColumnIndex(Tracks._ID))),
                        mCursor.getString(mCursor.getColumnIndex(Tracks.NAME)),
                        mCursor.getFloat(mCursor.getColumnIndex(Tracks.TOTAL_DISTANCE)),
                        mCursor.getLong(mCursor.getColumnIndex(Tracks.TOTAL_DURATION)),
                        mCursor.getLong(mCursor.getColumnIndex(Tracks.CREATION_TIME)),
                        Integer.valueOf(mCursor.getColumnIndex(Tracks.USER_ID)));

                // Read Segments and wayPoints
                String[] mSegmentsColumns = { BaseColumns._ID, SegmentsColumns.TRACK, SegmentsColumns.START_TIME,
                        SegmentsColumns.END_TIME };
                Uri segmentReadUri = Uri.withAppendedPath(Tracks.CONTENT_URI,
                        String.valueOf(trackId) + "/segments/");
                Cursor mSegmentsCursor = getContentResolver().query(segmentReadUri, mSegmentsColumns, null, null,
                        Segments._ID);

                if (mSegmentsCursor != null) {
                    SegmentModel segmentModel = null;
                    while (mSegmentsCursor.moveToNext()) {
                        segmentModel = new SegmentModel();
                        segmentModel.setId(Long
                                .valueOf(mSegmentsCursor.getString(mSegmentsCursor.getColumnIndex(Segments._ID))));
                        segmentModel.setTrackId(Long.valueOf(
                                mSegmentsCursor.getString(mSegmentsCursor.getColumnIndex(Segments.TRACK))));
                        segmentModel.setStartTimeInMillis(
                                mSegmentsCursor.getLong(mSegmentsCursor.getColumnIndex(Segments.START_TIME)));
                        segmentModel.setEndTimeInMillis(
                                mSegmentsCursor.getLong(mSegmentsCursor.getColumnIndex(Segments.END_TIME)));
                        trackModel.addSegment(segmentModel);
                    }
                }
            }
        }
        return trackModel;
    }

    private void registerExceptionHandler() {
        if (!(Thread.getDefaultUncaughtExceptionHandler() instanceof CustomExceptionHandler)) {
            Thread.setDefaultUncaughtExceptionHandler(
                    new CustomExceptionHandler(getExternalCacheDir().toString(), null));
        }
    }

    @Override
    public void onStatusChanged(String provider, int status, Bundle extras) {
        // TODO Auto-generated method stub

    }

    @Override
    public void onProviderEnabled(String provider) {
        // TODO Auto-generated method stub

    }

    @Override
    public void onProviderDisabled(String provider) {
        // TODO Auto-generated method stub

    }
}