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