From dc9173e015a3a246cc12bdd024772b79cab41862 Mon Sep 17 00:00:00 2001 From: Jimmy Shih Date: Wed, 8 Aug 2012 18:52:08 -0700 Subject: [PATCH] Update TrackRecordingService. --- .../mytracks/services/PreferenceManager.java | 139 -- .../services/TrackRecordingService.java | 1813 ++++++++--------- .../services/tasks/PeriodicTaskExecutor.java | 14 +- .../services/tasks/TimerTaskExecutor.java | 8 +- .../content/MyTracksProviderUtils.java | 7 + .../content/MyTracksProviderUtilsImpl.java | 23 + .../MyTracksProviderUtilsImplTest.java | 32 +- 7 files changed, 938 insertions(+), 1098 deletions(-) delete mode 100644 MyTracks/src/com/google/android/apps/mytracks/services/PreferenceManager.java diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/PreferenceManager.java b/MyTracks/src/com/google/android/apps/mytracks/services/PreferenceManager.java deleted file mode 100644 index 1e92b11d8..000000000 --- a/MyTracks/src/com/google/android/apps/mytracks/services/PreferenceManager.java +++ /dev/null @@ -1,139 +0,0 @@ -/* - * Copyright 2010 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.google.android.apps.mytracks.services; - -import com.google.android.apps.mytracks.Constants; -import com.google.android.apps.mytracks.util.PreferencesUtils; -import com.google.android.maps.mytracks.R; - -import android.content.Context; -import android.content.SharedPreferences; -import android.content.SharedPreferences.OnSharedPreferenceChangeListener; -import android.util.Log; - -/** - * A class that manages reading the shared preferences for the service. - * - * @author Sandor Dornbush - */ -public class PreferenceManager implements OnSharedPreferenceChangeListener { - private TrackRecordingService service; - private SharedPreferences sharedPreferences; - - public PreferenceManager(TrackRecordingService service) { - this.service = service; - this.sharedPreferences = service.getSharedPreferences( - Constants.SETTINGS_NAME, Context.MODE_PRIVATE); - sharedPreferences.registerOnSharedPreferenceChangeListener(this); - - // Refresh all properties. - onSharedPreferenceChanged(sharedPreferences, null); - } - - /** - * Notifies that preferences have changed. Call this with key == null to - * update all preferences in one call. - * - * @param key the key that changed (may be null to update all preferences) - */ - @Override - public void onSharedPreferenceChanged(SharedPreferences preferences, String key) { - if (service == null) { - Log.w(Constants.TAG, "onSharedPreferenceChanged: a preference change (key = " + key - + ") after a call to shutdown()"); - return; - } - if (key == null - || PreferencesUtils.getKey(service, R.string.recording_track_id_key).equals(key)) { - long recordingTrackId = PreferencesUtils.getLong(service, R.string.recording_track_id_key); - /* - * Only set the id if it is valid. Setting it to -1L should only happen - * in TrackRecordingService.endCurrentTrack() - */ - if (recordingTrackId != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT) { - service.setRecordingTrackId(recordingTrackId); - } - } - if (key == null || PreferencesUtils.getKey(service, R.string.metric_units_key).equals(key)) { - service.setMetricUnits(PreferencesUtils.getBoolean( - service, R.string.metric_units_key, PreferencesUtils.METRIC_UNITS_DEFAULT)); - } - if (key == null - || PreferencesUtils.getKey(service, R.string.announcement_frequency_key).equals(key)) { - service.setAnnouncementFrequency(PreferencesUtils.getInt( - service, R.string.announcement_frequency_key, - PreferencesUtils.ANNOUNCEMENT_FREQUENCY_DEFAULT)); - } - if (key == null || PreferencesUtils.getKey(service, R.string.split_frequency_key).equals(key)) { - service.setSplitFrequency(PreferencesUtils.getInt( - service, R.string.split_frequency_key, PreferencesUtils.SPLIT_FREQUENCY_DEFAULT)); - } - if (key == null - || PreferencesUtils.getKey(service, R.string.min_recording_interval_key).equals(key)) { - int minRecordingInterval = PreferencesUtils.getInt(service, - 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. min: 30 seconds, - * max: 5 minutes, inDist: 5 meters. - */ - service.setLocationListenerPolicy(new AdaptiveLocationListenerPolicy(30000, 300000, 5)); - break; - case PreferencesUtils.MIN_RECORDING_INTERVAL_ADAPT_ACCURACY: - /* - * Get all the updates. min: 1 second, max: 30 seconds, minDist: 0 - * meter. - */ - service.setLocationListenerPolicy(new AdaptiveLocationListenerPolicy(1000, 30000, 0)); - break; - default: - service.setLocationListenerPolicy( - new AbsoluteLocationListenerPolicy(minRecordingInterval * 1000)); - } - } - if (key == null - || PreferencesUtils.getKey(service, R.string.min_recording_distance_key).equals(key)) { - service.setMinRecordingDistance(PreferencesUtils.getInt( - service, R.string.min_recording_distance_key, - PreferencesUtils.MIN_RECORDING_DISTANCE_DEFAULT)); - } - if (key == null - || PreferencesUtils.getKey(service, R.string.max_recording_distance_key).equals(key)) { - service.setMaxRecordingDistance(PreferencesUtils.getInt( - service, R.string.max_recording_distance_key, - PreferencesUtils.MAX_RECORDING_DISTANCE_DEFAULT)); - } - - if (key == null - || PreferencesUtils.getKey(service, R.string.min_required_accuracy_key).equals(key)) { - service.setMinRequiredAccuracy(PreferencesUtils.getInt( - service, R.string.min_required_accuracy_key, - PreferencesUtils.MIN_REQUIRED_ACCURACY_DEFAULT)); - } - if (key == null - || PreferencesUtils.getKey(service, R.string.auto_resume_track_timeout_key).equals(key)) { - service.setAutoResumeTrackTimeout(PreferencesUtils.getInt( - service, R.string.auto_resume_track_timeout_key, - PreferencesUtils.AUTO_RESUME_TRACK_TIMEOUT_DEFAULT)); - } - } - - public void shutdown() { - sharedPreferences.unregisterOnSharedPreferenceChangeListener(this); - service = null; - } -} diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/TrackRecordingService.java b/MyTracks/src/com/google/android/apps/mytracks/services/TrackRecordingService.java index a11a2a969..ad34e5137 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/services/TrackRecordingService.java +++ b/MyTracks/src/com/google/android/apps/mytracks/services/TrackRecordingService.java @@ -13,16 +13,16 @@ * License for the specific language governing permissions and limitations under * the License. */ + package com.google.android.apps.mytracks.services; import static com.google.android.apps.mytracks.Constants.RESUME_TRACK_EXTRA_NAME; -import static com.google.android.apps.mytracks.Constants.TAG; import com.google.android.apps.mytracks.Constants; import com.google.android.apps.mytracks.TrackDetailActivity; -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.MyTracksProvider; 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; @@ -49,6 +49,8 @@ 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.Cursor; import android.database.sqlite.SQLiteException; import android.location.Location; @@ -66,7 +68,6 @@ 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; @@ -74,111 +75,187 @@ import java.util.concurrent.Executors; /** * A background service that registers a location listener and records track - * points. Track points are saved to the MyTracksProvider. - * + * points. Track points are saved to the {@link MyTracksProvider}. + * * @author Leif Hendrik Wilden */ public class TrackRecordingService extends Service { + private static final String TAG = TrackRecordingService.class.getSimpleName(); + // One second in milliseconds + private static final long ONE_SECOND = 1000; + // One minute in milliseconds + private static final long ONE_MINUTE = 60 * ONE_SECOND; + @VisibleForTesting static final int MAX_AUTO_RESUME_TRACK_RETRY_ATTEMPTS = 3; + // The following variables are set in onCreate: + private Context context; + private MyTracksProviderUtils myTracksProviderUtils; private LocationManager locationManager; - private WakeLock wakeLock; + private PeriodicTaskExecutor announcementExecutor; + private PeriodicTaskExecutor splitExecutor; + private ExecutorService executorService; + private SharedPreferences sharedPreferences; + private long recordingTrackId; + private LocationListenerPolicy locationListenerPolicy; + private int minRecordingDistance; + private int maxRecordingDistance; + private int minRequiredAccuracy; + private int autoResumeTrackTimeout; + private long currentRecordingInterval; + private Track recordingTrack; - 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; + // The following variables are set when recording: - 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. + /* + * Track length. Calculated from the recorded points to overlay waypoints + * precisely on the elevation chart. */ private double length; - /** - * Status announcer executor. - */ - private PeriodicTaskExecutor announcementExecutor; - private PeriodicTaskExecutor splitExecutor; + // Used for length calculation + private Location lastLengthLocation; + private TripStatisticsBuilder trackTripStatisticsBuilder; + private TripStatisticsBuilder markerTripStatisticsBuilder; + private WakeLock wakeLock; + private Location lastLocation; + private boolean isMoving; private SensorManager sensorManager; - private PreferenceManager prefereceManager; + // Timer to periodically invoke checkLocationListener + private final Timer timer = new Timer(); - /** - * The interval in milliseconds that we have requested to be notified of gps - * readings. - */ - private long currentRecordingInterval; + // Handler for the timer to post a runnable to the main thread + private final Handler handler = new Handler(); - /** - * The policy used to decide how often we should request gps updates. + private ServiceBinder binder = new ServiceBinder(this); + + /* + * Note that sharedPreferenceChangeListener cannot be an anonymous inner + * class. Anonymous inner class will get garbage collected. */ - private LocationListenerPolicy locationListenerPolicy = - new AbsoluteLocationListenerPolicy(0); + private final OnSharedPreferenceChangeListener + sharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() { + @Override + public void onSharedPreferenceChanged(SharedPreferences preferences, String key) { + if (key == null + || PreferencesUtils.getKey(context, R.string.recording_track_id_key).equals(key)) { + long id = PreferencesUtils.getLong(context, R.string.recording_track_id_key); + /* + * Only the TrackRecordingService can stop a recording and set the + * recordingTrackId to -1L. + */ + if (id != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT) { + recordingTrackId = id; + } + } + if (key == null + || PreferencesUtils.getKey(context, R.string.metric_units_key).equals(key)) { + boolean metricUnits = PreferencesUtils.getBoolean( + context, R.string.metric_units_key, PreferencesUtils.METRIC_UNITS_DEFAULT); + announcementExecutor.setMetricUnits(metricUnits); + splitExecutor.setMetricUnits(metricUnits); + } + if (key == null || PreferencesUtils.getKey(context, R.string.announcement_frequency_key) + .equals(key)) { + announcementExecutor.setTaskFrequency(PreferencesUtils.getInt( + context, R.string.announcement_frequency_key, + PreferencesUtils.ANNOUNCEMENT_FREQUENCY_DEFAULT)); + } + if (key == null + || PreferencesUtils.getKey(context, R.string.split_frequency_key).equals(key)) { + splitExecutor.setTaskFrequency(PreferencesUtils.getInt( + context, R.string.split_frequency_key, PreferencesUtils.SPLIT_FREQUENCY_DEFAULT)); + } + if (key == null || PreferencesUtils.getKey(context, R.string.min_recording_interval_key) + .equals(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 || PreferencesUtils.getKey(context, R.string.min_recording_distance_key) + .equals(key)) { + minRecordingDistance = PreferencesUtils.getInt(context, + R.string.min_recording_distance_key, + PreferencesUtils.MIN_RECORDING_DISTANCE_DEFAULT); + if (trackTripStatisticsBuilder != null && markerTripStatisticsBuilder != null) { + trackTripStatisticsBuilder.setMinRecordingDistance(minRecordingDistance); + markerTripStatisticsBuilder.setMinRecordingDistance(minRecordingDistance); + } + } + if (key == null || PreferencesUtils.getKey(context, R.string.max_recording_distance_key) + .equals(key)) { + maxRecordingDistance = PreferencesUtils.getInt(context, + R.string.max_recording_distance_key, + PreferencesUtils.MAX_RECORDING_DISTANCE_DEFAULT); + } + if (key == null + || PreferencesUtils.getKey(context, R.string.min_required_accuracy_key).equals(key)) { + minRequiredAccuracy = PreferencesUtils.getInt(context, + R.string.min_required_accuracy_key, PreferencesUtils.MIN_REQUIRED_ACCURACY_DEFAULT); + } + if (key == null || PreferencesUtils.getKey( + context, R.string.auto_resume_track_timeout_key).equals(key)) { + autoResumeTrackTimeout = PreferencesUtils.getInt(context, + R.string.auto_resume_track_timeout_key, + PreferencesUtils.AUTO_RESUME_TRACK_TIMEOUT_DEFAULT); + } + } + }; private LocationListener locationListener = new LocationListener() { - @Override + @Override public void onProviderDisabled(String provider) { // Do nothing } - @Override + @Override public void onProviderEnabled(String provider) { // Do nothing } - @Override + @Override public void onStatusChanged(String provider, int status, Bundle extras) { // Do nothing } - @Override + @Override public void onLocationChanged(final Location location) { if (executorService.isShutdown() || executorService.isTerminated()) { return; } - executorService.submit( - new Runnable() { + executorService.submit(new Runnable() { @Override - public void run() { - onLocationChangedAsync(location); - } - }); + 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 + @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(); } @@ -187,90 +264,51 @@ public class TrackRecordingService extends Service { } }; - /** - * 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. + * database. */ @Override public void onCreate() { super.onCreate(); - Log.d(TAG, "TrackRecordingService.onCreate"); - providerUtils = MyTracksProviderUtils.Factory.get(this); + context = this; + myTracksProviderUtils = MyTracksProviderUtils.Factory.get(this); locationManager = (LocationManager) getSystemService(LOCATION_SERVICE); - - setUpTaskExecutors(); + announcementExecutor = new PeriodicTaskExecutor(this, new StatusAnnouncerFactory()); + splitExecutor = new PeriodicTaskExecutor(this, new SplitTask.Factory()); executorService = Executors.newSingleThreadExecutor(); + sharedPreferences = getSharedPreferences(Constants.SETTINGS_NAME, Context.MODE_PRIVATE); + sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener); - prefereceManager = new PreferenceManager(this); + // onSharedPreferenceChanged might not set recordingTrackId. + recordingTrackId = PreferencesUtils.RECORDING_TRACK_ID_DEFAULT; + // Require announcementExecutor and splitExecutor to be created. + sharedPreferenceChangeListener.onSharedPreferenceChanged(sharedPreferences, null); + + // Require locationManager and all preferences set. 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); + timer.schedule(checkLocationListener, 5 * ONE_MINUTE, ONE_MINUTE); - // Try to restore previous recording state in case this service has been - // restarted by the system, which can sometimes happen. - recordingTrack = getRecordingTrack(); + /* + * Try to restart the previous recording track in case the service has been + * restarted by the system, which can sometimes happen. + */ + recordingTrack = myTracksProviderUtils.getTrack(recordingTrackId); if (recordingTrack != null) { - restoreStats(recordingTrack); - isRecording = true; + restartTrack(recordingTrack); } else { - if (recordingTrackId != -1L) { - // Make sure we have consistent state in shared preferences. - Log.w(TAG, "TrackRecordingService.onCreate: " - + "Resetting an orphaned recording track = " + recordingTrackId); + if (recordingTrackId != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT) { + Log.w(TAG, "recordingTrackId not -1L, but recordingTrack is null. " + recordingTrackId); + recordingTrackId = PreferencesUtils.RECORDING_TRACK_ID_DEFAULT; + PreferencesUtils.setLong(this, R.string.recording_track_id_key, recordingTrackId); } - recordingTrackId = -1L; - PreferencesUtils.setLong(this, R.string.recording_track_id_key, recordingTrackId); + showNotification(); } - showNotification(); } /* @@ -278,7 +316,7 @@ public class TrackRecordingService extends Service { * 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. + * database. */ @Override public void onStart(Intent intent, int startId) { @@ -290,7 +328,7 @@ public class TrackRecordingService extends Service { * 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. + * MyTracks database. */ @Override public int onStartCommand(Intent intent, int flags, int startId) { @@ -298,606 +336,99 @@ public class TrackRecordingService extends Service { 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(); - prefereceManager.shutdown(); + + sharedPreferences.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener); checkLocationListener.cancel(); checkLocationListener = null; timer.cancel(); timer.purge(); unregisterLocationListener(); - shutdownTaskExecutors(); + + try { + announcementExecutor.shutdown(); + } finally { + announcementExecutor = null; + } + + try { + splitExecutor.shutdown(); + } finally { + splitExecutor = null; + } + if (sensorManager != null) { SensorManagerFactory.releaseSystemSensorManager(); sensorManager = null; } // Make sure we have no indirect references to this service. + myTracksProviderUtils = null; locationManager = null; - providerUtils = null; binder.detachFromService(); binder = null; - // This should be the last operation. + // This should be the next to last operation releaseWakeLock(); - // Shutdown the executor service last to avoid sending events to a dead executor. + /* + * 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. + * Returns true if the service is recording. */ - 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); - } + public boolean isRecording() { + return recordingTrackId != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT; } /** - * Releases the wake lock if it's currently held. + * Gets the trip statistics. */ - private void releaseWakeLock() { - if (wakeLock != null && wakeLock.isHeld()) { - wakeLock.release(); - wakeLock = null; + public TripStatistics getTripStatistics() { + if (trackTripStatisticsBuilder == null) { + return null; } + return trackTripStatisticsBuilder.getTripStatistics(); } /** - * Shows the notification message and icon in the notification bar. + * Inserts a waypoint. + * + * @param waypointCreationRequest the waypoint creation request + * @return the waypoint id */ - 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.getTripStatistics().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.getTripStatistics(); - 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.getTripStatistics().getStartTime()); - waypoint.setTripStatistics(waypointStatsBuilder.getTripStatistics()); - 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) { + public long insertWaypoint(WaypointCreationRequest waypointCreationRequest) { if (!isRecording()) { - throw new IllegalStateException("Unable to insert marker while not recording!"); + throw new IllegalStateException("Cannot insert marker when not recording!"); } Waypoint waypoint = new Waypoint(); - if (request.getType() == WaypointType.WAYPOINT) { - buildWaypointMarker(waypoint, request); + if (waypointCreationRequest.getType() == WaypointType.WAYPOINT) { + buildWaypointMarker(waypoint, waypointCreationRequest); } else { - buildStatisticsMarker(waypoint, request); + buildStatisticsMarker(waypoint, waypointCreationRequest); } waypoint.setTrackId(recordingTrackId); waypoint.setLength(length); - if (lastLocation == null || statsBuilder == null || statsBuilder.getTripStatistics() == null) { - if (!request.isTrackStatistics()) { + + if (lastLocation != null && trackTripStatisticsBuilder != null + && trackTripStatisticsBuilder.getTripStatistics() != null) { + waypoint.setLocation(lastLocation); + waypoint.setDuration( + lastLocation.getTime() - trackTripStatisticsBuilder.getTripStatistics().getStartTime()); + } else { + if (!waypointCreationRequest.isTrackStatistics()) { return -1L; } /* @@ -908,105 +439,219 @@ public class TrackRecordingService extends Service { location.setLatitude(100); location.setLongitude(180); waypoint.setLocation(location); - } else { - waypoint.setLocation(lastLocation); - waypoint.setDuration(lastLocation.getTime() - statsBuilder.getTripStatistics().getStartTime()); } - Uri uri = providerUtils.insertWaypoint(waypoint); + Uri uri = myTracksProviderUtils.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; + /** + * Starts the service as a foreground service. + * + * @param notification the notification for the foreground service + */ + @VisibleForTesting + protected void startForegroundService(Notification notification) { + startForeground(1, notification); + } + + /** + * 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(recordingTrack)) { + Log.i(TAG, "Stop resume track."); + stopRecording(); + stopSelfResult(startId); + return; } - 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 + * Returns true if should resume. + * + * @param track the track */ - 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.getTripStatistics().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); + private boolean shouldResumeTrack(Track track) { + if (track == null) { + Log.d(TAG, "Not resuming. Track is null."); + return false; } - waypoint.setName(name); - waypoint.setTripStatistics(waypointStatsBuilder.getTripStatistics()); - waypoint.setDescription(descriptionGenerator.generateWaypointDescription(waypoint)); - waypoint.setIcon(getString(R.string.marker_statistics_icon_url)); + 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); - waypoint.setStartId(providerUtils.getLastLocationId(recordingTrackId)); + 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; + } - // Create a new stats keeper for the next marker. - waypointStatsBuilder = new TripStatisticsBuilder(time); + 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(); + length = 0; + lastLengthLocation = null; + trackTripStatisticsBuilder = new TripStatisticsBuilder(now); + trackTripStatisticsBuilder.setMinRecordingDistance(minRecordingDistance); + markerTripStatisticsBuilder = new TripStatisticsBuilder(now); + markerTripStatisticsBuilder.setMinRecordingDistance(minRecordingDistance); + + Track track = new Track(); + track.setStartId(-1L); + TripStatistics tripStatistics = track.getTripStatistics(); + tripStatistics.setStartTime(now); + Uri uri = myTracksProviderUtils.insertTrack(track); + + recordingTrackId = Long.parseLong(uri.getLastPathSegment()); + track.setId(recordingTrackId); + track.setName(TrackNameUtils.getTrackName(this, recordingTrackId, now, null)); + track.setCategory(PreferencesUtils.getString( + this, R.string.default_activity_key, PreferencesUtils.DEFAULT_ACTIVITY_DEFAULT)); + myTracksProviderUtils.updateTrack(track); + + insertWaypoint(WaypointCreationRequest.DEFAULT_START_TRACK); + PreferencesUtils.setLong(this, R.string.recording_track_id_key, recordingTrackId); + PreferencesUtils.setInt(this, R.string.auto_resume_track_current_retry_key, 0); + + registerLocationListener(); + startRecording(); + return recordingTrackId; + } + + /** + * Restart a track. + * + * @param track the track + */ + private void restartTrack(Track track) { + Log.d(TAG, "Restarting track: " + track.getId()); + + length = 0; + lastLengthLocation = null; + TripStatistics tripStatistics = track.getTripStatistics(); + trackTripStatisticsBuilder = new TripStatisticsBuilder(tripStatistics.getStartTime()); + trackTripStatisticsBuilder.setMinRecordingDistance(minRecordingDistance); + + long markerStartTime; + Waypoint waypoint = myTracksProviderUtils.getLastStatisticsWaypoint(recordingTrackId); + if (waypoint != null && waypoint.getTripStatistics() != null) { + markerStartTime = waypoint.getTripStatistics().getStopTime(); + } else { + markerStartTime = tripStatistics.getStartTime(); + } + markerTripStatisticsBuilder = new TripStatisticsBuilder(markerStartTime); + markerTripStatisticsBuilder.setMinRecordingDistance(minRecordingDistance); + + Cursor cursor = null; + try { + cursor = myTracksProviderUtils.getLocationsCursor( + recordingTrackId, -1, Constants.MAX_LOADED_TRACK_POINTS, true); + if (cursor == null) { + Log.e(TAG, "Cursor is null."); + } else { + if (cursor.moveToLast()) { + do { + Location location = myTracksProviderUtils.createLocation(cursor); + if (LocationUtils.isValidLocation(location)) { + trackTripStatisticsBuilder.addLocation(location, location.getTime()); + if (location.getTime() > markerStartTime) { + markerTripStatisticsBuilder.addLocation(location, location.getTime()); + } + if (lastLengthLocation != null) { + length += location.distanceTo(lastLengthLocation); + } + lastLengthLocation = location; + } + } while (cursor.moveToPrevious()); + } + trackTripStatisticsBuilder.pauseAt(tripStatistics.getStopTime()); + trackTripStatisticsBuilder.resumeAt(System.currentTimeMillis()); + } + } catch (RuntimeException e) { + Log.e(TAG, "RuntimeException", e); + } finally { + if (cursor != null) { + cursor.close(); + } + } + startRecording(); + } + + /** + * Common code for starting a recording, new track or restart track. + */ + private void startRecording() { + acquireWakeLock(); + + lastLocation = null; + isMoving = true; + sensorManager = SensorManagerFactory.getSystemSensorManager(this); + + showNotification(); + sendTrackBroadcast(R.string.track_started_broadcast_action, recordingTrackId); + announcementExecutor.restore(); + splitExecutor.restore(); + } + + /** + * Ends the current track. + */ private void endCurrentTrack() { - Log.d(TAG, "TrackRecordingService.endCurrentTrack"); - if (!isTrackInProgress()) { + if (!isRecording()) { + Log.d(TAG, "Ignore endCurrentTrack. Not recording."); 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); + Track track = myTracksProviderUtils.getTrack(recordingTrackId); + if (track != null) { + long lastLocationId = myTracksProviderUtils.getLastLocationId(recordingTrackId); + if (lastLocationId >= 0 && track.getStopId() >= 0) { + track.setStopId(lastLocationId); } - TripStatistics tripStatistics = recordedTrack.getTripStatistics(); - tripStatistics.setStopTime(System.currentTimeMillis()); - tripStatistics.setTotalTime(tripStatistics.getStopTime() - tripStatistics.getStartTime()); - providerUtils.updateTrack(recordedTrack); + updateTripStatisticsToTime(track.getTripStatistics(), System.currentTimeMillis()); + myTracksProviderUtils.updateTrack(track); } - showNotification(); - long recordedTrackId = recordingTrackId; - recordingTrackId = -1L; - PreferencesUtils.setLong(this, R.string.recording_track_id_key, recordingTrackId); + // Need to remember the trackId before calling stopRecording + long trackId = recordingTrackId; + stopRecording(); if (sensorManager != null) { SensorManagerFactory.releaseSystemSensorManager(); @@ -1014,120 +659,396 @@ public class TrackRecordingService extends Service { } releaseWakeLock(); - - // Notify the world that we're no longer recording. - sendTrackBroadcast( - R.string.track_stopped_broadcast_action, recordedTrackId); - + showNotification(); + sendTrackBroadcast(R.string.track_stopped_broadcast_action, trackId); 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. + /** + * Common code for stopping a recording. */ - - private Track getRecordingTrack() { - if (recordingTrackId < 0) { - return null; - } - - return providerUtils.getTrack(recordingTrackId); + private void stopRecording() { + recordingTrackId = PreferencesUtils.RECORDING_TRACK_ID_DEFAULT; + PreferencesUtils.setLong(this, R.string.recording_track_id_key, recordingTrackId); } - public boolean isRecording() { - return isRecording; - } - - public TripStatistics getTripStatistics() { - return statsBuilder.getTripStatistics(); - } - - Location getLastLocation() { - return lastLocation; - } - - long getRecordingTrackId() { - return recordingTrackId; - } - - void setRecordingTrackId(long recordingTrackId) { - this.recordingTrackId = recordingTrackId; - } - - void setMaxRecordingDistance(int maxRecordingDistance) { - this.maxRecordingDistance = maxRecordingDistance; - } - - void setMinRecordingDistance(int minRecordingDistance) { - this.minRecordingDistance = minRecordingDistance; - if (statsBuilder != null && waypointStatsBuilder != null) { - statsBuilder.setMinRecordingDistance(minRecordingDistance); - waypointStatsBuilder.setMinRecordingDistance(minRecordingDistance); - } - } - - void setMinRequiredAccuracy(int minRequiredAccuracy) { - this.minRequiredAccuracy = minRequiredAccuracy; - } - - void setLocationListenerPolicy(LocationListenerPolicy locationListenerPolicy) { - this.locationListenerPolicy = locationListenerPolicy; - } - - void setAutoResumeTrackTimeout(int autoResumeTrackTimeout) { - this.autoResumeTrackTimeout = autoResumeTrackTimeout; - } - - void setAnnouncementFrequency(int announcementFrequency) { - announcementExecutor.setTaskFrequency(announcementFrequency); - } - - void setSplitFrequency(int frequency) { - splitExecutor.setTaskFrequency(frequency); - } - - void setMetricUnits(boolean metric) { - announcementExecutor.setMetricUnits(metric); - splitExecutor.setMetricUnits(metric); - } - - /** - * TODO: There is a bug in Android that leaks Binder instances. This bug is + * Updates a {@link TripStatistics} to a time. + * + * @param tripStatistics the trip statistics + * @param time the time + */ + private void updateTripStatisticsToTime(TripStatistics tripStatistics, long time) { + tripStatistics.setStopTime(time); + tripStatistics.setTotalTime(time - tripStatistics.getStartTime()); + } + + /** + * Called when location changed. + * + * @param location the location + */ + private void onLocationChangedAsync(Location location) { + try { + if (location == null) { + Log.w(TAG, "Ignore onLocationChangedAsync. location is null."); + return; + } + + if (!isRecording()) { + Log.w(TAG, "Ignore onLocationChangedAsync. Not recording."); + return; + } + + if (location.getAccuracy() > minRequiredAccuracy) { + Log.d(TAG, "Ignore onLocationChangedAsync. Poor accuracy."); + return; + } + + recordingTrack = myTracksProviderUtils.getTrack(recordingTrackId); + if (recordingTrack == null) { + Log.d(TAG, "Ignore onLocationChangedAsync. recodingTrack is null."); + return; + } + + locationListenerPolicy.updateIdleTime(trackTripStatisticsBuilder.getIdleTime()); + + if (LocationUtils.isValidLocation(location)) { + long now = System.currentTimeMillis(); + trackTripStatisticsBuilder.addLocation(location, now); + markerTripStatisticsBuilder.addLocation(location, now); + } + + if (currentRecordingInterval != locationListenerPolicy.getDesiredPollingInterval()) { + registerLocationListener(); + } + + Location lastRecordedLocation = myTracksProviderUtils.getLastLocation(); + double distanceToLastRecordedLocation = lastRecordedLocation != null ? location.distanceTo( + lastRecordedLocation) + : Double.POSITIVE_INFINITY; + double distanceToLastLocation = lastLocation != null ? location.distanceTo(lastLocation) + : Double.POSITIVE_INFINITY; + boolean hasSensorData = sensorManager != null && sensorManager.isEnabled() + && sensorManager.getSensorDataSet() != null && sensorManager.isSensorDataSetValid(); + + // If stationary for two locations, make sure the first one is recorded + if (distanceToLastLocation == 0 && !hasSensorData) { + if (isMoving) { + isMoving = false; + if (lastLocation != null && !lastLocation.equals(lastRecordedLocation)) { + if (!insertLocation(lastLocation, lastRecordedLocation, recordingTrackId)) { + return; + } + } + } + lastLocation = location; + } else if (distanceToLastRecordedLocation >= minRecordingDistance || hasSensorData) { + if (!isMoving) { + isMoving = true; + // Make sure the lastLocation is added. + if (lastLocation != null && !lastLocation.equals(lastRecordedLocation)) { + if (!insertLocation(lastLocation, lastRecordedLocation, recordingTrackId)) { + return; + } + } + } + + boolean startNewSegment = LocationUtils.isValidLocation(lastRecordedLocation) + && distanceToLastRecordedLocation > maxRecordingDistance + && recordingTrack.getStartId() >= 0; + if (startNewSegment) { + Log.d(TAG, "Inserting a separator track point."); + Location separator = new Location(LocationManager.GPS_PROVIDER); + separator.setLongitude(0); + separator.setLatitude(100); + separator.setTime(lastRecordedLocation.getTime()); + myTracksProviderUtils.insertTrackPoint(separator, recordingTrackId); + } + + if (!insertLocation(location, lastRecordedLocation, recordingTrackId)) { + return; + } + lastLocation = location; + } else { + Log.d(TAG, "Ignore location. distanceToLastRecordedLocation < minRecordingDistance."); + } + } 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 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 + if (LocationUtils.isValidLocation(location)) { + if (lastLengthLocation != null) { + length += location.distanceTo(lastLengthLocation); + } + lastLengthLocation = location; + } + + try { + Location locationToInsert = location; + if (sensorManager != null && sensorManager.isEnabled()) { + SensorDataSet sensorDataSet = sensorManager.getSensorDataSet(); + if (sensorDataSet != null && sensorManager.isSensorDataSetValid()) { + locationToInsert = new MyTracksLocation(location, sensorDataSet); + } + } + Uri uri = myTracksProviderUtils.insertTrackPoint(locationToInsert, trackId); + int pointId = Integer.parseInt(uri.getLastPathSegment()); + + // Update the current track + if (lastRecordedLocation != null && lastRecordedLocation.getLatitude() <= 90) { + if (recordingTrack.getStartId() < 0) { + recordingTrack.setStartId(pointId); + } + recordingTrack.setStopId(pointId); + recordingTrack.setNumberOfPoints(recordingTrack.getNumberOfPoints() + 1); + + long now = System.currentTimeMillis(); + TripStatistics tripStatistics = trackTripStatisticsBuilder.getTripStatistics(); + updateTripStatisticsToTime(tripStatistics, now); + recordingTrack.setTripStatistics(tripStatistics); + + myTracksProviderUtils.updateTrack(recordingTrack); + + // Update the first waypoint + Waypoint waypoint = myTracksProviderUtils.getFirstWaypoint(recordingTrackId); + if (waypoint != null) { + TripStatistics stats = trackTripStatisticsBuilder.getTripStatistics(); + waypoint.setLength(length); + waypoint.setDuration(now - stats.getStartTime()); + waypoint.setTripStatistics(stats); + myTracksProviderUtils.updateWaypoint(waypoint); + } + } + } 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); + return false; + } + announcementExecutor.update(); + splitExecutor.update(); + return true; + } + + /** + * Builds a waypoint marker. + * + * @param waypoint the waypoint + * @param waypointCreationRequest the waypoint creation request + */ + private void buildWaypointMarker( + Waypoint waypoint, WaypointCreationRequest waypointCreationRequest) { + waypoint.setType(Waypoint.TYPE_WAYPOINT); + String name; + if (waypointCreationRequest.getName() != null) { + name = waypointCreationRequest.getName(); + } else { + int nextMarkerNumber = myTracksProviderUtils.getNextMarkerNumber(recordingTrackId, false); + if (nextMarkerNumber == -1) { + nextMarkerNumber = 0; + } + name = getString(R.string.marker_name_format, nextMarkerNumber); + } + waypoint.setName(name); + if (waypointCreationRequest.getDescription() != null) { + waypoint.setDescription(waypointCreationRequest.getDescription()); + } + if (waypointCreationRequest.getCategory() != null) { + waypoint.setCategory(waypointCreationRequest.getCategory()); + } + + String icon = waypointCreationRequest.getIconUrl(); + if (icon == null) { + icon = getString(R.string.marker_waypoint_icon_url); + } + waypoint.setIcon(icon); + } + + /** + * Build a statistics marker. A statistics marker holds the stats for the last + * segment up to this statistics marker. + * + * @param waypoint the waypoint + * @param waypointCreationRequest the waypoint creation request + */ + private void buildStatisticsMarker( + Waypoint waypoint, WaypointCreationRequest waypointCreationRequest) { + waypoint.setType(Waypoint.TYPE_STATISTICS); + + String name; + if (waypointCreationRequest.getName() != null) { + name = waypointCreationRequest.getName(); + } else { + int nextMarkerNumber = myTracksProviderUtils.getNextMarkerNumber(recordingTrackId, true); + if (nextMarkerNumber == -1) { + nextMarkerNumber = 0; + } + name = getString(R.string.marker_split_name_format, nextMarkerNumber); + } + waypoint.setName(name); + + long now = System.currentTimeMillis(); + markerTripStatisticsBuilder.pauseAt(now); + waypoint.setTripStatistics(markerTripStatisticsBuilder.getTripStatistics()); + + // Update description after updating tripStatistics + waypoint.setDescription( + new DescriptionGeneratorImpl(this).generateWaypointDescription(waypoint)); + waypoint.setIcon(getString(R.string.marker_statistics_icon_url)); + waypoint.setStartId(myTracksProviderUtils.getLastLocationId(recordingTrackId)); + // Update duration (from the the beginning of the track) + waypoint.setDuration(now - trackTripStatisticsBuilder.getTripStatistics().getStartTime()); + + // Create a new markerTripStatisticsBuilder for the next statistics marker + markerTripStatisticsBuilder = new TripStatisticsBuilder(now); + } + + /** + * Registers the location listener. + */ + private void registerLocationListener() { + if (locationManager == null) { + Log.e(TAG, "locationManager is null."); + return; + } + try { + long interval = locationListenerPolicy.getDesiredPollingInterval(); + locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 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 (locationManager == null) { + Log.e(TAG, "locationManager is null."); + return; + } + locationManager.removeUpdates(locationListener); + } + + /** + * Acquires the wake lock. + */ + private void acquireWakeLock() { + try { + PowerManager powerManager = (PowerManager) getSystemService(Context.POWER_SERVICE); + if (powerManager == null) { + Log.e(TAG, "powerManager is null."); + return; + } + if (wakeLock == null) { + wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG); + if (wakeLock == null) { + Log.e(TAG, "wakeLock is null."); + return; + } + } + if (!wakeLock.isHeld()) { + wakeLock.acquire(); + if (!wakeLock.isHeld()) { + Log.e(TAG, "Unable to hold wakeLock."); + } + } + } catch (RuntimeException e) { + Log.e(TAG, "Caught unexpected exception", e); + } + } + + /** + * Releases the wake lock. + */ + private void releaseWakeLock() { + if (wakeLock != null && wakeLock.isHeld()) { + wakeLock.release(); + wakeLock = null; + } + } + + /** + * Shows the notification. + */ + 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(); + } + } + + /** + * 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: + * 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 TrackRecordingService trackRecordingService; private DeathRecipient deathRecipient; - public ServiceBinder(TrackRecordingService service) { - this.service = service; + public ServiceBinder(TrackRecordingService trackRecordingService) { + this.trackRecordingService = trackRecordingService; } - // Logic for letting the actual service go up and down. - @Override public boolean isBinderAlive() { - // Pretend dead if the service went down. - return service != null; + return trackRecordingService != null; } @Override @@ -1145,31 +1066,84 @@ public class TrackRecordingService extends Service { 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(); + @Override + public boolean isRecording() { + if (!canAccess()) { + return false; } + return trackRecordingService.isRecording(); } - /** - * 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!"); + @Override + public long getRecordingTrackId() { + if (!canAccess()) { + return -1L; } + return trackRecordingService.recordingTrackId; + } + + @Override + public long startNewTrack() { + if (!canAccess()) { + return -1L; + } + return trackRecordingService.startNewTrack(); + } + + @Override + public long insertWaypoint(WaypointCreationRequest waypointCreationRequest) { + if (!canAccess()) { + return -1L; + } + return trackRecordingService.insertWaypoint(waypointCreationRequest); + } + + @Override + public void endCurrentTrack() { + if (!canAccess()) { + return; + } + trackRecordingService.endCurrentTrack(); + } + + @Override + public void recordLocation(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(); } /** @@ -1179,97 +1153,28 @@ public class TrackRecordingService extends Service { */ private boolean canAccess() { // As a precondition for access, must check if the service is available. - checkService(); + if (trackRecordingService == null) { + throw new IllegalStateException("The track recording service has been detached!"); + } if (Process.myPid() == Binder.getCallingPid()) { return true; } else { - return PreferencesUtils.getBoolean( - service, R.string.allow_access_key, PreferencesUtils.ALLOW_ACCESS_DEFAULT); + return PreferencesUtils.getBoolean(trackRecordingService, 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 + * Detaches from the track recording service. Clears the reference to the + * outer class to minimize the leak. */ - public long insertWaypoint(WaypointCreationRequest request) { - if (!canAccess()) { - return -1L; - } - return service.insertWaypoint(request); - } + private void detachFromService() { + trackRecordingService = null; + attachInterface(null, null); - @Override - public void endCurrentTrack() { - if (!canAccess()) { - return; + if (deathRecipient != null) { + deathRecipient.binderDied(); } - 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(); } } - } diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/tasks/PeriodicTaskExecutor.java b/MyTracks/src/com/google/android/apps/mytracks/services/tasks/PeriodicTaskExecutor.java index a02b201e6..14a92c684 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/services/tasks/PeriodicTaskExecutor.java +++ b/MyTracks/src/com/google/android/apps/mytracks/services/tasks/PeriodicTaskExecutor.java @@ -16,6 +16,7 @@ package com.google.android.apps.mytracks.services.tasks; import com.google.android.apps.mytracks.services.TrackRecordingService; +import com.google.android.apps.mytracks.stats.TripStatistics; import com.google.android.apps.mytracks.util.PreferencesUtils; import com.google.android.apps.mytracks.util.UnitConversions; @@ -118,7 +119,11 @@ public class PeriodicTaskExecutor { if (!isDistanceFrequency() || periodicTask == null) { return; } - double distance = trackRecordingService.getTripStatistics().getTotalDistance() + TripStatistics tripStatistics = trackRecordingService.getTripStatistics(); + if (tripStatistics == null) { + return; + } + double distance = tripStatistics.getTotalDistance() * UnitConversions.M_TO_KM; if (!metricUnits) { distance *= UnitConversions.KM_TO_MI; @@ -158,13 +163,18 @@ public class PeriodicTaskExecutor { return; } + TripStatistics tripStatistics = trackRecordingService.getTripStatistics(); + if (tripStatistics == null) { + return; + } + if (!isDistanceFrequency()) { nextTaskDistance = Double.MAX_VALUE; Log.d(TAG, "SplitManager: Distance splits disabled."); return; } - double distance = trackRecordingService.getTripStatistics().getTotalDistance() + double distance = tripStatistics.getTotalDistance() * UnitConversions.M_TO_KM; if (!metricUnits) { distance *= UnitConversions.KM_TO_MI; diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/tasks/TimerTaskExecutor.java b/MyTracks/src/com/google/android/apps/mytracks/services/tasks/TimerTaskExecutor.java index 86a61f9e4..4d9364de7 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/services/tasks/TimerTaskExecutor.java +++ b/MyTracks/src/com/google/android/apps/mytracks/services/tasks/TimerTaskExecutor.java @@ -17,6 +17,7 @@ package com.google.android.apps.mytracks.services.tasks; import com.google.android.apps.mytracks.services.TrackRecordingService; +import com.google.android.apps.mytracks.stats.TripStatistics; import java.util.Date; import java.util.Timer; @@ -53,6 +54,11 @@ public class TimerTaskExecutor { return; } + TripStatistics tripStatistics = trackRecordingService.getTripStatistics(); + if (tripStatistics == null) { + return; + } + if (timer != null) { timer.cancel(); timer.purge(); @@ -67,7 +73,7 @@ public class TimerTaskExecutor { } long now = System.currentTimeMillis(); - long next = trackRecordingService.getTripStatistics().getStartTime(); + long next = tripStatistics.getStartTime(); if (next < now) { next = now + interval - ((now - next) % interval); } diff --git a/MyTracksLib/src/com/google/android/apps/mytracks/content/MyTracksProviderUtils.java b/MyTracksLib/src/com/google/android/apps/mytracks/content/MyTracksProviderUtils.java index d5090a0db..f3bf1662d 100644 --- a/MyTracksLib/src/com/google/android/apps/mytracks/content/MyTracksProviderUtils.java +++ b/MyTracksLib/src/com/google/android/apps/mytracks/content/MyTracksProviderUtils.java @@ -174,6 +174,13 @@ public interface MyTracksProviderUtils { */ public long getLastWaypointId(long trackId); + /** + * Gets the last statistics waypoint for a track. Returns null if it doesn't exist. + * + * @param trackId the track id + */ + public Waypoint getLastStatisticsWaypoint(long trackId); + /** * Gets the next marker number. Returns -1 if not able to get the next marker * number. diff --git a/MyTracksLib/src/com/google/android/apps/mytracks/content/MyTracksProviderUtilsImpl.java b/MyTracksLib/src/com/google/android/apps/mytracks/content/MyTracksProviderUtilsImpl.java index 1d882bab6..85cf87625 100644 --- a/MyTracksLib/src/com/google/android/apps/mytracks/content/MyTracksProviderUtilsImpl.java +++ b/MyTracksLib/src/com/google/android/apps/mytracks/content/MyTracksProviderUtilsImpl.java @@ -443,6 +443,29 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { return -1L; } + @Override + public Waypoint getLastStatisticsWaypoint(long trackId) { + if (trackId < 0) { + return null; + } + Cursor cursor = null; + try { + String selection = WaypointsColumns.TRACKID + "=? AND " + WaypointsColumns.TYPE + "=" + + Waypoint.TYPE_STATISTICS; + String[] selectionArgs = new String[] { Long.toString(trackId) }; + cursor = getWaypointsCursor( + null, selection, selectionArgs, WaypointsColumns._ID + " DESC", 1); + if (cursor != null && cursor.moveToFirst()) { + return createWaypoint(cursor); + } + } finally { + if (cursor != null) { + cursor.close(); + } + } + return null; + } + @Override public int getNextMarkerNumber(long trackId, boolean statistics) { if (trackId < 0) { diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/content/MyTracksProviderUtilsImplTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/content/MyTracksProviderUtilsImplTest.java index c73458920..640009310 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/content/MyTracksProviderUtilsImplTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/content/MyTracksProviderUtilsImplTest.java @@ -430,7 +430,7 @@ public class MyTracksProviderUtilsImplTest extends AndroidTestCase { /** * Tests the method {@link MyTracksProviderUtilsImpl#getLastWaypointId(long)}. */ - public void testGetKLastWaypointId() { + public void testGetLastWaypointId() { long trackId = System.currentTimeMillis(); Track track = getTrack(trackId, 10); providerUtils.insertTrack(track); @@ -448,7 +448,35 @@ public class MyTracksProviderUtilsImplTest extends AndroidTestCase { assertEquals(-1L, providerUtils.getLastWaypointId(-1)); assertEquals(3L, providerUtils.getLastWaypointId(trackId)); } - + + /** + * Tests the method + * {@link MyTracksProviderUtils#getLastStatisticsWaypoint(long)}. + */ + public void testGetLastStatisticsWaypoint() { + long trackId = System.currentTimeMillis(); + Track track = getTrack(trackId, 10); + providerUtils.insertTrack(track); + + Waypoint waypoint1 = new Waypoint(); + waypoint1.setTrackId(trackId); + waypoint1.setType(Waypoint.TYPE_STATISTICS); + waypoint1.setDescription("Desc1"); + Waypoint waypoint2 = new Waypoint(); + waypoint2.setTrackId(trackId); + waypoint2.setType(Waypoint.TYPE_STATISTICS); + waypoint2.setDescription("Desc2"); + Waypoint waypoint3 = new Waypoint(); + waypoint3.setTrackId(trackId); + waypoint3.setType(Waypoint.TYPE_WAYPOINT); + waypoint3.setDescription("Desc3"); + providerUtils.insertWaypoint(waypoint1); + providerUtils.insertWaypoint(waypoint2); + providerUtils.insertWaypoint(waypoint3); + + assertEquals("Desc2", providerUtils.getLastStatisticsWaypoint(trackId).getDescription()); + } + /** * Tests the method {@link MyTracksProviderUtilsImpl#getNextMarkerNumber(long, boolean)}. */