org.dicadeveloper.runnerapp.services.TrackRecordingService.java Source code

Java tutorial

Introduction

Here is the source code for org.dicadeveloper.runnerapp.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 org.dicadeveloper.runnerapp.services;

import org.dicadeveloper.runnerapp.Constants;
import org.dicadeveloper.runnerapp.TrackDetailActivity;
import org.dicadeveloper.runnerapp.TrackListActivity;
import org.dicadeveloper.runnerapp.content.DescriptionGeneratorImpl;
import org.dicadeveloper.runnerapp.content.MyTracksLocation;
import org.dicadeveloper.runnerapp.content.MyTracksProvider;
import org.dicadeveloper.runnerapp.content.MyTracksProviderUtils;
import org.dicadeveloper.runnerapp.content.MyTracksProviderUtils.LocationIterator;
import org.dicadeveloper.runnerapp.content.Sensor;
import org.dicadeveloper.runnerapp.content.Sensor.SensorDataSet;
import org.dicadeveloper.runnerapp.content.Track;
import org.dicadeveloper.runnerapp.content.Waypoint;
import org.dicadeveloper.runnerapp.content.Waypoint.WaypointType;
import org.dicadeveloper.runnerapp.content.WaypointCreationRequest;
import org.dicadeveloper.runnerapp.services.sensors.SensorManager;
import org.dicadeveloper.runnerapp.services.sensors.SensorManagerFactory;
import org.dicadeveloper.runnerapp.services.tasks.AnnouncementPeriodicTaskFactory;
import org.dicadeveloper.runnerapp.services.tasks.PeriodicTaskExecutor;
import org.dicadeveloper.runnerapp.services.tasks.SplitPeriodicTaskFactory;
import org.dicadeveloper.runnerapp.stats.TripStatistics;
import org.dicadeveloper.runnerapp.stats.TripStatisticsUpdater;
import org.dicadeveloper.runnerapp.util.CalorieUtils;
import org.dicadeveloper.runnerapp.util.CalorieUtils.ActivityType;
import org.dicadeveloper.runnerapp.util.IntentUtils;
import org.dicadeveloper.runnerapp.util.LocationUtils;
import org.dicadeveloper.runnerapp.util.PreferencesUtils;
import org.dicadeveloper.runnerapp.util.SystemUtils;
import org.dicadeveloper.runnerapp.util.TrackIconUtils;
import org.dicadeveloper.runnerapp.util.TrackNameUtils;
import org.dicadeveloper.runnerapp.util.UnitConversions;
import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.GooglePlayServicesClient.ConnectionCallbacks;
import com.google.android.gms.common.GooglePlayServicesClient.OnConnectionFailedListener;
import com.google.android.gms.location.ActivityRecognitionClient;
import com.google.android.gms.location.DetectedActivity;
import com.google.android.gms.location.LocationListener;
import org.dicadeveloper.runnerapp.R;
import com.google.common.annotations.VisibleForTesting;

import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.database.sqlite.SQLiteException;
import android.location.Location;
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.WakeLock;
import android.os.Process;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.TaskStackBuilder;
import android.util.Log;

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 {@link MyTracksProvider}.
 * 
 * @author Leif Hendrik Wilden
 */
public class TrackRecordingService extends Service {

    /**
     * The name of extra intent property to indicate whether we want to resume a
     * previously recorded track.
     */
    public static final String RESUME_TRACK_EXTRA_NAME = "com.google.android.apps.mytracks.RESUME_TRACK";

    public static final double PAUSE_LATITUDE = 100.0;
    public static final double RESUME_LATITUDE = 200.0;

    /**
     * Anything faster than that (in meters per second) will be considered moving.
     */
    public static final double MAX_NO_MOVEMENT_SPEED = 0.224;

    private static final String TAG = TrackRecordingService.class.getSimpleName();

    // 1 second in milliseconds
    private static final long ONE_SECOND = (long) UnitConversions.S_TO_MS;

    // 1 minute in milliseconds
    private static final long ONE_MINUTE = (long) (UnitConversions.MIN_TO_S * UnitConversions.S_TO_MS);

    @VisibleForTesting
    static final int MAX_AUTO_RESUME_TRACK_RETRY_ATTEMPTS = 3;

    // The following variables are set in onCreate:
    private ExecutorService executorService;
    private Context context;
    private MyTracksProviderUtils myTracksProviderUtils;
    private Handler handler;
    private MyTracksLocationManager myTracksLocationManager;
    private PendingIntent activityRecognitionPendingIntent;
    private ActivityRecognitionClient activityRecognitionClient;
    private PeriodicTaskExecutor voiceExecutor;
    private PeriodicTaskExecutor splitExecutor;
    private SharedPreferences sharedPreferences;
    private long recordingTrackId;
    private boolean recordingTrackPaused;
    private LocationListenerPolicy locationListenerPolicy;
    private int recordingDistanceInterval;
    private int maxRecordingDistance;
    private int recordingGpsAccuracy;
    private int autoResumeTrackTimeout;
    private long currentRecordingInterval;
    private double weight;

    // The following variables are set when recording:
    private TripStatisticsUpdater trackTripStatisticsUpdater;
    private TripStatisticsUpdater markerTripStatisticsUpdater;
    private WakeLock wakeLock;
    private SensorManager sensorManager;
    private Location lastLocation;
    private boolean currentSegmentHasLocation;
    private boolean isIdle; // true if idle

    private ServiceBinder binder = new ServiceBinder(this);

    /*
     * Note that sharedPreferenceChangeListener cannot be an anonymous inner
     * class. Anonymous inner class will get garbage collected.
     */
    private final OnSharedPreferenceChangeListener sharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() {
        @Override
        public void onSharedPreferenceChanged(SharedPreferences preferences, String key) {
            if (key == null || key.equals(PreferencesUtils.getKey(context, R.string.recording_track_id_key))) {
                long trackId = PreferencesUtils.getLong(context, R.string.recording_track_id_key);
                /*
                 * Only through the TrackRecordingService can one stop a recording
                 * and set the recordingTrackId to -1L.
                 */
                if (trackId != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT) {
                    recordingTrackId = trackId;
                }
            }
            if (key == null || key.equals(PreferencesUtils.getKey(context, R.string.recording_track_paused_key))) {
                recordingTrackPaused = PreferencesUtils.getBoolean(context, R.string.recording_track_paused_key,
                        PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT);
            }
            if (key == null || key.equals(PreferencesUtils.getKey(context, R.string.stats_units_key))) {
                boolean metricUnits = PreferencesUtils.isMetricUnits(context);
                voiceExecutor.setMetricUnits(metricUnits);
                splitExecutor.setMetricUnits(metricUnits);
            }
            if (key == null || key.equals(PreferencesUtils.getKey(context, R.string.voice_frequency_key))) {
                voiceExecutor.setTaskFrequency(PreferencesUtils.getInt(context, R.string.voice_frequency_key,
                        PreferencesUtils.VOICE_FREQUENCY_DEFAULT));
            }
            if (key == null || key.equals(PreferencesUtils.getKey(context, R.string.split_frequency_key))) {
                splitExecutor.setTaskFrequency(PreferencesUtils.getInt(context, R.string.split_frequency_key,
                        PreferencesUtils.SPLIT_FREQUENCY_DEFAULT));
            }
            if (key == null || key.equals(PreferencesUtils.getKey(context, R.string.min_recording_interval_key))) {
                int minRecordingInterval = PreferencesUtils.getInt(context, R.string.min_recording_interval_key,
                        PreferencesUtils.MIN_RECORDING_INTERVAL_DEFAULT);
                switch (minRecordingInterval) {
                case PreferencesUtils.MIN_RECORDING_INTERVAL_ADAPT_BATTERY_LIFE:
                    // Choose battery life over moving time accuracy.
                    locationListenerPolicy = new AdaptiveLocationListenerPolicy(30 * ONE_SECOND, 5 * ONE_MINUTE, 5);
                    break;
                case PreferencesUtils.MIN_RECORDING_INTERVAL_ADAPT_ACCURACY:
                    // Get all the updates.
                    locationListenerPolicy = new AdaptiveLocationListenerPolicy(ONE_SECOND, 30 * ONE_SECOND, 0);
                    break;
                default:
                    locationListenerPolicy = new AbsoluteLocationListenerPolicy(minRecordingInterval * ONE_SECOND);
                }
            }
            if (key == null
                    || key.equals(PreferencesUtils.getKey(context, R.string.recording_distance_interval_key))) {
                recordingDistanceInterval = PreferencesUtils.getInt(context,
                        R.string.recording_distance_interval_key,
                        PreferencesUtils.RECORDING_DISTANCE_INTERVAL_DEFAULT);
            }
            if (key == null || key.equals(PreferencesUtils.getKey(context, R.string.max_recording_distance_key))) {
                maxRecordingDistance = PreferencesUtils.getInt(context, R.string.max_recording_distance_key,
                        PreferencesUtils.MAX_RECORDING_DISTANCE_DEFAULT);
            }
            if (key == null || key.equals(PreferencesUtils.getKey(context, R.string.recording_gps_accuracy_key))) {
                recordingGpsAccuracy = PreferencesUtils.getInt(context, R.string.recording_gps_accuracy_key,
                        PreferencesUtils.RECORDING_GPS_ACCURACY_DEFAULT);
            }
            if (key == null
                    || key.equals(PreferencesUtils.getKey(context, R.string.auto_resume_track_timeout_key))) {
                autoResumeTrackTimeout = PreferencesUtils.getInt(context, R.string.auto_resume_track_timeout_key,
                        PreferencesUtils.AUTO_RESUME_TRACK_TIMEOUT_DEFAULT);
            }
            if (key == null || key.equals(PreferencesUtils.getKey(context, R.string.weight_key))) {
                weight = PreferencesUtils.getFloat(context, R.string.weight_key,
                        PreferencesUtils.getDefaultWeight(context));
            }
        }
    };

    private LocationListener locationListener = new LocationListener() {
        @Override
        public void onLocationChanged(final Location location) {
            if (myTracksLocationManager == null || executorService == null || !myTracksLocationManager.isAllowed()
                    || executorService.isShutdown() || executorService.isTerminated()) {
                return;
            }
            executorService.submit(new Runnable() {
                @Override
                public void run() {
                    onLocationChangedAsync(location);
                }
            });
        }
    };

    private final ConnectionCallbacks activityRecognitionCallbacks = new ConnectionCallbacks() {
        @Override
        public void onDisconnected() {
        }

        @Override
        public void onConnected(Bundle bundle) {
            activityRecognitionClient.requestActivityUpdates(ONE_MINUTE, activityRecognitionPendingIntent);
        }
    };

    private final OnConnectionFailedListener activityRecognitionFailedListener = new OnConnectionFailedListener() {

        @Override
        public void onConnectionFailed(ConnectionResult connectionResult) {
        }
    };

    private final Runnable registerLocationRunnable = new Runnable() {
        @Override
        public void run() {
            if (isRecording() && !isPaused()) {
                registerLocationListener();
            }
            handler.postDelayed(this, ONE_MINUTE);
        }
    };

    /*
     * 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.
     */
    @Override
    public void onCreate() {
        super.onCreate();
        executorService = Executors.newSingleThreadExecutor();
        context = this;
        myTracksProviderUtils = MyTracksProviderUtils.Factory.get(this);
        handler = new Handler();
        myTracksLocationManager = new MyTracksLocationManager(this, handler.getLooper(), true);
        activityRecognitionPendingIntent = PendingIntent.getService(context, 0,
                new Intent(context, ActivityRecognitionIntentService.class), PendingIntent.FLAG_UPDATE_CURRENT);
        activityRecognitionClient = new ActivityRecognitionClient(context, activityRecognitionCallbacks,
                activityRecognitionFailedListener);
        activityRecognitionClient.connect();
        voiceExecutor = new PeriodicTaskExecutor(this, new AnnouncementPeriodicTaskFactory());
        splitExecutor = new PeriodicTaskExecutor(this, new SplitPeriodicTaskFactory());
        sharedPreferences = getSharedPreferences(Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
        sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);

        // onSharedPreferenceChanged might not set recordingTrackId.
        recordingTrackId = PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;

        // Require voiceExecutor and splitExecutor to be created.
        sharedPreferenceChangeListener.onSharedPreferenceChanged(sharedPreferences, null);

        handler.post(registerLocationRunnable);

        /*
         * Try to restart the previous recording track in case the service has been
         * restarted by the system, which can sometimes happen.
         */
        Track track = myTracksProviderUtils.getTrack(recordingTrackId);
        if (track != null) {
            restartTrack(track);
        } else {
            if (isRecording()) {
                Log.w(TAG, "track is null, but recordingTrackId not -1L. " + recordingTrackId);
                updateRecordingState(PreferencesUtils.RECORDING_TRACK_ID_DEFAULT, true);
            }
            showNotification(false);
        }
    }

    /*
     * 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.
     */
    @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.
     */
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        handleStartCommand(intent, startId);
        return START_STICKY;
    }

    @Override
    public IBinder onBind(Intent intent) {
        return binder;
    }

    @Override
    public void onDestroy() {
        if (sensorManager != null) {
            SensorManagerFactory.releaseSystemSensorManager();
            sensorManager = null;
        }

        // Reverse order from onCreate    
        showNotification(false);

        handler.removeCallbacks(registerLocationRunnable);
        unregisterLocationListener();

        // unregister sharedPreferences before shutting down splitExecutor and voiceExecutor
        sharedPreferences.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);

        try {
            splitExecutor.shutdown();
        } finally {
            splitExecutor = null;
        }

        try {
            voiceExecutor.shutdown();
        } finally {
            voiceExecutor = null;
        }

        if (activityRecognitionClient.isConnected()) {
            activityRecognitionClient.removeActivityUpdates(activityRecognitionPendingIntent);
        }
        activityRecognitionClient.disconnect();
        activityRecognitionPendingIntent.cancel();

        myTracksLocationManager.close();
        myTracksLocationManager = null;
        myTracksProviderUtils = null;

        binder.detachFromService();
        binder = null;

        // This should be the next to last operation
        releaseWakeLock();

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

    /**
     * Returns true if the service is recording.
     */
    public boolean isRecording() {
        return recordingTrackId != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
    }

    /**
     * Returns true if the current recording is paused.
     */
    public boolean isPaused() {
        return recordingTrackPaused;
    }

    /**
     * Gets the trip statistics.
     */
    public TripStatistics getTripStatistics() {
        if (trackTripStatisticsUpdater == null) {
            return null;
        }
        return trackTripStatisticsUpdater.getTripStatistics();
    }

    /**
     * Inserts a waypoint.
     * 
     * @param waypointCreationRequest the waypoint creation request
     * @return the waypoint id
     */
    public long insertWaypoint(WaypointCreationRequest waypointCreationRequest) {
        if (!isRecording() || isPaused()) {
            return -1L;
        }

        WaypointType waypointType = waypointCreationRequest.getType();
        boolean isStatistics = waypointType == WaypointType.STATISTICS;

        // Get name
        String name;
        if (waypointCreationRequest.getName() != null) {
            name = waypointCreationRequest.getName();
        } else {
            int nextWaypointNumber = myTracksProviderUtils.getNextWaypointNumber(recordingTrackId, waypointType);
            if (nextWaypointNumber == -1) {
                nextWaypointNumber = 0;
            }
            name = getString(isStatistics ? R.string.marker_split_name_format : R.string.marker_name_format,
                    nextWaypointNumber);
        }

        // Get category
        String category = waypointCreationRequest.getCategory() != null ? waypointCreationRequest.getCategory()
                : "";

        // Get tripStatistics, description, and icon
        TripStatistics tripStatistics;
        String description;
        String icon;
        if (isStatistics) {
            long now = System.currentTimeMillis();
            markerTripStatisticsUpdater.updateTime(now);
            tripStatistics = markerTripStatisticsUpdater.getTripStatistics();
            markerTripStatisticsUpdater = new TripStatisticsUpdater(now);
            description = new DescriptionGeneratorImpl(this).generateWaypointDescription(tripStatistics);
            icon = getString(R.string.marker_statistics_icon_url);
        } else {
            tripStatistics = null;
            description = waypointCreationRequest.getDescription() != null
                    ? waypointCreationRequest.getDescription()
                    : "";
            icon = getString(R.string.marker_waypoint_icon_url);
        }

        // Get length and duration
        double length;
        long duration;
        Location location = getLastValidTrackPointInCurrentSegment(recordingTrackId);
        if (location != null && trackTripStatisticsUpdater != null) {
            TripStatistics stats = trackTripStatisticsUpdater.getTripStatistics();
            length = stats.getTotalDistance();
            duration = stats.getTotalTime();
        } else {
            if (!waypointCreationRequest.isTrackStatistics()) {
                return -1L;
            }
            // For track statistics, make it an impossible location
            location = new Location("");
            location.setLatitude(100);
            location.setLongitude(180);
            length = 0.0;
            duration = 0L;
        }

        String photoUrl = waypointCreationRequest.getPhotoUrl() != null ? waypointCreationRequest.getPhotoUrl()
                : "";

        // Insert waypoint
        Waypoint waypoint = new Waypoint(name, description, category, icon, recordingTrackId, waypointType, length,
                duration, -1L, -1L, location, tripStatistics, photoUrl);
        Uri uri = myTracksProviderUtils.insertWaypoint(waypoint);
        return Long.parseLong(uri.getLastPathSegment());
    }

    /**
     * Starts the service as a foreground service.
     * 
     * @param pendingIntent the notification pending intent
     * @param messageId the notification message id
     */
    @VisibleForTesting
    protected void startForegroundService(PendingIntent pendingIntent, int messageId) {
        NotificationCompat.Builder builder = new NotificationCompat.Builder(this).setContentIntent(pendingIntent)
                .setContentText(getString(messageId)).setContentTitle(getString(R.string.my_tracks_app_name))
                .setOngoing(true).setSmallIcon(R.drawable.ic_stat_notify_recording)
                .setWhen(System.currentTimeMillis());
        startForeground(1, builder.build());
    }

    /**
     * Stops the service as a foreground service.
     */
    @VisibleForTesting
    protected void stopForegroundService() {
        stopForeground(true);
    }

    /**
     * Handles start command.
     * 
     * @param intent the intent
     * @param startId the start id
     */
    private void handleStartCommand(Intent intent, int startId) {
        // Check if the service is called to resume track (from phone reboot)
        if (intent != null && intent.getBooleanExtra(RESUME_TRACK_EXTRA_NAME, false)) {
            if (!shouldResumeTrack()) {
                Log.i(TAG, "Stop resume track.");
                updateRecordingState(PreferencesUtils.RECORDING_TRACK_ID_DEFAULT, true);
                stopSelfResult(startId);
                return;
            }
        }
    }

    /**
     * Returns true if should resume.
     */
    private boolean shouldResumeTrack() {
        Track track = myTracksProviderUtils.getTrack(recordingTrackId);

        if (track == null) {
            Log.d(TAG, "Not resuming. Track is null.");
            return false;
        }
        int retries = PreferencesUtils.getInt(this, R.string.auto_resume_track_current_retry_key,
                PreferencesUtils.AUTO_RESUME_TRACK_CURRENT_RETRY_DEFAULT);
        if (retries >= MAX_AUTO_RESUME_TRACK_RETRY_ATTEMPTS) {
            Log.d(TAG, "Not resuming. Exceeded maximum retry attempts.");
            return false;
        }
        PreferencesUtils.setInt(this, R.string.auto_resume_track_current_retry_key, retries + 1);

        if (autoResumeTrackTimeout == PreferencesUtils.AUTO_RESUME_TRACK_TIMEOUT_NEVER) {
            Log.d(TAG, "Not resuming. Auto-resume track timeout set to never.");
            return false;
        } else if (autoResumeTrackTimeout == PreferencesUtils.AUTO_RESUME_TRACK_TIMEOUT_ALWAYS) {
            Log.d(TAG, "Resuming. Auto-resume track timeout set to always.");
            return true;
        }

        if (track.getTripStatistics() == null) {
            Log.d(TAG, "Not resuming. No trip statistics.");
            return false;
        }
        long stopTime = track.getTripStatistics().getStopTime();
        return stopTime > 0 && (System.currentTimeMillis() - stopTime) <= autoResumeTrackTimeout * ONE_MINUTE;
    }

    /**
     * Starts a new track.
     * 
     * @return the track id
     */
    private long startNewTrack() {
        if (isRecording()) {
            Log.d(TAG, "Ignore startNewTrack. Already recording.");
            return -1L;
        }
        long now = System.currentTimeMillis();
        trackTripStatisticsUpdater = new TripStatisticsUpdater(now);
        markerTripStatisticsUpdater = new TripStatisticsUpdater(now);

        // Insert a track
        Track track = new Track();
        Uri uri = myTracksProviderUtils.insertTrack(track);
        long trackId = Long.parseLong(uri.getLastPathSegment());

        // Update shared preferences
        updateRecordingState(trackId, false);
        PreferencesUtils.setInt(this, R.string.auto_resume_track_current_retry_key, 0);
        PreferencesUtils.setInt(this, R.string.activity_recognition_type_key,
                PreferencesUtils.ACTIVITY_RECOGNITION_TYPE_DEFAULT);

        // Update database
        track.setId(trackId);
        track.setName(TrackNameUtils.getTrackName(this, trackId, now, null));

        String category = PreferencesUtils.getString(this, R.string.default_activity_key,
                PreferencesUtils.DEFAULT_ACTIVITY_DEFAULT);
        track.setCategory(category);
        track.setIcon(TrackIconUtils.getIconValue(this, category));
        track.setTripStatistics(trackTripStatisticsUpdater.getTripStatistics());
        myTracksProviderUtils.updateTrack(track);
        insertWaypoint(WaypointCreationRequest.DEFAULT_START_TRACK);

        startRecording(true);
        return trackId;
    }

    /**
     * Restart a track.
     * 
     * @param track the track
     */
    private void restartTrack(Track track) {
        Log.d(TAG, "Restarting track: " + track.getId());

        TripStatistics tripStatistics = track.getTripStatistics();
        trackTripStatisticsUpdater = new TripStatisticsUpdater(tripStatistics.getStartTime());

        long markerStartTime;
        Waypoint waypoint = myTracksProviderUtils.getLastWaypoint(recordingTrackId, WaypointType.STATISTICS);
        if (waypoint != null && waypoint.getTripStatistics() != null) {
            markerStartTime = waypoint.getTripStatistics().getStopTime();
        } else {
            markerStartTime = tripStatistics.getStartTime();
        }
        markerTripStatisticsUpdater = new TripStatisticsUpdater(markerStartTime);

        ActivityType activityType = CalorieUtils.getActivityType(context, track.getCategory());

        LocationIterator locationIterator = null;
        try {
            locationIterator = myTracksProviderUtils.getTrackPointLocationIterator(track.getId(), -1L, false,
                    MyTracksProviderUtils.DEFAULT_LOCATION_FACTORY);

            while (locationIterator.hasNext()) {
                Location location = locationIterator.next();
                trackTripStatisticsUpdater.addLocation(location, recordingDistanceInterval, true, activityType,
                        weight);
                if (location.getTime() > markerStartTime) {
                    markerTripStatisticsUpdater.addLocation(location, recordingDistanceInterval, true, activityType,
                            weight);
                }
            }
        } catch (RuntimeException e) {
            Log.e(TAG, "RuntimeException", e);
        } finally {
            if (locationIterator != null) {
                locationIterator.close();
            }
        }
        startRecording(true);
    }

    /**
     * Resumes current track.
     */
    private void resumeCurrentTrack() {
        if (!isRecording() || !isPaused()) {
            Log.d(TAG, "Ignore resumeCurrentTrack. Not recording or not paused.");
            return;
        }

        // Update shared preferences
        recordingTrackPaused = false;
        PreferencesUtils.setBoolean(this, R.string.recording_track_paused_key, false);

        // Update database
        Track track = myTracksProviderUtils.getTrack(recordingTrackId);
        if (track != null) {
            Location resume = new Location(LocationManager.GPS_PROVIDER);
            resume.setLongitude(0);
            resume.setLatitude(RESUME_LATITUDE);
            resume.setTime(System.currentTimeMillis());
            insertLocation(track, resume, null);
        }

        startRecording(false);
    }

    /**
     * Common code for starting a new track, resuming a track, or restarting after
     * phone reboot.
     * 
     * @param trackStarted true if track is started, false if track is resumed
     */
    private void startRecording(boolean trackStarted) {

        // Update instance variables
        sensorManager = SensorManagerFactory.getSystemSensorManager(this);
        lastLocation = null;
        currentSegmentHasLocation = false;
        isIdle = false;

        startGps();
        sendTrackBroadcast(
                trackStarted ? R.string.track_started_broadcast_action : R.string.track_resumed_broadcast_action,
                recordingTrackId);

        // Restore periodic tasks
        voiceExecutor.restore();
        splitExecutor.restore();
    }

    /**
     * Starts gps.
     */
    private void startGps() {
        wakeLock = SystemUtils.acquireWakeLock(this, wakeLock);
        registerLocationListener();
        showNotification(true);
    }

    /**
     * Ends the current track.
     */
    private void endCurrentTrack() {
        if (!isRecording()) {
            Log.d(TAG, "Ignore endCurrentTrack. Not recording.");
            return;
        }

        // Need to remember the recordingTrackId before setting it to -1L
        long trackId = recordingTrackId;
        boolean paused = recordingTrackPaused;

        // Update shared preferences
        updateRecordingState(PreferencesUtils.RECORDING_TRACK_ID_DEFAULT, true);

        // Update database
        Track track = myTracksProviderUtils.getTrack(trackId);
        if (track != null) {

            // If not paused, add the last location
            if (!paused) {
                insertLocation(track, lastLocation, getLastValidTrackPointInCurrentSegment(trackId));

                // Update the recording track time        
                updateRecordingTrack(track, myTracksProviderUtils.getLastTrackPointId(trackId), false);
            }

            String trackName = TrackNameUtils.getTrackName(this, trackId, track.getTripStatistics().getStartTime(),
                    myTracksProviderUtils.getFirstValidTrackPoint(trackId));
            if (trackName != null && !trackName.equals(track.getName())) {
                track.setName(trackName);
                myTracksProviderUtils.updateTrack(track);
            }

            if (track.getCategory().equals(PreferencesUtils.DEFAULT_ACTIVITY_DEFAULT)) {
                int activityRecognitionType = PreferencesUtils.getInt(this, R.string.activity_recognition_type_key,
                        PreferencesUtils.ACTIVITY_RECOGNITION_TYPE_DEFAULT);
                if (activityRecognitionType != PreferencesUtils.ACTIVITY_RECOGNITION_TYPE_DEFAULT) {
                    String iconValue = null;
                    switch (activityRecognitionType) {
                    case DetectedActivity.IN_VEHICLE:
                        iconValue = TrackIconUtils.DRIVE;
                        break;
                    case DetectedActivity.ON_BICYCLE:
                        iconValue = TrackIconUtils.BIKE;
                        break;
                    case DetectedActivity.ON_FOOT:
                        iconValue = TrackIconUtils.WALK;
                        break;
                    default:
                        break;
                    }
                    if (iconValue != null) {
                        track.setIcon(iconValue);
                        track.setCategory(getString(TrackIconUtils.getIconActivityType(iconValue)));
                        myTracksProviderUtils.updateTrack(track);
                        CalorieUtils.updateTrackCalorie(context, track);
                    }
                }
            }
        }
        endRecording(true, trackId);
    }

    /**
     * Pauses the current track.
     */
    private void pauseCurrentTrack() {
        if (!isRecording() || isPaused()) {
            Log.d(TAG, "Ignore pauseCurrentTrack. Not recording or paused.");
            return;
        }

        // Update shared preferences
        recordingTrackPaused = true;
        PreferencesUtils.setBoolean(this, R.string.recording_track_paused_key, true);

        // Update database
        Track track = myTracksProviderUtils.getTrack(recordingTrackId);
        if (track != null) {
            insertLocation(track, lastLocation, getLastValidTrackPointInCurrentSegment(track.getId()));

            Location pause = new Location(LocationManager.GPS_PROVIDER);
            pause.setLongitude(0);
            pause.setLatitude(PAUSE_LATITUDE);
            pause.setTime(System.currentTimeMillis());
            insertLocation(track, pause, null);
        }

        endRecording(false, recordingTrackId);
    }

    /**
     * Common code for ending a track or pausing a track.
     * 
     * @param trackStopped true if track is stopped, false if track is paused
     * @param trackId the track id
     */
    private void endRecording(boolean trackStopped, long trackId) {

        // Shutdown periodic tasks
        voiceExecutor.shutdown();
        splitExecutor.shutdown();

        // Update instance variables
        if (sensorManager != null) {
            SensorManagerFactory.releaseSystemSensorManager();
            sensorManager = null;
        }
        lastLocation = null;

        sendTrackBroadcast(
                trackStopped ? R.string.track_stopped_broadcast_action : R.string.track_paused_broadcast_action,
                trackId);
        stopGps(trackStopped);
    }

    /**
     * Stops gps.
     * 
     * @param stop true to stop self
     */
    private void stopGps(boolean stop) {
        unregisterLocationListener();
        showNotification(false);
        releaseWakeLock();
        if (stop) {
            stopSelf();
        }
    }

    /**
     * Gets the last valid track point in the current segment. Returns null if not
     * available.
     * 
     * @param trackId the track id
     */
    private Location getLastValidTrackPointInCurrentSegment(long trackId) {
        if (!currentSegmentHasLocation) {
            return null;
        }
        return myTracksProviderUtils.getLastValidTrackPoint(trackId);
    }

    /**
     * Updates the recording states.
     * 
     * @param trackId the recording track id
     * @param paused true if the recording is paused
     */
    private void updateRecordingState(long trackId, boolean paused) {
        recordingTrackId = trackId;
        PreferencesUtils.setLong(this, R.string.recording_track_id_key, trackId);
        recordingTrackPaused = paused;
        PreferencesUtils.setBoolean(this, R.string.recording_track_paused_key, recordingTrackPaused);
    }

    /**
     * Called when location changed.
     * 
     * @param location the location
     */
    private void onLocationChangedAsync(Location location) {
        try {
            if (!isRecording() || isPaused()) {
                Log.w(TAG, "Ignore onLocationChangedAsync. Not recording or paused.");
                return;
            }

            Track track = myTracksProviderUtils.getTrack(recordingTrackId);
            if (track == null) {
                Log.w(TAG, "Ignore onLocationChangedAsync. No track.");
                return;
            }

            if (!LocationUtils.isValidLocation(location)) {
                Log.w(TAG, "Ignore onLocationChangedAsync. location is invalid.");
                return;
            }

            if (!location.hasAccuracy() || location.getAccuracy() >= recordingGpsAccuracy) {
                Log.d(TAG, "Ignore onLocationChangedAsync. Poor accuracy.");
                return;
            }

            // Fix for phones that do not set the time field
            if (location.getTime() == 0L) {
                location.setTime(System.currentTimeMillis());
            }

            Location lastValidTrackPoint = getLastValidTrackPointInCurrentSegment(track.getId());
            long idleTime = 0L;
            if (lastValidTrackPoint != null && location.getTime() > lastValidTrackPoint.getTime()) {
                idleTime = location.getTime() - lastValidTrackPoint.getTime();
            }
            locationListenerPolicy.updateIdleTime(idleTime);
            if (currentRecordingInterval != locationListenerPolicy.getDesiredPollingInterval()) {
                registerLocationListener();
            }

            SensorDataSet sensorDataSet = getSensorDataSet();
            if (sensorDataSet != null) {
                location = new MyTracksLocation(location, sensorDataSet);
            }

            // Always insert the first segment location
            if (!currentSegmentHasLocation) {
                insertLocation(track, location, null);
                currentSegmentHasLocation = true;
                lastLocation = location;
                return;
            }

            if (!LocationUtils.isValidLocation(lastValidTrackPoint)) {
                /*
                 * Should not happen. The current segment should have a location. Just
                 * insert the current location.
                 */
                insertLocation(track, location, null);
                lastLocation = location;
                return;
            }

            double distanceToLastTrackLocation = location.distanceTo(lastValidTrackPoint);
            if (distanceToLastTrackLocation > maxRecordingDistance) {
                insertLocation(track, lastLocation, lastValidTrackPoint);

                Location pause = new Location(LocationManager.GPS_PROVIDER);
                pause.setLongitude(0);
                pause.setLatitude(PAUSE_LATITUDE);
                pause.setTime(lastLocation.getTime());
                insertLocation(track, pause, null);

                insertLocation(track, location, null);
                isIdle = false;
            } else if (sensorDataSet != null || distanceToLastTrackLocation >= recordingDistanceInterval) {
                insertLocation(track, lastLocation, lastValidTrackPoint);
                insertLocation(track, location, null);
                isIdle = false;
            } else if (!isIdle && location.hasSpeed() && location.getSpeed() < MAX_NO_MOVEMENT_SPEED) {
                insertLocation(track, lastLocation, lastValidTrackPoint);
                insertLocation(track, location, null);
                isIdle = true;
            } else if (isIdle && location.hasSpeed() && location.getSpeed() >= MAX_NO_MOVEMENT_SPEED) {
                insertLocation(track, lastLocation, lastValidTrackPoint);
                insertLocation(track, location, null);
                isIdle = false;
            } else {
                Log.d(TAG, "Not recording location, idle");
            }
            lastLocation = location;
        } catch (Error e) {
            Log.e(TAG, "Error in onLocationChangedAsync", e);
            throw e;
        } catch (RuntimeException e) {
            Log.e(TAG, "RuntimeException in onLocationChangedAsync", e);
            throw e;
        }
    }

    /**
     * Inserts a location.
     * 
     * @param track the track
     * @param location the location
     * @param lastValidTrackPoint the last valid track point, can be null
     */
    private void insertLocation(Track track, Location location, Location lastValidTrackPoint) {
        if (location == null) {
            Log.w(TAG, "Ignore insertLocation. loation is null.");
            return;
        }
        // Do not insert if inserted already
        if (lastValidTrackPoint != null && lastValidTrackPoint.getTime() == location.getTime()) {
            Log.w(TAG, "Ignore insertLocation. location time same as last valid track point time.");
            return;
        }

        try {
            Uri uri = myTracksProviderUtils.insertTrackPoint(location, track.getId());
            long trackPointId = Long.parseLong(uri.getLastPathSegment());
            ActivityType activityType = CalorieUtils.getActivityType(context, track.getCategory());
            trackTripStatisticsUpdater.addLocation(location, recordingDistanceInterval, true, activityType, weight);
            markerTripStatisticsUpdater.addLocation(location, recordingDistanceInterval, true, activityType,
                    weight);
            updateRecordingTrack(track, trackPointId, LocationUtils.isValidLocation(location));
        } 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, "SQLiteException", e);
        }
        voiceExecutor.update();
        splitExecutor.update();
        sendTrackBroadcast(R.string.track_update_broadcast_action, track.getId());
    }

    /**
     * Updates the recording track time. Also updates the startId and the stopId.
     * Increase the number of points if it is a new and valid track point.
     * 
     * @param track the track
     * @param lastTrackPointId the last track point id
     * @param increaseNumberOfPoints true to increase the number of points
     */
    private void updateRecordingTrack(Track track, long lastTrackPointId, boolean increaseNumberOfPoints) {
        if (lastTrackPointId >= 0) {
            if (track.getStartId() < 0) {
                track.setStartId(lastTrackPointId);
            }
            track.setStopId(lastTrackPointId);
        }
        if (increaseNumberOfPoints) {
            track.setNumberOfPoints(track.getNumberOfPoints() + 1);
        }

        trackTripStatisticsUpdater.updateTime(System.currentTimeMillis());
        track.setTripStatistics(trackTripStatisticsUpdater.getTripStatistics());
        myTracksProviderUtils.updateTrack(track);
    }

    private SensorDataSet getSensorDataSet() {
        if (sensorManager == null || !sensorManager.isEnabled() || !sensorManager.isSensorDataSetValid()) {
            return null;
        }
        return sensorManager.getSensorDataSet();
    }

    /**
     * Registers the location listener.
     */
    private void registerLocationListener() {
        if (myTracksLocationManager == null) {
            Log.e(TAG, "locationManager is null.");
            return;
        }
        try {
            long interval = locationListenerPolicy.getDesiredPollingInterval();
            myTracksLocationManager.requestLocationUpdates(interval, locationListenerPolicy.getMinDistance(),
                    locationListener);
            currentRecordingInterval = interval;
        } catch (RuntimeException e) {
            Log.e(TAG, "Could not register location listener.", e);
        }
    }

    /**
     * Unregisters the location manager.
     */
    private void unregisterLocationListener() {
        if (myTracksLocationManager == null) {
            Log.e(TAG, "locationManager is null.");
            return;
        }
        myTracksLocationManager.removeLocationUpdates(locationListener);
    }

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

    /**
     * Shows the notification.
     * 
     * @param isGpsStarted true if GPS is started
     */
    private void showNotification(boolean isGpsStarted) {
        if (isRecording()) {
            if (isPaused()) {
                stopForegroundService();
            } else {
                Intent intent = IntentUtils.newIntent(this, TrackDetailActivity.class)
                        .putExtra(TrackDetailActivity.EXTRA_TRACK_ID, recordingTrackId);
                PendingIntent pendingIntent = TaskStackBuilder.create(this)
                        .addParentStack(TrackDetailActivity.class).addNextIntent(intent)
                        .getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);
                startForegroundService(pendingIntent, R.string.track_record_notification);
            }
            return;
        } else {
            // Not recording
            if (isGpsStarted) {
                Intent intent = IntentUtils.newIntent(this, TrackListActivity.class);
                PendingIntent pendingIntent = TaskStackBuilder.create(this).addNextIntent(intent)
                        .getPendingIntent(0, 0);
                startForegroundService(pendingIntent, R.string.gps_starting);
            } else {
                stopForegroundService();
            }
        }
    }

    /**
     * Sends track broadcast.
     * 
     * @param actionId the intent action id
     * @param trackId the track id
     */
    private void sendTrackBroadcast(int actionId, long trackId) {
        Intent intent = new Intent().setAction(getString(actionId))
                .putExtra(getString(R.string.track_id_broadcast_extra), trackId);
        sendBroadcast(intent, getString(R.string.permission_notification_value));
        if (PreferencesUtils.getBoolean(this, R.string.allow_access_key, PreferencesUtils.ALLOW_ACCESS_DEFAULT)) {
            sendBroadcast(intent, getString(R.string.broadcast_notifications_permission));
        }
    }

    /**
     * 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 trackRecordingService;
        private DeathRecipient deathRecipient;

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

        @Override
        public boolean isBinderAlive() {
            return trackRecordingService != 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;
        }

        @Override
        public void startGps() {
            if (!canAccess()) {
                return;
            }
            if (!trackRecordingService.isRecording()) {
                trackRecordingService.startGps();
            }
        }

        public void stopGps() {
            if (!canAccess()) {
                return;
            }
            if (!trackRecordingService.isRecording()) {
                trackRecordingService.stopGps(true);
            }
        }

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

        @Override
        public void pauseCurrentTrack() {
            if (!canAccess()) {
                return;
            }
            trackRecordingService.pauseCurrentTrack();
        }

        @Override
        public void resumeCurrentTrack() {
            if (!canAccess()) {
                return;
            }
            trackRecordingService.resumeCurrentTrack();
        }

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

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

        @Override
        public boolean isPaused() {
            if (!canAccess()) {
                return false;
            }
            return trackRecordingService.isPaused();
        }

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

        @Override
        public long getTotalTime() {
            if (!canAccess()) {
                return 0;
            }
            TripStatisticsUpdater updater = trackRecordingService.trackTripStatisticsUpdater;
            if (updater == null) {
                return 0;
            }
            if (!trackRecordingService.isPaused()) {
                updater.updateTime(System.currentTimeMillis());
            }
            return updater.getTripStatistics().getTotalTime();
        }

        @Override
        public long insertWaypoint(WaypointCreationRequest waypointCreationRequest) {
            if (!canAccess()) {
                return -1L;
            }
            return trackRecordingService.insertWaypoint(waypointCreationRequest);
        }

        @Override
        public void insertTrackPoint(Location location) {
            if (!canAccess()) {
                return;
            }
            trackRecordingService.locationListener.onLocationChanged(location);
        }

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

        @Override
        public int getSensorState() {
            if (!canAccess()) {
                return Sensor.SensorState.NONE.getNumber();
            }
            if (trackRecordingService.sensorManager == null) {
                Log.d(TAG, "sensorManager is null.");
                return Sensor.SensorState.NONE.getNumber();
            }
            return trackRecordingService.sensorManager.getSensorState().getNumber();
        }

        /**
         * 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.
            if (trackRecordingService == null) {
                throw new IllegalStateException("The track recording service has been detached!");
            }
            if (Process.myPid() == Binder.getCallingPid()) {
                return true;
            } else {
                return PreferencesUtils.getBoolean(trackRecordingService, R.string.allow_access_key,
                        PreferencesUtils.ALLOW_ACCESS_DEFAULT);
            }
        }

        /**
         * Detaches from the track recording service. Clears the reference to the
         * outer class to minimize the leak.
         */
        private void detachFromService() {
            trackRecordingService = null;
            attachInterface(null, null);

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

        @Override
        public void updateCalorie() {
            if (!canAccess()) {
                return;
            }
            trackRecordingService.updateCalorie();
        }
    }

    /**
     * Updates the calorie of current recording track after the current track is
     * edited by user.
     */
    public void updateCalorie() {
        if (executorService == null || executorService.isShutdown() || executorService.isTerminated()) {
            return;
        }
        executorService.submit(new Runnable() {
            @Override
            public void run() {
                if (!isRecording()) {
                    Log.w(TAG, "Ignore updateCalorie. Not recording.");
                    return;
                }

                Track track = myTracksProviderUtils.getTrack(recordingTrackId);
                if (track == null) {
                    Log.w(TAG, "Ignore updateCalorie. No track.");
                    return;
                }

                double[] calories = CalorieUtils.updateTrackCalorie(context, track);

                // Update track statistics
                trackTripStatisticsUpdater.updateCalorie(calories[0]);

                // Update marker statistics      
                markerTripStatisticsUpdater.updateCalorie(calories[1]);
            }
        });
    }
}