From f983c933639c79e5ad3ed6223a8c1aa3a6d39735 Mon Sep 17 00:00:00 2001 From: youtaol Date: Thu, 9 Aug 2012 13:32:59 +0800 Subject: [PATCH] Merge heads --- .../mytracks/content/MyTracksProvider.java | 25 +- .../apps/mytracks/io/file/GpxImporter.java | 2 +- .../apps/mytracks/io/file/TcxTrackWriter.java | 1 + .../SendFusionTablesAsyncTask.java | 4 +- .../mytracks/io/maps/SendMapsAsyncTask.java | 4 +- .../AbsoluteLocationListenerPolicy.java | 31 +- .../AdaptiveLocationListenerPolicy.java | 69 +- .../services/LocationListenerPolicy.java | 23 +- .../mytracks/services/PreferenceManager.java | 139 -- .../services/TrackRecordingService.java | 1813 ++++++++--------- .../services/tasks/PeriodicTaskExecutor.java | 14 +- .../services/tasks/TimerTaskExecutor.java | 8 +- .../apps/mytracks/stats/DoubleBuffer.java | 92 +- .../mytracks/stats/TripStatisticsBuilder.java | 517 +++-- .../content/MyTracksProviderUtils.java | 7 + .../content/MyTracksProviderUtilsImpl.java | 23 + .../apps/mytracks/stats/ExtremityMonitor.java | 79 +- .../apps/mytracks/stats/TripStatistics.java | 653 +++--- MyTracksTest/AndroidManifest.xml | 4 + .../content/MyTracksProviderTest.java | 74 +- .../MyTracksProviderUtilsImplTest.java | 32 +- .../endtoendtest/CreateAndSendTrackTest.java | 16 +- .../endtoendtest/EndToEndTestRunner.java | 44 + .../endtoendtest/EndToEndTestUtils.java | 114 +- .../mytracks/endtoendtest/GoogleUtils.java | 19 +- .../mytracks/endtoendtest/MenuItemsTest.java | 2 +- .../mytracks/endtoendtest/SettingTest.java | 22 +- .../stats/TripStatisticsBuilderTest.java | 119 +- 28 files changed, 1862 insertions(+), 2088 deletions(-) delete mode 100644 MyTracks/src/com/google/android/apps/mytracks/services/PreferenceManager.java create mode 100644 MyTracksTest/src/com/google/android/apps/mytracks/endtoendtest/EndToEndTestRunner.java diff --git a/MyTracks/src/com/google/android/apps/mytracks/content/MyTracksProvider.java b/MyTracks/src/com/google/android/apps/mytracks/content/MyTracksProvider.java index f52dff8fa..29bf4ea4d 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/content/MyTracksProvider.java +++ b/MyTracks/src/com/google/android/apps/mytracks/content/MyTracksProvider.java @@ -57,7 +57,12 @@ public class MyTracksProvider extends ContentProvider { static class DatabaseHelper extends SQLiteOpenHelper { public DatabaseHelper(Context context) { - super(context, DATABASE_NAME, null, DATABASE_VERSION); + this(context, DATABASE_NAME); + } + + @VisibleForTesting + public DatabaseHelper(Context context, String databaseName) { + super(context, databaseName, null, DATABASE_VERSION); } @Override @@ -106,7 +111,8 @@ public class MyTracksProvider extends ContentProvider { * * @author Jimmy Shih */ - private enum UrlType { + @VisibleForTesting + enum UrlType { TRACKPOINTS, TRACKPOINTS_ID, TRACKS, TRACKS_ID, WAYPOINTS, WAYPOINTS_ID } @@ -139,7 +145,8 @@ public class MyTracksProvider extends ContentProvider { * @param context context to creates database * @return true means run successfully */ - public boolean onCreate(Context context) { + @VisibleForTesting + boolean onCreate(Context context) { if (!canAccess()) { return false; } @@ -457,14 +464,4 @@ public class MyTracksProvider extends ContentProvider { } throw new SQLException("Failed to insert a waypoint " + url); } - - /** - * Gets the db. - * - * @return the SQLiteDatabase object - */ - @VisibleForTesting - SQLiteDatabase getDb() { - return db; - } -} +} \ No newline at end of file diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/file/GpxImporter.java b/MyTracks/src/com/google/android/apps/mytracks/io/file/GpxImporter.java index 06f155df2..1eb63e69a 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/file/GpxImporter.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/file/GpxImporter.java @@ -306,7 +306,7 @@ public class GpxImporter extends DefaultHandler { tripStatisticsBuilder = new TripStatisticsBuilder(0); tripStatisticsBuilder.pauseAt(0); } - track.setTripStatistics(tripStatisticsBuilder.getStatistics()); + track.setTripStatistics(tripStatisticsBuilder.getTripStatistics()); track.setNumberOfPoints(numberOfLocations); myTracksProviderUtils.updateTrack(track); tracksIds.add(track.getId()); diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/file/TcxTrackWriter.java b/MyTracks/src/com/google/android/apps/mytracks/io/file/TcxTrackWriter.java index e2c12a9de..eeaf457a6 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/file/TcxTrackWriter.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/file/TcxTrackWriter.java @@ -169,6 +169,7 @@ public class TcxTrackWriter implements TrackFormatWriter { public void writeEndTrack(Location lastPoint) { if (printWriter != null) { printWriter.println(""); + printWriter.println("" + StringUtils.formatCData(track.getDescription()) + ""); printWriter.println(""); printWriter.println("" + StringUtils.formatCData(context.getString(R.string.send_google_by_my_tracks, "", "")) diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/fusiontables/SendFusionTablesAsyncTask.java b/MyTracks/src/com/google/android/apps/mytracks/io/fusiontables/SendFusionTablesAsyncTask.java index 72810f2f0..31370e64a 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/fusiontables/SendFusionTablesAsyncTask.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/fusiontables/SendFusionTablesAsyncTask.java @@ -253,7 +253,7 @@ public class SendFusionTablesAsyncTask extends AbstractSendAsyncTask { elevationBuffer.setNext(metricUnits ? location.getAltitude() : location.getAltitude() * UnitConversions.M_TO_FT); if (i % elevationSamplingFrequency == 0) { - distances.add(tripStatisticsBuilder.getStatistics().getTotalDistance()); + distances.add(tripStatisticsBuilder.getTripStatistics().getTotalDistance()); elevations.add(elevationBuffer.getAverage()); } lastLocation = location; @@ -279,7 +279,7 @@ public class SendFusionTablesAsyncTask extends AbstractSendAsyncTask { // Create an end marker if (lastLocation != null) { - distances.add(tripStatisticsBuilder.getStatistics().getTotalDistance()); + distances.add(tripStatisticsBuilder.getTripStatistics().getTotalDistance()); elevations.add(elevationBuffer.getAverage()); DescriptionGenerator descriptionGenerator = new DescriptionGeneratorImpl(context); track.setDescription( diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/maps/SendMapsAsyncTask.java b/MyTracks/src/com/google/android/apps/mytracks/io/maps/SendMapsAsyncTask.java index 660e0ead4..e46720b8c 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/maps/SendMapsAsyncTask.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/maps/SendMapsAsyncTask.java @@ -297,7 +297,7 @@ public class SendMapsAsyncTask extends AbstractSendAsyncTask { elevationBuffer.setNext(metricUnits ? location.getAltitude() : location.getAltitude() * UnitConversions.M_TO_FT); if (i % elevationSamplingFrequency == 0) { - distances.add(tripStatisticsBuilder.getStatistics().getTotalDistance()); + distances.add(tripStatisticsBuilder.getTripStatistics().getTotalDistance()); elevations.add(elevationBuffer.getAverage()); } lastLocation = location; @@ -323,7 +323,7 @@ public class SendMapsAsyncTask extends AbstractSendAsyncTask { // Create an end marker if (lastLocation != null) { - distances.add(tripStatisticsBuilder.getStatistics().getTotalDistance()); + distances.add(tripStatisticsBuilder.getTripStatistics().getTotalDistance()); elevations.add(elevationBuffer.getAverage()); track.setDescription(getTrackDescription(track, distances, elevations)); diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/AbsoluteLocationListenerPolicy.java b/MyTracks/src/com/google/android/apps/mytracks/services/AbsoluteLocationListenerPolicy.java index 2ae2aa899..f9240baca 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/services/AbsoluteLocationListenerPolicy.java +++ b/MyTracks/src/com/google/android/apps/mytracks/services/AbsoluteLocationListenerPolicy.java @@ -19,41 +19,34 @@ package com.google.android.apps.mytracks.services; /** * This is a simple location listener policy that will always dictate the same * polling interval. - * + * * @author Sandor Dornbush */ public class AbsoluteLocationListenerPolicy implements LocationListenerPolicy { - /** - * The interval to request for gps signal. - */ private final long interval; /** - * @param interval The interval to request for gps signal + * Constructor. + * + * @param interval the interval to request for gps signal */ - public AbsoluteLocationListenerPolicy(final long interval) { + public AbsoluteLocationListenerPolicy(long interval) { this.interval = interval; } - /** - * @return The interval given in the constructor - */ + @Override public long getDesiredPollingInterval() { return interval; } - /** - * Discards the idle time. - */ - public void updateIdleTime(long idleTime) { - } - - /** - * Returns the minimum distance between updates. - * Get all updates to properly measure moving time. - */ + @Override public int getMinDistance() { return 0; } + + @Override + public void updateIdleTime(long idleTime) { + // Ignore + } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/AdaptiveLocationListenerPolicy.java b/MyTracks/src/com/google/android/apps/mytracks/services/AdaptiveLocationListenerPolicy.java index 7d630e995..64ba60ddc 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/services/AdaptiveLocationListenerPolicy.java +++ b/MyTracks/src/com/google/android/apps/mytracks/services/AdaptiveLocationListenerPolicy.java @@ -16,67 +16,58 @@ package com.google.android.apps.mytracks.services; - /** - * A LocationListenerPolicy that will change based on how long the user has been - * stationary. - * - * This policy will dictate a policy based on a min, max and idle time. - * The policy will dictate an interval bounded by min and max whic is half of - * the idle time. - * + * A {@link LocationListenerPolicy} that will change based on how long the user + * has been stationary. This policy will dictate a policy based on a min, max + * and idle time. The policy will dictate an interval bounded by min and max, + * and is half of the idle time. + * * @author Sandor Dornbush */ public class AdaptiveLocationListenerPolicy implements LocationListenerPolicy { - /** - * Smallest interval this policy will dictate, in milliseconds. - */ private final long minInterval; - - /** - * Largest interval this policy will dictate, in milliseconds. - */ private final long maxInterval; - private final int minDistance; - - /** - * The time the user has been at the current location, in milliseconds. - */ + + // The time the user has been idle at the current location, in milliseconds. private long idleTime; /** - * Creates a policy that will be bounded by the given min and max. - * - * @param min Smallest interval this policy will dictate, in milliseconds - * @param max Largest interval this policy will dictate, in milliseconds + * Creates a policy that will be bounded by the given minInterval and + * maxInterval. + * + * @param minInterval the smallest interval this policy will dictate, in + * milliseconds + * @param maxInterval the largest interval this policy will dictate, in + * milliseconds + * @param minDistance the minimum distance in meters */ - public AdaptiveLocationListenerPolicy(long min, long max, int minDistance) { - this.minInterval = min; - this.maxInterval = max; + public AdaptiveLocationListenerPolicy(long minInterval, long maxInterval, int minDistance) { + this.minInterval = minInterval; + this.maxInterval = maxInterval; this.minDistance = minDistance; } - /** - * @return An interval bounded by min and max which is half of the idle time + /* + * Returns an interval half of the idle time, but bounded by minInteval and + * maxInterval. */ + @Override public long getDesiredPollingInterval() { long desiredInterval = idleTime / 2; - // Round to avoid setting the interval too often. + // Round to second to avoid setting the interval too often desiredInterval = (desiredInterval / 1000) * 1000; - return Math.max(Math.min(maxInterval, desiredInterval), - minInterval); + return Math.max(Math.min(maxInterval, desiredInterval), minInterval); } - public void updateIdleTime(long newIdleTime) { - this.idleTime = newIdleTime; - } - - /** - * Returns the minimum distance between updates. - */ + @Override public int getMinDistance() { return minDistance; } + + @Override + public void updateIdleTime(long newIdleTime) { + idleTime = newIdleTime; + } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/LocationListenerPolicy.java b/MyTracks/src/com/google/android/apps/mytracks/services/LocationListenerPolicy.java index 1fab6102f..18250f3de 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/services/LocationListenerPolicy.java +++ b/MyTracks/src/com/google/android/apps/mytracks/services/LocationListenerPolicy.java @@ -17,19 +17,18 @@ package com.google.android.apps.mytracks.services; /** - * This is an interface for classes that will manage the location listener policy. - * Different policy options are: - * Absolute - * Addaptive - * + * This is an interface for classes that will manage the location listener + * policy. Different policy options are: {@link AbsoluteLocationListenerPolicy} + * and {@link AdaptiveLocationListenerPolicy}. + * * @author Sandor Dornbush */ public interface LocationListenerPolicy { /** - * Returns the polling time this policy would like at this time. - * - * @return The polling that this policy dictates + * Returns the polling interval this policy would like at this moment. + * + * @return the polling interval */ public long getDesiredPollingInterval(); @@ -39,10 +38,10 @@ public interface LocationListenerPolicy { public int getMinDistance(); /** - * Notifies the amount of time the user has been idle at their current - * location. - * - * @param idleTime The time that the user has been idle at this spot + * Notifies the amount of time the user has been idle at his current location. + * + * @param idleTime the time that the user has been idle at his current + * location */ public void updateIdleTime(long idleTime); } 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 e50bac9f8..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.getStatistics().setMovingTime(stats.getMovingTime()); - statsBuilder.pauseAt(stats.getStopTime()); - statsBuilder.resumeAt(System.currentTimeMillis()); - } else { - Log.e(TAG, "Could not get track points cursor."); - } - } catch (RuntimeException e) { - Log.e(TAG, "Error while restoring track.", e); - } finally { - if (cursor != null) { - cursor.close(); - } - } - - announcementExecutor.restore(); - splitExecutor.restore(); - } - - private void onLocationChangedAsync(Location location) { - Log.d(TAG, "TrackRecordingService.onLocationChanged"); - - try { - // Don't record if the service has been asked to pause recording: - if (!isRecording) { - Log.w(TAG, - "Not recording because recording has been paused."); - return; - } - - // This should never happen, but just in case (we really don't want the - // service to crash): - if (location == null) { - Log.w(TAG, - "Location changed, but location is null."); - return; - } - - // Don't record if the accuracy is too bad: - if (location.getAccuracy() > minRequiredAccuracy) { - Log.d(TAG, - "Not recording. Bad accuracy."); - return; - } - - // At least one track must be available for appending points: - recordingTrack = getRecordingTrack(); - if (recordingTrack == null) { - Log.d(TAG, - "Not recording. No track to append to available."); - return; - } - - // Update the idle time if needed. - locationListenerPolicy.updateIdleTime(statsBuilder.getIdleTime()); - addLocationToStats(location); - if (currentRecordingInterval != - locationListenerPolicy.getDesiredPollingInterval()) { - registerLocationListener(); - } - - Location lastRecordedLocation = providerUtils.getLastLocation(); - double distanceToLastRecorded = Double.POSITIVE_INFINITY; - if (lastRecordedLocation != null) { - distanceToLastRecorded = location.distanceTo(lastRecordedLocation); - } - double distanceToLast = Double.POSITIVE_INFINITY; - if (lastLocation != null) { - distanceToLast = location.distanceTo(lastLocation); - } - boolean hasSensorData = sensorManager != null - && sensorManager.isEnabled() - && sensorManager.getSensorDataSet() != null - && sensorManager.isSensorDataSetValid(); - - // If the user has been stationary for two recording just record the first - // two and ignore the rest. This code will only have an effect if the - // maxRecordingDistance = 0 - if (distanceToLast == 0 && !hasSensorData) { - if (isMoving) { - Log.d(TAG, "Found two identical locations."); - isMoving = false; - if (lastLocation != null && lastRecordedLocation != null - && !lastRecordedLocation.equals(lastLocation)) { - // Need to write the last location. This will happen when - // lastRecordedLocation.distance(lastLocation) < - // minRecordingDistance - if (!insertLocation(lastLocation, lastRecordedLocation, recordingTrackId)) { - return; - } - } - } else { - Log.d(TAG, - "Not recording. More than two identical locations."); - } - } else if (distanceToLastRecorded > minRecordingDistance - || hasSensorData) { - if (lastLocation != null && !isMoving) { - // Last location was the last stationary location. Need to go back and - // add it. - if (!insertLocation(lastLocation, lastRecordedLocation, recordingTrackId)) { - return; - } - isMoving = true; - } - - // If separation from last recorded point is too large insert a - // separator to indicate end of a segment: - boolean startNewSegment = - lastRecordedLocation != null - && lastRecordedLocation.getLatitude() < 90 - && distanceToLastRecorded > maxRecordingDistance - && recordingTrack.getStartId() >= 0; - if (startNewSegment) { - // Insert a separator point to indicate start of new track: - Log.d(TAG, "Inserting a separator."); - Location separator = new Location(LocationManager.GPS_PROVIDER); - separator.setLongitude(0); - separator.setLatitude(100); - separator.setTime(lastRecordedLocation.getTime()); - providerUtils.insertTrackPoint(separator, recordingTrackId); - } - - if (!insertLocation(location, lastRecordedLocation, recordingTrackId)) { - return; - } - } else { - Log.d(TAG, String.format(Locale.US, - "Not recording. Distance to last recorded point (%f m) is less than %d m.", - distanceToLastRecorded, minRecordingDistance)); - // Return here so that the location is NOT recorded as the last location. - return; - } - } catch (Error e) { - // Probably important enough to rethrow. - Log.e(TAG, "Error in onLocationChanged", e); - throw e; - } catch (RuntimeException e) { - // Safe usually to trap exceptions. - Log.e(TAG, - "Trapping exception in onLocationChanged", e); - throw e; - } - lastLocation = location; - } - - /** - * Inserts a new location in the track points db and updates the corresponding - * track in the track db. - * - * @param location the location to be inserted - * @param lastRecordedLocation the last recorded location before this one (or - * null if none) - * @param trackId the id of the track - * @return true if successful. False if SQLite3 threw an exception. - */ - private boolean insertLocation(Location location, Location lastRecordedLocation, long trackId) { - - // Keep track of length along recorded track (needed when a waypoint is - // inserted): - if (LocationUtils.isValidLocation(location)) { - if (lastValidLocation != null) { - length += location.distanceTo(lastValidLocation); - } - lastValidLocation = location; - } - - // Insert the new location: - try { - Location locationToInsert = location; - if (sensorManager != null && sensorManager.isEnabled()) { - SensorDataSet sd = sensorManager.getSensorDataSet(); - if (sd != null && sensorManager.isSensorDataSetValid()) { - locationToInsert = new MyTracksLocation(location, sd); - } - } - Uri pointUri = providerUtils.insertTrackPoint(locationToInsert, trackId); - int pointId = Integer.parseInt(pointUri.getLastPathSegment()); - - // Update the current track: - if (lastRecordedLocation != null && lastRecordedLocation.getLatitude() < 90) { - TripStatistics tripStatistics = statsBuilder.getStatistics(); - tripStatistics.setStopTime(System.currentTimeMillis()); - - if (recordingTrack.getStartId() < 0) { - recordingTrack.setStartId(pointId); - } - recordingTrack.setStopId(pointId); - recordingTrack.setNumberOfPoints(recordingTrack.getNumberOfPoints() + 1); - recordingTrack.setTripStatistics(tripStatistics); - providerUtils.updateTrack(recordingTrack); - updateCurrentWaypoint(); - } - } catch (SQLiteException e) { - // Insert failed, most likely because of SqlLite error code 5 - // (SQLite_BUSY). This is expected to happen extremely rarely (if our - // listener gets invoked twice at about the same time). - Log.w(TAG, - "Caught SQLiteException: " + e.getMessage(), e); - return false; - } - announcementExecutor.update(); - splitExecutor.update(); - return true; - } - - private void updateCurrentWaypoint() { - if (currentWaypointId >= 0) { - Waypoint waypoint = providerUtils.getWaypoint(currentWaypointId); - if (waypoint != null) { - waypoint.setLength(length); - waypoint.setDuration( - System.currentTimeMillis() - statsBuilder.getStatistics().getStartTime()); - waypoint.setTripStatistics(waypointStatsBuilder.getStatistics()); - providerUtils.updateWaypoint(waypoint); - } - } - } - - private void addLocationToStats(Location location) { - if (LocationUtils.isValidLocation(location)) { - long now = System.currentTimeMillis(); - statsBuilder.addLocation(location, now); - waypointStatsBuilder.addLocation(location, now); - } - } - - /* - * Application lifetime events: ============================ - */ - - public long insertWaypoint(WaypointCreationRequest request) { + 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.getStatistics() == 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.getStatistics().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.getStatistics().getStartTime()); - - // Set the rest of the waypoint data - waypoint.setType(Waypoint.TYPE_STATISTICS); - String name; - if (request.getName() != null) { - name = request.getName(); - } else { - int nextMarkerNumber = providerUtils.getNextMarkerNumber(recordingTrackId, true); - if (nextMarkerNumber == -1) { - nextMarkerNumber = 0; - } - name = getString(R.string.marker_split_name_format, nextMarkerNumber); + private boolean shouldResumeTrack(Track track) { + if (track == null) { + Log.d(TAG, "Not resuming. Track is null."); + return false; } - waypoint.setName(name); - waypoint.setTripStatistics(waypointStatsBuilder.getStatistics()); - 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.getStatistics(); - } - - 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/MyTracks/src/com/google/android/apps/mytracks/stats/DoubleBuffer.java b/MyTracks/src/com/google/android/apps/mytracks/stats/DoubleBuffer.java index 356664617..7c86f0ba8 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/stats/DoubleBuffer.java +++ b/MyTracks/src/com/google/android/apps/mytracks/stats/DoubleBuffer.java @@ -20,66 +20,35 @@ package com.google.android.apps.mytracks.stats; * This class maintains a buffer of doubles. This buffer is a convenient class * for storing a series of doubles and calculating information about them. This * is a FIFO buffer. - * + * * @author Sandor Dornbush */ public class DoubleBuffer { - /** - * The location that the next write will occur at. - */ + // The location that the next write will occur at. private int index; - /** - * The sliding buffer of doubles. - */ + // The sliding buffer of doubles. private final double[] buffer; - /** - * Have all of the slots in the buffer been filled? - */ + // True if the buffer is full private boolean isFull; /** - * Creates a buffer with size elements. - * - * @param size the number of elements in the buffer - * @throws IllegalArgumentException if the size is not a positive value + * Creates a buffer with a certain size. + * + * @param size the size */ public DoubleBuffer(int size) { if (size < 1) { - throw new IllegalArgumentException("The buffer size must be positive."); + throw new IllegalArgumentException("The buffer size must be greater than 1."); } buffer = new double[size]; reset(); } /** - * Adds a double to the buffer. If the buffer is full the oldest element is - * overwritten. - * - * @param d the double to add - */ - public void setNext(double d) { - if (index == buffer.length) { - index = 0; - } - buffer[index] = d; - index++; - if (index == buffer.length) { - isFull = true; - } - } - - /** - * Are all of the entries in the buffer used? - */ - public boolean isFull() { - return isFull; - } - - /** - * Resets the buffer to the initial state. + * Resets the buffer. */ public void reset() { index = 0; @@ -87,16 +56,20 @@ public class DoubleBuffer { } /** - * Gets the average of values from the buffer. - * - * @return The average of the buffer + * Returns true if the buffer is full. + */ + public boolean isFull() { + return isFull; + } + + /** + * Gets the average of the buffer. */ public double getAverage() { int numberOfEntries = isFull ? buffer.length : index; if (numberOfEntries == 0) { return 0; } - double sum = 0; for (int i = 0; i < numberOfEntries; i++) { sum += buffer[i]; @@ -105,15 +78,15 @@ public class DoubleBuffer { } /** - * Gets the average and standard deviation of the buffer. - * - * @return An array of two elements - the first is the average, and the second - * is the variance + * Gets the average and the variance of the buffer. + * + * @return an array of two elements - the first is the average, the second is + * the variance */ public double[] getAverageAndVariance() { int numberOfEntries = isFull ? buffer.length : index; if (numberOfEntries == 0) { - return new double[]{0, 0}; + return new double[] { 0, 0 }; } double sum = 0; @@ -124,8 +97,25 @@ public class DoubleBuffer { } double average = sum / numberOfEntries; - return new double[]{average, - sumSquares / numberOfEntries - Math.pow(average, 2)}; + double variance = sumSquares / numberOfEntries - Math.pow(average, 2); + return new double[] { average, variance }; + } + + /** + * Adds a double to the buffer. If the buffer is full the oldest element is + * overwritten. + * + * @param value the double to add + */ + public void setNext(double value) { + if (index == buffer.length) { + index = 0; + } + buffer[index] = value; + index++; + if (index == buffer.length) { + isFull = true; + } } @Override diff --git a/MyTracks/src/com/google/android/apps/mytracks/stats/TripStatisticsBuilder.java b/MyTracks/src/com/google/android/apps/mytracks/stats/TripStatisticsBuilder.java index 3f6f7e770..18f7103b9 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/stats/TripStatisticsBuilder.java +++ b/MyTracks/src/com/google/android/apps/mytracks/stats/TripStatisticsBuilder.java @@ -20,352 +20,307 @@ import static com.google.android.apps.mytracks.Constants.TAG; import com.google.android.apps.mytracks.Constants; import com.google.android.apps.mytracks.util.PreferencesUtils; +import com.google.common.annotations.VisibleForTesting; import android.location.Location; import android.util.Log; /** - * Statistics keeper for a trip. + * Builder for {@link TripStatistics}. For keeping statistics as a track is + * paused/resumed and new locations are added. * * @author Sandor Dornbush * @author Rodrigo Damazio */ public class TripStatisticsBuilder { - /** - * Statistical data about the trip, which can be displayed to the user. - */ - private final TripStatistics data; - /** - * The last location that the gps reported. - */ - private Location lastLocation; - - /** - * The last location that contributed to the stats. It is also the last - * location the user was found to be moving. - */ - private Location lastMovingLocation; - - /** - * The current speed in meters/second as reported by the gps. - */ - private double currentSpeed; - - /** - * The current grade. This value is very noisy and not reported to the user. - */ - private double currentGrade; - - /** - * Is the trip currently paused? - * All trips start paused. - */ - private boolean paused = true; - - /** - * A buffer of the last speed readings in meters/second. - */ - private final DoubleBuffer speedBuffer = - new DoubleBuffer(Constants.SPEED_SMOOTHING_FACTOR); - - /** - * A buffer of the recent elevation readings in meters. - */ - private final DoubleBuffer elevationBuffer = - new DoubleBuffer(Constants.ELEVATION_SMOOTHING_FACTOR); - - /** - * A buffer of the distance between recent gps readings in meters. - */ - private final DoubleBuffer distanceBuffer = - new DoubleBuffer(Constants.DISTANCE_SMOOTHING_FACTOR); - - /** - * A buffer of the recent grade calculations. - */ - private final DoubleBuffer gradeBuffer = - new DoubleBuffer(Constants.GRADE_SMOOTHING_FACTOR); - - /** - * The total number of locations in this trip. - */ - private long totalLocations = 0; + // The trip statistics. + private final TripStatistics tripStatistics; + // The minimum recording distance. private int minRecordingDistance = PreferencesUtils.MIN_RECORDING_DISTANCE_DEFAULT; + // True if the trip is paused. All trips start as paused. + private boolean paused = true; + + // The last location as reported by GPS. + private Location lastLocation; + + // The last moving location that contributed to the moving statistics. + private Location lastMovingLocation; + + // A buffer of the recent speed readings (m/s) for calculating max speed. + private final DoubleBuffer speedBuffer = new DoubleBuffer(Constants.SPEED_SMOOTHING_FACTOR); + + // A buffer of the recent elevation readings (m). + private final DoubleBuffer elevationBuffer = new DoubleBuffer( + Constants.ELEVATION_SMOOTHING_FACTOR); + + // A buffer of the recent distance readings for calculating grade. + private final DoubleBuffer distanceBuffer = new DoubleBuffer(Constants.DISTANCE_SMOOTHING_FACTOR); + + // A buffer of the recent grade calculations + private final DoubleBuffer gradeBuffer = new DoubleBuffer(Constants.GRADE_SMOOTHING_FACTOR); + /** - * Creates a new trip starting at the given time. + * Creates a new trip starting at a start time. * - * @param startTime the start time. + * @param startTime the start time */ public TripStatisticsBuilder(long startTime) { - data = new TripStatistics(); + tripStatistics = new TripStatistics(); resumeAt(startTime); } /** - * Creates a new trip, starting with existing statistics data. - * - * @param statsData the statistics data to copy and start from - */ - public TripStatisticsBuilder(TripStatistics statsData) { - data = new TripStatistics(statsData); - if (data.getStartTime() > 0) { - resumeAt(data.getStartTime()); - } - } - - /** - * Adds a location to the current trip. This will update all of the internal - * variables with this new location. + * Creates a new trip, starting with an existing {@link TripStatistics}. * - * @param currentLocation the current gps location - * @param systemTime the time used for calculation of totalTime. This should - * be the phone's time (not GPS time) - * @return true if the person is moving + * @param other the existing {@link TripStatistics} */ - public boolean addLocation(Location currentLocation, long systemTime) { - if (paused) { - Log.w(TAG, - "Tried to account for location while track is paused"); - return false; + public TripStatisticsBuilder(TripStatistics other) { + tripStatistics = new TripStatistics(other); + if (tripStatistics.getStartTime() > 0) { + resumeAt(tripStatistics.getStartTime()); } - - totalLocations++; - - double elevationDifference = updateElevation(currentLocation.getAltitude()); - - // Update the "instant" values: - data.setTotalTime(systemTime - data.getStartTime()); - currentSpeed = currentLocation.getSpeed(); - - // This was the 1st location added, remember it and do nothing else: - if (lastLocation == null) { - lastLocation = currentLocation; - lastMovingLocation = currentLocation; - return false; - } - - updateBounds(currentLocation); - - // Don't do anything if we didn't move since last fix: - double distance = lastLocation.distanceTo(currentLocation); - if (distance < minRecordingDistance && - currentSpeed < Constants.MAX_NO_MOVEMENT_SPEED) { - lastLocation = currentLocation; - return false; - } - - data.addTotalDistance(lastMovingLocation.distanceTo(currentLocation)); - updateSpeed(currentLocation.getTime(), currentSpeed, - lastLocation.getTime(), lastLocation.getSpeed()); - - updateGrade(distance, elevationDifference); - lastLocation = currentLocation; - lastMovingLocation = currentLocation; - return true; } /** - * Updates the track's bounding box to include the given location. - */ - private void updateBounds(Location location) { - data.updateLatitudeExtremities(location.getLatitude()); - data.updateLongitudeExtremities(location.getLongitude()); - } - - /** - * Updates the elevation measurements. + * Sets the min recording distance. * - * @param elevation the current elevation + * @param minRecordingDistance the min recording distance */ - // @VisibleForTesting - double updateElevation(double elevation) { - double oldSmoothedElevation = getSmoothedElevation(); - elevationBuffer.setNext(elevation); - double smoothedElevation = getSmoothedElevation(); - data.updateElevationExtremities(smoothedElevation); - double elevationDifference = elevationBuffer.isFull() - ? smoothedElevation - oldSmoothedElevation - : 0.0; - if (elevationDifference > 0) { - data.addTotalElevationGain(elevationDifference); - } - return elevationDifference; + public void setMinRecordingDistance(int minRecordingDistance) { + this.minRecordingDistance = minRecordingDistance; } /** - * Updates the speed measurements. + * Resumes the current track at a given time. * - * @param updateTime the time of the speed update - * @param speed the current speed - * @param lastLocationTime the time of the last speed update - * @param lastLocationSpeed the speed of the last update + * @param time the time */ - // @VisibleForTesting - void updateSpeed(long updateTime, double speed, long lastLocationTime, - double lastLocationSpeed) { - // We are now sure the user is moving. - long timeDifference = updateTime - lastLocationTime; - if (timeDifference < 0) { - Log.e(TAG, - "Found negative time change: " + timeDifference); - } - data.addMovingTime(timeDifference); - - if (isValidSpeed(updateTime, speed, lastLocationTime, lastLocationSpeed, - speedBuffer)) { - speedBuffer.setNext(speed); - if (speed > data.getMaxSpeed()) { - data.setMaxSpeed(speed); - } - double movingSpeed = data.getAverageMovingSpeed(); - if (speedBuffer.isFull() && (movingSpeed > data.getMaxSpeed())) { - data.setMaxSpeed(movingSpeed); - } - } else { - Log.d(TAG, - "TripStatistics ignoring big change: Raw Speed: " + speed - + " old: " + lastLocationSpeed + " [" + toString() + "]"); - } - } - - /** - * Checks to see if this is a valid speed. - * - * @param updateTime The time at the current reading - * @param speed The current speed - * @param lastLocationTime The time at the last location - * @param lastLocationSpeed Speed at the last location - * @param speedBuffer A buffer of recent readings - * @return True if this is likely a valid speed - */ - public static boolean isValidSpeed(long updateTime, double speed, - long lastLocationTime, double lastLocationSpeed, - DoubleBuffer speedBuffer) { - - // We don't want to count 0 towards the speed. - if (speed == 0) { - return false; - } - // We are now sure the user is moving. - long timeDifference = updateTime - lastLocationTime; - - // There are a lot of noisy speed readings. - // Do the cheapest checks first, most expensive last. - // The following code will ignore unlikely to be real readings. - // - 128 m/s seems to be an internal android error code. - if (Math.abs(speed - 128) < 1) { - return false; - } - - // Another check for a spurious reading. See if the path seems physically - // likely. Ignore any speeds that imply accelaration greater than 2g's - // Really who can accelerate faster? - double speedDifference = Math.abs(lastLocationSpeed - speed); - if (speedDifference > Constants.MAX_ACCELERATION * timeDifference) { - return false; - } - - // There are three additional checks if the reading gets this far: - // - Only use the speed if the buffer is full - // - Check that the current speed is less than 10x the recent smoothed speed - // - Double check that the current speed does not imply crazy acceleration - double smoothedSpeed = speedBuffer.getAverage(); - double smoothedDiff = Math.abs(smoothedSpeed - speed); - return !speedBuffer.isFull() || - (speed < smoothedSpeed * 10 - && smoothedDiff < Constants.MAX_ACCELERATION * timeDifference); - } - - /** - * Updates the grade measurements. - * - * @param distance the distance the user just traveled - * @param elevationDifference the elevation difference between the current - * reading and the previous reading - */ - // @VisibleForTesting - void updateGrade(double distance, double elevationDifference) { - distanceBuffer.setNext(distance); - double smoothedDistance = distanceBuffer.getAverage(); - - // With the error in the altitude measurement it is dangerous to divide - // by anything less than 5. - if (!elevationBuffer.isFull() || !distanceBuffer.isFull() - || smoothedDistance < 5.0) { + public void resumeAt(long time) { + if (!paused) { return; } - currentGrade = elevationDifference / smoothedDistance; - gradeBuffer.setNext(currentGrade); - data.updateGradeExtremities(gradeBuffer.getAverage()); + + tripStatistics.setStartTime(time); + tripStatistics.setStopTime(-1L); + paused = false; + lastLocation = null; + lastMovingLocation = null; + speedBuffer.reset(); + elevationBuffer.reset(); + distanceBuffer.reset(); + gradeBuffer.reset(); } /** - * Pauses the track at the given time. + * Pauses the track at a given time. * * @param time the time to pause at */ public void pauseAt(long time) { - if (paused) { return; } - - data.setStopTime(time); - data.setTotalTime(time - data.getStartTime()); - lastLocation = null; // Make sure the counter restarts. + if (paused) { + return; + } + tripStatistics.setStopTime(time); + // TODO: total time needs to take into account pauses + tripStatistics.setTotalTime(time - tripStatistics.getStartTime()); paused = true; } /** - * Resumes the current track at the given time. - * - * @param time the time to resume at + * Gets the trip statistics. */ - public void resumeAt(long time) { - if (!paused) { return; } - - // TODO: The times are bogus if the track is paused then resumed again - data.setStartTime(time); - data.setStopTime(-1); - paused = false; - } - - @Override - public String toString() { - return "TripStatistics { Data: " + data.toString() - + "; Total Locations: " + totalLocations - + "; Paused: " + paused - + "; Current speed: " + currentSpeed - + "; Current grade: " + currentGrade - + "}"; + public TripStatistics getTripStatistics() { + // Take a snapshot - we don't want anyone messing with our internals + return new TripStatistics(tripStatistics); } /** - * Returns the amount of time the user has been idle or 0 if they are moving. + * Returns the amount of time the user has been idle or 0 if he is moving. */ public long getIdleTime() { - if (lastLocation == null || lastMovingLocation == null) + if (lastLocation == null || lastMovingLocation == null) { return 0; + } return lastLocation.getTime() - lastMovingLocation.getTime(); } /** - * Gets the current elevation smoothed over several readings. The elevation - * data is very noisy so it is better to use the smoothed elevation than the - * raw elevation for many tasks. - * - * @return The elevation smoothed over several readings + * Gets the smoothed elevation over several readings. The elevation readings + * is noisy so the smoothed elevation is better than the raw elevation for + * many tasks. */ public double getSmoothedElevation() { return elevationBuffer.getAverage(); } - public TripStatistics getStatistics() { - // Take a snapshot - we don't want anyone messing with our internals - return new TripStatistics(data); + /** + * Adds a location. This will update all of the internal variables with this + * new location. + * + * @param location the location + * @param systemTime the system time for calculating totalTime. This should be + * the phone's system time (not GPS time) + * @return true if the person is moving + */ + public boolean addLocation(Location location, long systemTime) { + if (paused) { + Log.w(TAG, "Track is paused. Ignore addLocation."); + return false; + } + + tripStatistics.setTotalTime(systemTime - tripStatistics.getStartTime()); + + double elevationDifference = updateElevation(location.getAltitude()); + tripStatistics.updateLatitudeExtremities(location.getLatitude()); + tripStatistics.updateLongitudeExtremities(location.getLongitude()); + + // If this is the first location, remember it and return. + if (lastLocation == null || lastMovingLocation == null) { + lastLocation = location; + lastMovingLocation = location; + return false; + } + + // Don't do anything more if we didn't move since the last location. + double distance = lastLocation.distanceTo(location); + if (distance < minRecordingDistance && location.getSpeed() < Constants.MAX_NO_MOVEMENT_SPEED) { + lastLocation = location; + return false; + } + + long timeDifference = location.getTime() - lastLocation.getTime(); + if (timeDifference < 0) { + Log.e(TAG, "Negative time difference: " + timeDifference); + lastLocation = location; + return false; + } + + tripStatistics.addTotalDistance(lastMovingLocation.distanceTo(location)); + tripStatistics.addMovingTime(timeDifference); + updateSpeed( + location.getTime(), location.getSpeed(), lastLocation.getTime(), lastLocation.getSpeed()); + updateGrade(distance, elevationDifference); + lastLocation = location; + lastMovingLocation = location; + return true; } - public void setMinRecordingDistance(int minRecordingDistance) { - this.minRecordingDistance = minRecordingDistance; + /** + * Updates a speed reading. Assumes the user is moving. + * + * @param time the time + * @param speed the speed + * @param lastLocationTime the last location time + * @param lastLocationSpeed the last location speed + */ + @VisibleForTesting + void updateSpeed(long time, double speed, long lastLocationTime, double lastLocationSpeed) { + if (!isValidSpeed(time, speed, lastLocationTime, lastLocationSpeed, speedBuffer)) { + Log.d(TAG, "Invalid speed. speed: " + speed + " lastLocationSpeed: " + lastLocationSpeed); + return; + } + speedBuffer.setNext(speed); + if (speed > tripStatistics.getMaxSpeed()) { + tripStatistics.setMaxSpeed(speed); + } + double movingSpeed = tripStatistics.getAverageMovingSpeed(); + if (speedBuffer.isFull() && movingSpeed > tripStatistics.getMaxSpeed()) { + tripStatistics.setMaxSpeed(movingSpeed); + } + } + + /** + * Updates an elevation reading. + * + * @param elevation the elevation + */ + @VisibleForTesting + double updateElevation(double elevation) { + double oldAverage = elevationBuffer.getAverage(); + elevationBuffer.setNext(elevation); + double newAverage = elevationBuffer.getAverage(); + tripStatistics.updateElevationExtremities(newAverage); + double elevationDifference = elevationBuffer.isFull() ? newAverage - oldAverage : 0.0; + if (elevationDifference > 0) { + tripStatistics.addTotalElevationGain(elevationDifference); + } + return elevationDifference; + } + + /** + * Updates a grade reading. + * + * @param distance the distance the user just traveled + * @param elevationDifference the elevation difference between the current + * reading and the previous reading + */ + @VisibleForTesting + void updateGrade(double distance, double elevationDifference) { + distanceBuffer.setNext(distance); + double smoothedDistance = distanceBuffer.getAverage(); + + /* + * With the error in the altitude measurement it is dangerous to divide by + * anything less than 5. + */ + if (!elevationBuffer.isFull() || !distanceBuffer.isFull() || smoothedDistance < 5.0) { + return; + } + gradeBuffer.setNext(elevationDifference / smoothedDistance); + tripStatistics.updateGradeExtremities(gradeBuffer.getAverage()); + } + + /** + * Returns true if the speed is valid. + * + * @param time the time + * @param speed the speed + * @param lastLocationTime the last location time + * @param lastLocationSpeed the last location speed + * @param speedBuffer a buffer of speed readings + */ + public static boolean isValidSpeed(long time, double speed, long lastLocationTime, + double lastLocationSpeed, DoubleBuffer speedBuffer) { + + /* + * There are a lot of noisy speed readings. Do the cheapest checks first, + * most expensive last. + */ + if (speed == 0) { + return false; + } + + /* + * The following code will ignore unlikely readings. 128 m/s seems to be an + * internal android error code. + */ + if (Math.abs(speed - 128) < 1) { + return false; + } + + /* + * See if the speed seems physically likely. Ignore any speeds that imply + * acceleration greater than 2g. + */ + long timeDifference = time - lastLocationTime; + double speedDifference = Math.abs(lastLocationSpeed - speed); + if (speedDifference > Constants.MAX_ACCELERATION * timeDifference) { + return false; + } + + /* + * Only check if the speed buffer is full. Check that the speed is less than + * 10X the smoothed average and the speed difference doesn't imply 2g + * acceleration. + */ + if (!speedBuffer.isFull()) { + return true; + } + double average = speedBuffer.getAverage(); + double diff = Math.abs(average - speed); + return (speed < average * 10) && (diff < Constants.MAX_ACCELERATION * timeDifference); } } 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/MyTracksLib/src/com/google/android/apps/mytracks/stats/ExtremityMonitor.java b/MyTracksLib/src/com/google/android/apps/mytracks/stats/ExtremityMonitor.java index bb65ee76d..8a4618588 100644 --- a/MyTracksLib/src/com/google/android/apps/mytracks/stats/ExtremityMonitor.java +++ b/MyTracksLib/src/com/google/android/apps/mytracks/stats/ExtremityMonitor.java @@ -18,19 +18,15 @@ package com.google.android.apps.mytracks.stats; /** * A helper class that tracks a minimum and a maximum of a variable. - * + * * @author Sandor Dornbush */ public class ExtremityMonitor { - /** - * The smallest value seen so far. - */ + // The smallest value seen so far. private double min; - /** - * The largest value seen so far. - */ + // The largest value seen so far. private double max; public ExtremityMonitor() { @@ -38,9 +34,31 @@ public class ExtremityMonitor { } /** - * Updates the min and the max with the new value. - * - * @param value the new value for the monitor + * Resets this object to it's initial state where the min and max are unknown. + */ + public void reset() { + min = Double.POSITIVE_INFINITY; + max = Double.NEGATIVE_INFINITY; + } + + /** + * Gets the minimum value seen. + */ + public double getMin() { + return min; + } + + /** + * Gets the maximum value seen. + */ + public double getMax() { + return max; + } + + /** + * Updates the min and the max with a new value. + * + * @param value the new value * @return true if an extremity was found */ public boolean update(double value) { @@ -56,34 +74,11 @@ public class ExtremityMonitor { return changed; } - /** - * Gets the minimum value seen. - * - * @return The minimum value passed into the update() function - */ - public double getMin() { - return min; - } - - /** - * Gets the maximum value seen. - * - * @return The maximum value passed into the update() function - */ - public double getMax() { - return max; - } - - /** - * Resets this object to it's initial state where the min and max are unknown. - */ - public void reset() { - min = Double.POSITIVE_INFINITY; - max = Double.NEGATIVE_INFINITY; - } - /** * Sets the minimum and maximum values. + * + * @param min the minimum value + * @param max the maximum value */ public void set(double min, double max) { this.min = min; @@ -92,6 +87,8 @@ public class ExtremityMonitor { /** * Sets the minimum value. + * + * @param min the minimum value */ public void setMin(double min) { this.min = min; @@ -99,16 +96,20 @@ public class ExtremityMonitor { /** * Sets the maximum value. + * + * @param max the maximum value */ public void setMax(double max) { this.max = max; } + /** + * Returns true if has data. + */ public boolean hasData() { - return min != Double.POSITIVE_INFINITY - && max != Double.NEGATIVE_INFINITY; + return min != Double.POSITIVE_INFINITY && max != Double.NEGATIVE_INFINITY; } - + @Override public String toString() { return "Min: " + min + " Max: " + max; diff --git a/MyTracksLib/src/com/google/android/apps/mytracks/stats/TripStatistics.java b/MyTracksLib/src/com/google/android/apps/mytracks/stats/TripStatistics.java index 4d82d2814..892c0d590 100644 --- a/MyTracksLib/src/com/google/android/apps/mytracks/stats/TripStatistics.java +++ b/MyTracksLib/src/com/google/android/apps/mytracks/stats/TripStatistics.java @@ -13,182 +13,226 @@ * License for the specific language governing permissions and limitations under * the License. */ + package com.google.android.apps.mytracks.stats; import android.os.Parcel; import android.os.Parcelable; /** - * Statistical data about a trip. - * The data in this class should be filled out by TripStatisticsBuilder. - * + * Statistical data about a trip. The data in this class should be filled out by + * TripStatisticsBuilder. + *

* TODO: hashCode and equals - * + * * @author Rodrigo Damazio */ public class TripStatistics implements Parcelable { - /** - * The start time for the trip. This is system time which might not match gps - * time. - */ + // The trip start time. This is the system time, might not match the GPs time. private long startTime = -1L; - /** - * The stop time for the trip. This is the system time which might not match - * gps time. - */ + // The trip stop time. This is the system time, might not match the GPS time. private long stopTime = -1L; - /** - * The total time that we believe the user was traveling in milliseconds. - */ - private long movingTime; - - /** - * The total time of the trip in milliseconds. - * This is only updated when new points are received, so it may be stale. - */ - private long totalTime; - - /** - * The total distance in meters that the user traveled on this trip. - */ + // The total trip distance (meters). private double totalDistance; - /** - * The total elevation gained on this trip in meters. - */ - private double totalElevationGain; + // The total time (ms). Updated when new points are received, may be stale. + private long totalTime; - /** - * The maximum speed in meters/second reported that we believe to be a valid - * speed. - */ - private double maxSpeed; + // The total moving time (ms). Based on when we believe the user is traveling. + private long movingTime; - /** - * The min and max latitude values seen in this trip. - */ + // The min and max latitude seen in this trip. private final ExtremityMonitor latitudeExtremities = new ExtremityMonitor(); - /** - * The min and max longitude values seen in this trip. - */ + // The min and max longitude seen in this trip. private final ExtremityMonitor longitudeExtremities = new ExtremityMonitor(); - /** - * The min and max elevation seen on this trip in meters. - */ + // The maximum speed (meters/second) that we believe is valid. + private double maxSpeed; + + // The min and max elevation (meters) seen on this trip. private final ExtremityMonitor elevationExtremities = new ExtremityMonitor(); - /** - * The minimum and maximum grade calculations on this trip. - */ + // The total elevation gained (meters). + private double totalElevationGain; + + // The min and max grade seen on this trip. private final ExtremityMonitor gradeExtremities = new ExtremityMonitor(); /** * Default constructor. */ - public TripStatistics() { - } + public TripStatistics() {} /** * Copy constructor. - * + * * @param other another statistics data object to copy from */ public TripStatistics(TripStatistics other) { - this.maxSpeed = other.maxSpeed; - this.movingTime = other.movingTime; this.startTime = other.startTime; this.stopTime = other.stopTime; this.totalDistance = other.totalDistance; - this.totalElevationGain = other.totalElevationGain; this.totalTime = other.totalTime; - - this.latitudeExtremities.set(other.latitudeExtremities.getMin(), - other.latitudeExtremities.getMax()); - this.longitudeExtremities.set(other.longitudeExtremities.getMin(), - other.longitudeExtremities.getMax()); - this.elevationExtremities.set(other.elevationExtremities.getMin(), - other.elevationExtremities.getMax()); - this.gradeExtremities.set(other.gradeExtremities.getMin(), - other.gradeExtremities.getMax()); + this.movingTime = other.movingTime; + this.latitudeExtremities.set( + other.latitudeExtremities.getMin(), other.latitudeExtremities.getMax()); + this.longitudeExtremities.set( + other.longitudeExtremities.getMin(), other.longitudeExtremities.getMax()); + this.maxSpeed = other.maxSpeed; + this.elevationExtremities.set( + other.elevationExtremities.getMin(), other.elevationExtremities.getMax()); + this.totalElevationGain = other.totalElevationGain; + this.gradeExtremities.set(other.gradeExtremities.getMin(), other.gradeExtremities.getMax()); } /** - * Combines these statistics with those from another object. - * This assumes that the time periods covered by each do not intersect. - * - * @param other the other waypoint + * Combines these statistics with those from another object. This assumes that + * the time periods covered by each do not intersect. + * + * @param other another statistics data object */ public void merge(TripStatistics other) { startTime = Math.min(startTime, other.startTime); stopTime = Math.max(stopTime, other.stopTime); + totalDistance += other.totalDistance; totalTime += other.totalTime; movingTime += other.movingTime; - totalDistance += other.totalDistance; - totalElevationGain += other.totalElevationGain; - maxSpeed = Math.max(maxSpeed, other.maxSpeed); - - latitudeExtremities.update(other.latitudeExtremities.getMax()); latitudeExtremities.update(other.latitudeExtremities.getMin()); - longitudeExtremities.update(other.longitudeExtremities.getMax()); + latitudeExtremities.update(other.latitudeExtremities.getMax()); longitudeExtremities.update(other.longitudeExtremities.getMin()); - elevationExtremities.update(other.elevationExtremities.getMax()); + longitudeExtremities.update(other.longitudeExtremities.getMax()); + maxSpeed = Math.max(maxSpeed, other.maxSpeed); elevationExtremities.update(other.elevationExtremities.getMin()); - gradeExtremities.update(other.gradeExtremities.getMax()); + elevationExtremities.update(other.elevationExtremities.getMax()); + totalElevationGain += other.totalElevationGain; gradeExtremities.update(other.gradeExtremities.getMin()); + gradeExtremities.update(other.gradeExtremities.getMax()); } /** - * Gets the time that this track started. - * - * @return The number of milliseconds since epoch to the time when this track - * started + * Gets the trip start time. The number of milliseconds since epoch. */ public long getStartTime() { return startTime; } /** - * Gets the time that this track stopped. - * - * @return The number of milliseconds since epoch to the time when this track - * stopped + * Gets the trip stop time. The number of milliseconds since epoch. */ public long getStopTime() { return stopTime; } /** - * Gets the total time that this track has been active. - * This statistic is only updated when a new point is added to the statistics, - * so it may be off. If you need to calculate the proper total time, use - * {@link #getStartTime} with the current time. - * - * @return The total number of milliseconds the track was active - */ - public long getTotalTime() { - return totalTime; - } - - /** - * Gets the total distance the user traveled. - * - * @return The total distance traveled in meters + * Gets the total distance the user traveled in meters. */ public double getTotalDistance() { return totalDistance; } /** - * Gets the the average speed the user traveled. - * This calculation only takes into account the displacement until the last - * point that was accounted for in statistics. - * - * @return The average speed in m/s + * Gets the total time in milliseconds that this track has been active. This + * statistic is only updated when a new point is added to the statistics, so + * it may be off. If you need to calculate the proper total time, use + * {@link #getStartTime} with the current time. + */ + public long getTotalTime() { + return totalTime; + } + + /** + * Gets the moving time in milliseconds. + */ + public long getMovingTime() { + return movingTime; + } + + /** + * Gets the topmost position (highest latitude) of the track, in signed + * degrees. + */ + public double getTopDegrees() { + return latitudeExtremities.getMax(); + } + + /** + * Gets the topmost position (highest latitude) of the track, in signed + * millions of degrees. + */ + public int getTop() { + return (int) (latitudeExtremities.getMax() * 1E6); + } + + /** + * Gets the bottommost position (lowest latitude) of the track, in signed + * degrees. + */ + public double getBottomDegrees() { + return latitudeExtremities.getMin(); + } + + /** + * Gets the bottommost position (lowest latitude) of the track, in signed + * millions of degrees. + */ + public int getBottom() { + return (int) (latitudeExtremities.getMin() * 1E6); + } + + /** + * Gets the leftmost position (lowest longitude) of the track, in signed + * degrees. + */ + public double getLeftDegrees() { + return longitudeExtremities.getMin(); + } + + /** + * Gets the leftmost position (lowest longitude) of the track, in signed + * millions of degrees. + */ + public int getLeft() { + return (int) (longitudeExtremities.getMin() * 1E6); + } + + /** + * Gets the rightmost position (highest longitude) of the track, in signed + * degrees. + */ + public double getRightDegrees() { + return longitudeExtremities.getMax(); + } + + /** + * Gets the rightmost position (highest longitude) of the track, in signed + * millions of degrees. + */ + public int getRight() { + return (int) (longitudeExtremities.getMax() * 1E6); + } + + /** + * Gets the mean latitude position of the track, in signed degrees. + */ + public double getMeanLatitude() { + return (getBottomDegrees() + getTopDegrees()) / 2.0; + } + + /** + * Gets the mean longitude position of the track, in signed degrees. + */ + public double getMeanLongitude() { + return (getLeftDegrees() + getRightDegrees()) / 2.0; + } + + /** + * Gets the average speed in meters/second. This calculation only takes into + * account the displacement until the last point that was accounted for in + * statistics. */ public double getAverageSpeed() { if (totalTime == 0L) { @@ -198,10 +242,7 @@ public class TripStatistics implements Parcelable { } /** - * Gets the the average speed the user traveled when they were actively - * moving. - * - * @return The average moving speed in m/s + * Gets the average moving speed in meters/second. */ public double getAverageMovingSpeed() { if (movingTime == 0L) { @@ -211,261 +252,80 @@ public class TripStatistics implements Parcelable { } /** - * Gets the the maximum speed for this track. - * - * @return The maximum speed in m/s + * Gets the maximum speed in meters/second. */ public double getMaxSpeed() { return maxSpeed; } /** - * Gets the moving time. - * - * @return The total number of milliseconds the user was moving - */ - public long getMovingTime() { - return movingTime; - } - - /** - * Gets the total elevation gain for this trip. This is calculated as the sum - * of all positive differences in the smoothed elevation. - * - * @return The elevation gain in meters for this trip - */ - public double getTotalElevationGain() { - return totalElevationGain; - } - - /** - * Returns the leftmost position (lowest longitude) of the track, in signed degrees. - */ - public double getLeftDegrees() { - return longitudeExtremities.getMin(); - } - - /** - * Returns the leftmost position (lowest longitude) of the track, in signed millions of degrees. - */ - public int getLeft() { - return (int) (longitudeExtremities.getMin() * 1E6); - } - - /** - * Returns the rightmost position (highest longitude) of the track, in signed degrees. - */ - public double getRightDegrees() { - return longitudeExtremities.getMax(); - } - - /** - * Returns the rightmost position (highest longitude) of the track, in signed millions of degrees. - */ - public int getRight() { - return (int) (longitudeExtremities.getMax() * 1E6); - } - - /** - * Returns the bottommost position (lowest latitude) of the track, in signed degrees. - */ - public double getBottomDegrees() { - return latitudeExtremities.getMin(); - } - - /** - * Returns the bottommost position (lowest latitude) of the track, in signed millions of degrees. - */ - public int getBottom() { - return (int) (latitudeExtremities.getMin() * 1E6); - } - - /** - * Returns the topmost position (highest latitude) of the track, in signed degrees. - */ - public double getTopDegrees() { - return latitudeExtremities.getMax(); - } - - /** - * Returns the topmost position (highest latitude) of the track, in signed millions of degrees. - */ - public int getTop() { - return (int) (latitudeExtremities.getMax() * 1E6); - } - - /** - * Returns the mean position (center latitude) of the track, in signed degrees. - */ - public double getMeanLatitude() { - return (getBottomDegrees() + getTopDegrees()) / 2.0; - } - - /** - * Returns the mean position (center longitude) of the track, in signed degrees. - */ - public double getMeanLongitude() { - return (getLeftDegrees() + getRightDegrees()) / 2.0; - } - - /** - * Gets the minimum elevation seen on this trip. This is calculated from the - * smoothed elevation so this can actually be more than the current elevation. - * - * @return The smallest elevation reading for this trip in meters + * Gets the minimum elevation. This is calculated from the smoothed elevation + * so this can actually be more than the current elevation. */ public double getMinElevation() { return elevationExtremities.getMin(); } /** - * Gets the maximum elevation seen on this trip. This is calculated from the - * smoothed elevation so this can actually be less than the current elevation. - * - * @return The largest elevation reading for this trip in meters + * Gets the maximum elevation. This is calculated from the smoothed elevation + * so this can actually be less than the current elevation. */ public double getMaxElevation() { return elevationExtremities.getMax(); } + /** + * Gets the total elevation gain in meters. This is calculated as the sum of + * all positive differences in the smoothed elevation. + */ + public double getTotalElevationGain() { + return totalElevationGain; + } + + /** + * Gets the minimum grade for this trip. + */ + public double getMinGrade() { + return gradeExtremities.getMin(); + } + /** * Gets the maximum grade for this trip. - * - * @return The maximum grade for this trip as a fraction */ public double getMaxGrade() { return gradeExtremities.getMax(); } /** - * Gets the minimum grade for this trip. - * - * @return The minimum grade for this trip as a fraction - */ - public double getMinGrade() { - return gradeExtremities.getMin(); - } - - // Setters - to be used when restoring state or loading from the DB - - /** - * Sets the start time for this trip. - * - * @param startTime the start time, in milliseconds since the epoch + * Sets the trip start time. + * + * @param startTime the trip start time in milliseconds since the epoch */ public void setStartTime(long startTime) { this.startTime = startTime; } /** - * Sets the stop time for this trip. - * - * @param stopTime the stop time, in milliseconds since the epoch + * Sets the trip stop time. + * + * @param stopTime the stop time in milliseconds since the epoch */ public void setStopTime(long stopTime) { this.stopTime = stopTime; } - /** - * Sets the total moving time. - * - * @param movingTime the moving time in milliseconds - */ - public void setMovingTime(long movingTime) { - this.movingTime = movingTime; - } - - /** - * Sets the total trip time. - * - * @param totalTime the total trip time in milliseconds - */ - public void setTotalTime(long totalTime) { - this.totalTime = totalTime; - } - /** * Sets the total trip distance. - * + * * @param totalDistance the trip distance in meters */ public void setTotalDistance(double totalDistance) { this.totalDistance = totalDistance; } - /** - * Sets the total elevation variation during the trip. - * - * @param totalElevationGain the elevation variation in meters - */ - public void setTotalElevationGain(double totalElevationGain) { - this.totalElevationGain = totalElevationGain; - } - - /** - * Sets the maximum speed reached during the trip. - * - * @param maxSpeed the maximum speed in meters per second - */ - public void setMaxSpeed(double maxSpeed) { - this.maxSpeed = maxSpeed; - } - - /** - * Sets the minimum elevation reached during the trip. - * - * @param elevation the minimum elevation in meters - */ - public void setMinElevation(double elevation) { - elevationExtremities.setMin(elevation); - } - - /** - * Sets the maximum elevation reached during the trip. - * - * @param elevation the maximum elevation in meters - */ - public void setMaxElevation(double elevation) { - elevationExtremities.setMax(elevation); - } - - /** - * Sets the minimum grade obtained during the trip. - * - * @param grade the grade as a fraction (-1.0 would mean vertical downwards) - */ - public void setMinGrade(double grade) { - gradeExtremities.setMin(grade); - } - - /** - * Sets the maximum grade obtained during the trip). - * - * @param grade the grade as a fraction (1.0 would mean vertical upwards) - */ - public void setMaxGrade(double grade) { - gradeExtremities.setMax(grade); - } - - /** - * Sets the bounding box for this trip. - * The unit for all parameters is signed decimal degrees (degrees * 1E6). - * - * @param leftE6 the westmost longitude reached - * @param topE6 the northmost latitude reached - * @param rightE6 the eastmost longitude reached - * @param bottomE6 the southmost latitude reached - */ - public void setBounds(int leftE6, int topE6, int rightE6, int bottomE6) { - latitudeExtremities.set(bottomE6 / 1E6, topE6 / 1E6); - longitudeExtremities.set(leftE6 / 1E6, rightE6 / 1E6); - } - - // Data manipulation methods - /** * Adds to the current total distance. - * + * * @param distance the distance to add in meters */ void addTotalDistance(double distance) { @@ -473,17 +333,26 @@ public class TripStatistics implements Parcelable { } /** - * Adds to the total elevation variation. - * - * @param gain the elevation variation in meters + * Sets the trip total time. + * + * @param totalTime the trip total time in milliseconds */ - void addTotalElevationGain(double gain) { - totalElevationGain += gain; + public void setTotalTime(long totalTime) { + this.totalTime = totalTime; } /** - * Adds to the total moving time of the trip. - * + * Sets the trip total moving time. + * + * @param movingTime the trip total moving time in milliseconds + */ + public void setMovingTime(long movingTime) { + this.movingTime = movingTime; + } + + /** + * Adds to the trip total moving time. + * * @param time the time in milliseconds */ void addMovingTime(long time) { @@ -491,8 +360,22 @@ public class TripStatistics implements Parcelable { } /** - * Accounts for a new latitude value for the bounding box. - * + * Sets the bounding box for this trip. The unit for all parameters is signed + * millions of degree (degrees * 1E6). + * + * @param leftE6 the leftmost longitude reached + * @param topE6 the topmost latitude reached + * @param rightE6 the rightmost longitude reached + * @param bottomE6 the bottommost latitude reached + */ + public void setBounds(int leftE6, int topE6, int rightE6, int bottomE6) { + latitudeExtremities.set(bottomE6 / 1E6, topE6 / 1E6); + longitudeExtremities.set(leftE6 / 1E6, rightE6 / 1E6); + } + + /** + * Updates a new latitude value. + * * @param latitude the latitude value in signed decimal degrees */ void updateLatitudeExtremities(double latitude) { @@ -500,8 +383,8 @@ public class TripStatistics implements Parcelable { } /** - * Accounts for a new longitude value for the bounding box. - * + * Updates a new longitude value. + * * @param longitude the longitude value in signed decimal degrees */ void updateLongitudeExtremities(double longitude) { @@ -509,8 +392,35 @@ public class TripStatistics implements Parcelable { } /** - * Accounts for a new elevation value for the bounding box. - * + * Sets the maximum speed. + * + * @param maxSpeed the maximum speed in meters/second + */ + public void setMaxSpeed(double maxSpeed) { + this.maxSpeed = maxSpeed; + } + + /** + * Sets the minimum elevation. + * + * @param elevation the minimum elevation in meters + */ + public void setMinElevation(double elevation) { + elevationExtremities.setMin(elevation); + } + + /** + * Sets the maximum elevation. + * + * @param elevation the maximum elevation in meters + */ + public void setMaxElevation(double elevation) { + elevationExtremities.setMax(elevation); + } + + /** + * Updates a new elevation. + * * @param elevation the elevation value in meters */ void updateElevationExtremities(double elevation) { @@ -518,49 +428,77 @@ public class TripStatistics implements Parcelable { } /** - * Accounts for a new grade value. - * + * Sets the total elevation gain. + * + * @param totalElevationGain the elevation gain in meters + */ + public void setTotalElevationGain(double totalElevationGain) { + this.totalElevationGain = totalElevationGain; + } + + /** + * Adds to the total elevation gain. + * + * @param gain the elevation gain in meters + */ + void addTotalElevationGain(double gain) { + totalElevationGain += gain; + } + + /** + * Sets the minimum grade. + * + * @param grade the grade as a fraction (-1.0 would mean vertical downwards) + */ + public void setMinGrade(double grade) { + gradeExtremities.setMin(grade); + } + + /** + * Sets the maximum grade. + * + * @param grade the grade as a fraction (1.0 would mean vertical upwards) + */ + public void setMaxGrade(double grade) { + gradeExtremities.setMax(grade); + } + + /** + * Updates a new grade value. + * * @param grade the grade value as a fraction */ void updateGradeExtremities(double grade) { gradeExtremities.update(grade); } - // String conversion - @Override public String toString() { - return "TripStatistics { Start Time: " + getStartTime() - + "; Total Time: " + getTotalTime() - + "; Moving Time: " + getMovingTime() - + "; Total Distance: " + getTotalDistance() - + "; Elevation Gain: " + getTotalElevationGain() - + "; Min Elevation: " + getMinElevation() - + "; Max Elevation: " + getMaxElevation() - + "; Average Speed: " + getAverageMovingSpeed() - + "; Min Grade: " + getMinGrade() - + "; Max Grade: " + getMaxGrade() - + "}"; + return "TripStatistics { Start Time: " + getStartTime() + "; Stop Time: " + getStopTime() + + "; Total Time: " + getTotalTime() + "; Total Distance: " + getTotalDistance() + + "; Total Time: " + getTotalTime() + "; Moving Time: " + getMovingTime() + + "; Min Latitude: " + getBottomDegrees() + "; Max Latitude: " + getTopDegrees() + + "; Min Longitude: " + getLeftDegrees() + "; Max Longitude: " + getRightDegrees() + + "; Max Elevation: " + getMaxElevation() + "; Max Speed: " + getMaxSpeed() + + "; Min Elevation: " + getMinElevation() + "; Max Elevation: " + getMaxElevation() + + "; Elevation Gain: " + getTotalElevationGain() + "; Min Grade: " + getMinGrade() + + "; Max Grade: " + getMaxGrade() + "}"; } - // Parcelable interface and creator - /** * Creator of statistics data from parcels. */ - public static class Creator - implements Parcelable.Creator { + public static class Creator implements Parcelable.Creator { @Override public TripStatistics createFromParcel(Parcel source) { TripStatistics data = new TripStatistics(); data.startTime = source.readLong(); - data.movingTime = source.readLong(); - data.totalTime = source.readLong(); + data.stopTime = source.readLong(); data.totalDistance = source.readDouble(); - data.totalElevationGain = source.readDouble(); - data.maxSpeed = source.readDouble(); + data.totalTime = source.readLong(); + data.movingTime = source.readLong(); double minLat = source.readDouble(); double maxLat = source.readDouble(); @@ -570,9 +508,12 @@ public class TripStatistics implements Parcelable { double maxLong = source.readDouble(); data.longitudeExtremities.set(minLong, maxLong); + data.maxSpeed = source.readDouble(); + double minElev = source.readDouble(); double maxElev = source.readDouble(); data.elevationExtremities.set(minElev, maxElev); + data.totalElevationGain = source.readDouble(); double minGrade = source.readDouble(); double maxGrade = source.readDouble(); @@ -600,18 +541,18 @@ public class TripStatistics implements Parcelable { @Override public void writeToParcel(Parcel dest, int flags) { dest.writeLong(startTime); - dest.writeLong(movingTime); - dest.writeLong(totalTime); + dest.writeLong(stopTime); dest.writeDouble(totalDistance); - dest.writeDouble(totalElevationGain); - dest.writeDouble(maxSpeed); - + dest.writeLong(totalTime); + dest.writeLong(movingTime); dest.writeDouble(latitudeExtremities.getMin()); dest.writeDouble(latitudeExtremities.getMax()); dest.writeDouble(longitudeExtremities.getMin()); dest.writeDouble(longitudeExtremities.getMax()); + dest.writeDouble(maxSpeed); dest.writeDouble(elevationExtremities.getMin()); dest.writeDouble(elevationExtremities.getMax()); + dest.writeDouble(totalElevationGain); dest.writeDouble(gradeExtremities.getMin()); dest.writeDouble(gradeExtremities.getMax()); } diff --git a/MyTracksTest/AndroidManifest.xml b/MyTracksTest/AndroidManifest.xml index e4ca106fb..cfaef984a 100644 --- a/MyTracksTest/AndroidManifest.xml +++ b/MyTracksTest/AndroidManifest.xml @@ -22,6 +22,10 @@ limitations under the License. + + -1) { - return true; + if (e.getMessage().indexOf("no such column") > -1) { + return false; } } - return false; + return true; } -} +} \ No newline at end of file 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)}. */ diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/endtoendtest/CreateAndSendTrackTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/endtoendtest/CreateAndSendTrackTest.java index 982b761c2..c33bb6931 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/endtoendtest/CreateAndSendTrackTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/endtoendtest/CreateAndSendTrackTest.java @@ -246,7 +246,8 @@ public class CreateAndSendTrackTest extends ActivityInstrumentationTestCase2 0); @@ -297,12 +299,10 @@ public class CreateAndSendTrackTest extends ActivityInstrumentationTestCase2 allViews = SOLO.getViews(); for (View view : allViews) { @@ -499,18 +513,25 @@ public class EndToEndTestUtils { } } - ArrayList