Add support for pause and resume in TrackRecordingService.

This commit is contained in:
Jimmy Shih
2012-08-21 18:36:04 -07:00
parent 832e2499c9
commit ccaf652c8b
13 changed files with 527 additions and 477 deletions
@@ -242,6 +242,7 @@ public class TrackDetailActivity extends AbstractMyTracksActivity implements Del
sendGoogleMenuItem = menu.findItem(R.id.track_detail_send_google); sendGoogleMenuItem = menu.findItem(R.id.track_detail_send_google);
saveMenuItem = menu.findItem(R.id.track_detail_save); saveMenuItem = menu.findItem(R.id.track_detail_save);
updateMenuItems(trackId == recordingTrackId);
return true; return true;
} }
@@ -20,6 +20,7 @@ import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.content.Track; import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.stats.TripStatisticsBuilder; import com.google.android.apps.mytracks.stats.TripStatisticsBuilder;
import com.google.android.apps.mytracks.util.LocationUtils; import com.google.android.apps.mytracks.util.LocationUtils;
import com.google.android.apps.mytracks.util.PreferencesUtils;
import com.google.android.apps.mytracks.util.StringUtils; import com.google.android.apps.mytracks.util.StringUtils;
import android.location.Location; import android.location.Location;
@@ -300,11 +301,11 @@ public class GpxImporter extends DefaultHandler {
flushPoints(); flushPoints();
// Calculate statistics for the imported track // Calculate statistics for the imported track
tripStatisticsBuilder.pauseAt(lastLocation.getTime()); tripStatisticsBuilder.updateTime(lastLocation.getTime());
track.setStopId(getLastPointId()); track.setStopId(getLastPointId());
} else { } else {
tripStatisticsBuilder = new TripStatisticsBuilder(0); tripStatisticsBuilder = new TripStatisticsBuilder(0L);
tripStatisticsBuilder.pauseAt(0); tripStatisticsBuilder.updateTime(0L);
} }
track.setTripStatistics(tripStatisticsBuilder.getTripStatistics()); track.setTripStatistics(tripStatisticsBuilder.getTripStatistics());
track.setNumberOfPoints(numberOfLocations); track.setNumberOfPoints(numberOfLocations);
@@ -380,7 +381,7 @@ public class GpxImporter extends DefaultHandler {
// first point did not have a time, start stats builder without it // first point did not have a time, start stats builder without it
tripStatisticsBuilder = new TripStatisticsBuilder(0); tripStatisticsBuilder = new TripStatisticsBuilder(0);
} }
tripStatisticsBuilder.addLocation(location, location.getTime()); tripStatisticsBuilder.addLocation(location, PreferencesUtils.MIN_RECORDING_DISTANCE_DEFAULT);
// insert in db // insert in db
insertPoint(location); insertPoint(location);
@@ -248,7 +248,7 @@ public class SendFusionTablesAsyncTask extends AbstractSendAsyncTask {
// Add to the distances and elevations vectors // Add to the distances and elevations vectors
if (LocationUtils.isValidLocation(location)) { if (LocationUtils.isValidLocation(location)) {
tripStatisticsBuilder.addLocation(location, location.getTime()); tripStatisticsBuilder.addLocation(location, PreferencesUtils.MIN_RECORDING_DISTANCE_DEFAULT);
// All points go into the smoothing buffer // All points go into the smoothing buffer
elevationBuffer.setNext(metricUnits ? location.getAltitude() elevationBuffer.setNext(metricUnits ? location.getAltitude()
: location.getAltitude() * UnitConversions.M_TO_FT); : location.getAltitude() * UnitConversions.M_TO_FT);
@@ -292,7 +292,7 @@ public class SendMapsAsyncTask extends AbstractSendAsyncTask {
// Add to the distances and elevations vectors // Add to the distances and elevations vectors
if (LocationUtils.isValidLocation(location)) { if (LocationUtils.isValidLocation(location)) {
tripStatisticsBuilder.addLocation(location, location.getTime()); tripStatisticsBuilder.addLocation(location, PreferencesUtils.MIN_RECORDING_DISTANCE_DEFAULT);
// All points go into the smoothing buffer // All points go into the smoothing buffer
elevationBuffer.setNext(metricUnits ? location.getAltitude() elevationBuffer.setNext(metricUnits ? location.getAltitude()
: location.getAltitude() * UnitConversions.M_TO_FT); : location.getAltitude() * UnitConversions.M_TO_FT);
@@ -17,6 +17,7 @@
package com.google.android.apps.mytracks.services; package com.google.android.apps.mytracks.services;
import com.google.android.apps.mytracks.util.GoogleLocationUtils; import com.google.android.apps.mytracks.util.GoogleLocationUtils;
import com.google.common.annotations.VisibleForTesting;
import android.content.ContentResolver; import android.content.ContentResolver;
import android.content.Context; import android.content.Context;
@@ -44,7 +45,8 @@ public class MyTracksLocationManager {
private static final String USE_LOCATION_FOR_SERVICES = "use_location_for_services"; private static final String USE_LOCATION_FOR_SERVICES = "use_location_for_services";
// User has agreed to use location for Google services. // User has agreed to use location for Google services.
private static final String USE_LOCATION_FOR_SERVICES_ON = "1"; @VisibleForTesting
static final String USE_LOCATION_FOR_SERVICES_ON = "1";
private static final String NAME = "name"; private static final String NAME = "name";
private static final String VALUE = "value"; private static final String VALUE = "value";
@@ -82,6 +82,9 @@ import java.util.concurrent.Executors;
public class TrackRecordingService extends Service { public class TrackRecordingService extends Service {
private static final String TAG = TrackRecordingService.class.getSimpleName(); private static final String TAG = TrackRecordingService.class.getSimpleName();
public static final int PAUSE_LATITUDE = 100;
private static final int RESUME_LATITUDE = 200;
// One second in milliseconds // One second in milliseconds
private static final long ONE_SECOND = 1000; private static final long ONE_SECOND = 1000;
// One minute in milliseconds // One minute in milliseconds
@@ -105,25 +108,14 @@ public class TrackRecordingService extends Service {
private int minRequiredAccuracy; private int minRequiredAccuracy;
private int autoResumeTrackTimeout; private int autoResumeTrackTimeout;
private long currentRecordingInterval; private long currentRecordingInterval;
private Track recordingTrack;
// The following variables are set when recording: // The following variables are set when recording:
/*
* Track length. Calculated from the recorded points to overlay waypoints
* precisely on the elevation chart.
*/
private double length;
// Used for length calculation
private Location lastLengthLocation;
private TripStatisticsBuilder trackTripStatisticsBuilder; private TripStatisticsBuilder trackTripStatisticsBuilder;
private TripStatisticsBuilder markerTripStatisticsBuilder; private TripStatisticsBuilder markerTripStatisticsBuilder;
private WakeLock wakeLock; private WakeLock wakeLock;
private Location lastLocation;
private boolean isMoving;
private SensorManager sensorManager; private SensorManager sensorManager;
private Location lastLocation;
private boolean currentSegmentHasLocation;
// Timer to periodically invoke checkLocationListener // Timer to periodically invoke checkLocationListener
private final Timer timer = new Timer(); private final Timer timer = new Timer();
@@ -202,10 +194,6 @@ public class TrackRecordingService extends Service {
minRecordingDistance = PreferencesUtils.getInt(context, minRecordingDistance = PreferencesUtils.getInt(context,
R.string.min_recording_distance_key, R.string.min_recording_distance_key,
PreferencesUtils.MIN_RECORDING_DISTANCE_DEFAULT); PreferencesUtils.MIN_RECORDING_DISTANCE_DEFAULT);
if (trackTripStatisticsBuilder != null && markerTripStatisticsBuilder != null) {
trackTripStatisticsBuilder.setMinRecordingDistance(minRecordingDistance);
markerTripStatisticsBuilder.setMinRecordingDistance(minRecordingDistance);
}
} }
if (key == null || key.equals( if (key == null || key.equals(
PreferencesUtils.getKey(context, R.string.max_recording_distance_key))) { PreferencesUtils.getKey(context, R.string.max_recording_distance_key))) {
@@ -262,10 +250,9 @@ public class TrackRecordingService extends Service {
private TimerTask checkLocationListener = new TimerTask() { private TimerTask checkLocationListener = new TimerTask() {
@Override @Override
public void run() { public void run() {
if (isRecording()) { if (isRecording() && !isPaused()) {
handler.post(new Runnable() { handler.post(new Runnable() {
public void run() { public void run() {
unregisterLocationListener();
registerLocationListener(); registerLocationListener();
} }
}); });
@@ -298,21 +285,18 @@ public class TrackRecordingService extends Service {
// Require announcementExecutor and splitExecutor to be created. // Require announcementExecutor and splitExecutor to be created.
sharedPreferenceChangeListener.onSharedPreferenceChanged(sharedPreferences, null); sharedPreferenceChangeListener.onSharedPreferenceChanged(sharedPreferences, null);
// Require locationManager and all preferences set.
registerLocationListener();
timer.schedule(checkLocationListener, 5 * ONE_MINUTE, ONE_MINUTE); timer.schedule(checkLocationListener, 5 * ONE_MINUTE, ONE_MINUTE);
/* /*
* Try to restart the previous recording track in case the service has been * Try to restart the previous recording track in case the service has been
* restarted by the system, which can sometimes happen. * restarted by the system, which can sometimes happen.
*/ */
recordingTrack = myTracksProviderUtils.getTrack(recordingTrackId); Track track = myTracksProviderUtils.getTrack(recordingTrackId);
if (recordingTrack != null) { if (track != null) {
restartTrack(recordingTrack); restartTrack(track);
} else { } else {
if (recordingTrackId != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT) { if (isRecording()) {
Log.w(TAG, "recordingTrackId not -1L, but recordingTrack is null. " + recordingTrackId); Log.w(TAG, "track is null, but recordingTrackId not -1L. " + recordingTrackId);
updateRecordingState(PreferencesUtils.RECORDING_TRACK_ID_DEFAULT, true); updateRecordingState(PreferencesUtils.RECORDING_TRACK_ID_DEFAULT, true);
} }
showNotification(); showNotification();
@@ -426,11 +410,11 @@ public class TrackRecordingService extends Service {
* @return the waypoint id * @return the waypoint id
*/ */
public long insertWaypoint(WaypointCreationRequest waypointCreationRequest) { public long insertWaypoint(WaypointCreationRequest waypointCreationRequest) {
if (!isRecording()) { if (!isRecording() || isPaused()) {
throw new IllegalStateException("Cannot insert marker when not recording!"); return -1L;
} }
boolean isStatistics = waypointCreationRequest.getType() == WaypointType.STATISTICS;
boolean isStatistics = waypointCreationRequest.getType() == WaypointType.STATISTICS;
String name; String name;
if (waypointCreationRequest.getName() != null) { if (waypointCreationRequest.getName() != null) {
name = waypointCreationRequest.getName(); name = waypointCreationRequest.getName();
@@ -449,7 +433,7 @@ public class TrackRecordingService extends Service {
String description; String description;
if (isStatistics) { if (isStatistics) {
long now = System.currentTimeMillis(); long now = System.currentTimeMillis();
markerTripStatisticsBuilder.pauseAt(now); markerTripStatisticsBuilder.updateTime(now);
tripStatistics = markerTripStatisticsBuilder.getTripStatistics(); tripStatistics = markerTripStatisticsBuilder.getTripStatistics();
markerTripStatisticsBuilder = new TripStatisticsBuilder(now); markerTripStatisticsBuilder = new TripStatisticsBuilder(now);
description = new DescriptionGeneratorImpl(this).generateWaypointDescription(tripStatistics); description = new DescriptionGeneratorImpl(this).generateWaypointDescription(tripStatistics);
@@ -466,24 +450,27 @@ public class TrackRecordingService extends Service {
String icon = getString( String icon = getString(
isStatistics ? R.string.marker_statistics_icon_url : R.string.marker_waypoint_icon_url); isStatistics ? R.string.marker_statistics_icon_url : R.string.marker_waypoint_icon_url);
int type = isStatistics ? Waypoint.TYPE_STATISTICS : Waypoint.TYPE_WAYPOINT; int type = isStatistics ? Waypoint.TYPE_STATISTICS : Waypoint.TYPE_WAYPOINT;
long duration; long duration;
Location location = myTracksProviderUtils.getLastTrackLocation(recordingTrackId); double length;
if (location != null && trackTripStatisticsBuilder != null Location lastTrackLocation = myTracksProviderUtils.getLastTrackLocation(recordingTrackId);
&& trackTripStatisticsBuilder.getTripStatistics() != null) { if (currentSegmentHasLocation && LocationUtils.isValidLocation(lastTrackLocation)
duration = location.getTime() - trackTripStatisticsBuilder.getTripStatistics().getStartTime(); && trackTripStatisticsBuilder != null) {
TripStatistics stats = trackTripStatisticsBuilder.getTripStatistics();
length = stats.getTotalDistance();
duration = stats.getTotalTime();
} else { } else {
if (!waypointCreationRequest.isTrackStatistics()) { if (!waypointCreationRequest.isTrackStatistics()) {
return -1L; return -1L;
} }
// For track statistics, make it an impossible location // For track statistics, make it an impossible location
location = new Location(""); lastTrackLocation = new Location("");
location.setLatitude(100); lastTrackLocation.setLatitude(100);
location.setLongitude(180); lastTrackLocation.setLongitude(180);
length = 0;
duration = 0; duration = 0;
} }
Waypoint waypoint = new Waypoint(name, description, category, icon, recordingTrackId, type, Waypoint waypoint = new Waypoint(name, description, category, icon, recordingTrackId, type,
length, duration, -1L, -1L, location, tripStatistics); length, duration, -1L, -1L, lastTrackLocation, tripStatistics);
Uri uri = myTracksProviderUtils.insertWaypoint(waypoint); Uri uri = myTracksProviderUtils.insertWaypoint(waypoint);
return Long.parseLong(uri.getLastPathSegment()); return Long.parseLong(uri.getLastPathSegment());
} }
@@ -515,7 +502,7 @@ public class TrackRecordingService extends Service {
private void handleStartCommand(Intent intent, int startId) { private void handleStartCommand(Intent intent, int startId) {
// Check if the service is called to resume track (from phone reboot) // Check if the service is called to resume track (from phone reboot)
if (intent != null && intent.getBooleanExtra(RESUME_TRACK_EXTRA_NAME, false)) { if (intent != null && intent.getBooleanExtra(RESUME_TRACK_EXTRA_NAME, false)) {
if (!shouldResumeTrack(recordingTrack)) { if (!shouldResumeTrack()) {
Log.i(TAG, "Stop resume track."); Log.i(TAG, "Stop resume track.");
updateRecordingState(PreferencesUtils.RECORDING_TRACK_ID_DEFAULT, true); updateRecordingState(PreferencesUtils.RECORDING_TRACK_ID_DEFAULT, true);
stopSelfResult(startId); stopSelfResult(startId);
@@ -526,10 +513,10 @@ public class TrackRecordingService extends Service {
/** /**
* Returns true if should resume. * Returns true if should resume.
*
* @param track the track
*/ */
private boolean shouldResumeTrack(Track track) { private boolean shouldResumeTrack() {
Track track = myTracksProviderUtils.getTrack(recordingTrackId);
if (track == null) { if (track == null) {
Log.d(TAG, "Not resuming. Track is null."); Log.d(TAG, "Not resuming. Track is null.");
return false; return false;
@@ -570,32 +557,28 @@ public class TrackRecordingService extends Service {
return -1L; return -1L;
} }
long now = System.currentTimeMillis(); long now = System.currentTimeMillis();
length = 0;
lastLengthLocation = null;
trackTripStatisticsBuilder = new TripStatisticsBuilder(now); trackTripStatisticsBuilder = new TripStatisticsBuilder(now);
trackTripStatisticsBuilder.setMinRecordingDistance(minRecordingDistance);
markerTripStatisticsBuilder = new TripStatisticsBuilder(now); markerTripStatisticsBuilder = new TripStatisticsBuilder(now);
markerTripStatisticsBuilder.setMinRecordingDistance(minRecordingDistance);
// Insert a track
Track track = new Track(); Track track = new Track();
track.setStartId(-1L);
TripStatistics tripStatistics = track.getTripStatistics();
tripStatistics.setStartTime(now);
Uri uri = myTracksProviderUtils.insertTrack(track); Uri uri = myTracksProviderUtils.insertTrack(track);
long trackId = Long.parseLong(uri.getLastPathSegment());
updateRecordingState(Long.parseLong(uri.getLastPathSegment()), false); // Update shared preferences
updateRecordingState(trackId, false);
PreferencesUtils.setInt(this, R.string.auto_resume_track_current_retry_key, 0);
// Update database
track.setId(recordingTrackId); track.setId(recordingTrackId);
track.setName(TrackNameUtils.getTrackName(this, recordingTrackId, now, null)); track.setName(TrackNameUtils.getTrackName(this, recordingTrackId, now, null));
track.setCategory(PreferencesUtils.getString( track.setCategory(PreferencesUtils.getString(
this, R.string.default_activity_key, PreferencesUtils.DEFAULT_ACTIVITY_DEFAULT)); this, R.string.default_activity_key, PreferencesUtils.DEFAULT_ACTIVITY_DEFAULT));
track.setTripStatistics(trackTripStatisticsBuilder.getTripStatistics());
myTracksProviderUtils.updateTrack(track); myTracksProviderUtils.updateTrack(track);
insertWaypoint(WaypointCreationRequest.DEFAULT_START_TRACK); insertWaypoint(WaypointCreationRequest.DEFAULT_START_TRACK);
PreferencesUtils.setInt(this, R.string.auto_resume_track_current_retry_key, 0); startRecording(true);
registerLocationListener();
startRecording();
return recordingTrackId; return recordingTrackId;
} }
@@ -607,11 +590,8 @@ public class TrackRecordingService extends Service {
private void restartTrack(Track track) { private void restartTrack(Track track) {
Log.d(TAG, "Restarting track: " + track.getId()); Log.d(TAG, "Restarting track: " + track.getId());
length = 0;
lastLengthLocation = null;
TripStatistics tripStatistics = track.getTripStatistics(); TripStatistics tripStatistics = track.getTripStatistics();
trackTripStatisticsBuilder = new TripStatisticsBuilder(tripStatistics.getStartTime()); trackTripStatisticsBuilder = new TripStatisticsBuilder(tripStatistics.getStartTime());
trackTripStatisticsBuilder.setMinRecordingDistance(minRecordingDistance);
long markerStartTime; long markerStartTime;
Waypoint waypoint = myTracksProviderUtils.getLastStatisticsWaypoint(recordingTrackId); Waypoint waypoint = myTracksProviderUtils.getLastStatisticsWaypoint(recordingTrackId);
@@ -621,10 +601,10 @@ public class TrackRecordingService extends Service {
markerStartTime = tripStatistics.getStartTime(); markerStartTime = tripStatistics.getStartTime();
} }
markerTripStatisticsBuilder = new TripStatisticsBuilder(markerStartTime); markerTripStatisticsBuilder = new TripStatisticsBuilder(markerStartTime);
markerTripStatisticsBuilder.setMinRecordingDistance(minRecordingDistance);
Cursor cursor = null; Cursor cursor = null;
try { try {
// TODO: how to handle very long track.
cursor = myTracksProviderUtils.getLocationsCursor( cursor = myTracksProviderUtils.getLocationsCursor(
recordingTrackId, -1, Constants.MAX_LOADED_TRACK_POINTS, true); recordingTrackId, -1, Constants.MAX_LOADED_TRACK_POINTS, true);
if (cursor == null) { if (cursor == null) {
@@ -633,20 +613,12 @@ public class TrackRecordingService extends Service {
if (cursor.moveToLast()) { if (cursor.moveToLast()) {
do { do {
Location location = myTracksProviderUtils.createLocation(cursor); Location location = myTracksProviderUtils.createLocation(cursor);
if (LocationUtils.isValidLocation(location)) { trackTripStatisticsBuilder.addLocation(location, minRecordingDistance);
trackTripStatisticsBuilder.addLocation(location, location.getTime()); if (location.getTime() > markerStartTime) {
if (location.getTime() > markerStartTime) { markerTripStatisticsBuilder.addLocation(location, minRecordingDistance);
markerTripStatisticsBuilder.addLocation(location, location.getTime());
}
if (lastLengthLocation != null) {
length += location.distanceTo(lastLengthLocation);
}
lastLengthLocation = location;
} }
} while (cursor.moveToPrevious()); } while (cursor.moveToPrevious());
} }
trackTripStatisticsBuilder.pauseAt(tripStatistics.getStopTime());
trackTripStatisticsBuilder.resumeAt(System.currentTimeMillis());
} }
} catch (RuntimeException e) { } catch (RuntimeException e) {
Log.e(TAG, "RuntimeException", e); Log.e(TAG, "RuntimeException", e);
@@ -655,34 +627,59 @@ public class TrackRecordingService extends Service {
cursor.close(); cursor.close();
} }
} }
// TODO: update recordingTrackPaused variable based on Track Points table state startRecording(true);
startRecording();
} }
/** /**
* Common code for starting a recording, new track or restart track. * Resumes current track.
*/ */
private void startRecording() { private void resumeCurrentTrack() {
if (!isRecording() || !isPaused()) {
Log.d(TAG, "Ignore resumeCurrentTrack. Not recording or not paused.");
return;
}
// Update shared preferences
recordingTrackPaused = false;
PreferencesUtils.setBoolean(this, R.string.recording_track_paused_key, false);
// Update database
Location resume = new Location(LocationManager.GPS_PROVIDER);
resume.setLongitude(0);
resume.setLatitude(RESUME_LATITUDE);
resume.setTime(System.currentTimeMillis());
insertLocation(resume, null, null);
startRecording(false);
}
/**
* Common code for starting a new track, resuming a track, or restarting after
* phone reboot.
*
* @param trackStarted true if track is started, false if track is resumed
*/
private void startRecording(boolean trackStarted) {
acquireWakeLock(); acquireWakeLock();
lastLocation = null; // Update instance variables
isMoving = true;
sensorManager = SensorManagerFactory.getSystemSensorManager(this); sensorManager = SensorManagerFactory.getSystemSensorManager(this);
lastLocation = null;
currentSegmentHasLocation = false;
// Register notifications
registerLocationListener();
// Send notifications
showNotification(); showNotification();
sendTrackBroadcast(R.string.track_started_broadcast_action, recordingTrackId); sendTrackBroadcast(trackStarted ? R.string.track_started_broadcast_action
: R.string.track_resumed_broadcast_action, recordingTrackId);
// Restore periodic tasks
announcementExecutor.restore(); announcementExecutor.restore();
splitExecutor.restore(); splitExecutor.restore();
} }
private void pauseCurrentTrack() {
PreferencesUtils.setBoolean(this, R.string.recording_track_paused_key, true);
}
private void resumeCurrentTrack() {
PreferencesUtils.setBoolean(this, R.string.recording_track_paused_key, false);
}
/** /**
* Ends the current track. * Ends the current track.
*/ */
@@ -691,30 +688,81 @@ public class TrackRecordingService extends Service {
Log.d(TAG, "Ignore endCurrentTrack. Not recording."); Log.d(TAG, "Ignore endCurrentTrack. Not recording.");
return; return;
} }
announcementExecutor.shutdown();
splitExecutor.shutdown();
Track track = myTracksProviderUtils.getTrack(recordingTrackId);
if (track != null) {
long lastLocationId = myTracksProviderUtils.getLastTrackLocationId(recordingTrackId);
if (lastLocationId >= 0 && track.getStopId() >= 0) {
track.setStopId(lastLocationId);
}
updateTripStatisticsToTime(track.getTripStatistics(), System.currentTimeMillis());
myTracksProviderUtils.updateTrack(track);
}
// Need to remember the recordingTrackId before setting it to -1L // Need to remember the recordingTrackId before setting it to -1L
long trackId = recordingTrackId; long trackId = recordingTrackId;
// Update shared preferences
updateRecordingState(PreferencesUtils.RECORDING_TRACK_ID_DEFAULT, true); updateRecordingState(PreferencesUtils.RECORDING_TRACK_ID_DEFAULT, true);
// Update database
insertLocation(lastLocation, getSensorDataSet(),
myTracksProviderUtils.getLastTrackLocation(recordingTrackId));
Track track = myTracksProviderUtils.getTrack(recordingTrackId);
if (track != null) {
updateRecordingTrack(
track, myTracksProviderUtils.getLastTrackLocationId(recordingTrackId), false);
}
endRecording(true, trackId);
stopSelf();
}
/**
* Pauses the current track.
*/
private void pauseCurrentTrack() {
if (!isRecording() || isPaused()) {
Log.d(TAG, "Ignore pauseCurrentTrack. Not recording or paused.");
return;
}
// Update shared preferences
recordingTrackPaused = true;
PreferencesUtils.setBoolean(this, R.string.recording_track_paused_key, true);
// Update database
insertLocation(lastLocation, getSensorDataSet(),
myTracksProviderUtils.getLastTrackLocation(recordingTrackId));
Location pause = new Location(LocationManager.GPS_PROVIDER);
pause.setLongitude(0);
pause.setLatitude(PAUSE_LATITUDE);
pause.setTime(System.currentTimeMillis());
insertLocation(pause, null, null);
endRecording(false, recordingTrackId);
}
/**
* Common code for ending a track or pausing a track.
*
* @param trackStopped true if track is stopped, false if track is paused
* @param trackId
*/
private void endRecording(boolean trackStopped, long trackId) {
// Shutdown periodic tasks
announcementExecutor.shutdown();
splitExecutor.shutdown();
// Update instance variables
if (sensorManager != null) { if (sensorManager != null) {
SensorManagerFactory.releaseSystemSensorManager(); SensorManagerFactory.releaseSystemSensorManager();
sensorManager = null; sensorManager = null;
} }
lastLocation = null;
// Unregister notifications
unregisterLocationListener();
// Send notifications
showNotification();
sendTrackBroadcast(trackStopped ? R.string.track_stopped_broadcast_action
: R.string.track_paused_broadcast_action, trackId);
releaseWakeLock(); releaseWakeLock();
showNotification();
sendTrackBroadcast(R.string.track_stopped_broadcast_action, trackId);
stopSelf();
} }
/** /**
@@ -730,17 +778,6 @@ public class TrackRecordingService extends Service {
PreferencesUtils.setBoolean(this, R.string.recording_track_paused_key, recordingTrackPaused); PreferencesUtils.setBoolean(this, R.string.recording_track_paused_key, recordingTrackPaused);
} }
/**
* 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. * Called when location changed.
* *
@@ -748,13 +785,13 @@ public class TrackRecordingService extends Service {
*/ */
private void onLocationChangedAsync(Location location) { private void onLocationChangedAsync(Location location) {
try { try {
if (location == null) { if (!isRecording() || isPaused()) {
Log.w(TAG, "Ignore onLocationChangedAsync. location is null."); Log.w(TAG, "Ignore onLocationChangedAsync. Not recording or paused.");
return; return;
} }
if (!isRecording()) { if (!LocationUtils.isValidLocation(location)) {
Log.w(TAG, "Ignore onLocationChangedAsync. Not recording."); Log.w(TAG, "Ignore onLocationChangedAsync. location is invalid.");
return; return;
} }
@@ -763,74 +800,56 @@ public class TrackRecordingService extends Service {
return; return;
} }
recordingTrack = myTracksProviderUtils.getTrack(recordingTrackId); Location lastTrackLocation = myTracksProviderUtils.getLastTrackLocation(recordingTrackId);
if (recordingTrack == null) { SensorDataSet sensorDataSet = getSensorDataSet();
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);
}
long idleTime = currentSegmentHasLocation && LocationUtils.isValidLocation(lastTrackLocation) ?
location.getTime() - lastTrackLocation.getTime()
: 0;
locationListenerPolicy.updateIdleTime(idleTime);
if (currentRecordingInterval != locationListenerPolicy.getDesiredPollingInterval()) { if (currentRecordingInterval != locationListenerPolicy.getDesiredPollingInterval()) {
registerLocationListener(); registerLocationListener();
} }
Location lastRecordedLocation = myTracksProviderUtils.getLastLocation(); // Always insert the first segment location
double distanceToLastRecordedLocation = lastRecordedLocation != null ? location.distanceTo( if (!currentSegmentHasLocation) {
lastRecordedLocation) insertLocation(location, sensorDataSet, lastTrackLocation);
: Double.POSITIVE_INFINITY; currentSegmentHasLocation = true;
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; lastLocation = location;
} else if (distanceToLastRecordedLocation >= minRecordingDistance || hasSensorData) { return;
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.");
} }
if (!LocationUtils.isValidLocation(lastTrackLocation)) {
/*
* Should not happen. The current segment should have a location. Just
* insert the current location.
*/
insertLocation(location, sensorDataSet, lastTrackLocation);
lastLocation = location;
return;
}
double distanceToLastTrackLocation = location.distanceTo(lastTrackLocation);
if (distanceToLastTrackLocation < minRecordingDistance && sensorDataSet == null) {
// skip
} else if (distanceToLastTrackLocation > maxRecordingDistance) {
insertLocation(lastLocation, sensorDataSet, lastTrackLocation);
Location pause = new Location(LocationManager.GPS_PROVIDER);
pause.setLongitude(0);
pause.setLatitude(PAUSE_LATITUDE);
pause.setTime(lastLocation.getTime());
insertLocation(pause, null, null);
insertLocation(location, sensorDataSet, lastTrackLocation);
} else {
/*
* (distanceToLastTrackLocation >= minRecordingDistance ||
* hasSensorData) && distanceToLastTrackLocation <= maxRecordingDistance
*/
insertLocation(lastLocation, sensorDataSet, lastTrackLocation);
insertLocation(location, sensorDataSet, lastTrackLocation);
}
lastLocation = location;
} catch (Error e) { } catch (Error e) {
Log.e(TAG, "Error in onLocationChangedAsync", e); Log.e(TAG, "Error in onLocationChangedAsync", e);
throw e; throw e;
@@ -840,62 +859,28 @@ public class TrackRecordingService extends Service {
} }
} }
/** private void insertLocation(
* Inserts a new location in the track points db and updates the corresponding Location location, SensorDataSet sensorDataSet, Location lastRecordedLocation) {
* track in the track db. Track track = myTracksProviderUtils.getTrack(recordingTrackId);
* if (track == null) {
* @param location the location to be inserted return;
* @param lastRecordedLocation the last recorded location before this one (or }
* null if none) if (location == null) {
* @param trackId the id of the track return;
* @return true if successful. False if SQLite3 threw an exception. }
*/ if (location.equals(lastRecordedLocation)) {
private boolean insertLocation(Location location, Location lastRecordedLocation, long trackId) { return;
// Keep track of length along recorded track
if (LocationUtils.isValidLocation(location)) {
if (lastLengthLocation != null) {
length += location.distanceTo(lastLengthLocation);
}
lastLengthLocation = location;
} }
try { try {
Location locationToInsert = location; if (sensorDataSet != null) {
if (sensorManager != null && sensorManager.isEnabled()) { location = new MyTracksLocation(location, sensorDataSet);
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);
}
} }
Uri uri = myTracksProviderUtils.insertTrackPoint(location, recordingTrackId);
long trackPointId = Long.parseLong(uri.getLastPathSegment());
trackTripStatisticsBuilder.addLocation(location, minRecordingDistance);
markerTripStatisticsBuilder.addLocation(location, minRecordingDistance);
updateRecordingTrack(track, trackPointId, LocationUtils.isValidLocation(location));
} catch (SQLiteException e) { } catch (SQLiteException e) {
/* /*
* Insert failed, most likely because of SqlLite error code 5 * Insert failed, most likely because of SqlLite error code 5
@@ -903,17 +888,42 @@ public class TrackRecordingService extends Service {
* listener gets invoked twice at about the same time). * listener gets invoked twice at about the same time).
*/ */
Log.w(TAG, "SQLiteException", e); Log.w(TAG, "SQLiteException", e);
return false;
} }
announcementExecutor.update(); announcementExecutor.update();
splitExecutor.update(); splitExecutor.update();
return true; }
private void updateRecordingTrack(
Track track, long trackPointId, boolean isTrackPointNewAndValid) {
if (trackPointId >= 0) {
if (track.getStartId() < 0) {
track.setStartId(trackPointId);
}
track.setStopId(trackPointId);
}
if (isTrackPointNewAndValid) {
track.setNumberOfPoints(track.getNumberOfPoints() + 1);
}
trackTripStatisticsBuilder.updateTime(System.currentTimeMillis());
track.setTripStatistics(trackTripStatisticsBuilder.getTripStatistics());
myTracksProviderUtils.updateTrack(track);
}
private SensorDataSet getSensorDataSet() {
if (sensorManager == null || !sensorManager.isEnabled()
|| !sensorManager.isSensorDataSetValid()) {
return null;
}
return sensorManager.getSensorDataSet();
} }
/** /**
* Registers the location listener. * Registers the location listener.
*/ */
private void registerLocationListener() { private void registerLocationListener() {
unregisterLocationListener();
if (myTracksLocationManager == null) { if (myTracksLocationManager == null) {
Log.e(TAG, "locationManager is null."); Log.e(TAG, "locationManager is null.");
return; return;
@@ -981,7 +991,7 @@ public class TrackRecordingService extends Service {
* Shows the notification. * Shows the notification.
*/ */
private void showNotification() { private void showNotification() {
if (isRecording()) { if (isRecording() && !isPaused()) {
Intent intent = IntentUtils.newIntent(this, TrackDetailActivity.class) Intent intent = IntentUtils.newIntent(this, TrackDetailActivity.class)
.putExtra(TrackDetailActivity.EXTRA_TRACK_ID, recordingTrackId); .putExtra(TrackDetailActivity.EXTRA_TRACK_ID, recordingTrackId);
TaskStackBuilder taskStackBuilder = TaskStackBuilder.from(this); TaskStackBuilder taskStackBuilder = TaskStackBuilder.from(this);
@@ -62,8 +62,8 @@ public class PeriodicTaskExecutor {
* Restores the executor. * Restores the executor.
*/ */
public void restore() { public void restore() {
if (!trackRecordingService.isRecording()) { if (!trackRecordingService.isRecording() || trackRecordingService.isPaused()) {
Log.d(TAG, "Not recording."); Log.d(TAG, "Not recording or paused.");
return; return;
} }
@@ -159,7 +159,7 @@ public class PeriodicTaskExecutor {
* Calculates the next distance for the distance periodic task. * Calculates the next distance for the distance periodic task.
*/ */
private void calculateNextTaskDistance() { private void calculateNextTaskDistance() {
if (!trackRecordingService.isRecording() || periodicTask == null) { if (!trackRecordingService.isRecording() || trackRecordingService.isPaused() || periodicTask == null) {
return; return;
} }
@@ -50,7 +50,7 @@ public class TimerTaskExecutor {
* @param interval the interval in milliseconds * @param interval the interval in milliseconds
*/ */
public void scheduleTask(long interval) { public void scheduleTask(long interval) {
if (!trackRecordingService.isRecording()) { if (!trackRecordingService.isRecording() || trackRecordingService.isPaused()) {
return; return;
} }
@@ -19,132 +19,125 @@ package com.google.android.apps.mytracks.stats;
import static com.google.android.apps.mytracks.Constants.TAG; import static com.google.android.apps.mytracks.Constants.TAG;
import com.google.android.apps.mytracks.Constants; import com.google.android.apps.mytracks.Constants;
import com.google.android.apps.mytracks.util.PreferencesUtils; import com.google.android.apps.mytracks.services.TrackRecordingService;
import com.google.android.apps.mytracks.util.LocationUtils;
import com.google.common.annotations.VisibleForTesting; import com.google.common.annotations.VisibleForTesting;
import android.location.Location; import android.location.Location;
import android.util.Log; import android.util.Log;
/** /**
* Builder for {@link TripStatistics}. For keeping statistics as a track is * Builder for {@link TripStatistics}. For keeping track statistics as new
* paused/resumed and new locations are added. * locations are added. Note that some of the locations represent pause/resume
* separator.
* *
* @author Sandor Dornbush * @author Sandor Dornbush
* @author Rodrigo Damazio * @author Rodrigo Damazio
*/ */
public class TripStatisticsBuilder { public class TripStatisticsBuilder {
// The trip statistics. // The track's trip statistics
private final TripStatistics tripStatistics; private final TripStatistics tripStatistics;
// The minimum recording distance. // The current segment's trip statistics
private int minRecordingDistance = PreferencesUtils.MIN_RECORDING_DISTANCE_DEFAULT; private TripStatistics currentSegment;
// True if the trip is paused. All trips start as paused. // Current segment's last location.
private boolean paused = true;
// The last location as reported by GPS.
private Location lastLocation; private Location lastLocation;
// The last moving location that contributed to the moving statistics. // Current segment's last moving location
private Location lastMovingLocation; private Location lastMovingLocation;
// A buffer of the recent speed readings (m/s) for calculating max speed. // A buffer of the recent speed readings (m/s) for calculating max speed
private final DoubleBuffer speedBuffer = new DoubleBuffer(Constants.SPEED_SMOOTHING_FACTOR); private final DoubleBuffer speedBuffer = new DoubleBuffer(Constants.SPEED_SMOOTHING_FACTOR);
// A buffer of the recent elevation readings (m). // A buffer of the recent elevation readings (m)
private final DoubleBuffer elevationBuffer = new DoubleBuffer( private final DoubleBuffer elevationBuffer = new DoubleBuffer(
Constants.ELEVATION_SMOOTHING_FACTOR); Constants.ELEVATION_SMOOTHING_FACTOR);
// A buffer of the recent distance readings for calculating grade. // A buffer of the recent distance readings (m) for calculating grade
private final DoubleBuffer distanceBuffer = new DoubleBuffer(Constants.DISTANCE_SMOOTHING_FACTOR); private final DoubleBuffer distanceBuffer = new DoubleBuffer(Constants.DISTANCE_SMOOTHING_FACTOR);
// A buffer of the recent grade calculations // A buffer of the recent grade calculations (%)
private final DoubleBuffer gradeBuffer = new DoubleBuffer(Constants.GRADE_SMOOTHING_FACTOR); private final DoubleBuffer gradeBuffer = new DoubleBuffer(Constants.GRADE_SMOOTHING_FACTOR);
/** /**
* Creates a new trip starting at a start time. * Creates a new trip statistics builder.
* *
* @param startTime the start time * @param startTime the start time
*/ */
public TripStatisticsBuilder(long startTime) { public TripStatisticsBuilder(long startTime) {
tripStatistics = new TripStatistics(); tripStatistics = init(startTime);
resumeAt(startTime); currentSegment = init(startTime);
}
public void updateTime(long time) {
currentSegment.setStopTime(time);
currentSegment.setTotalTime(time - currentSegment.getStartTime());
} }
/** /**
* Creates a new trip, starting with an existing {@link TripStatistics}. * Gets the track's trip statistics.
*
* @param other the existing {@link TripStatistics}
*/
public TripStatisticsBuilder(TripStatistics other) {
tripStatistics = new TripStatistics(other);
if (tripStatistics.getStartTime() > 0) {
resumeAt(tripStatistics.getStartTime());
}
}
/**
* Sets the min recording distance.
*
* @param minRecordingDistance the min recording distance
*/
public void setMinRecordingDistance(int minRecordingDistance) {
this.minRecordingDistance = minRecordingDistance;
}
/**
* Resumes the current track at a given time.
*
* @param time the time
*/
public void resumeAt(long time) {
if (!paused) {
return;
}
tripStatistics.setStartTime(time);
tripStatistics.setStopTime(-1L);
paused = false;
lastLocation = null;
lastMovingLocation = null;
speedBuffer.reset();
elevationBuffer.reset();
distanceBuffer.reset();
gradeBuffer.reset();
}
/**
* Pauses the track at a given time.
*
* @param time the time to pause at
*/
public void pauseAt(long time) {
if (paused) {
return;
}
tripStatistics.setStopTime(time);
// TODO: total time needs to take into account pauses
tripStatistics.setTotalTime(time - tripStatistics.getStartTime());
paused = true;
}
/**
* Gets the trip statistics.
*/ */
public TripStatistics getTripStatistics() { public TripStatistics getTripStatistics() {
// Take a snapshot - we don't want anyone messing with our internals // Take a snapshot - we don't want anyone messing with our tripStatistics
return new TripStatistics(tripStatistics); TripStatistics stats = new TripStatistics(tripStatistics);
stats.merge(currentSegment);
return stats;
} }
/** /**
* Returns the amount of time the user has been idle or 0 if he is moving. * Adds a location. TODO: This assume location has a valid time.
*
* @param location the location
* @param minRecordingDistance the min recording distance
*/ */
public long getIdleTime() { public void addLocation(Location location, int minRecordingDistance) {
if (lastLocation == null || lastMovingLocation == null) { if (!LocationUtils.isValidLocation(location)) {
return 0; updateTime(location.getTime());
if (location.getLatitude() == TrackRecordingService.PAUSE_LATITUDE) {
tripStatistics.merge(currentSegment);
}
currentSegment = init(location.getTime());
lastLocation = null;
lastMovingLocation = null;
speedBuffer.reset();
elevationBuffer.reset();
distanceBuffer.reset();
gradeBuffer.reset();
return;
} }
return lastLocation.getTime() - lastMovingLocation.getTime(); double elevationDifference = updateElevation(location.getAltitude());
currentSegment.updateLatitudeExtremities(location.getLatitude());
currentSegment.updateLongitudeExtremities(location.getLongitude());
if (lastLocation == null || lastMovingLocation == null) {
updateTime(location.getTime());
lastLocation = location;
lastMovingLocation = location;
return;
}
double movingDistance = lastMovingLocation.distanceTo(location);
if (movingDistance < minRecordingDistance
&& location.getSpeed() < Constants.MAX_NO_MOVEMENT_SPEED) {
updateTime(location.getTime());
lastLocation = location;
return;
}
long movingTime = location.getTime() - lastLocation.getTime();
if (movingTime < 0) {
updateTime(location.getTime());
lastLocation = location;
return;
}
currentSegment.addTotalDistance(movingDistance);
currentSegment.addMovingTime(movingTime);
updateSpeed(
location.getTime(), location.getSpeed(), lastLocation.getTime(), lastLocation.getSpeed());
updateGrade(lastLocation.distanceTo(location), elevationDifference);
updateTime(location.getTime());
lastLocation = location;
lastMovingLocation = location;
} }
/** /**
@@ -152,62 +145,11 @@ public class TripStatisticsBuilder {
* is noisy so the smoothed elevation is better than the raw elevation for * is noisy so the smoothed elevation is better than the raw elevation for
* many tasks. * many tasks.
*/ */
public double getSmoothedElevation() { @VisibleForTesting
double getSmoothedElevation() {
return elevationBuffer.getAverage(); return elevationBuffer.getAverage();
} }
/**
* 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;
}
/** /**
* Updates a speed reading. Assumes the user is moving. * Updates a speed reading. Assumes the user is moving.
* *
@@ -223,12 +165,12 @@ public class TripStatisticsBuilder {
return; return;
} }
speedBuffer.setNext(speed); speedBuffer.setNext(speed);
if (speed > tripStatistics.getMaxSpeed()) { if (speed > currentSegment.getMaxSpeed()) {
tripStatistics.setMaxSpeed(speed); currentSegment.setMaxSpeed(speed);
} }
double movingSpeed = tripStatistics.getAverageMovingSpeed(); double movingSpeed = currentSegment.getAverageMovingSpeed();
if (speedBuffer.isFull() && movingSpeed > tripStatistics.getMaxSpeed()) { if (speedBuffer.isFull() && movingSpeed > currentSegment.getMaxSpeed()) {
tripStatistics.setMaxSpeed(movingSpeed); currentSegment.setMaxSpeed(movingSpeed);
} }
} }
@@ -242,10 +184,10 @@ public class TripStatisticsBuilder {
double oldAverage = elevationBuffer.getAverage(); double oldAverage = elevationBuffer.getAverage();
elevationBuffer.setNext(elevation); elevationBuffer.setNext(elevation);
double newAverage = elevationBuffer.getAverage(); double newAverage = elevationBuffer.getAverage();
tripStatistics.updateElevationExtremities(newAverage); currentSegment.updateElevationExtremities(newAverage);
double elevationDifference = elevationBuffer.isFull() ? newAverage - oldAverage : 0.0; double elevationDifference = elevationBuffer.isFull() ? newAverage - oldAverage : 0.0;
if (elevationDifference > 0) { if (elevationDifference > 0) {
tripStatistics.addTotalElevationGain(elevationDifference); currentSegment.addTotalElevationGain(elevationDifference);
} }
return elevationDifference; return elevationDifference;
} }
@@ -270,7 +212,14 @@ public class TripStatisticsBuilder {
return; return;
} }
gradeBuffer.setNext(elevationDifference / smoothedDistance); gradeBuffer.setNext(elevationDifference / smoothedDistance);
tripStatistics.updateGradeExtremities(gradeBuffer.getAverage()); currentSegment.updateGradeExtremities(gradeBuffer.getAverage());
}
private TripStatistics init(long time) {
TripStatistics stats = new TripStatistics();
stats.setStartTime(time);
stats.setStopTime(time);
return stats;
} }
/** /**
+2
View File
@@ -23,6 +23,8 @@ limitations under the License.
<!-- Broadcast notification Intent constants --> <!-- Broadcast notification Intent constants -->
<string name="track_started_broadcast_action">com.google.android.apps.mytracks.TRACK_STARTED</string> <string name="track_started_broadcast_action">com.google.android.apps.mytracks.TRACK_STARTED</string>
<string name="track_paused_broadcast_action">com.google.android.apps.mytracks.TRACK_PAUSED</string>
<string name="track_resumed_broadcast_action">com.google.android.apps.mytracks.TRACK_RESUMED</string>
<string name="track_stopped_broadcast_action">com.google.android.apps.mytracks.TRACK_STOPPED</string> <string name="track_stopped_broadcast_action">com.google.android.apps.mytracks.TRACK_STOPPED</string>
<!-- Next API refactor depricate track_id_broadcast_extra and rename it to remove the broadcast as it is used when SHARING tracks as --> <!-- Next API refactor depricate track_id_broadcast_extra and rename it to remove the broadcast as it is used when SHARING tracks as -->
<string name="track_id_broadcast_extra">com.google.android.apps.mytracks.TRACK_ID</string> <string name="track_id_broadcast_extra">com.google.android.apps.mytracks.TRACK_ID</string>
@@ -101,16 +101,24 @@ public class TripStatistics implements Parcelable {
totalDistance += other.totalDistance; totalDistance += other.totalDistance;
totalTime += other.totalTime; totalTime += other.totalTime;
movingTime += other.movingTime; movingTime += other.movingTime;
latitudeExtremities.update(other.latitudeExtremities.getMin()); if (other.latitudeExtremities.hasData()) {
latitudeExtremities.update(other.latitudeExtremities.getMax()); latitudeExtremities.update(other.latitudeExtremities.getMin());
longitudeExtremities.update(other.longitudeExtremities.getMin()); latitudeExtremities.update(other.latitudeExtremities.getMax());
longitudeExtremities.update(other.longitudeExtremities.getMax()); }
if (other.longitudeExtremities.hasData()) {
longitudeExtremities.update(other.longitudeExtremities.getMin());
longitudeExtremities.update(other.longitudeExtremities.getMax());
}
maxSpeed = Math.max(maxSpeed, other.maxSpeed); maxSpeed = Math.max(maxSpeed, other.maxSpeed);
elevationExtremities.update(other.elevationExtremities.getMin()); if (other.elevationExtremities.hasData()) {
elevationExtremities.update(other.elevationExtremities.getMax()); elevationExtremities.update(other.elevationExtremities.getMin());
elevationExtremities.update(other.elevationExtremities.getMax());
}
totalElevationGain += other.totalElevationGain; totalElevationGain += other.totalElevationGain;
gradeExtremities.update(other.gradeExtremities.getMin()); if (other.gradeExtremities.hasData()) {
gradeExtremities.update(other.gradeExtremities.getMax()); gradeExtremities.update(other.gradeExtremities.getMin());
gradeExtremities.update(other.gradeExtremities.getMax());
}
} }
/** /**
@@ -46,6 +46,7 @@ import android.test.RenamingDelegatingContext;
import android.test.ServiceTestCase; import android.test.ServiceTestCase;
import android.test.mock.MockContentProvider; import android.test.mock.MockContentProvider;
import android.test.mock.MockContentResolver; import android.test.mock.MockContentResolver;
import android.test.mock.MockCursor;
import android.test.suitebuilder.annotation.MediumTest; import android.test.suitebuilder.annotation.MediumTest;
import android.test.suitebuilder.annotation.SmallTest; import android.test.suitebuilder.annotation.SmallTest;
import android.util.Log; import android.util.Log;
@@ -143,6 +144,7 @@ public class TrackRecordingServiceTest extends ServiceTestCase<TestRecordingServ
MyTracksProvider myTracksProvider = new MyTracksProvider(); MyTracksProvider myTracksProvider = new MyTracksProvider();
myTracksProvider.attachInfo(context, null); myTracksProvider.attachInfo(context, null);
mockContentResolver.addProvider(MyTracksProviderUtils.AUTHORITY, myTracksProvider); mockContentResolver.addProvider(MyTracksProviderUtils.AUTHORITY, myTracksProvider);
MockContentProvider settingsProvider = new MockContentProvider(context) { MockContentProvider settingsProvider = new MockContentProvider(context) {
@Override @Override
public Bundle call(String method, String arg, Bundle extras) { public Bundle call(String method, String arg, Bundle extras) {
@@ -156,6 +158,35 @@ public class TrackRecordingServiceTest extends ServiceTestCase<TestRecordingServ
}; };
mockContentResolver.addProvider(Settings.AUTHORITY, settingsProvider); mockContentResolver.addProvider(Settings.AUTHORITY, settingsProvider);
MockContentProvider googleSettingsProvider = new MockContentProvider(context) {
@Override
public Bundle call(String method, String arg, Bundle extras) {
return null;
}
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
String sortOrder) {
MockCursor mockCursor = new MockCursor() {
@Override
public int getCount() {
return 1;
}
@Override
public boolean moveToNext() {
return true;
}
@Override
public String getString(int columnIndex) {
return MyTracksLocationManager.USE_LOCATION_FOR_SERVICES_ON;
}
@Override
public void close() {}
};
return mockCursor;
}
};
mockContentResolver.addProvider("com.google.settings", googleSettingsProvider);
// Set the context // Set the context
setContext(context); setContext(context);
@@ -483,12 +514,8 @@ public class TrackRecordingServiceTest extends ServiceTestCase<TestRecordingServ
ITrackRecordingService service = bindAndGetService(createStartIntent()); ITrackRecordingService service = bindAndGetService(createStartIntent());
assertFalse(service.isRecording()); assertFalse(service.isRecording());
try { long waypointId = service.insertWaypoint(WaypointCreationRequest.DEFAULT_STATISTICS);
service.insertWaypoint(WaypointCreationRequest.DEFAULT_STATISTICS); assertEquals(-1L, waypointId);
fail("Expecting IllegalStateException");
} catch (IllegalStateException e) {
// Expected.
}
} }
@MediumTest @MediumTest
@@ -497,6 +524,7 @@ public class TrackRecordingServiceTest extends ServiceTestCase<TestRecordingServ
ITrackRecordingService service = bindAndGetService(createStartIntent()); ITrackRecordingService service = bindAndGetService(createStartIntent());
assertTrue(service.isRecording()); assertTrue(service.isRecording());
assertFalse(service.isPaused());
insertLocation(service); insertLocation(service);
assertEquals(1, service.insertWaypoint(WaypointCreationRequest.DEFAULT_STATISTICS)); assertEquals(1, service.insertWaypoint(WaypointCreationRequest.DEFAULT_STATISTICS));
@@ -520,12 +548,8 @@ public class TrackRecordingServiceTest extends ServiceTestCase<TestRecordingServ
ITrackRecordingService service = bindAndGetService(createStartIntent()); ITrackRecordingService service = bindAndGetService(createStartIntent());
assertFalse(service.isRecording()); assertFalse(service.isRecording());
try { long waypointId = service.insertWaypoint(WaypointCreationRequest.DEFAULT_WAYPOINT);
service.insertWaypoint(WaypointCreationRequest.DEFAULT_WAYPOINT); assertEquals(-1L, waypointId);
fail("Expecting IllegalStateException");
} catch (IllegalStateException e) {
// Expected.
}
} }
@MediumTest @MediumTest
@@ -700,6 +724,7 @@ public class TrackRecordingServiceTest extends ServiceTestCase<TestRecordingServ
assertEquals(track.getId(), providerUtils.getTrack(track.getId()).getId()); assertEquals(track.getId(), providerUtils.getTrack(track.getId()).getId());
PreferencesUtils.setLong(context, R.string.recording_track_id_key, isRecording ? track.getId() PreferencesUtils.setLong(context, R.string.recording_track_id_key, isRecording ? track.getId()
: PreferencesUtils.RECORDING_TRACK_ID_DEFAULT); : PreferencesUtils.RECORDING_TRACK_ID_DEFAULT);
PreferencesUtils.setBoolean(context, R.string.recording_track_paused_key, !isRecording);
} }
private void fullRecordingSession() throws Exception { private void fullRecordingSession() throws Exception {
@@ -3,31 +3,35 @@
package com.google.android.apps.mytracks.stats; package com.google.android.apps.mytracks.stats;
import com.google.android.apps.mytracks.Constants; import com.google.android.apps.mytracks.Constants;
import com.google.android.apps.mytracks.util.PreferencesUtils;
import android.location.Location; import android.location.Location;
import junit.framework.TestCase; import junit.framework.TestCase;
/** /**
* Test the the function of the TripStatisticsBuilder class. * Tests {@link TripStatisticsBuilder}.
* *
* @author Sandor Dornbush * @author Sandor Dornbush
*/ */
public class TripStatisticsBuilderTest extends TestCase { public class TripStatisticsBuilderTest extends TestCase {
private TripStatisticsBuilder builder = null; private static final long ONE_SECOND = 1000;
private static final long TEN_SECONDS = 10 * ONE_SECOND;
private TripStatisticsBuilder tripStatisticsBuilder = null;
@Override @Override
protected void setUp() throws Exception { protected void setUp() throws Exception {
super.setUp(); tripStatisticsBuilder = new TripStatisticsBuilder(System.currentTimeMillis());
builder = new TripStatisticsBuilder(System.currentTimeMillis());
} }
public void testAddLocationSimple() throws Exception { public void testAddLocationSimple() throws Exception {
builder = new TripStatisticsBuilder(1000); long startTime = 1000;
TripStatistics tripStatistics = builder.getTripStatistics(); tripStatisticsBuilder = new TripStatisticsBuilder(startTime);
TripStatistics tripStatistics = tripStatisticsBuilder.getTripStatistics();
assertEquals(0.0, builder.getSmoothedElevation()); assertEquals(0.0, tripStatisticsBuilder.getSmoothedElevation());
assertEquals(Double.POSITIVE_INFINITY, tripStatistics.getMinElevation()); assertEquals(Double.POSITIVE_INFINITY, tripStatistics.getMinElevation());
assertEquals(Double.NEGATIVE_INFINITY, tripStatistics.getMaxElevation()); assertEquals(Double.NEGATIVE_INFINITY, tripStatistics.getMaxElevation());
assertEquals(0.0, tripStatistics.getMaxSpeed()); assertEquals(0.0, tripStatistics.getMaxSpeed());
@@ -36,6 +40,8 @@ public class TripStatisticsBuilderTest extends TestCase {
assertEquals(0.0, tripStatistics.getTotalElevationGain()); assertEquals(0.0, tripStatistics.getTotalElevationGain());
assertEquals(0, tripStatistics.getMovingTime()); assertEquals(0, tripStatistics.getMovingTime());
assertEquals(0.0, tripStatistics.getTotalDistance()); assertEquals(0.0, tripStatistics.getTotalDistance());
float speed = 11.1f;
for (int i = 0; i < 100; i++) { for (int i = 0; i < 100; i++) {
Location location = new Location("test"); Location location = new Location("test");
location.setAccuracy(1.0f); location.setAccuracy(1.0f);
@@ -43,126 +49,172 @@ public class TripStatisticsBuilderTest extends TestCase {
// Going up by 1 meter each time. // Going up by 1 meter each time.
location.setAltitude(i); location.setAltitude(i);
location.setLatitude(i * .001); // Moving by .001 degree latitude (111 meters)
location.setSpeed(11.1f);
// Each time slice is 10 seconds.
long time = 1000 + 10000 * i;
location.setTime(time);
boolean moving = builder.addLocation(location, time);
assertEquals((i != 0), moving);
tripStatistics = builder.getTripStatistics(); // Moving by .001 degree latitude (111 meters)
assertEquals(10000 * i, tripStatistics.getTotalTime()); location.setLatitude(i * .001);
assertEquals(10000 * i, tripStatistics.getMovingTime());
assertEquals(i, builder.getSmoothedElevation(), Constants.ELEVATION_SMOOTHING_FACTOR / 2); location.setSpeed(speed);
// Each time slice is 10 seconds.
location.setTime(startTime + i * TEN_SECONDS);
tripStatisticsBuilder.addLocation(location, PreferencesUtils.MIN_RECORDING_DISTANCE_DEFAULT);
tripStatistics = tripStatisticsBuilder.getTripStatistics();
assertEquals(i * TEN_SECONDS, tripStatistics.getTotalTime());
assertEquals(i * TEN_SECONDS, tripStatistics.getMovingTime());
assertEquals(i, tripStatisticsBuilder.getSmoothedElevation(),
Constants.ELEVATION_SMOOTHING_FACTOR / 2);
assertEquals(0.0, tripStatistics.getMinElevation()); assertEquals(0.0, tripStatistics.getMinElevation());
assertEquals(i, tripStatistics.getMaxElevation(), Constants.ELEVATION_SMOOTHING_FACTOR / 2); assertEquals(i, tripStatistics.getMaxElevation(), Constants.ELEVATION_SMOOTHING_FACTOR / 2);
assertEquals(i, tripStatistics.getTotalElevationGain(), Constants.ELEVATION_SMOOTHING_FACTOR); assertEquals(i, tripStatistics.getTotalElevationGain(), Constants.ELEVATION_SMOOTHING_FACTOR);
if (i > Constants.SPEED_SMOOTHING_FACTOR) { if (i >= Constants.SPEED_SMOOTHING_FACTOR) {
assertEquals(11.1f, tripStatistics.getMaxSpeed(), 0.1); assertEquals(speed, tripStatistics.getMaxSpeed(), 0.1);
} }
if (i > Constants.DISTANCE_SMOOTHING_FACTOR && i > Constants.ELEVATION_SMOOTHING_FACTOR) { if (i >= Constants.DISTANCE_SMOOTHING_FACTOR && i >= Constants.ELEVATION_SMOOTHING_FACTOR) {
// 1 m / 111 m = .009 // 1 m / 111 m = .009
assertEquals(0.009, tripStatistics.getMinGrade(), 0.0001); assertEquals(0.009, tripStatistics.getMinGrade(), 0.0001);
assertEquals(0.009, tripStatistics.getMaxGrade(), 0.0001); assertEquals(0.009, tripStatistics.getMaxGrade(), 0.0001);
} }
assertEquals(111.0 * i, tripStatistics.getTotalDistance(), 100); assertEquals(i * 111.0, tripStatistics.getTotalDistance(), 100);
} }
} }
/** /**
* Test that elevation works if the user is stable. * Tests {@link TripStatisticsBuilder#updateElevation(double)} with constant
* elevations.
*/ */
public void testElevationSimple() throws Exception { public void testElevationSimple() throws Exception {
for (double elevation = 0; elevation < 1000; elevation += 10) { for (double elevation = 0; elevation < 1000; elevation += 10) {
builder = new TripStatisticsBuilder(System.currentTimeMillis()); tripStatisticsBuilder = new TripStatisticsBuilder(System.currentTimeMillis());
for (int j = 0; j < 100; j++) { for (int i = 0; i < 100; i++) {
assertEquals(0.0, builder.updateElevation(elevation)); assertEquals(0.0, tripStatisticsBuilder.updateElevation(elevation));
assertEquals(elevation, builder.getSmoothedElevation()); assertEquals(elevation, tripStatisticsBuilder.getSmoothedElevation());
TripStatistics data = builder.getTripStatistics();
assertEquals(elevation, data.getMinElevation()); TripStatistics tripStatistics = tripStatisticsBuilder.getTripStatistics();
assertEquals(elevation, data.getMaxElevation()); assertEquals(elevation, tripStatistics.getMinElevation());
assertEquals(0.0, data.getTotalElevationGain()); assertEquals(elevation, tripStatistics.getMaxElevation());
assertEquals(0.0, tripStatistics.getTotalElevationGain());
} }
} }
} }
/**
* Tests {@link TripStatisticsBuilder#updateGrade(double, double)} with
* elevation gain.
*/
public void testElevationGain() throws Exception { public void testElevationGain() throws Exception {
for (double i = 0; i < 1000; i++) { for (double i = 0; i < 1000; i++) {
double expectedGain; double expectedGain;
if (i < (Constants.ELEVATION_SMOOTHING_FACTOR - 1)) { if (i < Constants.ELEVATION_SMOOTHING_FACTOR - 1) {
expectedGain = 0; expectedGain = 0;
} else if (i < Constants.ELEVATION_SMOOTHING_FACTOR) { } else if (i < Constants.ELEVATION_SMOOTHING_FACTOR) {
expectedGain = 0.5; expectedGain = 0.5;
} else { } else {
expectedGain = 1.0; expectedGain = 1.0;
} }
assertEquals(expectedGain, builder.updateElevation(i)); assertEquals(expectedGain, tripStatisticsBuilder.updateElevation(i));
assertEquals(i, builder.getSmoothedElevation(), 20); assertEquals(i, tripStatisticsBuilder.getSmoothedElevation(),
TripStatistics data = builder.getTripStatistics(); Constants.ELEVATION_SMOOTHING_FACTOR / 2);
assertEquals(0.0, data.getMinElevation(), 0.0);
assertEquals(i, data.getMaxElevation(), Constants.ELEVATION_SMOOTHING_FACTOR); TripStatistics data = tripStatisticsBuilder.getTripStatistics();
assertEquals(0.0, data.getMinElevation());
assertEquals(i, data.getMaxElevation(), Constants.ELEVATION_SMOOTHING_FACTOR / 2);
assertEquals(i, data.getTotalElevationGain(), Constants.ELEVATION_SMOOTHING_FACTOR); assertEquals(i, data.getTotalElevationGain(), Constants.ELEVATION_SMOOTHING_FACTOR);
} }
} }
/**
* Tests {@link TripStatisticsBuilder#updateGrade(double, double)} with grade
* of 1 and -1.
*/
public void testGradeSimple() throws Exception { public void testGradeSimple() throws Exception {
for (double i = 0; i < 1000; i++) { for (double i = 0; i < 1000; i++) {
// The value of the elevation does not matter. This is just to fill the /*
// buffer. * The value of the elevation does not matter. This is just to fill the
builder.updateElevation(i); * elevation buffer.
builder.updateGrade(100, 100); */
if ((i > Constants.GRADE_SMOOTHING_FACTOR) && (i > Constants.ELEVATION_SMOOTHING_FACTOR)) { tripStatisticsBuilder.updateElevation(i);
assertEquals(1.0, builder.getTripStatistics().getMaxGrade()); tripStatisticsBuilder.updateGrade(100, 100);
assertEquals(1.0, builder.getTripStatistics().getMinGrade()); if (i >= Constants.GRADE_SMOOTHING_FACTOR && i >= Constants.ELEVATION_SMOOTHING_FACTOR) {
assertEquals(1.0, tripStatisticsBuilder.getTripStatistics().getMaxGrade());
assertEquals(1.0, tripStatisticsBuilder.getTripStatistics().getMinGrade());
} }
} }
for (double i = 0; i < 1000; i++) { for (double i = 0; i < 1000; i++) {
// The value of the elevation does not matter. This is just to fill the /*
// buffer. * The value of the elevation does not matter. This is just to fill the
builder.updateElevation(i); * elevation buffer.
builder.updateGrade(100, -100); */
if ((i > Constants.GRADE_SMOOTHING_FACTOR) && (i > Constants.ELEVATION_SMOOTHING_FACTOR)) { tripStatisticsBuilder.updateElevation(i);
assertEquals(1.0, builder.getTripStatistics().getMaxGrade()); tripStatisticsBuilder.updateGrade(100, -100);
assertEquals(-1.0, builder.getTripStatistics().getMinGrade()); if (i >= Constants.GRADE_SMOOTHING_FACTOR && i >= Constants.ELEVATION_SMOOTHING_FACTOR) {
assertEquals(1.0, tripStatisticsBuilder.getTripStatistics().getMaxGrade());
assertEquals(-1.0, tripStatisticsBuilder.getTripStatistics().getMinGrade());
} }
} }
} }
/**
* Tests {@link TripStatisticsBuilder#updateGrade(double, double)} with
* distance of 1. The grade should get ignored.
*/
public void testGradeIgnoreShort() throws Exception { public void testGradeIgnoreShort() throws Exception {
for (double i = 0; i < 100; i++) { for (double i = 0; i < 100; i++) {
// The value of the elevation does not matter. This is just to fill the /*
// buffer. * The value of the elevation does not matter. This is just to fill the
builder.updateElevation(i); * elevation buffer.
builder.updateGrade(1, 100); */
assertEquals(Double.NEGATIVE_INFINITY, builder.getTripStatistics().getMaxGrade()); tripStatisticsBuilder.updateElevation(i);
assertEquals(Double.POSITIVE_INFINITY, builder.getTripStatistics().getMinGrade()); tripStatisticsBuilder.updateGrade(1, 100);
assertEquals(
Double.NEGATIVE_INFINITY, tripStatisticsBuilder.getTripStatistics().getMaxGrade());
assertEquals(
Double.POSITIVE_INFINITY, tripStatisticsBuilder.getTripStatistics().getMinGrade());
} }
} }
/**
* Tests {@link TripStatisticsBuilder#updateSpeed(long, double, long, double)}
* with speed of zero.
*/
public void testUpdateSpeedIncludeZero() { public void testUpdateSpeedIncludeZero() {
for (int i = 0; i < 1000; i++) { for (int i = 0; i < 1000; i++) {
builder.updateSpeed(i + 1000, 0.0, i, 4.0); tripStatisticsBuilder.updateSpeed(i + ONE_SECOND, 0.0, i, 4.0);
assertEquals(0.0, builder.getTripStatistics().getMaxSpeed()); assertEquals(0.0, tripStatisticsBuilder.getTripStatistics().getMaxSpeed());
} }
} }
/**
* Tests {@link TripStatisticsBuilder#updateSpeed(long, double, long, double)}
* with the error code 128. The speed should get ignored.
*/
public void testUpdateSpeedIngoreErrorCode() { public void testUpdateSpeedIngoreErrorCode() {
builder.updateSpeed(12345000, 128.0, 12344000, 0.0); long time = 12344000;
assertEquals(0.0, builder.getTripStatistics().getMaxSpeed()); tripStatisticsBuilder.updateSpeed(time + ONE_SECOND, 128.0, time, 0.0);
assertEquals(0.0, tripStatisticsBuilder.getTripStatistics().getMaxSpeed());
} }
/**
* Tests {@link TripStatisticsBuilder#updateSpeed(long, double, long, double)}
* with a large speed change. The speed should get ignored.
*/
public void testUpdateSpeedIngoreLargeAcceleration() { public void testUpdateSpeedIngoreLargeAcceleration() {
builder.updateSpeed(12345000, 100.0, 12344000, 1.0); long time = 12344000;
assertEquals(0.0, builder.getTripStatistics().getMaxSpeed()); tripStatisticsBuilder.updateSpeed(time + ONE_SECOND, 100.0, time, 1.0);
assertEquals(0.0, tripStatisticsBuilder.getTripStatistics().getMaxSpeed());
} }
/**
* Tests {@link TripStatisticsBuilder#updateSpeed(long, double, long, double)}
* with constant speed.
*/
public void testUpdateSpeed() { public void testUpdateSpeed() {
double speed = 4.0;
for (int i = 0; i < 1000; i++) { for (int i = 0; i < 1000; i++) {
builder.updateSpeed(i + 1000, 4.0, i, 4.0); tripStatisticsBuilder.updateSpeed(i + ONE_SECOND, speed, i, speed);
if (i > Constants.SPEED_SMOOTHING_FACTOR) { if (i >= Constants.SPEED_SMOOTHING_FACTOR) {
assertEquals(4.0, builder.getTripStatistics().getMaxSpeed()); assertEquals(speed, tripStatisticsBuilder.getTripStatistics().getMaxSpeed());
} }
} }
} }