com.nogago.android.tracks.services.TrackRecordingService.java Source code

Java tutorial

Introduction

Here is the source code for com.nogago.android.tracks.services.TrackRecordingService.java

Source

/*
 * Copyright 2008 Google Inc.
 *
 * 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 com.nogago.android.tracks.services;

import static com.nogago.android.tracks.Constants.RESUME_TRACK_EXTRA_NAME;
import static com.nogago.android.tracks.Constants.TAG;

import com.google.android.apps.mytracks.content.DescriptionGenerator;
import com.google.android.apps.mytracks.content.DescriptionGeneratorImpl;
import com.google.android.apps.mytracks.content.MyTracksLocation;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.content.Sensor;
import com.google.android.apps.mytracks.content.Sensor.SensorDataSet;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.content.WaypointCreationRequest;
import com.google.android.apps.mytracks.content.WaypointCreationRequest.WaypointType;
import com.google.android.apps.mytracks.services.AbsoluteLocationListenerPolicy;
import com.google.android.apps.mytracks.services.ITrackRecordingService;
import com.google.android.apps.mytracks.services.LocationListenerPolicy;
import com.google.android.apps.mytracks.services.PreferenceManager;
import com.google.android.apps.mytracks.services.sensors.SensorManager;
import com.google.android.apps.mytracks.services.sensors.SensorManagerFactory;
import com.google.android.apps.mytracks.services.tasks.PeriodicTaskExecutor;
import com.google.android.apps.mytracks.services.tasks.SplitTask;
import com.google.android.apps.mytracks.services.tasks.StatusAnnouncerFactory;
import com.google.android.apps.mytracks.stats.TripStatistics;
import com.google.android.apps.mytracks.stats.TripStatisticsBuilder;
import com.google.android.apps.mytracks.util.IntentUtils;
import com.google.android.apps.mytracks.util.LocationUtils;
import com.google.android.apps.mytracks.util.PreferencesUtils;
import com.google.android.apps.mytracks.util.TrackNameUtils;
import com.google.common.annotations.VisibleForTesting;
import com.nogago.android.tracks.Constants;
import com.nogago.android.tracks.R;
import com.nogago.android.tracks.TrackDetailActivity;

import android.app.Notification;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.database.sqlite.SQLiteException;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.net.Uri;
import android.os.Binder;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.PowerManager;
import android.os.PowerManager.WakeLock;
import android.os.Process;
import android.os.RemoteException;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.TaskStackBuilder;
import android.util.Log;

import java.util.Locale;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * A background service that registers a location listener and records track
 * points. Track points are saved to the MyTracksProvider.
 *
 * @author Leif Hendrik Wilden
 */
public class TrackRecordingService extends Service {

    static final int MAX_AUTO_RESUME_TRACK_RETRY_ATTEMPTS = 3;

    private LocationManager locationManager;
    private WakeLock wakeLock;

    private int minRecordingDistance = PreferencesUtils.MIN_RECORDING_DISTANCE_DEFAULT;
    private int maxRecordingDistance = PreferencesUtils.MAX_RECORDING_DISTANCE_DEFAULT;
    private int minRequiredAccuracy = PreferencesUtils.MIN_REQUIRED_ACCURACY_DEFAULT;
    private int autoResumeTrackTimeout = PreferencesUtils.AUTO_RESUME_TRACK_TIMEOUT_DEFAULT;

    private long recordingTrackId = -1;

    private long currentWaypointId = -1;

    /** The timer posts a runnable to the main thread via this handler. */
    private final Handler handler = new Handler();

    /**
     * Utilities to deal with the database.
     */
    private MyTracksProviderUtils providerUtils;

    private TripStatisticsBuilder statsBuilder;
    private TripStatisticsBuilder waypointStatsBuilder;

    /**
     * Current length of the recorded track. This length is calculated from the
     * recorded points (as compared to each location fix). It's used to overlay
     * waypoints precisely in the elevation profile chart.
     */
    private double length;

    /**
     * Status announcer executor.
     */
    private PeriodicTaskExecutor announcementExecutor;
    private PeriodicTaskExecutor splitExecutor;

    private SensorManager sensorManager;

    private PreferenceManager prefManager;

    /**
     * The interval in milliseconds that we have requested to be notified of gps
     * readings.
     */
    private long currentRecordingInterval;

    /**
     * The policy used to decide how often we should request gps updates.
     */
    private LocationListenerPolicy locationListenerPolicy = new AbsoluteLocationListenerPolicy(0);

    private LocationListener locationListener = new LocationListener() {
        @Override
        public void onProviderDisabled(String provider) {
            // Do nothing
        }

        @Override
        public void onProviderEnabled(String provider) {
            // Do nothing
        }

        @Override
        public void onStatusChanged(String provider, int status, Bundle extras) {
            // Do nothing
        }

        @Override
        public void onLocationChanged(final Location location) {
            if (executorService.isShutdown() || executorService.isTerminated()) {
                return;
            }
            executorService.submit(new Runnable() {
                @Override
                public void run() {
                    onLocationChangedAsync(location);
                }
            });
        }
    };

    /**
     * Task invoked by a timer periodically to make sure the location listener is
     * still registered.
     */
    private TimerTask checkLocationListener = new TimerTask() {
        @Override
        public void run() {
            // It's always safe to assume that if isRecording() is true, it implies
            // that onCreate() has finished.
            if (isRecording()) {
                handler.post(new Runnable() {
                    public void run() {
                        Log.d(Constants.TAG, "Re-registering location listener with TrackRecordingService.");
                        unregisterLocationListener();
                        registerLocationListener();
                    }
                });
            }
        }
    };

    /**
     * This timer invokes periodically the checkLocationListener timer task.
     */
    private final Timer timer = new Timer();

    /**
     * Is the phone currently moving?
     */
    private boolean isMoving = true;

    /**
     * The most recent recording track.
     */
    private Track recordingTrack;

    /**
     * Is the service currently recording a track?
     */
    private boolean isRecording;

    /**
     * Last good location the service has received from the location listener
     */
    private Location lastLocation;

    /**
     * Last valid location (i.e. not a marker) that was recorded.
     */
    private Location lastValidLocation;

    /**
     * A service to run tasks outside of the main thread.
     */
    private ExecutorService executorService;

    private ServiceBinder binder = new ServiceBinder(this);

    /*
     * Application lifetime events:
     */

    /*
     * Note that this service, through the AndroidManifest.xml, is configured to
     * allow both MyTracks and third party apps to invoke it. For the onCreate
     * callback, we cannot tell whether the caller is MyTracks or a third party
     * app, thus it cannot start/stop a recording or write/update MyTracks
     * database. However, it can resume a recording.
     */
    @Override
    public void onCreate() {
        super.onCreate();
        Log.d(TAG, "TrackRecordingService.onCreate");
        providerUtils = MyTracksProviderUtils.Factory.get(this);
        locationManager = (LocationManager) getSystemService(LOCATION_SERVICE);

        setUpTaskExecutors();
        executorService = Executors.newSingleThreadExecutor();

        prefManager = new PreferenceManager(this);

        registerLocationListener();

        /*
         * After 5 min, check every minute that location listener still is
         * registered and spit out additional debugging info to the logs:
         */
        timer.schedule(checkLocationListener, 1000 * 60 * 5, 1000 * 60);

        // Try to restore previous recording state in case this service has been
        // restarted by the system, which can sometimes happen.
        recordingTrack = getRecordingTrack();
        if (recordingTrack != null) {
            restoreStats(recordingTrack);
            isRecording = true;
        } else {
            if (recordingTrackId != -1L) {
                // Make sure we have consistent state in shared preferences.
                Log.w(TAG, "TrackRecordingService.onCreate: " + "Resetting an orphaned recording track = "
                        + recordingTrackId);
            }
            recordingTrackId = -1L;
            PreferencesUtils.setLong(this, R.string.recording_track_id_key, recordingTrackId);
        }
        showNotification();
    }

    /*
     * Note that this service, through the AndroidManifest.xml, is configured to
     * allow both MyTracks and third party apps to invoke it. For the onStart
     * callback, we cannot tell whether the caller is MyTracks or a third party
     * app, thus it cannot start/stop a recording or write/update MyTracks
     * database. However, it can resume a recording.
     */
    @Override
    public void onStart(Intent intent, int startId) {
        handleStartCommand(intent, startId);
    }

    /*
     * Note that this service, through the AndroidManifest.xml, is configured to
     * allow both MyTracks and third party apps to invoke it. For the
     * onStartCommand callback, we cannot tell whether the caller is MyTracks or a
     * third party app, thus it cannot start/stop a recording or write/update
     * MyTracks database. However, it can resume a recording.
     */
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        handleStartCommand(intent, startId);
        return START_STICKY;
    }

    private void handleStartCommand(Intent intent, int startId) {
        Log.d(TAG, "TrackRecordingService.handleStartCommand: " + startId);

        if (intent == null) {
            return;
        }

        // Check if called on phone reboot with resume intent.
        if (intent.getBooleanExtra(RESUME_TRACK_EXTRA_NAME, false)) {
            resumeTrack(startId);
        }
    }

    private boolean isTrackInProgress() {
        return recordingTrackId != -1 || isRecording;
    }

    private void resumeTrack(int startId) {
        Log.d(TAG, "TrackRecordingService: requested resume");

        // Make sure that the current track exists and is fresh enough.
        if (recordingTrack == null || !shouldResumeTrack(recordingTrack)) {
            Log.i(TAG, "TrackRecordingService: Not resuming, because the previous track (" + recordingTrack
                    + ") doesn't exist or is too old");
            isRecording = false;
            recordingTrackId = -1L;
            PreferencesUtils.setLong(this, R.string.recording_track_id_key, recordingTrackId);
            stopSelfResult(startId);
            return;
        }

        Log.i(TAG, "TrackRecordingService: resuming");
    }

    @Override
    public IBinder onBind(Intent intent) {
        Log.d(TAG, "TrackRecordingService.onBind");
        return binder;
    }

    @Override
    public boolean onUnbind(Intent intent) {
        Log.d(TAG, "TrackRecordingService.onUnbind");
        return super.onUnbind(intent);
    }

    @Override
    public void onDestroy() {
        Log.d(TAG, "TrackRecordingService.onDestroy");

        isRecording = false;
        showNotification();
        prefManager.shutdown();
        prefManager = null;
        checkLocationListener.cancel();
        checkLocationListener = null;
        timer.cancel();
        timer.purge();
        unregisterLocationListener();
        shutdownTaskExecutors();
        if (sensorManager != null) {
            SensorManagerFactory.releaseSystemSensorManager();
            sensorManager = null;
        }

        // Make sure we have no indirect references to this service.
        locationManager = null;
        providerUtils = null;
        binder.detachFromService();
        binder = null;

        // This should be the last operation.
        releaseWakeLock();

        // Shutdown the executor service last to avoid sending events to a dead executor.
        executorService.shutdown();
        super.onDestroy();
    }

    private void setAutoResumeTrackRetries(int retryAttempts) {
        Log.d(TAG, "Updating auto-resume retry attempts to: " + retryAttempts);
        PreferencesUtils.setInt(this, R.string.auto_resume_track_current_retry_key, retryAttempts);
    }

    private boolean shouldResumeTrack(Track track) {
        Log.d(TAG, "shouldResumeTrack: autoResumeTrackTimeout = " + autoResumeTrackTimeout);

        // Check if we haven't exceeded the maximum number of retry attempts.
        int retries = PreferencesUtils.getInt(this, R.string.auto_resume_track_current_retry_key,
                PreferencesUtils.AUTO_RESUME_TRACK_CURRENT_RETRY_DEFAULT);
        Log.d(TAG, "shouldResumeTrack: Attempting to auto-resume the track (" + (retries + 1) + "/"
                + MAX_AUTO_RESUME_TRACK_RETRY_ATTEMPTS + ")");
        if (retries >= MAX_AUTO_RESUME_TRACK_RETRY_ATTEMPTS) {
            Log.i(TAG, "shouldResumeTrack: Not resuming because exceeded the maximum "
                    + "number of auto-resume retries");
            return false;
        }

        // Increase number of retry attempts.
        setAutoResumeTrackRetries(retries + 1);

        // Check for special cases.
        if (autoResumeTrackTimeout == PreferencesUtils.AUTO_RESUME_TRACK_TIMEOUT_NEVER) {
            // Never resume.
            Log.d(TAG, "shouldResumeTrack: Auto-resume disabled (never resume)");
            return false;
        } else if (autoResumeTrackTimeout == PreferencesUtils.AUTO_RESUME_TRACK_TIMEOUT_ALWAYS) {
            // Always resume.
            Log.d(TAG, "shouldResumeTrack: Auto-resume forced (always resume)");
            return true;
        }

        // Check if the last modified time is within the acceptable range.
        long lastModified = track.getTripStatistics() != null ? track.getTripStatistics().getStopTime() : 0;
        Log.d(TAG, "shouldResumeTrack: lastModified = " + lastModified + ", autoResumeTrackTimeout: "
                + autoResumeTrackTimeout);
        return lastModified > 0
                && System.currentTimeMillis() - lastModified <= autoResumeTrackTimeout * 60L * 1000L;
    }

    /*
     * Setup/shutdown methods.
     */

    /**
     * Tries to acquire a partial wake lock if not already acquired. Logs errors
     * and gives up trying in case the wake lock cannot be acquired.
     */
    private void acquireWakeLock() {
        try {
            PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
            if (pm == null) {
                Log.e(TAG, "TrackRecordingService: Power manager not found!");
                return;
            }
            if (wakeLock == null) {
                wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
                if (wakeLock == null) {
                    Log.e(TAG, "TrackRecordingService: Could not create wake lock (null).");
                    return;
                }
            }
            if (!wakeLock.isHeld()) {
                wakeLock.acquire();
                if (!wakeLock.isHeld()) {
                    Log.e(TAG, "TrackRecordingService: Could not acquire wake lock.");
                }
            }
        } catch (RuntimeException e) {
            Log.e(TAG, "TrackRecordingService: Caught unexpected exception: " + e.getMessage(), e);
        }
    }

    /**
     * Releases the wake lock if it's currently held.
     */
    private void releaseWakeLock() {
        if (wakeLock != null && wakeLock.isHeld()) {
            wakeLock.release();
            wakeLock = null;
        }
    }

    /**
     * Shows the notification message and icon in the notification bar.
     */
    private void showNotification() {
        if (isRecording) {
            Intent intent = IntentUtils.newIntent(this, TrackDetailActivity.class)
                    .putExtra(TrackDetailActivity.EXTRA_TRACK_ID, recordingTrackId);
            TaskStackBuilder taskStackBuilder = TaskStackBuilder.from(this);
            taskStackBuilder.addNextIntent(intent);

            NotificationCompat.Builder builder = new NotificationCompat.Builder(this)
                    .setContentIntent(taskStackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT))
                    .setContentText(getString(R.string.track_record_notification))
                    .setContentTitle(getString(R.string.my_tracks_app_name)).setOngoing(true)
                    .setSmallIcon(R.drawable.my_tracks_notification_icon).setWhen(System.currentTimeMillis());
            startForegroundService(builder.getNotification());
        } else {
            stopForegroundService();
        }
    }

    @VisibleForTesting
    protected void startForegroundService(Notification notification) {
        startForeground(1, notification);
    }

    @VisibleForTesting
    protected void stopForegroundService() {
        stopForeground(true);
    }

    private void setUpTaskExecutors() {
        announcementExecutor = new PeriodicTaskExecutor(this, new StatusAnnouncerFactory());
        splitExecutor = new PeriodicTaskExecutor(this, new SplitTask.Factory());
    }

    private void shutdownTaskExecutors() {
        Log.d(TAG, "TrackRecordingService.shutdownExecuters");
        try {
            announcementExecutor.shutdown();
        } finally {
            announcementExecutor = null;
        }
        try {
            splitExecutor.shutdown();
        } finally {
            splitExecutor = null;
        }
    }

    private void registerLocationListener() {
        if (locationManager == null) {
            Log.e(TAG, "TrackRecordingService: Do not have any location manager.");
            return;
        }
        Log.d(TAG, "Preparing to register location listener w/ TrackRecordingService...");
        try {
            long desiredInterval = locationListenerPolicy.getDesiredPollingInterval();
            locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, desiredInterval,
                    locationListenerPolicy.getMinDistance(),
                    // , 0 /* minDistance, get all updates to properly time pauses */
                    locationListener);
            currentRecordingInterval = desiredInterval;
            Log.d(TAG,
                    "...location listener now registered w/ TrackRecordingService @ " + currentRecordingInterval);
        } catch (RuntimeException e) {
            Log.e(TAG, "Could not register location listener: " + e.getMessage(), e);
        }
    }

    private void unregisterLocationListener() {
        if (locationManager == null) {
            Log.e(TAG, "TrackRecordingService: Do not have any location manager.");
            return;
        }
        locationManager.removeUpdates(locationListener);
        Log.d(TAG, "Location listener now unregistered w/ TrackRecordingService.");
    }

    /*
     * Recording lifecycle.
     */

    private long startNewTrack() {
        Log.d(TAG, "TrackRecordingService.startNewTrack");
        if (isTrackInProgress()) {
            return -1L;
        }

        long startTime = System.currentTimeMillis();
        acquireWakeLock();

        Track track = new Track();
        TripStatistics trackStats = track.getTripStatistics();
        trackStats.setStartTime(startTime);
        track.setStartId(-1);
        Uri trackUri = providerUtils.insertTrack(track);
        recordingTrackId = Long.parseLong(trackUri.getLastPathSegment());
        track.setId(recordingTrackId);
        track.setName(TrackNameUtils.getTrackName(this, recordingTrackId, startTime, null));
        track.setCategory(PreferencesUtils.getString(this, R.string.default_activity_key,
                PreferencesUtils.DEFAULT_ACTIVITY_DEFAULT));
        isRecording = true;
        isMoving = true;

        providerUtils.updateTrack(track);
        statsBuilder = new TripStatisticsBuilder(startTime);
        statsBuilder.setMinRecordingDistance(minRecordingDistance);
        waypointStatsBuilder = new TripStatisticsBuilder(startTime);
        waypointStatsBuilder.setMinRecordingDistance(minRecordingDistance);
        currentWaypointId = insertWaypoint(WaypointCreationRequest.DEFAULT_START_TRACK);
        length = 0;
        showNotification();
        registerLocationListener();
        sensorManager = SensorManagerFactory.getSystemSensorManager(this);

        // Reset the number of auto-resume retries.
        setAutoResumeTrackRetries(0);
        // Persist the current recording track.
        PreferencesUtils.setLong(this, R.string.recording_track_id_key, recordingTrackId);

        // Notify the world that we're now recording.
        sendTrackBroadcast(R.string.track_started_broadcast_action, recordingTrackId);
        announcementExecutor.restore();
        splitExecutor.restore();

        return recordingTrackId;
    }

    private void restoreStats(Track track) {
        Log.d(TAG, "Restoring stats of track with ID: " + track.getId());

        TripStatistics stats = track.getTripStatistics();
        statsBuilder = new TripStatisticsBuilder(stats.getStartTime());
        statsBuilder.setMinRecordingDistance(minRecordingDistance);

        length = 0;
        lastValidLocation = null;

        Waypoint waypoint = providerUtils.getFirstWaypoint(recordingTrackId);
        if (waypoint != null && waypoint.getTripStatistics() != null) {
            currentWaypointId = waypoint.getId();
            waypointStatsBuilder = new TripStatisticsBuilder(waypoint.getTripStatistics());
        } else {
            // This should never happen, but we got to do something so life goes on:
            waypointStatsBuilder = new TripStatisticsBuilder(stats.getStartTime());
            currentWaypointId = -1;
        }
        waypointStatsBuilder.setMinRecordingDistance(minRecordingDistance);

        Cursor cursor = null;
        try {
            cursor = providerUtils.getLocationsCursor(recordingTrackId, -1, Constants.MAX_LOADED_TRACK_POINTS,
                    true);
            if (cursor != null) {
                if (cursor.moveToLast()) {
                    do {
                        Location location = providerUtils.createLocation(cursor);
                        if (LocationUtils.isValidLocation(location)) {
                            statsBuilder.addLocation(location, location.getTime());
                            if (lastValidLocation != null) {
                                length += location.distanceTo(lastValidLocation);
                            }
                            lastValidLocation = location;
                        }
                    } while (cursor.moveToPrevious());
                }
                statsBuilder.getStatistics().setMovingTime(stats.getMovingTime());
                statsBuilder.pauseAt(stats.getStopTime());
                statsBuilder.resumeAt(System.currentTimeMillis());
            } else {
                Log.e(TAG, "Could not get track points cursor.");
            }
        } catch (RuntimeException e) {
            Log.e(TAG, "Error while restoring track.", e);
        } finally {
            if (cursor != null) {
                cursor.close();
            }
        }

        announcementExecutor.restore();
        splitExecutor.restore();
    }

    private void onLocationChangedAsync(Location location) {
        Log.d(TAG, "TrackRecordingService.onLocationChanged");

        try {
            // Don't record if the service has been asked to pause recording:
            if (!isRecording) {
                Log.w(TAG, "Not recording because recording has been paused.");
                return;
            }

            // This should never happen, but just in case (we really don't want the
            // service to crash):
            if (location == null) {
                Log.w(TAG, "Location changed, but location is null.");
                return;
            }

            // Don't record if the accuracy is too bad:
            if (location.getAccuracy() > minRequiredAccuracy) {
                Log.d(TAG, "Not recording. Bad accuracy.");
                return;
            }

            // At least one track must be available for appending points:
            recordingTrack = getRecordingTrack();
            if (recordingTrack == null) {
                Log.d(TAG, "Not recording. No track to append to available.");
                return;
            }

            // Update the idle time if needed.
            locationListenerPolicy.updateIdleTime(statsBuilder.getIdleTime());
            addLocationToStats(location);
            if (currentRecordingInterval != locationListenerPolicy.getDesiredPollingInterval()) {
                registerLocationListener();
            }

            Location lastRecordedLocation = providerUtils.getLastLocation();
            double distanceToLastRecorded = Double.POSITIVE_INFINITY;
            if (lastRecordedLocation != null) {
                distanceToLastRecorded = location.distanceTo(lastRecordedLocation);
            }
            double distanceToLast = Double.POSITIVE_INFINITY;
            if (lastLocation != null) {
                distanceToLast = location.distanceTo(lastLocation);
            }
            boolean hasSensorData = sensorManager != null && sensorManager.isEnabled()
                    && sensorManager.getSensorDataSet() != null && sensorManager.isSensorDataSetValid();

            // If the user has been stationary for two recording just record the first
            // two and ignore the rest. This code will only have an effect if the
            // maxRecordingDistance = 0
            if (distanceToLast == 0 && !hasSensorData) {
                if (isMoving) {
                    Log.d(TAG, "Found two identical locations.");
                    isMoving = false;
                    if (lastLocation != null && lastRecordedLocation != null
                            && !lastRecordedLocation.equals(lastLocation)) {
                        // Need to write the last location. This will happen when
                        // lastRecordedLocation.distance(lastLocation) <
                        // minRecordingDistance
                        if (!insertLocation(lastLocation, lastRecordedLocation, recordingTrackId)) {
                            return;
                        }
                    }
                } else {
                    Log.d(TAG, "Not recording. More than two identical locations.");
                }
            } else if (distanceToLastRecorded > minRecordingDistance || hasSensorData) {
                if (lastLocation != null && !isMoving) {
                    // Last location was the last stationary location. Need to go back and
                    // add it.
                    if (!insertLocation(lastLocation, lastRecordedLocation, recordingTrackId)) {
                        return;
                    }
                    isMoving = true;
                }

                // If separation from last recorded point is too large insert a
                // separator to indicate end of a segment:
                boolean startNewSegment = lastRecordedLocation != null && lastRecordedLocation.getLatitude() < 90
                        && distanceToLastRecorded > maxRecordingDistance && recordingTrack.getStartId() >= 0;
                if (startNewSegment) {
                    // Insert a separator point to indicate start of new track:
                    Log.d(TAG, "Inserting a separator.");
                    Location separator = new Location(LocationManager.GPS_PROVIDER);
                    separator.setLongitude(0);
                    separator.setLatitude(100);
                    separator.setTime(lastRecordedLocation.getTime());
                    providerUtils.insertTrackPoint(separator, recordingTrackId);
                }

                if (!insertLocation(location, lastRecordedLocation, recordingTrackId)) {
                    return;
                }
            } else {
                Log.d(TAG,
                        String.format(Locale.US,
                                "Not recording. Distance to last recorded point (%f m) is less than %d m.",
                                distanceToLastRecorded, minRecordingDistance));
                // Return here so that the location is NOT recorded as the last location.
                return;
            }
        } catch (Error e) {
            // Probably important enough to rethrow.
            Log.e(TAG, "Error in onLocationChanged", e);
            throw e;
        } catch (RuntimeException e) {
            // Safe usually to trap exceptions.
            Log.e(TAG, "Trapping exception in onLocationChanged", e);
            throw e;
        }
        lastLocation = location;
    }

    /**
     * Inserts a new location in the track points db and updates the corresponding
     * track in the track db.
     *
     * @param location the location to be inserted
     * @param lastRecordedLocation the last recorded location before this one (or
     *        null if none)
     * @param trackId the id of the track
     * @return true if successful. False if SQLite3 threw an exception.
     */
    private boolean insertLocation(Location location, Location lastRecordedLocation, long trackId) {

        // Keep track of length along recorded track (needed when a waypoint is
        // inserted):
        if (LocationUtils.isValidLocation(location)) {
            if (lastValidLocation != null) {
                length += location.distanceTo(lastValidLocation);
            }
            lastValidLocation = location;
        }

        // Insert the new location:
        try {
            Location locationToInsert = location;
            if (sensorManager != null && sensorManager.isEnabled()) {
                SensorDataSet sd = sensorManager.getSensorDataSet();
                if (sd != null && sensorManager.isSensorDataSetValid()) {
                    locationToInsert = new MyTracksLocation(location, sd);
                }
            }
            Uri pointUri = providerUtils.insertTrackPoint(locationToInsert, trackId);
            int pointId = Integer.parseInt(pointUri.getLastPathSegment());

            // Update the current track:
            if (lastRecordedLocation != null && lastRecordedLocation.getLatitude() < 90) {
                TripStatistics tripStatistics = statsBuilder.getStatistics();
                tripStatistics.setStopTime(System.currentTimeMillis());

                if (recordingTrack.getStartId() < 0) {
                    recordingTrack.setStartId(pointId);
                }
                recordingTrack.setStopId(pointId);
                recordingTrack.setNumberOfPoints(recordingTrack.getNumberOfPoints() + 1);
                recordingTrack.setTripStatistics(tripStatistics);
                providerUtils.updateTrack(recordingTrack);
                updateCurrentWaypoint();
            }
        } catch (SQLiteException e) {
            // Insert failed, most likely because of SqlLite error code 5
            // (SQLite_BUSY). This is expected to happen extremely rarely (if our
            // listener gets invoked twice at about the same time).
            Log.w(TAG, "Caught SQLiteException: " + e.getMessage(), e);
            return false;
        }
        announcementExecutor.update();
        splitExecutor.update();
        return true;
    }

    private void updateCurrentWaypoint() {
        if (currentWaypointId >= 0) {
            Waypoint waypoint = providerUtils.getWaypoint(currentWaypointId);
            if (waypoint != null) {
                waypoint.setLength(length);
                waypoint.setDuration(System.currentTimeMillis() - statsBuilder.getStatistics().getStartTime());
                waypoint.setTripStatistics(waypointStatsBuilder.getStatistics());
                providerUtils.updateWaypoint(waypoint);
            }
        }
    }

    private void addLocationToStats(Location location) {
        if (LocationUtils.isValidLocation(location)) {
            long now = System.currentTimeMillis();
            statsBuilder.addLocation(location, now);
            waypointStatsBuilder.addLocation(location, now);
        }
    }

    /*
     * Application lifetime events: ============================
     */

    public long insertWaypoint(WaypointCreationRequest request) {
        if (!isRecording()) {
            throw new IllegalStateException("Unable to insert marker while not recording!");
        }
        Waypoint waypoint = new Waypoint();
        if (request.getType() == WaypointType.WAYPOINT) {
            buildWaypointMarker(waypoint, request);
        } else {
            buildStatisticsMarker(waypoint, request);
        }
        waypoint.setTrackId(recordingTrackId);
        waypoint.setLength(length);
        if (lastLocation == null || statsBuilder == null || statsBuilder.getStatistics() == null) {
            if (!request.isTrackStatistics()) {
                return -1L;
            }
            /*
             * For track statistics, a null location is OK. Make it an impossible
             * location.
             */
            Location location = new Location("");
            location.setLatitude(100);
            location.setLongitude(180);
            waypoint.setLocation(location);
        } else {
            waypoint.setLocation(lastLocation);
            waypoint.setDuration(lastLocation.getTime() - statsBuilder.getStatistics().getStartTime());
        }
        Uri uri = providerUtils.insertWaypoint(waypoint);
        return Long.parseLong(uri.getLastPathSegment());
    }

    private void buildWaypointMarker(Waypoint wpt, WaypointCreationRequest request) {
        wpt.setType(Waypoint.TYPE_WAYPOINT);
        if (request.getIconUrl() == null) {
            wpt.setIcon(getString(R.string.marker_waypoint_icon_url));
        } else {
            wpt.setIcon(request.getIconUrl());
        }
        String name;
        if (request.getName() != null) {
            name = request.getName();
        } else {
            int nextMarkerNumber = providerUtils.getNextMarkerNumber(recordingTrackId, false);
            if (nextMarkerNumber == -1) {
                nextMarkerNumber = 0;
            }
            name = getString(R.string.marker_name_format, nextMarkerNumber);
        }
        wpt.setName(name);
        if (request.getCategory() != null) {
            wpt.setCategory(request.getCategory());
        }
        if (request.getDescription() != null) {
            wpt.setDescription(request.getDescription());
        }
    }

    /**
     * Build a statistics marker.
     * A statistics marker holds the stats for the* last segment up to this marker.
     *
     * @param waypoint The waypoint which will be populated with stats data
     * @param request The waypoint creation request
     */
    private void buildStatisticsMarker(Waypoint waypoint, WaypointCreationRequest request) {
        DescriptionGenerator descriptionGenerator = new DescriptionGeneratorImpl(this);

        // Set stop and total time in the stats data
        final long time = System.currentTimeMillis();
        waypointStatsBuilder.pauseAt(time);

        // Override the duration - it's not the duration from the last waypoint, but
        // the duration from the beginning of the whole track
        waypoint.setDuration(time - statsBuilder.getStatistics().getStartTime());

        // Set the rest of the waypoint data
        waypoint.setType(Waypoint.TYPE_STATISTICS);
        String name;
        if (request.getName() != null) {
            name = request.getName();
        } else {
            int nextMarkerNumber = providerUtils.getNextMarkerNumber(recordingTrackId, true);
            if (nextMarkerNumber == -1) {
                nextMarkerNumber = 0;
            }
            name = getString(R.string.marker_split_name_format, nextMarkerNumber);
        }
        waypoint.setName(name);
        waypoint.setTripStatistics(waypointStatsBuilder.getStatistics());
        waypoint.setDescription(descriptionGenerator.generateWaypointDescription(waypoint));
        waypoint.setIcon(getString(R.string.marker_statistics_icon_url));

        waypoint.setStartId(providerUtils.getLastLocationId(recordingTrackId));

        // Create a new stats keeper for the next marker.
        waypointStatsBuilder = new TripStatisticsBuilder(time);
    }

    private void endCurrentTrack() {
        Log.d(TAG, "TrackRecordingService.endCurrentTrack");
        if (!isTrackInProgress()) {
            return;
        }

        announcementExecutor.shutdown();
        splitExecutor.shutdown();
        isRecording = false;
        Track recordedTrack = providerUtils.getTrack(recordingTrackId);
        if (recordedTrack != null) {
            long lastRecordedLocationId = providerUtils.getLastLocationId(recordingTrackId);
            if (lastRecordedLocationId >= 0 && recordedTrack.getStopId() >= 0) {
                recordedTrack.setStopId(lastRecordedLocationId);
            }
            TripStatistics tripStatistics = recordedTrack.getTripStatistics();
            tripStatistics.setStopTime(System.currentTimeMillis());
            tripStatistics.setTotalTime(tripStatistics.getStopTime() - tripStatistics.getStartTime());
            providerUtils.updateTrack(recordedTrack);
        }
        showNotification();
        long recordedTrackId = recordingTrackId;
        recordingTrackId = -1L;
        PreferencesUtils.setLong(this, R.string.recording_track_id_key, recordingTrackId);

        if (sensorManager != null) {
            SensorManagerFactory.releaseSystemSensorManager();
            sensorManager = null;
        }

        releaseWakeLock();

        // Notify the world that we're no longer recording.
        sendTrackBroadcast(R.string.track_stopped_broadcast_action, recordedTrackId);

        stopSelf();
    }

    private void sendTrackBroadcast(int actionResId, long trackId) {
        Intent broadcastIntent = new Intent().setAction(getString(actionResId))
                .putExtra(getString(R.string.track_id_broadcast_extra), trackId);
        sendBroadcast(broadcastIntent, getString(R.string.permission_notification_value));
        if (PreferencesUtils.getBoolean(this, R.string.allow_access_key, PreferencesUtils.ALLOW_ACCESS_DEFAULT)) {
            sendBroadcast(broadcastIntent, getString(R.string.broadcast_notifications_permission));
        }
    }

    /*
     * Data/state access.
     */

    private Track getRecordingTrack() {
        if (recordingTrackId < 0) {
            return null;
        }

        return providerUtils.getTrack(recordingTrackId);
    }

    public boolean isRecording() {
        return isRecording;
    }

    public TripStatistics getTripStatistics() {
        return statsBuilder.getStatistics();
    }

    Location getLastLocation() {
        return lastLocation;
    }

    long getRecordingTrackId() {
        return recordingTrackId;
    }

    public void setRecordingTrackId(long recordingTrackId) {
        this.recordingTrackId = recordingTrackId;
    }

    public void setMaxRecordingDistance(int maxRecordingDistance) {
        this.maxRecordingDistance = maxRecordingDistance;
    }

    public void setMinRecordingDistance(int minRecordingDistance) {
        this.minRecordingDistance = minRecordingDistance;
        if (statsBuilder != null && waypointStatsBuilder != null) {
            statsBuilder.setMinRecordingDistance(minRecordingDistance);
            waypointStatsBuilder.setMinRecordingDistance(minRecordingDistance);
        }
    }

    public void setMinRequiredAccuracy(int minRequiredAccuracy) {
        this.minRequiredAccuracy = minRequiredAccuracy;
    }

    public void setLocationListenerPolicy(LocationListenerPolicy locationListenerPolicy) {
        this.locationListenerPolicy = locationListenerPolicy;
    }

    public void setAutoResumeTrackTimeout(int autoResumeTrackTimeout) {
        this.autoResumeTrackTimeout = autoResumeTrackTimeout;
    }

    public void setAnnouncementFrequency(int announcementFrequency) {
        announcementExecutor.setTaskFrequency(announcementFrequency);
    }

    public void setSplitFrequency(int frequency) {
        splitExecutor.setTaskFrequency(frequency);
    }

    public void setMetricUnits(boolean metric) {
        announcementExecutor.setMetricUnits(metric);
        splitExecutor.setMetricUnits(metric);
    }

    /**
     * TODO: There is a bug in Android that leaks Binder instances.  This bug is
     * especially visible if we have a non-static class, as there is no way to
     * nullify reference to the outer class (the service).
     * A workaround is to use a static class and explicitly clear service
     * and detach it from the underlying Binder.  With this approach, we minimize
     * the leak to 24 bytes per each service instance.
     *
     * For more details, see the following bug:
     * http://code.google.com/p/android/issues/detail?id=6426.
     */
    private static class ServiceBinder extends ITrackRecordingService.Stub {
        private TrackRecordingService service;
        private DeathRecipient deathRecipient;

        public ServiceBinder(TrackRecordingService service) {
            this.service = service;
        }

        // Logic for letting the actual service go up and down.

        @Override
        public boolean isBinderAlive() {
            // Pretend dead if the service went down.
            return service != null;
        }

        @Override
        public boolean pingBinder() {
            return isBinderAlive();
        }

        @Override
        public void linkToDeath(DeathRecipient recipient, int flags) {
            deathRecipient = recipient;
        }

        @Override
        public boolean unlinkToDeath(DeathRecipient recipient, int flags) {
            if (!isBinderAlive()) {
                return false;
            }

            deathRecipient = null;
            return true;
        }

        /**
         * Clears the reference to the outer class to minimize the leak.
         */
        private void detachFromService() {
            this.service = null;
            attachInterface(null, null);

            if (deathRecipient != null) {
                deathRecipient.binderDied();
            }
        }

        /**
         * Checks if the service is available. If not, throws an
         * {@link IllegalStateException}.
         */
        private void checkService() {
            if (service == null) {
                throw new IllegalStateException("The service has been already detached!");
            }
        }

        /**
         * Returns true if the RPC caller is from the same application or if the
         * "Allow access" setting indicates that another app can invoke this
         * service's RPCs.
         */
        private boolean canAccess() {
            // As a precondition for access, must check if the service is available.
            checkService();
            if (Process.myPid() == Binder.getCallingPid()) {
                return true;
            } else {
                return PreferencesUtils.getBoolean(service, R.string.allow_access_key,
                        PreferencesUtils.ALLOW_ACCESS_DEFAULT);
            }
        }

        // Service method delegates.

        @Override
        public boolean isRecording() {
            if (!canAccess()) {
                return false;
            }
            return service.isRecording();
        }

        @Override
        public long getRecordingTrackId() {
            if (!canAccess()) {
                return -1L;
            }
            return service.recordingTrackId;
        }

        @Override
        public long startNewTrack() {
            if (!canAccess()) {
                return -1L;
            }
            return service.startNewTrack();
        }

        /**
         * Inserts a waypoint marker in the track being recorded.
         *
         * @param request Details of the waypoint to insert
         * @return the unique ID of the inserted marker
         */
        public long insertWaypoint(WaypointCreationRequest request) {
            if (!canAccess()) {
                return -1L;
            }
            return service.insertWaypoint(request);
        }

        @Override
        public void endCurrentTrack() {
            if (!canAccess()) {
                return;
            }
            service.endCurrentTrack();
        }

        @Override
        public void recordLocation(Location loc) {
            if (!canAccess()) {
                return;
            }
            service.locationListener.onLocationChanged(loc);
        }

        @Override
        public byte[] getSensorData() {
            if (!canAccess()) {
                return null;
            }
            if (service.sensorManager == null) {
                Log.d(TAG, "No sensor manager for data.");
                return null;
            }
            if (service.sensorManager.getSensorDataSet() == null) {
                Log.d(TAG, "Sensor data set is null.");
                return null;
            }
            return service.sensorManager.getSensorDataSet().toByteArray();
        }

        @Override
        public int getSensorState() {
            if (!canAccess()) {
                return Sensor.SensorState.NONE.getNumber();
            }
            if (service.sensorManager == null) {
                Log.d(TAG, "No sensor manager for data.");
                return Sensor.SensorState.NONE.getNumber();
            }
            return service.sensorManager.getSensorState().getNumber();
        }

        @Override
        public boolean isStartNewRecording() throws RemoteException {
            return false;
        }
    }

}