From b6fd63a512bf792b77663c59e2eb8dc4d98a81ab Mon Sep 17 00:00:00 2001 From: youtaol Date: Fri, 27 Jul 2012 10:24:20 +0800 Subject: [PATCH] Merge heads --- .../android/apps/mytracks/ChartView.java | 7 +- .../android/apps/mytracks/Constants.java | 4 + .../android/apps/mytracks/MapOverlay.java | 643 +++++--- .../apps/mytracks/MyTracksApplication.java | 17 +- .../apps/mytracks/TrackDetailActivity.java | 14 +- .../apps/mytracks/content/DataSource.java | 16 +- .../mytracks/content/DataSourceManager.java | 30 +- .../apps/mytracks/content/ListenerState.java | 105 -- .../apps/mytracks/content/TrackDataHub.java | 1456 +++++++---------- .../mytracks/content/TrackDataListener.java | 159 +- .../mytracks/content/TrackDataManager.java | 37 +- .../apps/mytracks/content/TrackDataType.java | 9 +- .../mytracks/fragments/ChartFragment.java | 30 +- .../apps/mytracks/fragments/MapFragment.java | 19 +- .../mytracks/fragments/StatsFragment.java | 32 +- .../maps/DynamicSpeedTrackPathDescriptor.java | 127 +- .../maps/DynamicSpeedTrackPathPainter.java | 149 +- .../maps/FixedSpeedTrackPathDescriptor.java | 29 +- .../maps/SingleColorTrackPathPainter.java | 113 +- .../mytracks/maps/TrackPathDescriptor.java | 22 +- .../apps/mytracks/maps/TrackPathPainter.java | 54 +- .../maps/TrackPathPainterFactory.java | 22 +- ...PathUtilities.java => TrackPathUtils.java} | 21 +- .../content/MyTracksProviderUtilsImpl.java | 7 +- MyTracksTest/libs/robotium-solo-3.1.jar | Bin 49691 -> 0 bytes MyTracksTest/libs/robotium-solo-3.3.jar | Bin 0 -> 55662 bytes .../android/apps/mytracks/MapOverlayTest.java | 166 +- .../apps/mytracks/MockMyTracksOverlay.java | 12 +- .../android/apps/mytracks/MockPath.java | 29 +- .../android/apps/mytracks/MockProjection.java | 8 +- .../MyTracksProviderUtilsImplTest.java | 48 +- .../mytracks/content/TrackDataHubTest.java | 1240 +++++++------- .../endtoendtest/CreateAndSendTrackTest.java | 19 +- .../endtoendtest/EndToEndTestUtils.java | 40 +- .../DynamicSpeedTrackPathDescriptorTest.java | 38 +- .../DynamicSpeedTrackPathPainterTest.java | 6 +- .../maps/SingleColorTrackPathPainterTest.java | 30 +- .../TrackPathDescriptorDynamicSpeedTest.java | 9 +- .../TrackPathDescriptorFixedSpeedTest.java | 10 +- .../maps/TrackPathPainterFactoryTest.java | 9 +- .../maps/TrackPathPainterSingleColorTest.java | 7 +- 41 files changed, 2215 insertions(+), 2578 deletions(-) delete mode 100644 MyTracks/src/com/google/android/apps/mytracks/content/ListenerState.java rename MyTracks/src/com/google/android/apps/mytracks/maps/{TrackPathUtilities.java => TrackPathUtils.java} (71%) delete mode 100644 MyTracksTest/libs/robotium-solo-3.1.jar create mode 100644 MyTracksTest/libs/robotium-solo-3.3.jar diff --git a/MyTracks/src/com/google/android/apps/mytracks/ChartView.java b/MyTracks/src/com/google/android/apps/mytracks/ChartView.java index 0c5588245..4f6cd3c85 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/ChartView.java +++ b/MyTracks/src/com/google/android/apps/mytracks/ChartView.java @@ -81,9 +81,6 @@ public class ChartView extends View { private static final int SPACER = 4; private static final int Y_AXIS_OFFSET = 16; - // Marker pin x position / marker icon width - private static final double MARKER_PIN_X_POSITION_PERCENTAGE = 13 / 48.0; - private final ChartValueSeries[] series = new ChartValueSeries[NUM_SERIES]; private final ArrayList chartData = new ArrayList(); private final ArrayList waypoints = new ArrayList(); @@ -559,8 +556,8 @@ public class ChartView extends View { float x = getX(getWaypointXValue(waypoint)); canvas.drawLine( x, topBorder + spacer + markerHeight / 2, x, topBorder + effectiveHeight, markerPaint); - canvas.translate( - x - (float) (markerWidth * MARKER_PIN_X_POSITION_PERCENTAGE), topBorder + spacer); + canvas.translate(x - (float) (markerWidth * Constants.WAYPOINT_X_OFFSET_PERCENTAGE), + topBorder + spacer); if (waypoints.get(i).getType() == Waypoint.TYPE_STATISTICS) { statisticsMarker.draw(canvas); } else { diff --git a/MyTracks/src/com/google/android/apps/mytracks/Constants.java b/MyTracks/src/com/google/android/apps/mytracks/Constants.java index 371e26333..0df851792 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/Constants.java +++ b/MyTracks/src/com/google/android/apps/mytracks/Constants.java @@ -126,6 +126,10 @@ public abstract class Constants { public static final String SETTINGS_NAME = "SettingsActivity"; + public static final double WAYPOINT_X_OFFSET_PERCENTAGE = 13 / 48.0; + + public static final double MARKER_Y_OFFSET_PERCENTAGE = 91 / 96.0; + /** * This is an abstract utility class. */ diff --git a/MyTracks/src/com/google/android/apps/mytracks/MapOverlay.java b/MyTracks/src/com/google/android/apps/mytracks/MapOverlay.java index a7b3ef48c..f8d74bb02 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/MapOverlay.java +++ b/MyTracks/src/com/google/android/apps/mytracks/MapOverlay.java @@ -13,6 +13,7 @@ * License for the specific language governing permissions and limitations under * the License. */ + package com.google.android.apps.mytracks; import static com.google.android.apps.mytracks.Constants.TAG; @@ -20,7 +21,7 @@ import static com.google.android.apps.mytracks.Constants.TAG; import com.google.android.apps.mytracks.content.Waypoint; import com.google.android.apps.mytracks.maps.TrackPathPainter; import com.google.android.apps.mytracks.maps.TrackPathPainterFactory; -import com.google.android.apps.mytracks.maps.TrackPathUtilities; +import com.google.android.apps.mytracks.maps.TrackPathUtils; import com.google.android.apps.mytracks.util.IntentUtils; import com.google.android.apps.mytracks.util.LocationUtils; import com.google.android.apps.mytracks.util.PreferencesUtils; @@ -30,6 +31,7 @@ import com.google.android.maps.MapView; import com.google.android.maps.Overlay; import com.google.android.maps.Projection; import com.google.android.maps.mytracks.R; +import com.google.common.annotations.VisibleForTesting; import android.content.Context; import android.content.Intent; @@ -50,47 +52,47 @@ import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; /** - * A map overlay that displays a "MyLocation" arrow, an error circle, the - * currently recording track and optionally a selected track. - * + * A map overlay that displays my location arrow, error circle, and track info. + * * @author Leif Hendrik Wilden */ public class MapOverlay extends Overlay implements OnSharedPreferenceChangeListener { - private final Drawable[] arrows; - private final int arrowWidth, arrowHeight; - private final Drawable statsMarker; - private final Drawable waypointMarker; - private final Drawable startMarker; - private final Drawable endMarker; - private final int markerWidth, markerHeight; - private final Paint errorCirclePaint; private final Context context; private final List waypoints; private final List points; private final BlockingQueue pendingPoints; + private final Drawable[] arrows; + private final int arrowWidth; + private final int arrowHeight; + private final Drawable statsMarker; + private final Drawable waypointMarker; + private final Drawable startMarker; + private final Drawable endMarker; + private final int markerWidth; + private final int markerHeight; + private final Paint errorCirclePaint; + private TrackPathPainter trackPathPainter; private boolean trackDrawingEnabled; - private int lastHeading = 0; - private Location myLocation; private boolean showEndMarker = true; - // TODO: Remove it completely after completing performance tests. - private boolean alwaysVisible = true; + private int headingIndex = 0; + private Location myLocation; private GeoPoint lastReferencePoint; private Rect lastViewRect; - private boolean lastPathExists; - private TrackPathPainter trackPathPainter; /** - * Represents a pre-processed {@code Location} to speed up drawing. - * This class is more like a data object and doesn't provide accessors. + * A pre-processed {@link Location} to speed up drawing. + * + * @author Jimmy Shih */ public static class CachedLocation { - public final boolean valid; - public final GeoPoint geoPoint; - public final int speed; - + + private final boolean valid; + private final GeoPoint geoPoint; + private final int speed; + /** * Constructor for an invalid cached location. */ @@ -108,41 +110,53 @@ public class MapOverlay extends Overlay implements OnSharedPreferenceChangeListe this.geoPoint = valid ? LocationUtils.getGeoPoint(location) : null; this.speed = (int) Math.floor(location.getSpeed() * UnitConversions.MS_TO_KMH); } + + /** + * Returns true if the location is valid. + */ + public boolean isValid() { + return valid; + } + + /** + * Gets the {@link GeoPoint}. + */ + public GeoPoint getGeoPoint() { + return geoPoint; + } + + /** + * Gets the speed in kilometers per hour. + */ + public int getSpeed() { + return speed; + } }; public MapOverlay(Context context) { this.context = context; - this.waypoints = new ArrayList(); this.points = new ArrayList(1024); this.pendingPoints = new ArrayBlockingQueue( Constants.MAX_DISPLAYED_TRACK_POINTS, true); - // TODO: Can we use a FrameAnimation or similar here rather - // than individual resources for each arrow direction? + /* + * TODO: Use animation rather than individual resources for each arrow + * direction. + */ final Resources resources = context.getResources(); - arrows = new Drawable[] { - resources.getDrawable(R.drawable.arrow_0), - resources.getDrawable(R.drawable.arrow_20), - resources.getDrawable(R.drawable.arrow_40), - resources.getDrawable(R.drawable.arrow_60), - resources.getDrawable(R.drawable.arrow_80), - resources.getDrawable(R.drawable.arrow_100), - resources.getDrawable(R.drawable.arrow_120), - resources.getDrawable(R.drawable.arrow_140), - resources.getDrawable(R.drawable.arrow_160), - resources.getDrawable(R.drawable.arrow_180), - resources.getDrawable(R.drawable.arrow_200), - resources.getDrawable(R.drawable.arrow_220), - resources.getDrawable(R.drawable.arrow_240), - resources.getDrawable(R.drawable.arrow_260), - resources.getDrawable(R.drawable.arrow_280), - resources.getDrawable(R.drawable.arrow_300), - resources.getDrawable(R.drawable.arrow_320), - resources.getDrawable(R.drawable.arrow_340) - }; - arrowWidth = arrows[lastHeading].getIntrinsicWidth(); - arrowHeight = arrows[lastHeading].getIntrinsicHeight(); + arrows = new Drawable[] { resources.getDrawable(R.drawable.arrow_0), + resources.getDrawable(R.drawable.arrow_20), resources.getDrawable(R.drawable.arrow_40), + resources.getDrawable(R.drawable.arrow_60), resources.getDrawable(R.drawable.arrow_80), + resources.getDrawable(R.drawable.arrow_100), resources.getDrawable(R.drawable.arrow_120), + resources.getDrawable(R.drawable.arrow_140), resources.getDrawable(R.drawable.arrow_160), + resources.getDrawable(R.drawable.arrow_180), resources.getDrawable(R.drawable.arrow_200), + resources.getDrawable(R.drawable.arrow_220), resources.getDrawable(R.drawable.arrow_240), + resources.getDrawable(R.drawable.arrow_260), resources.getDrawable(R.drawable.arrow_280), + resources.getDrawable(R.drawable.arrow_300), resources.getDrawable(R.drawable.arrow_320), + resources.getDrawable(R.drawable.arrow_340) }; + arrowWidth = arrows[headingIndex].getIntrinsicWidth(); + arrowHeight = arrows[headingIndex].getIntrinsicHeight(); for (Drawable arrow : arrows) { arrow.setBounds(0, 0, arrowWidth, arrowHeight); } @@ -152,36 +166,36 @@ public class MapOverlay extends Overlay implements OnSharedPreferenceChangeListe markerHeight = statsMarker.getIntrinsicHeight(); statsMarker.setBounds(0, 0, markerWidth, markerHeight); + waypointMarker = resources.getDrawable(R.drawable.blue_pushpin); + waypointMarker.setBounds(0, 0, markerWidth, markerHeight); + startMarker = resources.getDrawable(R.drawable.green_dot); startMarker.setBounds(0, 0, markerWidth, markerHeight); endMarker = resources.getDrawable(R.drawable.red_dot); endMarker.setBounds(0, 0, markerWidth, markerHeight); - waypointMarker = resources.getDrawable(R.drawable.blue_pushpin); - waypointMarker.setBounds(0, 0, markerWidth, markerHeight); - - errorCirclePaint = TrackPathUtilities.getPaint(R.color.blue, context); + errorCirclePaint = TrackPathUtils.getPaint(context, R.color.blue); errorCirclePaint.setAlpha(127); - + trackPathPainter = TrackPathPainterFactory.getTrackPathPainter(context); - + context.getSharedPreferences(Constants.SETTINGS_NAME, Context.MODE_PRIVATE) .registerOnSharedPreferenceChangeListener(this); } /** * Add a location to the map overlay. - * + *

* NOTE: This method doesn't take ownership of the given location, so it is * safe to reuse the same location while calling this method. - * - * @param l the location to add. + * + * @param location the location */ - public void addLocation(Location l) { - // Queue up in the pending queue until it's merged with {@code #points}. - if (!pendingPoints.offer(new CachedLocation(l))) { - Log.e(TAG, "Unable to add pending points"); + public void addLocation(Location location) { + // Queue up in the pendingPoints until it's merged with points. + if (!pendingPoints.offer(new CachedLocation(location))) { + Log.e(TAG, "Unable to add to pendingPoints."); } } @@ -189,81 +203,125 @@ public class MapOverlay extends Overlay implements OnSharedPreferenceChangeListe * Adds a segment split to the map overlay. */ public void addSegmentSplit() { + // Queue up in the pendingPoints until it's merged with points. if (!pendingPoints.offer(new CachedLocation())) { - Log.e(TAG, "Unable to add pending points"); + Log.e(TAG, "Unable to add to pendingPoints"); } } - public void addWaypoint(Waypoint wpt) { - // Note: We don't cache waypoints, because it's not worth the effort. - if (wpt != null && wpt.getLocation() != null) { + /** + * Clears the locations. + */ + public void clearPoints() { + synchronized (points) { + points.clear(); + pendingPoints.clear(); + trackPathPainter.clearPath(); + lastReferencePoint = null; + lastViewRect = null; + } + } + + /** + * Adds a waypoint to the map overlay. + * + * @param waypoint the waypoint + */ + public void addWaypoint(Waypoint waypoint) { + // Note: We don't cache waypoints because it's not worth the effort. + if (waypoint != null && waypoint.getLocation() != null) { synchronized (waypoints) { - waypoints.add(wpt); + waypoints.add(waypoint); } } } - public int getNumLocations() { - synchronized (points) { - return points.size() + pendingPoints.size(); - } - } - - // Visible for testing. - public int getNumWaypoints() { - synchronized (waypoints) { - return waypoints.size(); - } - } - - public void clearPoints() { - synchronized (getPoints()) { - getPoints().clear(); - pendingPoints.clear(); - lastPathExists = false; - lastViewRect = null; - trackPathPainter.clear(); - } - } - + /** + * Clears the waypoints. + */ public void clearWaypoints() { synchronized (waypoints) { waypoints.clear(); } } + /** + * Sets whether to draw the track or not. + * + * @param trackDrawingEnabled true to draw track + */ public void setTrackDrawingEnabled(boolean trackDrawingEnabled) { this.trackDrawingEnabled = trackDrawingEnabled; } + /** + * Sets whether to draw the end maker or not. + * + * @param showEndMarker true to draw end marker + */ public void setShowEndMarker(boolean showEndMarker) { this.showEndMarker = showEndMarker; } + /** + * Sets my location. + * + * @param myLocation my location + */ + public void setMyLocation(Location myLocation) { + this.myLocation = myLocation; + } + + /** + * Sets the heading. + * + * @param heading the heading + * @return true if the visible heading has changed. + */ + public boolean setHeading(float heading) { + /* + * Use -heading because the arrow images are counter-clockwise rather than + * clockwise. + */ + int index = Math.round(-heading / 360 * 18); + while (index < 0) { + index += 18; + } + while (index > 17) { + index -= 18; + } + if (index != headingIndex) { + headingIndex = index; + return true; + } else { + return false; + } + } + @Override public void draw(Canvas canvas, MapView mapView, boolean shadow) { if (shadow) { return; } - // It's safe to keep projection within a single draw operation. - final Projection projection = getMapProjection(mapView); + // It's safe to keep projection within a single draw operation + Projection projection = getMapProjection(mapView); if (projection == null) { - Log.w(TAG, "No projection, unable to draw"); + Log.w(TAG, "No projection, unable to draw."); return; } - // Get the current viewing window. if (trackDrawingEnabled) { + // Get the current viewing Rect Rect viewRect = getMapViewRect(mapView); - // Draw the selected track: + // Draw the selected track drawTrack(canvas, projection, viewRect); - // Draw the "Start" and "End" markers: + // Draw the start and end markers drawMarkers(canvas, projection); - - // Draw the waypoints: + + // Draw the waypoints drawWaypoints(canvas, projection); } @@ -271,164 +329,19 @@ public class MapOverlay extends Overlay implements OnSharedPreferenceChangeListe drawMyLocation(canvas, projection); } - private void drawMarkers(Canvas canvas, Projection projection) { - // Draw the "End" marker. - if (showEndMarker) { - for (int i = getPoints().size() - 1; i >= 0; --i) { - if (getPoints().get(i).valid) { - drawElement(canvas, projection, getPoints().get(i).geoPoint, endMarker, - -markerWidth / 2, -markerHeight); - break; - } - } - } - - // Draw the "Start" marker. - for (int i = 0; i < getPoints().size(); ++i) { - if (getPoints().get(i).valid) { - drawElement(canvas, projection, getPoints().get(i).geoPoint, startMarker, - -markerWidth / 2, -markerHeight); - break; - } - } - } - - // Visible for testing. - Projection getMapProjection(MapView mapView) { - return mapView.getProjection(); - } - - // Visible for testing. - Rect getMapViewRect(MapView mapView) { - int w = mapView.getLongitudeSpan(); - int h = mapView.getLatitudeSpan(); - int cx = mapView.getMapCenter().getLongitudeE6(); - int cy = mapView.getMapCenter().getLatitudeE6(); - return new Rect(cx - w / 2, cy - h / 2, cx + w / 2, cy + h / 2); - } - - // For use in testing only. - public TrackPathPainter getTrackPathPainter() { - return trackPathPainter; - } - - // For use in testing only. - public void setTrackPathPainter(TrackPathPainter trackPathPainter) { - this.trackPathPainter = trackPathPainter; - } - - private void drawWaypoints(Canvas canvas, Projection projection) { - synchronized (waypoints) {; - for (Waypoint wpt : waypoints) { - Location loc = wpt.getLocation(); - drawElement(canvas, projection, LocationUtils.getGeoPoint(loc), - wpt.getType() == Waypoint.TYPE_STATISTICS ? statsMarker - : waypointMarker, -(markerWidth / 2) + 3, -markerHeight); - } - } - } - - private void drawMyLocation(Canvas canvas, Projection projection) { - // Draw the arrow icon. - if (myLocation == null) { - return; - } - - Point pt = drawElement(canvas, projection, - LocationUtils.getGeoPoint(myLocation), arrows[lastHeading], - -(arrowWidth / 2) + 3, -(arrowHeight / 2)); - // Draw the error circle. - float radius = projection.metersToEquatorPixels(myLocation.getAccuracy()); - canvas.drawCircle(pt.x, pt.y, radius, errorCirclePaint); - } - - private void drawTrack(Canvas canvas, Projection projection, Rect viewRect) - { - boolean draw; - - synchronized (points) { - // Merge the pending points with the list of cached locations. - final GeoPoint referencePoint = projection.fromPixels(0, 0); - int newPoints = pendingPoints.drainTo(points); - boolean newProjection = !viewRect.equals(lastViewRect) || - !referencePoint.equals(lastReferencePoint); - if (newPoints == 0 && lastPathExists && !newProjection) { - // No need to recreate path (same points and viewing area). - draw = true; - } else { - int numPoints = points.size(); - if (numPoints < 2) { - // Not enough points to draw a path. - draw = false; - } else if (!trackPathPainter.needsRedraw() && lastPathExists && !newProjection) { - // Incremental update of the path, without repositioning the view. - draw = true; - trackPathPainter.updatePath(projection, viewRect, numPoints - newPoints, alwaysVisible, points); - } else { - // The view has changed so we have to start from scratch. - draw = true; - trackPathPainter.updatePath(projection, viewRect, 0, alwaysVisible, points); - } - } - lastReferencePoint = referencePoint; - lastViewRect = viewRect; - } - if (draw) { - trackPathPainter.drawTrack(canvas); - } - } - - // Visible for testing. - Point drawElement(Canvas canvas, Projection projection, GeoPoint geoPoint, - Drawable element, int offsetX, int offsetY) { - Point pt = new Point(); - projection.toPixels(geoPoint, pt); - canvas.save(); - canvas.translate(pt.x + offsetX, pt.y + offsetY); - element.draw(canvas); - canvas.restore(); - return pt; - } - - /** - * Sets the pointer location (will be drawn on next invalidate). - */ - public void setMyLocation(Location myLocation) { - this.myLocation = myLocation; - } - - /** - * Sets the pointer heading in degrees (will be drawn on next invalidate). - * - * @return true if the visible heading changed (i.e. a redraw of pointer is - * potentially necessary) - */ - public boolean setHeading(float heading) { - int newhdg = Math.round(-heading / 360 * 18 + 180); - while (newhdg < 0) - newhdg += 18; - while (newhdg > 17) - newhdg -= 18; - if (newhdg != lastHeading) { - lastHeading = newhdg; - return true; - } else { - return false; - } - } - @Override - public boolean onTap(GeoPoint p, MapView mapView) { - if (p.equals(mapView.getMapCenter())) { - // There is (unfortunately) no good way to determine whether the tap was - // caused by an actual tap on the screen or the track ball. If the - // location is equal to the map center,then it was a track ball press with - // very high likelihood. + public boolean onTap(GeoPoint geoPoint, MapView mapView) { + if (geoPoint.equals(mapView.getMapCenter())) { + /* + * There is (unfortunately) no good way to determine whether the tap was + * caused by an actual tap on the screen or the track ball. If the + * location is equal to the map center,then it was a track ball press with + * very high likelihood. + */ return false; } - - final Location tapLocation = LocationUtils.getLocation(p); - double dmin = Double.MAX_VALUE; + final Location tapLocation = LocationUtils.getLocation(geoPoint); + double minDistance = Double.MAX_VALUE; Waypoint waypoint = null; synchronized (waypoints) { for (int i = 0; i < waypoints.size(); i++) { @@ -436,35 +349,231 @@ public class MapOverlay extends Overlay implements OnSharedPreferenceChangeListe if (waypointLocation == null) { continue; } - final double d = waypointLocation.distanceTo(tapLocation); - if (d < dmin) { - dmin = d; + final double distance = waypointLocation.distanceTo(tapLocation); + if (distance < minDistance) { + minDistance = distance; waypoint = waypoints.get(i); } } } - if (waypoint != null && dmin < 15000000 / Math.pow(2, mapView.getZoomLevel())) { + if (waypoint != null && minDistance < 15000000 / Math.pow(2, mapView.getZoomLevel())) { Intent intent = IntentUtils.newIntent(context, MarkerDetailActivity.class) .putExtra(MarkerDetailActivity.EXTRA_MARKER_ID, waypoint.getId()); context.startActivity(intent); return true; } - return super.onTap(p, mapView); + return super.onTap(geoPoint, mapView); } - - /** - * @return the points - */ - public List getPoints() { - return points; - } - + @Override public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { - Log.d(TAG, "MapOverlay: onSharedPreferences changed " + key); if (PreferencesUtils.getKey(context, R.string.track_color_mode_key).equals(key)) { trackPathPainter = TrackPathPainterFactory.getTrackPathPainter(context); } } + + /** + * Gets the points. + */ + @VisibleForTesting + public List getPoints() { + return points; + } + + /** + * Gets the track path painter. + */ + @VisibleForTesting + public TrackPathPainter getTrackPathPainter() { + return trackPathPainter; + } + + /** + * Sets the track path painter. + * + * @param trackPathPainter the track path painter + */ + @VisibleForTesting + public void setTrackPathPainter(TrackPathPainter trackPathPainter) { + this.trackPathPainter = trackPathPainter; + } + + /** + * Gets the map view projection. + * + * @param mapView the map view + */ + @VisibleForTesting + protected Projection getMapProjection(MapView mapView) { + return mapView.getProjection(); + } + + /** + * Gets the map view Rect. + * + * @param mapView the map view + */ + @VisibleForTesting + protected Rect getMapViewRect(MapView mapView) { + int width = mapView.getLongitudeSpan(); + int height = mapView.getLatitudeSpan(); + int centerX = mapView.getMapCenter().getLongitudeE6(); + int centerY = mapView.getMapCenter().getLatitudeE6(); + return new Rect(centerX - width / 2, centerY - height / 2, centerX + width / 2, centerY + height + / 2); + } + + /** + * Gets number of locations. + */ + @VisibleForTesting + int getNumLocations() { + synchronized (points) { + return points.size() + pendingPoints.size(); + } + } + + /** + * Gets number of waypoints. + */ + @VisibleForTesting + int getNumWaypoints() { + synchronized (waypoints) { + return waypoints.size(); + } + } + + /** + * Draws the track. + * + * @param canvas the canvas + * @param projection the projection + * @param viewRect the view rect + */ + private void drawTrack(Canvas canvas, Projection projection, Rect viewRect) { + boolean draw; + + synchronized (points) { + // Merge the pending points with the list of cached locations. + GeoPoint referencePoint = projection.fromPixels(0, 0); + int newPoints = pendingPoints.drainTo(points); + boolean newProjection = !viewRect.equals(lastViewRect) + || !referencePoint.equals(lastReferencePoint); + // Call updateState first to trigger its side effects. + boolean currentPathValid = !trackPathPainter.updateState() && !newProjection + && trackPathPainter.hasPath(); + if (newPoints == 0 && currentPathValid) { + // No need to update + draw = true; + } else { + int numPoints = points.size(); + if (numPoints < 2) { + // Not enough points to draw a path + draw = false; + } else if (currentPathValid) { + // Incremental update of the path + draw = true; + trackPathPainter.updatePath(projection, viewRect, numPoints - newPoints, points); + } else { + // Reload the path + draw = true; + trackPathPainter.clearPath(); + trackPathPainter.updatePath(projection, viewRect, 0, points); + } + } + lastReferencePoint = referencePoint; + lastViewRect = viewRect; + } + if (draw) { + trackPathPainter.drawPath(canvas); + } + } + + /** + * Draws the start and end markers. + * + * @param canvas the canvas + * @param projection the projection + */ + private void drawMarkers(Canvas canvas, Projection projection) { + int offsetY = (int) (markerHeight * Constants.MARKER_Y_OFFSET_PERCENTAGE); + // Draw the end marker + if (showEndMarker) { + for (int i = points.size() - 1; i >= 0; i--) { + if (points.get(i).valid) { + drawElement( + canvas, projection, points.get(i).geoPoint, endMarker, -markerWidth / 2, -offsetY); + break; + } + } + } + + // Draw the start marker + for (int i = 0; i < points.size(); i++) { + if (points.get(i).valid) { + drawElement( + canvas, projection, points.get(i).geoPoint, startMarker, -markerWidth / 2, -offsetY); + break; + } + } + } + + /** + * Draws the waypoints. + * + * @param canvas the canvas + * @param projection the projection + */ + private void drawWaypoints(Canvas canvas, Projection projection) { + synchronized (waypoints) { + int offsetX = (int) (markerWidth * Constants.WAYPOINT_X_OFFSET_PERCENTAGE); + for (Waypoint waypoint : waypoints) { + Location location = waypoint.getLocation(); + Drawable drawable = waypoint.getType() == Waypoint.TYPE_STATISTICS ? statsMarker + : waypointMarker; + drawElement(canvas, projection, LocationUtils.getGeoPoint(location), drawable, -offsetX, + -markerHeight); + } + } + } + + /** + * Draws my location. + * + * @param canvas the canvas + * @param projection the projection + */ + private void drawMyLocation(Canvas canvas, Projection projection) { + if (myLocation == null) { + return; + } + Point point = drawElement(canvas, projection, LocationUtils.getGeoPoint(myLocation), + arrows[headingIndex], -(arrowWidth / 2), -(arrowHeight / 2)); + // Draw the error circle + float radius = projection.metersToEquatorPixels(myLocation.getAccuracy()); + canvas.drawCircle(point.x, point.y, radius, errorCirclePaint); + } + + /** + * Draws an element. + * + * @param canvas the canvas + * @param projection the projection + * @param geoPoint the geo point + * @param drawable the drawable + * @param offsetX the x offset + * @param offsetY the y offset + * @return the point of the drawing. + */ + private Point drawElement(Canvas canvas, Projection projection, GeoPoint geoPoint, + Drawable drawable, int offsetX, int offsetY) { + Point point = new Point(); + projection.toPixels(geoPoint, point); + canvas.save(); + canvas.translate(point.x + offsetX, point.y + offsetY); + drawable.draw(canvas); + canvas.restore(); + return point; + } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/MyTracksApplication.java b/MyTracks/src/com/google/android/apps/mytracks/MyTracksApplication.java index 92610c570..2788cd88d 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/MyTracksApplication.java +++ b/MyTracks/src/com/google/android/apps/mytracks/MyTracksApplication.java @@ -15,7 +15,6 @@ */ package com.google.android.apps.mytracks; -import com.google.android.apps.mytracks.content.TrackDataHub; import com.google.android.apps.mytracks.services.RemoveTempFilesService; import com.google.android.apps.mytracks.util.AnalyticsUtils; import com.google.android.apps.mytracks.util.ApiAdapterFactory; @@ -26,13 +25,11 @@ import android.content.Intent; /** * MyTracksApplication for keeping global state. - * + * * @author Jimmy Shih */ public class MyTracksApplication extends Application { - private TrackDataHub trackDataHub; - @Override public void onCreate() { super.onCreate(); @@ -43,16 +40,4 @@ public class MyTracksApplication extends Application { Intent intent = new Intent(this, RemoveTempFilesService.class); startService(intent); } - - /** - * Gets the application's TrackDataHub. - * - * Note: use synchronized to make sure only one instance is created per application. - */ - public synchronized TrackDataHub getTrackDataHub() { - if (trackDataHub == null) { - trackDataHub = TrackDataHub.newInstance(getApplicationContext()); - } - return trackDataHub; - } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/TrackDetailActivity.java b/MyTracks/src/com/google/android/apps/mytracks/TrackDetailActivity.java index 058bd574c..a476ec7fd 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/TrackDetailActivity.java +++ b/MyTracks/src/com/google/android/apps/mytracks/TrackDetailActivity.java @@ -137,8 +137,7 @@ public class TrackDetailActivity extends AbstractMyTracksActivity implements Del getSharedPreferences(Constants.SETTINGS_NAME, Context.MODE_PRIVATE) .registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener); trackRecordingServiceConnection = new TrackRecordingServiceConnection(this, null); - trackDataHub = ((MyTracksApplication) getApplication()).getTrackDataHub(); - trackDataHub.loadTrack(trackId); + trackDataHub = TrackDataHub.newInstance(this); mapViewContainer = getLayoutInflater().inflate(R.layout.map, null); ApiAdapterFactory.getApiAdapter().disableHardwareAccelerated(mapViewContainer); @@ -167,7 +166,6 @@ public class TrackDetailActivity extends AbstractMyTracksActivity implements Del public void onNewIntent(Intent intent) { setIntent(intent); handleIntent(intent); - trackDataHub.loadTrack(trackId); showMarker(); } @@ -180,6 +178,7 @@ public class TrackDetailActivity extends AbstractMyTracksActivity implements Del @Override protected void onResume() { super.onResume(); + trackDataHub.loadTrack(trackId); TrackRecordingServiceConnectionUtils.resume(this, trackRecordingServiceConnection); setTitle(trackId == PreferencesUtils.getLong(this, R.string.recording_track_id_key)); } @@ -350,12 +349,19 @@ public class TrackDetailActivity extends AbstractMyTracksActivity implements Del } /** - * @return the mapViewContainer + * Gets the map view container. */ public View getMapViewContainer() { return mapViewContainer; } + /** + * Gets the {@link TrackDataHub}. + */ + public TrackDataHub getTrackDataHub() { + return trackDataHub; + } + /** * Handles the data in the intent. */ diff --git a/MyTracks/src/com/google/android/apps/mytracks/content/DataSource.java b/MyTracks/src/com/google/android/apps/mytracks/content/DataSource.java index 5cda864d7..6c82e9a0a 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/content/DataSource.java +++ b/MyTracks/src/com/google/android/apps/mytracks/content/DataSource.java @@ -149,25 +149,25 @@ public class DataSource { } /** - * Registers a compass listener. + * Registers a heading listener. * * @param listener the listener */ - public void registerCompassListener(SensorEventListener listener) { - Sensor compass = sensorManager.getDefaultSensor(Sensor.TYPE_ORIENTATION); - if (compass == null) { - Log.d(TAG, "No compass sensor."); + public void registerHeadingListener(SensorEventListener listener) { + Sensor heading = sensorManager.getDefaultSensor(Sensor.TYPE_ORIENTATION); + if (heading == null) { + Log.d(TAG, "No heading sensor."); return; } - sensorManager.registerListener(listener, compass, SensorManager.SENSOR_DELAY_UI); + sensorManager.registerListener(listener, heading, SensorManager.SENSOR_DELAY_UI); } /** - * Unregisters a compass listener. + * Unregisters a heading listener. * * @param listener the listener */ - public void unregisterCompassListener(SensorEventListener listener) { + public void unregisterHeadingListener(SensorEventListener listener) { sensorManager.unregisterListener(listener); } diff --git a/MyTracks/src/com/google/android/apps/mytracks/content/DataSourceManager.java b/MyTracks/src/com/google/android/apps/mytracks/content/DataSourceManager.java index 8da7841a7..03417a6a0 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/content/DataSourceManager.java +++ b/MyTracks/src/com/google/android/apps/mytracks/content/DataSourceManager.java @@ -133,11 +133,11 @@ public class DataSourceManager { } /** - * Listener for compass changes. + * Listener for heading changes. * * @author Jimmy Shih */ - private class CompassListener implements SensorEventListener { + private class HeadingListener implements SensorEventListener { @Override public void onAccuracyChanged(Sensor sensor, int accuracy) { @@ -174,7 +174,7 @@ public class DataSourceManager { private final WaypointsTableObserver waypointsTableObserver; private final TrackPointsTableObserver trackPointsTableObserver; private final CurrentLocationListener currentLocationListener; - private final CompassListener compassListener; + private final HeadingListener headingListener; private final PreferenceListener preferenceListener; public DataSourceManager(DataSource dataSource, DataSourceListener dataSourceListener) { @@ -186,7 +186,7 @@ public class DataSourceManager { waypointsTableObserver = new WaypointsTableObserver(); trackPointsTableObserver = new TrackPointsTableObserver(); currentLocationListener = new CurrentLocationListener(); - compassListener = new CompassListener(); + headingListener = new HeadingListener(); preferenceListener = new PreferenceListener(); } @@ -202,9 +202,9 @@ public class DataSourceManager { * Map SAMPLED_OUT_POINT_UPDATES to POINT_UPDATES since they correspond to * the same internal listener */ - if (neededListeners.contains(TrackDataType.SAMPLED_OUT_TRACK_POINTS)) { - neededListeners.remove(TrackDataType.SAMPLED_OUT_TRACK_POINTS); - neededListeners.add(TrackDataType.TRACK_POINTS_TABLE); + if (neededListeners.contains(TrackDataType.SAMPLED_OUT_TRACK_POINTS_TABLE)) { + neededListeners.remove(TrackDataType.SAMPLED_OUT_TRACK_POINTS_TABLE); + neededListeners.add(TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE); } Log.d(TAG, "Updating listeners " + neededListeners); @@ -248,18 +248,18 @@ public class DataSourceManager { case WAYPOINTS_TABLE: dataSource.registerContentObserver(WaypointsColumns.CONTENT_URI, waypointsTableObserver); break; - case TRACK_POINTS_TABLE: + case SAMPLED_IN_TRACK_POINTS_TABLE: dataSource.registerContentObserver( TrackPointsColumns.CONTENT_URI, trackPointsTableObserver); break; - case SAMPLED_OUT_TRACK_POINTS: + case SAMPLED_OUT_TRACK_POINTS_TABLE: // Do nothing. SAMPLED_OUT_POINT_UPDATES is mapped to POINT_UPDATES. break; case LOCATION: dataSource.registerLocationListener(currentLocationListener); break; - case COMPASS: - dataSource.registerCompassListener(compassListener); + case HEADING: + dataSource.registerHeadingListener(headingListener); break; case PREFERENCE: dataSource.registerOnSharedPreferenceChangeListener(preferenceListener); @@ -285,17 +285,17 @@ public class DataSourceManager { case WAYPOINTS_TABLE: dataSource.unregisterContentObserver(waypointsTableObserver); break; - case TRACK_POINTS_TABLE: + case SAMPLED_IN_TRACK_POINTS_TABLE: dataSource.unregisterContentObserver(trackPointsTableObserver); break; - case SAMPLED_OUT_TRACK_POINTS: + case SAMPLED_OUT_TRACK_POINTS_TABLE: // Do nothing. SAMPLED_OUT_POINT_UPDATES is mapped to POINT_UPDATES. break; case LOCATION: dataSource.unregisterLocationListener(currentLocationListener); break; - case COMPASS: - dataSource.unregisterCompassListener(compassListener); + case HEADING: + dataSource.unregisterHeadingListener(headingListener); break; case PREFERENCE: dataSource.unregisterOnSharedPreferenceChangeListener(preferenceListener); diff --git a/MyTracks/src/com/google/android/apps/mytracks/content/ListenerState.java b/MyTracks/src/com/google/android/apps/mytracks/content/ListenerState.java deleted file mode 100644 index e1e3598a7..000000000 --- a/MyTracks/src/com/google/android/apps/mytracks/content/ListenerState.java +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Copyright 2011 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.content; - -import java.util.EnumSet; - -/** - * State for a registered {@link TrackDataListener}. - * - * @author Jimmy Shih - */ -public class ListenerState { - - private TrackDataListener trackDataListener; - private EnumSet trackDataTypes; - - private long lastTrackId; - private long lastPointId; - private int lastSamplingFrequency; - private int numberOfLoadedPoints; - - public ListenerState( - TrackDataListener trackDataListener, EnumSet trackDataTypes) { - this.trackDataListener = trackDataListener; - this.trackDataTypes = trackDataTypes; - } - - /** - * Gets the {@link TrackDataListener}. - */ - public TrackDataListener getTrackDataListener() { - return trackDataListener; - } - - /** - * Gets the track data types. - */ - public EnumSet getTrackDataTypes() { - return trackDataTypes; - } - - /** - * Gets the last track id. - */ - public long getLastTrackId() { - return lastTrackId; - } - - /** - * Gets the last point id. - */ - public long getLastPointId() { - return lastPointId; - } - - /** - * Gets the last sampling frequency. - */ - public int getLastSamplingFrequency() { - return lastSamplingFrequency; - } - - /** - * Get the number of loaded points. - */ - public int getNumberOfLoadedPoints() { - return numberOfLoadedPoints; - } - - /** - * Sets the listener state. - * - * @param trackId track id - * @param pointId point id - * @param samplingFrequency sampling frequency - * @param loadedPoints number of loaded points - */ - public void setState(long trackId, long pointId, int samplingFrequency, int loadedPoints) { - lastTrackId = trackId; - lastPointId = pointId; - lastSamplingFrequency = samplingFrequency; - numberOfLoadedPoints = loadedPoints; - } - - /** - * Resets state. - */ - public void resetState() { - setState(0L, 0L, 0, 0); - } -} \ No newline at end of file diff --git a/MyTracks/src/com/google/android/apps/mytracks/content/TrackDataHub.java b/MyTracks/src/com/google/android/apps/mytracks/content/TrackDataHub.java index 52c8c3c4a..dcd2189fc 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/content/TrackDataHub.java +++ b/MyTracks/src/com/google/android/apps/mytracks/content/TrackDataHub.java @@ -13,20 +13,22 @@ * License for the specific language governing permissions and limitations under * the License. */ + package com.google.android.apps.mytracks.content; import static com.google.android.apps.mytracks.Constants.MAX_DISPLAYED_WAYPOINTS_POINTS; import static com.google.android.apps.mytracks.Constants.MAX_LOCATION_AGE_MS; import static com.google.android.apps.mytracks.Constants.MAX_NETWORK_AGE_MS; -import static com.google.android.apps.mytracks.Constants.TAG; import static com.google.android.apps.mytracks.Constants.TARGET_DISPLAYED_TRACK_POINTS; +import com.google.android.apps.mytracks.Constants; import com.google.android.apps.mytracks.content.MyTracksProviderUtils.DoubleBufferedLocationFactory; import com.google.android.apps.mytracks.content.MyTracksProviderUtils.LocationIterator; -import com.google.android.apps.mytracks.content.TrackDataListener.ProviderState; +import com.google.android.apps.mytracks.content.TrackDataListener.LocationState; import com.google.android.apps.mytracks.util.LocationUtils; import com.google.android.apps.mytracks.util.PreferencesUtils; import com.google.android.maps.mytracks.R; +import com.google.common.annotations.VisibleForTesting; import android.content.Context; import android.database.Cursor; @@ -42,1020 +44,734 @@ import java.util.EnumSet; import java.util.Set; /** - * Track data hub, which receives data (both live and recorded) from many - * different sources and distributes it to those interested after some standard - * processing. - * - * TODO: Simplify the threading model here, it's overly complex and it's not obvious why - * certain race conditions won't happen. - * + * Track data hub. Receives data from {@link DataSource} and distributes it to + * {@link TrackDataListener} after some processing. + * * @author Rodrigo Damazio */ -public class TrackDataHub { +public class TrackDataHub implements DataSourceListener { - // Overridable constants - private final int targetNumPoints; + private static final String TAG = TrackDataHub.class.getSimpleName(); - /** Listener which receives events from the system. */ - private class HubDataSourceListener implements DataSourceListener { - @Override - public void notifyTracksTableUpdated() { - TrackDataHub.this.notifyTrackUpdated(getListenersFor(TrackDataType.TRACKS_TABLE)); - } + // One hour in milliseconds + private static final int ONE_HOUR = 60 * 60 * 1000; - @Override - public void notifyWaypointsTableUpdated() { - TrackDataHub.this.notifyWaypointUpdated(getListenersFor(TrackDataType.WAYPOINTS_TABLE)); - } - - @Override - public void notifyTrackPointsTableUpdated() { - TrackDataHub.this.notifyPointsUpdated(true, 0, 0, - getListenersFor(TrackDataType.TRACK_POINTS_TABLE), - getListenersFor(TrackDataType.SAMPLED_OUT_TRACK_POINTS)); - } - - @Override - public void notifyPreferenceChanged(String key) { - TrackDataHub.this.notifyPreferenceChanged(key); - } - - @Override - public void notifyLocationProviderEnabled(boolean enabled) { - hasProviderEnabled = enabled; - TrackDataHub.this.notifyFixType(); - } - - @Override - public void notifyLocationProviderAvailable(boolean available) { - hasFix = available; - TrackDataHub.this.notifyFixType(); - } - - @Override - public void notifyLocationChanged(Location loc) { - TrackDataHub.this.notifyLocationChanged(loc, - getListenersFor(TrackDataType.LOCATION)); - } - - @Override - public void notifyHeadingChanged(float heading) { - lastSeenMagneticHeading = heading; - maybeUpdateDeclination(); - TrackDataHub.this.notifyHeadingChanged(getListenersFor(TrackDataType.COMPASS)); - } - } - - // Application services private final Context context; - private final MyTracksProviderUtils providerUtils; - - // Get content notifications on the main thread, send listener callbacks in another. - // This ensures listener calls are serialized. - private HandlerThread listenerHandlerThread; - private Handler listenerHandler; - - /** Manager for external listeners (those from activities). */ private final TrackDataManager trackDataManager; + private final MyTracksProviderUtils myTracksProviderUtils; + private final int targetNumPoints; + private final DoubleBufferedLocationFactory locationFactory; + private boolean started; + private HandlerThread handlerThread; + private Handler handler; private DataSource dataSource; private DataSourceManager dataSourceManager; - /** Condensed listener for system data listener events. */ - private final DataSourceListener dataSourceListener = new HubDataSourceListener(); - - // Cached preference values - private int minRequiredAccuracy; + // Preference values + private long selectedTrackId; private boolean metricUnits; private boolean reportSpeed; + private int minRequiredAccuracy; - // Cached sensor readings - private float declination; - private long lastDeclinationUpdate; - private float lastSeenMagneticHeading; + // Heading values + private float lastHeading = 0; + private float lastDeclination = 0; + private long lastDeclinationUpdate = 0; - // Cached GPS readings - private Location lastSeenLocation; + // Location values + private Location lastSeenLocation = null; private boolean hasProviderEnabled = true; - private boolean hasFix; - private boolean hasGoodFix; + private boolean hasFix = false; + private boolean hasGoodFix = false; - // Transient state about the selected track - private long selectedTrackId; + // Track points sampling state + private int numLoadedPoints; private long firstSeenLocationId; private long lastSeenLocationId; - private int numLoadedPoints; - private int lastSamplingFrequency; - private DoubleBufferedLocationFactory locationFactory; - - private boolean started = false; /** - * Builds a new {@link TrackDataHub} instance. + * Creates a new instance. */ public synchronized static TrackDataHub newInstance(Context context) { - MyTracksProviderUtils providerUtils = MyTracksProviderUtils.Factory.get(context); - return new TrackDataHub( - context, new TrackDataManager(), providerUtils, TARGET_DISPLAYED_TRACK_POINTS); + return new TrackDataHub(context, new TrackDataManager(), MyTracksProviderUtils.Factory.get( + context), TARGET_DISPLAYED_TRACK_POINTS); } /** - * Injection constructor. + * Constructor. + * + * @param context the context + * @param trackDataManager the track data manager + * @param myTracksProviderUtils the my tracks provider utils + * @param targetNumPoints the target number of points */ - // @VisibleForTesting - TrackDataHub(Context ctx, TrackDataManager trackDataManager, MyTracksProviderUtils providerUtils, - int targetNumPoints) { - this.context = ctx; + @VisibleForTesting + TrackDataHub(Context context, TrackDataManager trackDataManager, + MyTracksProviderUtils myTracksProviderUtils, int targetNumPoints) { + this.context = context; this.trackDataManager = trackDataManager; - this.providerUtils = providerUtils; + this.myTracksProviderUtils = myTracksProviderUtils; this.targetNumPoints = targetNumPoints; this.locationFactory = new DoubleBufferedLocationFactory(); - - resetState(); + resetSamplingState(); } /** - * Starts listening to data sources and reporting the data to external - * listeners. + * Starts. */ public void start() { - Log.i(TAG, "TrackDataHub.start"); - if (isStarted()) { - Log.w(TAG, "Already started, ignoring"); + if (started) { + Log.i(TAG, "TrackDataHub already started, ignoring start."); return; } started = true; - - listenerHandlerThread = new HandlerThread("trackDataContentThread"); - listenerHandlerThread.start(); - listenerHandler = new Handler(listenerHandlerThread.getLooper()); + handlerThread = new HandlerThread("TrackDataHubHandlerThread"); + handlerThread.start(); + handler = new Handler(handlerThread.getLooper()); dataSource = newDataSource(); - dataSourceManager = new DataSourceManager(dataSource, dataSourceListener); + dataSourceManager = new DataSourceManager(dataSource, this); - // This may or may not register internal listeners, depending on whether - // we already had external listeners. - dataSourceManager.updateListeners(getNeededListenerTypes()); - loadSharedPreferences(); - - // If there were listeners already registered, make sure they become up-to-date. - loadDataForAllListeners(); - } - - // @VisibleForTesting - protected DataSource newDataSource() { - return new DataSource(context); + runInHanderThread(new Runnable() { + @Override + public void run() { + dataSourceManager.updateListeners(trackDataManager.getRegisteredTrackDataTypes()); + selectedTrackId = PreferencesUtils.getLong(context, R.string.selected_track_id_key); + metricUnits = PreferencesUtils.getBoolean( + context, R.string.metric_units_key, PreferencesUtils.METRIC_UNITS_DEFAULT); + reportSpeed = PreferencesUtils.getBoolean( + context, R.string.report_speed_key, PreferencesUtils.REPORT_SPEED_DEFAULT); + minRequiredAccuracy = PreferencesUtils.getInt(context, R.string.min_required_accuracy_key, + PreferencesUtils.MIN_REQUIRED_ACCURACY_DEFAULT); + loadDataForAll(); + } + }); } /** - * Stops listening to data sources and reporting the data to external - * listeners. + * Stops. */ public void stop() { - Log.i(TAG, "TrackDataHub.stop"); - if (!isStarted()) { - Log.w(TAG, "Not started, ignoring"); + if (!started) { + Log.i(TAG, "TrackDataHub not started, ignoring stop."); return; } - - // Unregister internal listeners even if there are external listeners registered. - dataSourceManager.unregisterAllListeners(); - listenerHandlerThread.getLooper().quit(); - started = false; + dataSourceManager.unregisterAllListeners(); + handlerThread.getLooper().quit(); + + handlerThread = null; + handler = null; dataSource = null; dataSourceManager = null; - listenerHandlerThread = null; - listenerHandler = null; - } - - private boolean isStarted() { - return started; - } - - @Override - protected void finalize() throws Throwable { - if (isStarted() || - (listenerHandlerThread != null && listenerHandlerThread.isAlive())) { - Log.e(TAG, "Forgot to stop() TrackDataHub"); - } - - super.finalize(); - } - - private void loadSharedPreferences() { - selectedTrackId = PreferencesUtils.getLong(context, R.string.selected_track_id_key); - metricUnits = PreferencesUtils.getBoolean( - context, R.string.metric_units_key, PreferencesUtils.METRIC_UNITS_DEFAULT); - reportSpeed = PreferencesUtils.getBoolean( - context, R.string.report_speed_key, PreferencesUtils.REPORT_SPEED_DEFAULT); - minRequiredAccuracy = PreferencesUtils.getInt(context, R.string.min_required_accuracy_key, - PreferencesUtils.MIN_REQUIRED_ACCURACY_DEFAULT); - } - - /** Updates known magnetic declination if needed. */ - private void maybeUpdateDeclination() { - if (lastSeenLocation == null) { - // We still don't know where we are. - return; - } - - // Update the declination every hour - long now = System.currentTimeMillis(); - if (now - lastDeclinationUpdate < 60 * 60 * 1000) { - return; - } - - lastDeclinationUpdate = now; - long timestamp = lastSeenLocation.getTime(); - if (timestamp == 0) { - // Hack for Samsung phones which don't populate the time field - timestamp = now; - } - - declination = getDeclinationFor(lastSeenLocation, timestamp); - Log.i(TAG, "Updated magnetic declination to " + declination); - } - - // @VisibleForTesting - protected float getDeclinationFor(Location location, long timestamp) { - GeomagneticField field = new GeomagneticField( - (float) location.getLatitude(), - (float) location.getLongitude(), - (float) location.getAltitude(), - timestamp); - return field.getDeclination(); } /** - * Forces the current location to be updated and reported to all listeners. - * The reported location may be from the network provider if the GPS provider - * is not available or doesn't have a fix. + * Loads a track. + * + * @param trackId the track id + */ + public void loadTrack(final long trackId) { + runInHanderThread(new Runnable() { + @Override + public void run() { + if (trackId == selectedTrackId) { + Log.i(TAG, "Not reloading track " + trackId); + return; + } + selectedTrackId = trackId; + PreferencesUtils.setLong(context, R.string.selected_track_id_key, selectedTrackId); + loadDataForAll(); + } + }); + } + + /** + * Registers a {@link TrackDataListener}. + * + * @param trackDataListener the track data listener + * @param trackDataTypes the track data types + */ + public void registerTrackDataListener( + final TrackDataListener trackDataListener, final EnumSet trackDataTypes) { + runInHanderThread(new Runnable() { + @Override + public void run() { + trackDataManager.registerListener(trackDataListener, trackDataTypes); + dataSourceManager.updateListeners(trackDataManager.getRegisteredTrackDataTypes()); + loadDataForListener(trackDataListener); + } + }); + } + + /** + * Unregisters a {@link TrackDataListener}. + * + * @param trackDataListener the track data listener + */ + public void unregisterTrackDataListener(final TrackDataListener trackDataListener) { + runInHanderThread(new Runnable() { + @Override + public void run() { + trackDataManager.unregisterListener(trackDataListener); + dataSourceManager.updateListeners(trackDataManager.getRegisteredTrackDataTypes()); + } + }); + } + + /** + * Reloads data for a {@link TrackDataListener}. + */ + public void reloadDataForListener(final TrackDataListener trackDataListener) { + runInHanderThread(new Runnable() { + @Override + public void run() { + loadDataForListener(trackDataListener); + } + }); + } + + /** + * Forces update location and reports to all listeners. */ public void forceUpdateLocation() { - if (!isStarted()) { - Log.w(TAG, "Not started, not forcing location update"); - return; - } - Log.i(TAG, "Forcing location update"); - - Location loc = dataSource.getLastKnownLocation(); - if (loc != null) { - notifyLocationChanged(loc, getListenersFor(TrackDataType.LOCATION)); - } + final Location location = dataSource.getLastKnownLocation(); + runInHanderThread(new Runnable() { + @Override + public void run() { + notifyLocationChanged( + location, false, trackDataManager.getListeners(TrackDataType.LOCATION)); + } + }); } - - /** Returns the ID of the currently-selected track. */ - public long getSelectedTrackId() { - if (!isStarted()) { - loadSharedPreferences(); - } - return selectedTrackId; - } - - /** Returns whether there's a track currently selected. */ - public boolean isATrackSelected() { - return getSelectedTrackId() > 0; - } - - /** Returns whether the selected track is still being recorded. */ - public boolean isRecordingSelected() { - if (!isStarted()) { - loadSharedPreferences(); - } + /** + * Returns true if the selected track is recording. + */ + public boolean isSelectedTrackRecording() { long recordingTrackId = PreferencesUtils.getLong(context, R.string.recording_track_id_key); return recordingTrackId != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT && recordingTrackId == selectedTrackId; } - /** - * Loads the given track and makes it the currently-selected one. - * It is ok to call this method before {@link #start}, and in that case - * the data will only be passed to listeners when {@link #start} is called. - * - * @param trackId the ID of the track to load - */ - public void loadTrack(long trackId) { - if (trackId == selectedTrackId) { - Log.w(TAG, "Not reloading track, id=" + trackId); - return; - } - - // Save the selection to memory and flush. - selectedTrackId = trackId; - PreferencesUtils.setLong(context, R.string.selected_track_id_key, selectedTrackId); - - // Force it to reload data from the beginning. - Log.d(TAG, "Loading track"); - resetState(); - - loadDataForAllListeners(); - } - - /** - * Resets the internal state of what data has already been loaded into listeners. - */ - private void resetState() { - firstSeenLocationId = -1; - lastSeenLocationId = -1; - numLoadedPoints = 0; - lastSamplingFrequency = -1; - } - - /** - * Unloads the currently-selected track. - */ - public void unloadCurrentTrack() { - loadTrack(-1); - } - - public void registerTrackDataListener( - TrackDataListener listener, EnumSet dataTypes) { - synchronized (trackDataManager) { - ListenerState listenerState = trackDataManager.registerListener(listener, dataTypes); - - // Don't load any data or start internal listeners if start() hasn't been - // called. When it is called, we'll do both things. - if (!isStarted()) { - return; - } - - loadNewDataForListener(listenerState); - - dataSourceManager.updateListeners(getNeededListenerTypes()); - } - } - - public void unregisterTrackDataListener(TrackDataListener listener) { - synchronized (trackDataManager) { - trackDataManager.unregisterListener(listener); - - // Don't load any data or start internal listeners if start() hasn't been - // called. When it is called, we'll do both things. - if (!isStarted()) return; - - dataSourceManager.updateListeners(getNeededListenerTypes()); - } - } - - /** - * Reloads all track data received so far into the specified listeners. - */ - public void reloadDataForListener(TrackDataListener listener) { - synchronized (trackDataManager) { - ListenerState listenerState = trackDataManager.getListenerState(listener); - listenerState.resetState(); - loadNewDataForListener(listenerState); - } - } - - /** - * Reloads all track data received so far into the specified listeners. - * - * Assumes it's called from a block that synchronizes on {@link #trackDataManager}. - */ - private void loadNewDataForListener(final ListenerState listenerState) { - if (!isStarted()) { - Log.w(TAG, "Not started, not reloading"); - return; - } - if (listenerState == null) { - Log.w(TAG, "Not reloading for null listener state"); - return; - } - - // If a listener happens to be added after this method but before the Runnable below is - // executed, it will have triggered a separate call to load data only up to the point this - // listener got to. This is ensured by being synchronized on listeners. - final boolean isOnlyListener = (trackDataManager.getNumberOfListeners() == 1); - - runInListenerThread(new Runnable() { - @SuppressWarnings("unchecked") - @Override + @Override + public void notifyTracksTableUpdated() { + runInHanderThread(new Runnable() { + @Override public void run() { - // Reload everything if either it's a different track, or the track has been resampled - // (this also covers the case of a new registration). - boolean reloadAll = listenerState.getLastTrackId() != selectedTrackId || - listenerState.getLastSamplingFrequency() != lastSamplingFrequency; - Log.d(TAG, "Doing a " + (reloadAll ? "full" : "partial") + " reload for " + listenerState); + notifyTracksTableUpdate(trackDataManager.getListeners(TrackDataType.TRACKS_TABLE)); + } + }); + } - TrackDataListener listener = listenerState.getTrackDataListener(); - Set listenerSet = Collections.singleton(listener); + @Override + public void notifyWaypointsTableUpdated() { + runInHanderThread(new Runnable() { + @Override + public void run() { + notifyWaypointsTableUpdate(trackDataManager.getListeners(TrackDataType.WAYPOINTS_TABLE)); + } + }); + } - EnumSet trackDataTypes = listenerState.getTrackDataTypes(); - - if (trackDataTypes.contains(TrackDataType.PREFERENCE)) { - reloadAll |= listener.onUnitsChanged(metricUnits); - reloadAll |= listener.onReportSpeedChanged(reportSpeed); - } + @Override + public void notifyTrackPointsTableUpdated() { + runInHanderThread(new Runnable() { + @Override + public void run() { + notifyTrackPointsTableUpdate( + true, trackDataManager.getListeners(TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE), + trackDataManager.getListeners(TrackDataType.SAMPLED_OUT_TRACK_POINTS_TABLE)); + } + }); + } - if (reloadAll && trackDataTypes.contains(TrackDataType.SELECTED_TRACK)) { - notifySelectedTrackChanged(selectedTrackId, listenerSet); - } - - if (trackDataTypes.contains(TrackDataType.TRACKS_TABLE)) { - notifyTrackUpdated(listenerSet); - } - - boolean interestedInPoints = trackDataTypes.contains(TrackDataType.TRACK_POINTS_TABLE); - boolean interestedInSampledOutPoints = trackDataTypes.contains( - TrackDataType.SAMPLED_OUT_TRACK_POINTS); - if (interestedInPoints || interestedInSampledOutPoints) { - long minPointId = 0; - int previousNumPoints = 0; - - if (reloadAll) { - // Clear existing points and send them all again - notifyPointsCleared(listenerSet); - } else { - // Send only new points - minPointId = listenerState.getLastPointId() + 1; - previousNumPoints = listenerState.getNumberOfLoadedPoints(); + @Override + public void notifyPreferenceChanged(final String key) { + runInHanderThread(new Runnable() { + @Override + public void run() { + if (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); + } else if (PreferencesUtils.getKey(context, R.string.metric_units_key).equals(key)) { + metricUnits = PreferencesUtils.getBoolean( + context, R.string.metric_units_key, PreferencesUtils.METRIC_UNITS_DEFAULT); + Set trackDataListeners = trackDataManager.getListeners( + TrackDataType.PREFERENCE); + for (TrackDataListener trackDataListener : trackDataListeners) { + if (trackDataListener.onMetricUnitsChanged(metricUnits)) { + loadDataForListener(trackDataListener); + } } - - // If this is the only listener we have registered, keep the state that we serve to it as - // a reference for other future listeners. - if (isOnlyListener && reloadAll) { - resetState(); + } else if (PreferencesUtils.getKey(context, R.string.report_speed_key).equals(key)) { + reportSpeed = PreferencesUtils.getBoolean( + context, R.string.report_speed_key, PreferencesUtils.REPORT_SPEED_DEFAULT); + Set trackDataListeners = trackDataManager.getListeners( + TrackDataType.PREFERENCE); + for (TrackDataListener trackDataListener : trackDataListeners) { + if (trackDataListener.onReportSpeedChanged(reportSpeed)) { + loadDataForListener(trackDataListener); + } } - - notifyPointsUpdated(isOnlyListener, - minPointId, - previousNumPoints, - listenerSet, - interestedInSampledOutPoints ? listenerSet : Collections.EMPTY_SET); - } - - if (trackDataTypes.contains(TrackDataType.WAYPOINTS_TABLE)) { - notifyWaypointUpdated(listenerSet); - } - - if (trackDataTypes.contains(TrackDataType.LOCATION)) { - if (lastSeenLocation != null) { - notifyLocationChanged(lastSeenLocation, true, listenerSet); - } else { - notifyFixType(); + } else if (PreferencesUtils.getKey(context, R.string.selected_track_id_key).equals(key)) { + long trackId = PreferencesUtils.getLong(context, R.string.selected_track_id_key); + if (trackId == selectedTrackId) { + Log.i(TAG, "Not reloading track " + trackId); + return; } - } - - if (trackDataTypes.contains(TrackDataType.COMPASS)) { - notifyHeadingChanged(listenerSet); + selectedTrackId = trackId; + loadDataForAll(); } } }); } - /** - * Reloads all track data received so far into the specified listeners. - */ - private void loadDataForAllListeners() { - if (!isStarted()) { - Log.w(TAG, "Not started, not reloading"); - return; - } - synchronized (trackDataManager) { - if (trackDataManager.getNumberOfListeners() == 0) { - Log.d(TAG, "No listeners, not reloading"); - return; - } - } - - runInListenerThread(new Runnable() { - @Override + @Override + public void notifyLocationProviderEnabled(final boolean enabled) { + runInHanderThread(new Runnable() { + @Override public void run() { - // Ignore the return values here, we're already sending the full data set anyway - for (TrackDataListener listener : - getListenersFor(TrackDataType.PREFERENCE)) { - listener.onUnitsChanged(metricUnits); - listener.onReportSpeedChanged(reportSpeed); - } + hasProviderEnabled = enabled; + notifyLocationStateChanged(trackDataManager.getListeners(TrackDataType.LOCATION)); + } + }); + } - notifySelectedTrackChanged(selectedTrackId, - getListenersFor(TrackDataType.SELECTED_TRACK)); + @Override + public void notifyLocationProviderAvailable(final boolean available) { + runInHanderThread(new Runnable() { + @Override + public void run() { + hasFix = available; + notifyLocationStateChanged(trackDataManager.getListeners(TrackDataType.LOCATION)); + } + }); + } - notifyTrackUpdated(getListenersFor(TrackDataType.TRACKS_TABLE)); + @Override + public void notifyLocationChanged(final Location location) { + runInHanderThread(new Runnable() { + @Override + public void run() { + notifyLocationChanged( + location, false, trackDataManager.getListeners(TrackDataType.LOCATION)); + } + }); + } - Set pointListeners = - getListenersFor(TrackDataType.TRACK_POINTS_TABLE); - Set sampledOutPointListeners = - getListenersFor(TrackDataType.SAMPLED_OUT_TRACK_POINTS); - notifyPointsCleared(pointListeners); - notifyPointsUpdated(true, 0, 0, pointListeners, sampledOutPointListeners); - - notifyWaypointUpdated(getListenersFor(TrackDataType.WAYPOINTS_TABLE)); + @Override + public void notifyHeadingChanged(final float heading) { + runInHanderThread(new Runnable() { + @Override + public void run() { + lastHeading = heading; if (lastSeenLocation != null) { - notifyLocationChanged(lastSeenLocation, true, - getListenersFor(TrackDataType.LOCATION)); - } else { - notifyFixType(); + // Update the declination at most once an hour + long now = System.currentTimeMillis(); + if (now - lastDeclinationUpdate > ONE_HOUR) { + lastDeclinationUpdate = now; + long timestamp = lastSeenLocation.getTime(); + if (timestamp == 0) { + // Hack for Samsung phones which don't populate the time field + timestamp = now; + } + lastDeclination = getDeclination(lastSeenLocation, timestamp); + } } - - notifyHeadingChanged(getListenersFor(TrackDataType.COMPASS)); + notifyHeadingChange(trackDataManager.getListeners(TrackDataType.HEADING)); } }); } /** - * Called when a preference changes. + * Loads data for all listeners. To be run in the {@link #handler} thread. + */ + private void loadDataForAll() { + resetSamplingState(); + if (trackDataManager.getNumberOfListeners() == 0) { + return; + } + + for (TrackDataListener trackDataListener : + trackDataManager.getListeners(TrackDataType.PREFERENCE)) { + trackDataListener.onMetricUnitsChanged(metricUnits); + trackDataListener.onReportSpeedChanged(reportSpeed); + } + + notifySelectedTrackChanged(trackDataManager.getListeners(TrackDataType.SELECTED_TRACK)); + notifyTracksTableUpdate(trackDataManager.getListeners(TrackDataType.TRACKS_TABLE)); + + for (TrackDataListener listener : + trackDataManager.getListeners(TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE)) { + listener.clearTrackPoints(); + } + notifyTrackPointsTableUpdate(true, + trackDataManager.getListeners(TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE), + trackDataManager.getListeners(TrackDataType.SAMPLED_OUT_TRACK_POINTS_TABLE)); + notifyWaypointsTableUpdate(trackDataManager.getListeners(TrackDataType.WAYPOINTS_TABLE)); + + if (lastSeenLocation != null) { + notifyLocationChanged( + lastSeenLocation, true, trackDataManager.getListeners(TrackDataType.LOCATION)); + } else { + notifyLocationStateChanged(trackDataManager.getListeners(TrackDataType.LOCATION)); + } + notifyHeadingChange(trackDataManager.getListeners(TrackDataType.HEADING)); + } + + /** + * Loads data for a listener. To be run in the {@link #handler} thread. * - * @param key the key to the preference that changed + * @param trackDataListener the track data listener. */ - private void notifyPreferenceChanged(String key) { - if (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); - } else if (PreferencesUtils.getKey(context, R.string.metric_units_key).equals(key)) { - metricUnits = PreferencesUtils.getBoolean( - context, R.string.metric_units_key, PreferencesUtils.METRIC_UNITS_DEFAULT); - notifyUnitsChanged(); - } else if (PreferencesUtils.getKey(context, R.string.report_speed_key).equals(key)) { - reportSpeed = PreferencesUtils.getBoolean( - context, R.string.report_speed_key, PreferencesUtils.REPORT_SPEED_DEFAULT); - notifySpeedReportingChanged(); - } else if (PreferencesUtils.getKey(context, R.string.selected_track_id_key).equals(key)) { - loadTrack(PreferencesUtils.getLong(context, R.string.selected_track_id_key)); + private void loadDataForListener(TrackDataListener trackDataListener) { + Set trackDataListeners = Collections.singleton(trackDataListener); + EnumSet trackDataTypes = trackDataManager.getTrackDataTypes(trackDataListener); + + if (trackDataTypes.contains(TrackDataType.PREFERENCE)) { + trackDataListener.onMetricUnitsChanged(metricUnits); + trackDataListener.onReportSpeedChanged(reportSpeed); + } + + if (trackDataTypes.contains(TrackDataType.SELECTED_TRACK)) { + notifySelectedTrackChanged(trackDataListeners); + } + + if (trackDataTypes.contains(TrackDataType.TRACKS_TABLE)) { + notifyTracksTableUpdate(trackDataListeners); + } + + boolean hasSampledIn = trackDataTypes.contains(TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE); + boolean hasSampledOut = trackDataTypes.contains(TrackDataType.SAMPLED_OUT_TRACK_POINTS_TABLE); + if (hasSampledIn || hasSampledOut) { + trackDataListener.clearTrackPoints(); + boolean isOnlyListener = trackDataManager.getNumberOfListeners() == 1; + if (isOnlyListener) { + resetSamplingState(); + } + Set sampledInListeners = trackDataListeners; + Set sampledOutListeners = hasSampledOut ? trackDataListeners + : Collections. emptySet(); + notifyTrackPointsTableUpdate(isOnlyListener, sampledInListeners, sampledOutListeners); + } + + if (trackDataTypes.contains(TrackDataType.WAYPOINTS_TABLE)) { + notifyWaypointsTableUpdate(trackDataListeners); + } + + if (trackDataTypes.contains(TrackDataType.LOCATION)) { + if (lastSeenLocation != null) { + notifyLocationChanged(lastSeenLocation, true, trackDataListeners); + } else { + notifyLocationStateChanged(trackDataListeners); + } + } + + if (trackDataTypes.contains(TrackDataType.HEADING)) { + notifyHeadingChange(trackDataListeners); } } - /** Called when the speed/pace reporting preference changes. */ - private void notifySpeedReportingChanged() { - if (!isStarted()) return; + /** + * Notifies selected track changed. To be run in the {@link #handler} thread. + * + * @param trackDataListeners the track data listeners to notify + */ + private void notifySelectedTrackChanged(Set trackDataListeners) { + if (trackDataListeners.isEmpty()) { + return; + } + Track track = myTracksProviderUtils.getTrack(selectedTrackId); + for (TrackDataListener trackDataListener : trackDataListeners) { + trackDataListener.onSelectedTrackChanged(track, isSelectedTrackRecording()); + } + } - runInListenerThread(new Runnable() { - @Override - public void run() { - Set displayListeners = - getListenersFor(TrackDataType.PREFERENCE); + /** + * Notifies track table update. To be run in the {@link #handler} thread. + * + * @param trackDataListeners the track data listeners to notify + */ + private void notifyTracksTableUpdate(Set trackDataListeners) { + if (trackDataListeners.isEmpty()) { + return; + } + Track track = myTracksProviderUtils.getTrack(selectedTrackId); + for (TrackDataListener trackDataListener : trackDataListeners) { + trackDataListener.onTrackUpdated(track); + } + } - for (TrackDataListener listener : displayListeners) { - // TODO: Do the reloading just once for all interested listeners - if (listener.onReportSpeedChanged(reportSpeed)) { - synchronized (trackDataManager) { - reloadDataForListener(listener); - } + /** + * Notifies waypoint table update. Currently, reloads all the waypoints up to + * {@link Constants#MAX_DISPLAYED_WAYPOINTS_POINTS}. To be run in the + * {@link #handler} thread. + * + * @param trackDataListeners the track data listeners to notify + */ + private void notifyWaypointsTableUpdate(Set trackDataListeners) { + if (trackDataListeners.isEmpty()) { + return; + } + + for (TrackDataListener trackDataListener : trackDataListeners) { + trackDataListener.clearWaypoints(); + } + + Cursor cursor = null; + try { + cursor = myTracksProviderUtils.getWaypointsCursor( + selectedTrackId, 0L, MAX_DISPLAYED_WAYPOINTS_POINTS); + if (cursor != null && cursor.moveToFirst()) { + do { + Waypoint waypoint = myTracksProviderUtils.createWaypoint(cursor); + if (!LocationUtils.isValidLocation(waypoint.getLocation())) { + continue; } - } - } - }); - } - - /** Called when the metric units setting changes. */ - private void notifyUnitsChanged() { - if (!isStarted()) return; - - runInListenerThread(new Runnable() { - @Override - public void run() { - Set displayListeners = getListenersFor(TrackDataType.PREFERENCE); - - for (TrackDataListener listener : displayListeners) { - if (listener.onUnitsChanged(metricUnits)) { - synchronized (trackDataManager) { - reloadDataForListener(listener); - } + for (TrackDataListener trackDataListener : trackDataListeners) { + trackDataListener.onNewWaypoint(waypoint); } - } + } while (cursor.moveToNext()); } - }); - } - - /** Notifies about the current GPS fix state. */ - private void notifyFixType() { - final TrackDataListener.ProviderState state; - if (!hasProviderEnabled) { - state = ProviderState.DISABLED; - } else if (!hasFix) { - state = ProviderState.NO_FIX; - } else if (!hasGoodFix) { - state = ProviderState.BAD_FIX; - } else { - state = ProviderState.GOOD_FIX; - } - - runInListenerThread(new Runnable() { - @Override - public void run() { - // Notify to everyone. - Log.d(TAG, "Notifying fix type: " + state); - for (TrackDataListener listener : - getListenersFor(TrackDataType.LOCATION)) { - listener.onProviderStateChange(state); - } - } - }); - } - - /** - * Notifies the the current location has changed, without any filtering. - * If the state of GPS fix has changed, that will also be reported. - * - * @param location the current location - * @param listeners the listeners to notify - */ - private void notifyLocationChanged(Location location, Set listeners) { - notifyLocationChanged(location, false, listeners); - } - - /** - * Notifies that the current location has changed, without any filtering. - * If the state of GPS fix has changed, that will also be reported. - * - * @param location the current location - * @param forceUpdate whether to force the notifications to happen - * @param listeners the listeners to notify - */ - private void notifyLocationChanged(Location location, boolean forceUpdate, - final Set listeners) { - if (location == null) return; - if (listeners.isEmpty()) return; - - boolean isGpsLocation = location.getProvider().equals(LocationManager.GPS_PROVIDER); - - boolean oldHasFix = hasFix; - boolean oldHasGoodFix = hasGoodFix; - - long now = System.currentTimeMillis(); - if (isGpsLocation) { - // We consider a good fix to be a recent one with reasonable accuracy. - hasFix = !isLocationOld(location, now, MAX_LOCATION_AGE_MS); - hasGoodFix = (location.getAccuracy() <= minRequiredAccuracy); - } else { - if (!isLocationOld(lastSeenLocation, now, MAX_LOCATION_AGE_MS)) { - // This is a network location, but we have a recent/valid GPS location, just ignore this. - return; - } - - // We haven't gotten a GPS location in a while (or at all), assume we have no fix anymore. - hasFix = false; - hasGoodFix = false; - - // If the network location is recent, we'll use that. - if (isLocationOld(location, now, MAX_NETWORK_AGE_MS)) { - // Alas, we have no clue where we are. - location = null; + } finally { + if (cursor != null) { + cursor.close(); } } - if (hasFix != oldHasFix || hasGoodFix != oldHasGoodFix || forceUpdate) { - notifyFixType(); + for (TrackDataListener trackDataListener : trackDataListeners) { + trackDataListener.onNewWaypointsDone(); } - - lastSeenLocation = location; - final Location finalLoc = location; - runInListenerThread(new Runnable() { - @Override - public void run() { - for (TrackDataListener listener : listeners) { - listener.onCurrentLocationChanged(finalLoc); - } - } - }); } /** - * Returns true if the given location is either invalid or too old. - * - * @param location the location to test - * @param now the current timestamp in milliseconds - * @param maxAge the maximum age in milliseconds - * @return true if it's invalid or too old, false otherwise + * Notifies track points table update. To be run in the {@link #handler} + * thread. + * + * @param updateSamplingState true to update the sampling state + * @param sampledInListeners the sampled-in listeners + * @param sampledOutListeners the sampled-out listeners */ - private static boolean isLocationOld(Location location, long now, long maxAge) { - return !LocationUtils.isValidLocation(location) || now - location.getTime() > maxAge; - } - - /** - * Notifies that the current heading has changed. - * - * @param listeners the listeners to notify - */ - private void notifyHeadingChanged(final Set listeners) { - if (listeners.isEmpty()) return; - - runInListenerThread(new Runnable() { - @Override - public void run() { - float heading = lastSeenMagneticHeading + declination; - for (TrackDataListener listener : listeners) { - listener.onCurrentHeadingChanged(heading); - } - } - }); - } - - /** - * Notifies that a new track has been selected.. - * - * @param trackId the new selected track - * @param listeners the listeners to notify - */ - private void notifySelectedTrackChanged(long trackId, - final Set listeners) { - if (listeners.isEmpty()) return; - - Log.i(TAG, "New track selected, id=" + trackId); - final Track track = providerUtils.getTrack(trackId); - - runInListenerThread(new Runnable() { - @Override - public void run() { - for (TrackDataListener listener : listeners) { - listener.onSelectedTrackChanged(track, isRecordingSelected()); - } - } - }); - } - - /** - * Notifies that the currently-selected track's data has been updated. - * - * @param listeners the listeners to notify - */ - private void notifyTrackUpdated(final Set listeners) { - if (listeners.isEmpty()) return; - - final Track track = providerUtils.getTrack(selectedTrackId); - - runInListenerThread(new Runnable() { - @Override - public void run() { - for (TrackDataListener listener : listeners) { - listener.onTrackUpdated(track); - } - } - }); - } - - /** - * Notifies that waypoints have been updated. - * We assume few waypoints, so we reload them all every time. - * - * @param listeners the listeners to notify - */ - private void notifyWaypointUpdated(final Set listeners) { - if (listeners.isEmpty()) return; - - // Always reload all the waypoints. - final Cursor cursor = providerUtils.getWaypointsCursor( - selectedTrackId, 0L, MAX_DISPLAYED_WAYPOINTS_POINTS); - - runInListenerThread(new Runnable() { - @Override - public void run() { - Log.d(TAG, "Reloading waypoints"); - for (TrackDataListener listener : listeners) { - listener.clearWaypoints(); - } - - try { - if (cursor != null && cursor.moveToFirst()) { - do { - Waypoint waypoint = providerUtils.createWaypoint(cursor); - if (!LocationUtils.isValidLocation(waypoint.getLocation())) { - continue; - } - - for (TrackDataListener listener : listeners) { - listener.onNewWaypoint(waypoint); - } - } while (cursor.moveToNext()); - } - } finally { - if (cursor != null) { - cursor.close(); - } - } - - for (TrackDataListener listener : listeners) { - listener.onNewWaypointsDone(); - } - } - }); - } - - /** - * Tells listeners to clear the current list of points. - * - * @param listeners the listeners to notify - */ - private void notifyPointsCleared(final Set listeners) { - if (listeners.isEmpty()) return; - - runInListenerThread(new Runnable() { - @Override - public void run() { - for (TrackDataListener listener : listeners) { - listener.clearTrackPoints(); - } - } - }); - } - - /** - * Notifies the given listeners about track points in the given ID range. - * - * @param keepState whether to load and save state about the already-notified points. - * If true, only new points are reported. - * If false, then the whole track will be loaded, without affecting the state. - * @param minPointId the first point ID to notify, inclusive, or 0 to determine from - * internal state - * @param previousNumPoints the number of points to assume were previously loaded for - * these listeners, or 0 to assume it's the kept state - */ - private void notifyPointsUpdated(final boolean keepState, - final long minPointId, final int previousNumPoints, - final Set sampledListeners, - final Set sampledOutListeners) { - if (sampledListeners.isEmpty() && sampledOutListeners.isEmpty()) return; - - runInListenerThread(new Runnable() { - @Override - public void run() { - notifyPointsUpdatedSync(keepState, minPointId, previousNumPoints, sampledListeners, sampledOutListeners); - } - }); - } - - /** - * Synchronous version of the above method. - */ - private void notifyPointsUpdatedSync(boolean keepState, - long minPointId, int previousNumPoints, - Set sampledListeners, - Set sampledOutListeners) { - // If we're loading state, start from after the last seen point up to the last recorded one - // (all new points) - // If we're not loading state, then notify about all the previously-seen points. - if (minPointId <= 0) { - minPointId = keepState ? lastSeenLocationId + 1 : 0; + private void notifyTrackPointsTableUpdate(boolean updateSamplingState, + Set sampledInListeners, Set sampledOutListeners) { + if (sampledInListeners.isEmpty() && sampledOutListeners.isEmpty()) { + return; } - long maxPointId = keepState ? -1 : lastSeenLocationId; - - // TODO: Move (re)sampling to a separate class. - if (numLoadedPoints >= targetNumPoints) { - // We're about to exceed the maximum desired number of points, so reload - // the whole track with fewer points (the sampling frequency will be - // lower). We do this for every listener even if we were loading just for - // a few of them (why miss the oportunity?). - - Log.i(TAG, "Resampling point set after " + numLoadedPoints + " points."); - resetState(); - synchronized (trackDataManager) { - sampledListeners = getListenersFor(TrackDataType.TRACK_POINTS_TABLE); - sampledOutListeners = getListenersFor(TrackDataType.SAMPLED_OUT_TRACK_POINTS); - } - maxPointId = -1; - minPointId = 0; - previousNumPoints = 0; - keepState = true; - - for (TrackDataListener listener : sampledListeners) { + if (updateSamplingState && numLoadedPoints >= targetNumPoints) { + // Reload and resample the track at a lower frequency. + Log.i(TAG, "Resampling track after " + numLoadedPoints + " points."); + resetSamplingState(); + for (TrackDataListener listener : sampledInListeners) { listener.clearTrackPoints(); } } - // Keep the originally selected track ID so we can stop if it changes. - long currentSelectedTrackId = selectedTrackId; + int localNumLoadedPoints = updateSamplingState ? numLoadedPoints : 0; + long localFirstSeenLocationId = updateSamplingState ? firstSeenLocationId : -1L; + long localLastSeenLocationId = updateSamplingState ? lastSeenLocationId : -1L; + long maxPointId = updateSamplingState ? -1L : lastSeenLocationId; - // If we're ignoring state, start from the beginning of the track - int localNumLoadedPoints = previousNumPoints; - if (previousNumPoints <= 0) { - localNumLoadedPoints = keepState ? numLoadedPoints : 0; - } - long localFirstSeenLocationId = keepState ? firstSeenLocationId : -1; - long localLastSeenLocationId = minPointId; - long lastStoredLocationId = providerUtils.getLastLocationId(currentSelectedTrackId); - int pointSamplingFrequency = -1; + long lastLocationId = myTracksProviderUtils.getLastLocationId(selectedTrackId); + int samplingFrequency = -1; + LocationIterator iterator = myTracksProviderUtils.getLocationIterator( + selectedTrackId, localLastSeenLocationId + 1, false, locationFactory); + boolean includeNextPoint = false; + while (iterator.hasNext()) { + Location location = iterator.next(); + long locationId = iterator.getLocationId(); - LocationIterator it = providerUtils.getLocationIterator( - currentSelectedTrackId, minPointId, false, locationFactory); - - while (it.hasNext()) { - if (currentSelectedTrackId != selectedTrackId) { - // The selected track changed beneath us, stop. - break; - } - - Location location = it.next(); - long locationId = it.getLocationId(); - - // If past the last wanted point, stop. - // This happens when adding a new listener after data has already been loaded, - // in which case we only want to bring that listener up to the point where the others - // were. In case it does happen, we should be wasting few points (only the ones not - // yet notified to other listeners). - if (maxPointId > 0 && locationId > maxPointId) { + // Stop if past the last wanted point + if (maxPointId != -1L && locationId > maxPointId) { break; } if (localFirstSeenLocationId == -1) { - // This was our first point, keep its ID localFirstSeenLocationId = locationId; } - if (pointSamplingFrequency == -1) { - // Now we already have at least one point, calculate the sampling - // frequency. - // It should be noted that a non-obvious consequence of this sampling is that - // no matter how many points we get in the newest batch, we'll never exceed - // MAX_DISPLAYED_TRACK_POINTS = 2 * TARGET_DISPLAYED_TRACK_POINTS before resampling. - long numTotalPoints = lastStoredLocationId - localFirstSeenLocationId; - numTotalPoints = Math.max(0L, numTotalPoints); - pointSamplingFrequency = (int) (1 + numTotalPoints / targetNumPoints); + if (samplingFrequency == -1) { + long numTotalPoints = Math.max(0L, lastLocationId - localFirstSeenLocationId); + samplingFrequency = 1 + (int) (numTotalPoints / targetNumPoints); } - notifyNewPoint(location, locationId, lastStoredLocationId, - localNumLoadedPoints, pointSamplingFrequency, sampledListeners, sampledOutListeners); + if (!LocationUtils.isValidLocation(location)) { + // TODO: also include the last valid point before a split + for (TrackDataListener trackDataListener : sampledInListeners) { + trackDataListener.onSegmentSplit(); + includeNextPoint = true; + } + } else { + // Also include the last point if the selected track is not recording. + if (includeNextPoint || (localNumLoadedPoints % samplingFrequency == 0) + || (locationId == lastLocationId && !isSelectedTrackRecording())) { + includeNextPoint = false; + for (TrackDataListener trackDataListener : sampledInListeners) { + trackDataListener.onSampledInTrackPoint(location); + } + } else { + for (TrackDataListener trackDataListener : sampledOutListeners) { + trackDataListener.onSampledOutTrackPoint(location); + } + } + } localNumLoadedPoints++; localLastSeenLocationId = locationId; } - it.close(); + iterator.close(); - if (keepState) { + if (updateSamplingState) { numLoadedPoints = localNumLoadedPoints; firstSeenLocationId = localFirstSeenLocationId; lastSeenLocationId = localLastSeenLocationId; } - // Always keep the sampling frequency - if it changes we'll do a full reload above anyway. - lastSamplingFrequency = pointSamplingFrequency; - - for (TrackDataListener listener : sampledListeners) { + for (TrackDataListener listener : sampledInListeners) { listener.onNewTrackPointsDone(); - - // Update the listener state - ListenerState listenerState = trackDataManager.getListenerState(listener); - if (listenerState != null) { - listenerState.setState(currentSelectedTrackId, localLastSeenLocationId, - pointSamplingFrequency, localNumLoadedPoints); - } } } - private void notifyNewPoint(Location location, - long locationId, - long lastStoredLocationId, - int loadedPoints, - int pointSamplingFrequency, - Set sampledListeners, - Set sampledOutListeners) { - boolean isValid = LocationUtils.isValidLocation(location); - if (!isValid) { - // Invalid points are segment splits - report those separately. - // TODO: Always send last valid point before and first valid point after a split - for (TrackDataListener listener : sampledListeners) { - listener.onSegmentSplit(); - } + /** + * Notifies location state changed. To be run in the {@link #handler} thread. + * + * @param trackDataListeners the track data listeners to notify + */ + private void notifyLocationStateChanged(Set trackDataListeners) { + if (trackDataListeners.isEmpty()) { return; } - - // Include a point if it fits one of the following criteria: - // - Has the mod for the sampling frequency (includes first point). - // - Is the last point and we are not recording this track. - boolean recordingSelected = isRecordingSelected(); - boolean includeInSample = - (loadedPoints % pointSamplingFrequency == 0 || - (!recordingSelected && locationId == lastStoredLocationId)); - - if (!includeInSample) { - for (TrackDataListener listener : sampledOutListeners) { - listener.onSampledOutTrackPoint(location); - } + TrackDataListener.LocationState locationState; + if (!hasProviderEnabled) { + locationState = LocationState.DISABLED; + } else if (!hasFix) { + locationState = LocationState.NO_FIX; + } else if (!hasGoodFix) { + locationState = LocationState.BAD_FIX; } else { - // Point is valid and included in sample. - for (TrackDataListener listener : sampledListeners) { - // No need to allocate a new location (we can safely reuse the existing). - listener.onNewTrackPoint(location); - } + locationState = LocationState.GOOD_FIX; + } + for (TrackDataListener trackDataListener : trackDataListeners) { + trackDataListener.onLocationStateChanged(locationState); } } - // @VisibleForTesting - protected void runInListenerThread(Runnable runnable) { - if (listenerHandler == null) { - // Use a Throwable to ensure the stack trace is logged. - Log.e(TAG, "Tried to use listener thread before start()", new Throwable()); + /** + * Notifies location changed. To be run in the {@link #handler} thread. + * + * @param location the location + * @param notifyLocationStateChange true to always notify location state + * change + * @param trackDataListeners the track data listeners to notify + */ + private void notifyLocationChanged(Location location, boolean notifyLocationStateChange, + Set trackDataListeners) { + if (location == null) { + return; + } + boolean oldHasFix = hasFix; + boolean oldHasGoodFix = hasGoodFix; + long now = System.currentTimeMillis(); + if (location.getProvider().equals(LocationManager.GPS_PROVIDER)) { + hasFix = !isLocationOld(location, now, MAX_LOCATION_AGE_MS); + hasGoodFix = location.getAccuracy() <= minRequiredAccuracy; + lastSeenLocation = location; + } else { + // A network location. + + // If has a recent GPS location, ignore the network location. + if (!isLocationOld(lastSeenLocation, now, MAX_LOCATION_AGE_MS)) { + return; + } + + hasFix = false; + hasGoodFix = false; + lastSeenLocation = isLocationOld(location, now, MAX_NETWORK_AGE_MS) ? null : location; + } + + if (trackDataListeners.isEmpty()) { return; } - listenerHandler.post(runnable); - } + if (notifyLocationStateChange || hasFix != oldHasFix || hasGoodFix != oldHasGoodFix) { + notifyLocationStateChanged(trackDataListeners); + } - private Set getListenersFor(TrackDataType type) { - synchronized (trackDataManager) { - return trackDataManager.getListeners(type); + for (TrackDataListener trackDataListener : trackDataListeners) { + trackDataListener.onLocationChanged(lastSeenLocation); } } - private EnumSet getNeededListenerTypes() { - EnumSet neededTypes = trackDataManager.getRegisteredTrackDataTypes(); - - // We always want preference updates. - neededTypes.add(TrackDataType.PREFERENCE); - - return neededTypes; + /** + * Notifies heading change. To be run in the {@link #handler} thread. + * + * @param trackDataListeners the track data listeners to notify + */ + private void notifyHeadingChange(Set trackDataListeners) { + if (trackDataListeners.isEmpty()) { + return; + } + float value = lastHeading + lastDeclination; + for (TrackDataListener trackDataListener : trackDataListeners) { + trackDataListener.onHeadingChanged(value); + } } -} + + /** + * Resets the track points sampling states. + */ + private void resetSamplingState() { + numLoadedPoints = 0; + firstSeenLocationId = -1L; + lastSeenLocationId = -1L; + } + + /** + * Returns true if a location is invalid or too old. + * + * @param location the location + * @param now the current time + * @param maxAge the maximum age + */ + private boolean isLocationOld(Location location, long now, long maxAge) { + return !LocationUtils.isValidLocation(location) || (now - location.getTime() > maxAge); + } + + /** + * Creates a {@link DataSource}. + */ + @VisibleForTesting + protected DataSource newDataSource() { + return new DataSource(context); + } + + /** + * Gets a declination. + * + * @param location the location + * @param time the time + */ + @VisibleForTesting + protected float getDeclination(Location location, long time) { + GeomagneticField field = new GeomagneticField((float) location.getLatitude(), (float) location + .getLongitude(), (float) location.getAltitude(), time); + return field.getDeclination(); + } + + /** + * Run in the handler thread. + * + * @param runnable the runnable + */ + @VisibleForTesting + protected void runInHanderThread(Runnable runnable) { + if (handler == null) { + // Use a Throwable to ensure the stack trace is logged. + Log.e(TAG, "handler is null.", new Throwable()); + return; + } + handler.post(runnable); + } +} \ No newline at end of file diff --git a/MyTracks/src/com/google/android/apps/mytracks/content/TrackDataListener.java b/MyTracks/src/com/google/android/apps/mytracks/content/TrackDataListener.java index be71e785e..f40439a09 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/content/TrackDataListener.java +++ b/MyTracks/src/com/google/android/apps/mytracks/content/TrackDataListener.java @@ -13,138 +13,131 @@ * License for the specific language governing permissions and limitations under * the License. */ -package com.google.android.apps.mytracks.content; -import com.google.android.apps.mytracks.content.Track; -import com.google.android.apps.mytracks.content.Waypoint; +package com.google.android.apps.mytracks.content; import android.location.Location; /** - * Listener for track data, for both initial and incremental loading. - * + * Listener for track data changes. + * * @author Rodrigo Damazio */ public interface TrackDataListener { - /** States for the GPS location provider. */ - public enum ProviderState { - DISABLED, - NO_FIX, - BAD_FIX, - GOOD_FIX; + /** + * Location state. + * + * @author Jimmy Shih + */ + public enum LocationState { + DISABLED, NO_FIX, BAD_FIX, GOOD_FIX; } /** - * Called when the location provider changes state. + * Called when the location state changes. */ - void onProviderStateChange(ProviderState state); + public void onLocationStateChanged(LocationState locationState); /** - * Called when the current location changes. - * This is meant for immediate location display only - track point data is - * delivered by other methods below, such as {@link #onNewTrackPoint}. - * - * @param loc the last known location - */ - void onCurrentLocationChanged(Location loc); - - /** - * Called when the current heading changes. - * - * @param heading the current heading, already accounting magnetic declination - */ - void onCurrentHeadingChanged(double heading); - - /** - * Called when the currently-selected track changes. - * This will be followed by calls to data methods such as - * {@link #onTrackUpdated}, {@link #clearTrackPoints}, - * {@link #onNewTrackPoint(Location)}, etc., even if no track is currently - * selected (in which case you'll only get calls to clear the current data). + * Called when the location changes. This is meant for location display only, + * track point data is reported with other methods like + * {@link #onSampledInTrackPoint(Location)} and + * {@link #onSampledOutTrackPoint(Location)}. * - * @param track the selected track, or null if no track is selected - * @param isRecording whether we're currently recording the selected track + * @param location the location */ - void onSelectedTrackChanged(Track track, boolean isRecording); + public void onLocationChanged(Location location); /** - * Called when the track and/or its statistics have been updated. - * - * @param track the updated version of the track + * Called when the heading changes. + * + * @param heading the heading */ - void onTrackUpdated(Track track); + public void onHeadingChanged(double heading); /** - * Called to clear any previously-sent track points. - * This can be called at any time that we decide the data needs to be - * reloaded, such as when it needs to be resampled. + * Called when the selected track changes. This will be followed by calls to + * data methods such as {@link #onTrackUpdated(Track)}, + * {@link #clearTrackPoints()}, {@link #onSampledInTrackPoint(Location)}, + * etc., even if no track is currently selected (in which case you'll only get + * calls to clear the current data). + * + * @param track the selected track or null if no track is selected + * @param isRecording true if the selected track is recording */ - void clearTrackPoints(); + public void onSelectedTrackChanged(Track track, boolean isRecording); /** - * Called when a new interesting track point is read. - * In this case, interesting means that the point has already undergone - * sampling and invalid point filtering. - * - * @param loc the new track point + * Called when the track or its statistics has been updated. + * + * @param track the track */ - void onNewTrackPoint(Location loc); + public void onTrackUpdated(Track track); /** - * Called when a uninteresting track point is read. - * Uninteresting points are all points that get sampled out of the track. - * - * @param loc the new track point + * Called to clear previously-sent track points. */ - void onSampledOutTrackPoint(Location loc); + public void clearTrackPoints(); /** - * Called when an invalid point (representing a segment split) is read. + * Called when a sampled in track point is read. + * + * @param location the location */ - void onSegmentSplit(); + public void onSampledInTrackPoint(Location location); /** - * Called when we're done (for the time being) sending new points. - * This gets called after every batch of calls to {@link #onNewTrackPoint}, - * {@link #onSampledOutTrackPoint} and {@link #onSegmentSplit}. + * Called when a sampled out track point is read. + * + * @param location the location */ - void onNewTrackPointsDone(); + public void onSampledOutTrackPoint(Location location); /** - * Called to clear any previously-sent waypoints. - * This can be called at any time that we decide the data needs to be - * reloaded. + * Called when an invalid track point representing a segment split is read. */ - void clearWaypoints(); + public void onSegmentSplit(); + + /** + * Called when finish sending new track points. This gets called after every + * batch of calls to {@link #onSampledInTrackPoint(Location)}, + * {@link #onSampledOutTrackPoint(Location)} and {@link #onSegmentSplit()}. + */ + public void onNewTrackPointsDone(); + + /** + * Called to clear previously sent waypoints. + */ + public void clearWaypoints(); /** * Called when a new waypoint is read. - * - * @param wpt the new waypoint + * + * @param waypoint the waypoint */ - void onNewWaypoint(Waypoint wpt); + public void onNewWaypoint(Waypoint waypoint); /** - * Called when we're done (for the time being) sending new waypoints. - * This gets called after every batch of calls to {@link #clearWaypoints} and - * {@link #onNewWaypoint}. + * Called when finish sending new waypoints. This gets called after every + * batch of calls to {@link #clearWaypoints()} and + * {@link #onNewWaypoint(Waypoint)}. */ - void onNewWaypointsDone(); + public void onNewWaypointsDone(); /** - * Called when the display units are changed by the user. - * - * @param metric true if the units are metric, false if imperial - * @return true to reload all the data, false otherwise + * Called when the metric units preference value is change. + * + * @param metricUnits true to use metric units, false to use imperial units + * @return true to reload all the data, false otherwise. */ - boolean onUnitsChanged(boolean metric); + public boolean onMetricUnitsChanged(boolean metricUnits); /** - * Called when the speed/pace display unit is changed by the user. - * - * @param reportSpeed true to report speed, false for pace - * @return true to reload all the data, false otherwise + * Called when the report speed preference value is changed. + * + * @param reportSpeed true to report speed, false to report pace + * @return true to reload all the data, false otherwise. */ - boolean onReportSpeedChanged(boolean reportSpeed); + public boolean onReportSpeedChanged(boolean reportSpeed); } diff --git a/MyTracks/src/com/google/android/apps/mytracks/content/TrackDataManager.java b/MyTracks/src/com/google/android/apps/mytracks/content/TrackDataManager.java index 9c3ededd7..e816d2408 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/content/TrackDataManager.java +++ b/MyTracks/src/com/google/android/apps/mytracks/content/TrackDataManager.java @@ -35,9 +35,9 @@ import java.util.Set; */ public class TrackDataManager { - // Map of listener to its state - private final Map - listenerToStateMap = new HashMap(); + // Map of listener to its track data types + private final Map> + listenerToTypesMap = new HashMap>(); // Map of track data type to listeners private final Map> @@ -55,20 +55,15 @@ public class TrackDataManager { * @param listener the listener * @param trackDataTypes the track data types the listener is interested */ - public ListenerState registerListener( + public void registerListener( TrackDataListener listener, EnumSet trackDataTypes) { - if (listenerToStateMap.containsKey(listener)) { + if (listenerToTypesMap.containsKey(listener)) { throw new IllegalStateException("Listener is already registered"); } - - ListenerState listenerState = new ListenerState(listener, trackDataTypes); - listenerToStateMap.put(listener, listenerState); - + listenerToTypesMap.put(listener, trackDataTypes); for (TrackDataType trackDataType : trackDataTypes) { typeToListenersMap.get(trackDataType).add(listener); } - - return listenerState; } /** @@ -77,14 +72,14 @@ public class TrackDataManager { * @param listener the listener */ public void unregisterListener(TrackDataListener listener) { - ListenerState removed = listenerToStateMap.remove(listener); - if (removed == null) { + EnumSet removedTypes = listenerToTypesMap.remove(listener); + if (removedTypes == null) { Log.w(TAG, "Tried to unregister a listener that is not registered."); return; } // Remove the listener from the typeToListenersMap - for (TrackDataType trackDataType : removed.getTrackDataTypes()) { + for (TrackDataType trackDataType : removedTypes) { typeToListenersMap.get(trackDataType).remove(listener); } } @@ -93,16 +88,16 @@ public class TrackDataManager { * Gets the number of {@link TrackDataListener}. */ public int getNumberOfListeners() { - return listenerToStateMap.size(); + return listenerToTypesMap.size(); } /** - * Gets the track listener state. + * Gets the track data types for a listener. * * @param listener the listener */ - public ListenerState getListenerState(TrackDataListener listener) { - return listenerToStateMap.get(listener); + public EnumSet getTrackDataTypes(TrackDataListener listener) { + return listenerToTypesMap.get(listener); } /** @@ -119,9 +114,11 @@ public class TrackDataManager { */ public EnumSet getRegisteredTrackDataTypes() { EnumSet types = EnumSet.noneOf(TrackDataType.class); - for (ListenerState registration : this.listenerToStateMap.values()) { - types.addAll(registration.getTrackDataTypes()); + for (EnumSet value : listenerToTypesMap.values()) { + types.addAll(value); } + // Always include preference + types.add(TrackDataType.PREFERENCE); return types; } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/content/TrackDataType.java b/MyTracks/src/com/google/android/apps/mytracks/content/TrackDataType.java index 319b5bbc0..87a5f9549 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/content/TrackDataType.java +++ b/MyTracks/src/com/google/android/apps/mytracks/content/TrackDataType.java @@ -22,13 +22,12 @@ package com.google.android.apps.mytracks.content; * @author Jimmy Shih */ public enum TrackDataType { - SELECTED_TRACK, // selected track changes TRACKS_TABLE, // tracks table changes WAYPOINTS_TABLE, // waypoints table changes - TRACK_POINTS_TABLE, // track points table changes - SAMPLED_OUT_TRACK_POINTS, // sampled-out track points changes + SAMPLED_IN_TRACK_POINTS_TABLE, // sampled-in track points table changes + SAMPLED_OUT_TRACK_POINTS_TABLE, // sampled-out track points table changes LOCATION, // location changes - COMPASS, // compass changes - PREFERENCE; // preference changes + HEADING, // heading changes + PREFERENCE // preference changes } \ No newline at end of file diff --git a/MyTracks/src/com/google/android/apps/mytracks/fragments/ChartFragment.java b/MyTracks/src/com/google/android/apps/mytracks/fragments/ChartFragment.java index e1dab8720..a7964af05 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/fragments/ChartFragment.java +++ b/MyTracks/src/com/google/android/apps/mytracks/fragments/ChartFragment.java @@ -18,14 +18,14 @@ package com.google.android.apps.mytracks.fragments; import com.google.android.apps.mytracks.ChartView; import com.google.android.apps.mytracks.Constants; -import com.google.android.apps.mytracks.MyTracksApplication; -import com.google.android.apps.mytracks.content.TrackDataType; +import com.google.android.apps.mytracks.TrackDetailActivity; import com.google.android.apps.mytracks.content.MyTracksLocation; import com.google.android.apps.mytracks.content.Sensor; import com.google.android.apps.mytracks.content.Sensor.SensorDataSet; import com.google.android.apps.mytracks.content.Track; import com.google.android.apps.mytracks.content.TrackDataHub; import com.google.android.apps.mytracks.content.TrackDataListener; +import com.google.android.apps.mytracks.content.TrackDataType; import com.google.android.apps.mytracks.content.Waypoint; import com.google.android.apps.mytracks.stats.DoubleBuffer; import com.google.android.apps.mytracks.stats.TripStatisticsBuilder; @@ -96,7 +96,7 @@ public class ChartFragment extends Fragment implements TrackDataListener { zoomControls.setIsZoomInEnabled(chartView.canZoomIn()); zoomControls.setIsZoomOutEnabled(chartView.canZoomOut()); - chartView.setShowPointer(isRecording()); + chartView.setShowPointer(isSelectedTrackRecording()); chartView.invalidate(); } }; @@ -164,17 +164,17 @@ public class ChartFragment extends Fragment implements TrackDataListener { } @Override - public void onProviderStateChange(ProviderState state) { + public void onLocationStateChanged(LocationState state) { // We don't care. } @Override - public void onCurrentLocationChanged(Location loc) { + public void onLocationChanged(Location loc) { // We don't care. } @Override - public void onCurrentHeadingChanged(double heading) { + public void onHeadingChanged(double heading) { // We don't care. } @@ -213,7 +213,7 @@ public class ChartFragment extends Fragment implements TrackDataListener { } @Override - public void onNewTrackPoint(Location location) { + public void onSampledInTrackPoint(Location location) { if (LocationUtils.isValidLocation(location)) { double[] data = new double[ChartView.NUM_SERIES + 1]; fillDataPoint(location, data); @@ -259,7 +259,7 @@ public class ChartFragment extends Fragment implements TrackDataListener { } @Override - public boolean onUnitsChanged(boolean metric) { + public boolean onMetricUnitsChanged(boolean metric) { if (metricUnits == metric) { return false; } @@ -358,13 +358,13 @@ public class ChartFragment extends Fragment implements TrackDataListener { * accessed by multiple threads. */ private synchronized void resumeTrackDataHub() { - trackDataHub = ((MyTracksApplication) getActivity().getApplication()).getTrackDataHub(); + trackDataHub = ((TrackDetailActivity) getActivity()).getTrackDataHub(); trackDataHub.registerTrackDataListener(this, EnumSet.of( TrackDataType.SELECTED_TRACK, TrackDataType.TRACKS_TABLE, TrackDataType.WAYPOINTS_TABLE, - TrackDataType.TRACK_POINTS_TABLE, - TrackDataType.SAMPLED_OUT_TRACK_POINTS, + TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE, + TrackDataType.SAMPLED_OUT_TRACK_POINTS_TABLE, TrackDataType.PREFERENCE)); } @@ -378,11 +378,11 @@ public class ChartFragment extends Fragment implements TrackDataListener { } /** - * Returns true if recording. Needs to be synchronized because trackDataHub - * can be accessed by multiple threads. + * Returns true if the selected track is recording. Needs to be synchronized + * because trackDataHub can be accessed by multiple threads. */ - private synchronized boolean isRecording() { - return trackDataHub != null && trackDataHub.isRecordingSelected(); + private synchronized boolean isSelectedTrackRecording() { + return trackDataHub != null && trackDataHub.isSelectedTrackRecording(); } /** diff --git a/MyTracks/src/com/google/android/apps/mytracks/fragments/MapFragment.java b/MyTracks/src/com/google/android/apps/mytracks/fragments/MapFragment.java index 5a5e5c72c..02dbc55f6 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/fragments/MapFragment.java +++ b/MyTracks/src/com/google/android/apps/mytracks/fragments/MapFragment.java @@ -17,14 +17,13 @@ package com.google.android.apps.mytracks.fragments; import com.google.android.apps.mytracks.MapOverlay; -import com.google.android.apps.mytracks.MyTracksApplication; import com.google.android.apps.mytracks.TrackDetailActivity; -import com.google.android.apps.mytracks.content.TrackDataType; import com.google.android.apps.mytracks.content.MyTracksProviderUtils; import com.google.android.apps.mytracks.content.MyTracksProviderUtils.Factory; import com.google.android.apps.mytracks.content.Track; import com.google.android.apps.mytracks.content.TrackDataHub; import com.google.android.apps.mytracks.content.TrackDataListener; +import com.google.android.apps.mytracks.content.TrackDataType; import com.google.android.apps.mytracks.content.Waypoint; import com.google.android.apps.mytracks.stats.TripStatistics; import com.google.android.apps.mytracks.util.ApiAdapterFactory; @@ -271,7 +270,7 @@ public class MapFragment extends Fragment } @Override - public void onProviderStateChange(ProviderState state) { + public void onLocationStateChanged(LocationState state) { final int messageId; final boolean isGpsDisabled; switch (state) { @@ -315,13 +314,13 @@ public class MapFragment extends Fragment } @Override - public void onCurrentLocationChanged(Location location) { + public void onLocationChanged(Location location) { currentLocation = location; updateCurrentLocation(); } @Override - public void onCurrentHeadingChanged(double heading) { + public void onHeadingChanged(double heading) { if (mapOverlay.setHeading((float) heading)) { mapView.postInvalidate(); } @@ -362,7 +361,7 @@ public class MapFragment extends Fragment } @Override - public void onNewTrackPoint(Location location) { + public void onSampledInTrackPoint(Location location) { if (LocationUtils.isValidLocation(location)) { mapOverlay.addLocation(location); } @@ -402,7 +401,7 @@ public class MapFragment extends Fragment } @Override - public boolean onUnitsChanged(boolean metric) { + public boolean onMetricUnitsChanged(boolean metric) { // We don't care. return false; } @@ -418,13 +417,13 @@ public class MapFragment extends Fragment * accessed by multiple threads. */ private synchronized void resumeTrackDataHub() { - trackDataHub = ((MyTracksApplication) getActivity().getApplication()).getTrackDataHub(); + trackDataHub = ((TrackDetailActivity) getActivity()).getTrackDataHub(); trackDataHub.registerTrackDataListener(this, EnumSet.of( TrackDataType.SELECTED_TRACK, TrackDataType.WAYPOINTS_TABLE, - TrackDataType.TRACK_POINTS_TABLE, + TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE, TrackDataType.LOCATION, - TrackDataType.COMPASS)); + TrackDataType.HEADING)); } /** diff --git a/MyTracks/src/com/google/android/apps/mytracks/fragments/StatsFragment.java b/MyTracks/src/com/google/android/apps/mytracks/fragments/StatsFragment.java index 0eaf01a89..580ed784f 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/fragments/StatsFragment.java +++ b/MyTracks/src/com/google/android/apps/mytracks/fragments/StatsFragment.java @@ -16,11 +16,11 @@ package com.google.android.apps.mytracks.fragments; -import com.google.android.apps.mytracks.MyTracksApplication; -import com.google.android.apps.mytracks.content.TrackDataType; +import com.google.android.apps.mytracks.TrackDetailActivity; import com.google.android.apps.mytracks.content.Track; import com.google.android.apps.mytracks.content.TrackDataHub; import com.google.android.apps.mytracks.content.TrackDataListener; +import com.google.android.apps.mytracks.content.TrackDataType; import com.google.android.apps.mytracks.content.Waypoint; import com.google.android.apps.mytracks.stats.TripStatistics; import com.google.android.apps.mytracks.util.PreferencesUtils; @@ -61,7 +61,7 @@ public class StatsFragment extends Fragment implements TrackDataListener { // A runnable to update the total time field. private final Runnable updateTotalTime = new Runnable() { public void run() { - if (isRecording()) { + if (isSelectedTrackRecording()) { StatsUtils.setTotalTimeValue(getActivity(), System.currentTimeMillis() - startTime); } } @@ -117,8 +117,8 @@ public class StatsFragment extends Fragment implements TrackDataListener { } @Override - public void onProviderStateChange(ProviderState state) { - if (isResumed() && (state == ProviderState.DISABLED || state == ProviderState.NO_FIX)) { + public void onLocationStateChanged(LocationState state) { + if (isResumed() && (state == LocationState.DISABLED || state == LocationState.NO_FIX)) { getActivity().runOnUiThread(new Runnable() { @Override public void run() { @@ -130,8 +130,8 @@ public class StatsFragment extends Fragment implements TrackDataListener { } @Override - public void onCurrentLocationChanged(final Location location) { - if (isResumed() && isRecording()) { + public void onLocationChanged(final Location location) { + if (isResumed() && isSelectedTrackRecording()) { getActivity().runOnUiThread(new Runnable() { @Override public void run() { @@ -143,7 +143,7 @@ public class StatsFragment extends Fragment implements TrackDataListener { } @Override - public void onCurrentHeadingChanged(double heading) { + public void onHeadingChanged(double heading) { // We don't care. } @@ -175,7 +175,7 @@ public class StatsFragment extends Fragment implements TrackDataListener { lastTripStatistics = track.getTripStatistics(); startTime = track.getTripStatistics().getStartTime(); - if (!isRecording()) { + if (!isSelectedTrackRecording()) { lastLocation = null; } updateUi(); @@ -190,7 +190,7 @@ public class StatsFragment extends Fragment implements TrackDataListener { } @Override - public void onNewTrackPoint(Location loc) { + public void onSampledInTrackPoint(Location loc) { // We don't care. } @@ -225,7 +225,7 @@ public class StatsFragment extends Fragment implements TrackDataListener { } @Override - public boolean onUnitsChanged(final boolean metric) { + public boolean onMetricUnitsChanged(final boolean metric) { if (isResumed()) { getActivity().runOnUiThread(new Runnable() { @Override @@ -255,7 +255,7 @@ public class StatsFragment extends Fragment implements TrackDataListener { * be accessed by multiple threads. */ private synchronized void resumeTrackDataHub() { - trackDataHub = ((MyTracksApplication) getActivity().getApplication()).getTrackDataHub(); + trackDataHub = ((TrackDetailActivity) getActivity()).getTrackDataHub(); trackDataHub.registerTrackDataListener(this, EnumSet.of( TrackDataType.SELECTED_TRACK, TrackDataType.TRACKS_TABLE, @@ -273,11 +273,11 @@ public class StatsFragment extends Fragment implements TrackDataListener { } /** - * Returns true if recording. Needs to be synchronized because trackDataHub - * can be accessed by multiple threads. + * Returns true if the selected track is recording. Needs to be synchronized + * because trackDataHub can be accessed by multiple threads. */ - private synchronized boolean isRecording() { - return trackDataHub != null && trackDataHub.isRecordingSelected(); + private synchronized boolean isSelectedTrackRecording() { + return trackDataHub != null && trackDataHub.isSelectedTrackRecording(); } /** diff --git a/MyTracks/src/com/google/android/apps/mytracks/maps/DynamicSpeedTrackPathDescriptor.java b/MyTracks/src/com/google/android/apps/mytracks/maps/DynamicSpeedTrackPathDescriptor.java index 6e636ce6f..604d05954 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/maps/DynamicSpeedTrackPathDescriptor.java +++ b/MyTracks/src/com/google/android/apps/mytracks/maps/DynamicSpeedTrackPathDescriptor.java @@ -13,6 +13,7 @@ * License for the specific language governing permissions and limitations under * the License. */ + package com.google.android.apps.mytracks.maps; import static com.google.android.apps.mytracks.Constants.TAG; @@ -36,60 +37,43 @@ import android.util.Log; * * @author Vangelis S. */ -public class DynamicSpeedTrackPathDescriptor implements TrackPathDescriptor, - OnSharedPreferenceChangeListener { +public class DynamicSpeedTrackPathDescriptor + implements TrackPathDescriptor, OnSharedPreferenceChangeListener { + private final Context context; + private int speedMargin; private int slowSpeed; private int normalSpeed; - private int speedMargin; private double averageMovingSpeed; - private final Context context; + @VisibleForTesting static final int CRITICAL_DIFFERENCE_PERCENTAGE = 20; public DynamicSpeedTrackPathDescriptor(Context context) { this.context = context; - context.getSharedPreferences(Constants.SETTINGS_NAME, Context.MODE_PRIVATE) - .registerOnSharedPreferenceChangeListener(this); - speedMargin = PreferencesUtils.getInt(context, R.string.track_color_mode_percentage_key, PreferencesUtils.TRACK_COLOR_MODE_PERCENTAGE_DEFAULT); + context.getSharedPreferences(Constants.SETTINGS_NAME, Context.MODE_PRIVATE) + .registerOnSharedPreferenceChangeListener(this); } - /** - * Get the slow speed calculated based on the % below the average speed. - * - * @return The speed limit considered as slow. - */ + @Override public int getSlowSpeed() { - slowSpeed = (int) (averageMovingSpeed - (averageMovingSpeed * speedMargin / 100)); + slowSpeed = (int) (averageMovingSpeed - (averageMovingSpeed * speedMargin / 100.0)); return slowSpeed; } - /** - * Gets the medium speed calculated based on the % above the average speed. - * - * @return The speed limit considered as normal. - */ + @Override public int getNormalSpeed() { - normalSpeed = (int) (averageMovingSpeed + (averageMovingSpeed * speedMargin / 100)); + normalSpeed = (int) (averageMovingSpeed + (averageMovingSpeed * speedMargin / 100.0)); return normalSpeed; } @Override - public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { - Log.d(TAG, "DynamicSpeedTrackPathDescriptor: onSharedPreferences changed " + key); - if (PreferencesUtils.getKey(context, R.string.track_color_mode_percentage_key).equals(key)) { - speedMargin = PreferencesUtils.getInt(context, R.string.track_color_mode_percentage_key, - PreferencesUtils.TRACK_COLOR_MODE_PERCENTAGE_DEFAULT); - } - } - - @Override - public boolean needsRedraw() { + public boolean updateState() { long selectedTrackId = PreferencesUtils.getLong(context, R.string.selected_track_id_key); if (selectedTrackId == PreferencesUtils.SELECTED_TRACK_ID_DEFAULT) { - // Could not find track. + Log.d(TAG, "No selected track id."); return false; } Track track = MyTracksProviderUtils.Factory.get(context).getTrack(selectedTrackId); @@ -97,40 +81,48 @@ public class DynamicSpeedTrackPathDescriptor implements TrackPathDescriptor, Log.d(TAG, "No track for " + selectedTrackId); return false; } - TripStatistics stats = track.getTripStatistics(); + TripStatistics tripStatistics = track.getTripStatistics(); double newAverageMovingSpeed = (int) Math.floor( - stats.getAverageMovingSpeed() * UnitConversions.MS_TO_KMH); + tripStatistics.getAverageMovingSpeed() * UnitConversions.MS_TO_KMH); - return isDifferenceSignificant(averageMovingSpeed, newAverageMovingSpeed); - } - - /** - * Checks whether the old speed and the new speed differ significantly or not. - */ - public boolean isDifferenceSignificant(double oldAverageMovingSpeed, double newAverageMovingSpeed) { - if (oldAverageMovingSpeed == 0) { - if (newAverageMovingSpeed == 0) { - return false; - } else { - averageMovingSpeed = newAverageMovingSpeed; - return true; - } - } - - // Here, both oldAverageMovingSpeed and newAverageMovingSpeed are not zero. - double maxValue = Math.max(oldAverageMovingSpeed, newAverageMovingSpeed); - double differencePercentage = Math.abs(oldAverageMovingSpeed - newAverageMovingSpeed) - / maxValue * 100; - if (differencePercentage >= CRITICAL_DIFFERENCE_PERCENTAGE) { + if (isDifferenceSignificant(averageMovingSpeed, newAverageMovingSpeed)) { averageMovingSpeed = newAverageMovingSpeed; return true; + } else { + return false; + } + } + + @Override + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { + if (PreferencesUtils.getKey(context, R.string.track_color_mode_percentage_key).equals(key)) { + speedMargin = PreferencesUtils.getInt(context, R.string.track_color_mode_percentage_key, + PreferencesUtils.TRACK_COLOR_MODE_PERCENTAGE_DEFAULT); } - return false; } /** - * Gets the value of variable speedMargin to check the result of test. - * @return the value of speedMargin. + * Returns true if the average moving speed and the new average moving speed + * are significantly different. + * + * @param oldAverageMovingSpeed + * @param newAverageMovingSpeed + */ + @VisibleForTesting + boolean isDifferenceSignificant(double oldAverageMovingSpeed, double newAverageMovingSpeed) { + if (oldAverageMovingSpeed == 0) { + return newAverageMovingSpeed != 0; + } + + // Here, both oldAverageMovingSpeed and newAverageMovingSpeed are not zero. + double maxValue = Math.max(oldAverageMovingSpeed, newAverageMovingSpeed); + double differencePercentage = Math.abs(oldAverageMovingSpeed - newAverageMovingSpeed) / maxValue + * 100.0; + return differencePercentage >= CRITICAL_DIFFERENCE_PERCENTAGE; + } + + /** + * Gets the speed margin. */ @VisibleForTesting int getSpeedMargin() { @@ -138,21 +130,20 @@ public class DynamicSpeedTrackPathDescriptor implements TrackPathDescriptor, } /** - * Sets the value of newAverageMovingSpeed to test the method isDifferenceSignificant. - * @param newAverageMovingSpeed the value to set. - */ - @VisibleForTesting - void setAverageMovingSpeed(double newAverageMovingSpeed) { - averageMovingSpeed = newAverageMovingSpeed; - } - - /** - * Gets the value of averageMovingSpeed to check the result of test. - * - * @return the value of averageMovingSpeed + * Gets the average moving speed. */ @VisibleForTesting double getAverageMovingSpeed() { return averageMovingSpeed; } + + /** + * Sets the average moving speed. + * + * @param value the value + */ + @VisibleForTesting + void setAverageMovingSpeed(double value) { + averageMovingSpeed = value; + } } \ No newline at end of file diff --git a/MyTracks/src/com/google/android/apps/mytracks/maps/DynamicSpeedTrackPathPainter.java b/MyTracks/src/com/google/android/apps/mytracks/maps/DynamicSpeedTrackPathPainter.java index 34e5bd954..9609d1fe6 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/maps/DynamicSpeedTrackPathPainter.java +++ b/MyTracks/src/com/google/android/apps/mytracks/maps/DynamicSpeedTrackPathPainter.java @@ -25,7 +25,6 @@ import com.google.common.annotations.VisibleForTesting; import android.content.Context; import android.graphics.Canvas; import android.graphics.Paint; -import android.graphics.Path; import android.graphics.Point; import android.graphics.Rect; @@ -33,117 +32,105 @@ import java.util.ArrayList; import java.util.List; /** - * A path painter that varies the path colors based on fixed speeds or average speed margin - * depending of the TrackPathDescriptor passed to its constructor. + * A path painter that varies the path colors based on fixed speeds or average + * speed margin depending of the TrackPathDescriptor passed to its constructor. * - * @author Vangelis S. + * @author Vangelis S. */ -public class DynamicSpeedTrackPathPainter implements TrackPathPainter { - private final Paint selectedTrackPaintSlow; - private final Paint selectedTrackPaintMedium; - private final Paint selectedTrackPaintFast; - private final List coloredPaths; +public class DynamicSpeedTrackPathPainter implements TrackPathPainter { private final TrackPathDescriptor trackPathDescriptor; - private int slowSpeed; - private int normalSpeed; - - public DynamicSpeedTrackPathPainter (Context context, TrackPathDescriptor trackPathDescriptor) { + private final Paint slowPaint; + private final Paint mediumPaint; + private final Paint fastPaint; + private final List coloredPaths; + + public DynamicSpeedTrackPathPainter(Context context, TrackPathDescriptor trackPathDescriptor) { this.trackPathDescriptor = trackPathDescriptor; - - selectedTrackPaintSlow = TrackPathUtilities.getPaint(R.color.slow_path, context); - selectedTrackPaintMedium = TrackPathUtilities.getPaint(R.color.normal_path, context); - selectedTrackPaintFast = TrackPathUtilities.getPaint(R.color.fast_path, context); - - this.coloredPaths = new ArrayList(); + slowPaint = TrackPathUtils.getPaint(context, R.color.slow_path); + mediumPaint = TrackPathUtils.getPaint(context, R.color.normal_path); + fastPaint = TrackPathUtils.getPaint(context, R.color.fast_path); + coloredPaths = new ArrayList(); } - + @Override - public void drawTrack(Canvas canvas) { - for(int i = 0; i < coloredPaths.size(); ++i) { - ColoredPath coloredPath = coloredPaths.get(i); - canvas.drawPath(coloredPath.getPath(), coloredPath.getPathPaint()); + public boolean hasPath() { + return !coloredPaths.isEmpty(); + } + + @Override + public boolean updateState() { + return trackPathDescriptor.updateState(); + } + + @Override + public void updatePath( + Projection projection, Rect viewRect, int startIndex, List points) { + boolean hasLastPoint = startIndex != 0 && points.get(startIndex -1).isValid(); + Point point = new Point(); + if (hasLastPoint) { + GeoPoint geoPoint = points.get(startIndex -1).getGeoPoint(); + projection.toPixels(geoPoint, point); } - } - - @Override - public void updatePath(Projection projection, Rect viewRect, int startLocationIdx, - Boolean alwaysVisible, List points) { - // Whether to start a new segment on new valid and visible point. - boolean newSegment = startLocationIdx <= 0 || !points.get(startLocationIdx - 1).valid; - boolean lastVisible = !newSegment; - final Point pt = new Point(); - - clear(); - - slowSpeed = trackPathDescriptor.getSlowSpeed(); - normalSpeed = trackPathDescriptor.getNormalSpeed(); - - // Loop over track points. - for (int i = startLocationIdx; i < points.size(); ++i) { - CachedLocation loc = points.get(i); - - // Check if valid, if not then indicate a new segment. - if (!loc.valid) { + boolean newSegment = !hasLastPoint; + // Assume if last point exists, it is visible + boolean lastPointVisible = hasLastPoint; + int slowSpeed = trackPathDescriptor.getSlowSpeed(); + int normalSpeed = trackPathDescriptor.getNormalSpeed(); + + for (int i = startIndex; i < points.size(); ++i) { + CachedLocation cachedLocation = points.get(i); + + // If not valid, start a new segment + if (!cachedLocation.isValid()) { newSegment = true; continue; } - - final GeoPoint geoPoint = loc.geoPoint; + + GeoPoint geoPoint = cachedLocation.getGeoPoint(); // Check if this breaks the existing segment. - boolean visible = alwaysVisible || viewRect.contains( - geoPoint.getLongitudeE6(), geoPoint.getLatitudeE6()); - if (!visible && !lastVisible) { - // This is a point outside view not connected to a visible one. + boolean pointVisible = viewRect.contains(geoPoint.getLongitudeE6(), geoPoint.getLatitudeE6()); + if (!pointVisible && !lastPointVisible) { + // This point and the last point are both outside visible area. newSegment = true; } - lastVisible = visible; - - // Either move to beginning of a new segment or continue the old one. + lastPointVisible = pointVisible; + + // Either update point or draw a line from the last point if (newSegment) { - projection.toPixels(geoPoint, pt); + projection.toPixels(geoPoint, point); newSegment = false; } else { ColoredPath coloredPath; - if(loc.speed <= slowSpeed) { - coloredPath = new ColoredPath(selectedTrackPaintSlow); - } - else if(loc.speed <= normalSpeed) { - coloredPath = new ColoredPath(selectedTrackPaintMedium); + if (cachedLocation.getSpeed() <= slowSpeed) { + coloredPath = new ColoredPath(slowPaint); + } else if (cachedLocation.getSpeed() <= normalSpeed) { + coloredPath = new ColoredPath(mediumPaint); } else { - coloredPath = new ColoredPath(selectedTrackPaintFast); + coloredPath = new ColoredPath(fastPaint); } - coloredPath.getPath().moveTo(pt.x, pt.y); - projection.toPixels(geoPoint, pt); - coloredPath.getPath().lineTo(pt.x, pt.y); + coloredPath.getPath().moveTo(point.x, point.y); + projection.toPixels(geoPoint, point); + coloredPath.getPath().lineTo(point.x, point.y); coloredPaths.add(coloredPath); } } } - + @Override - public void clear() - { + public void clearPath() { coloredPaths.clear(); } @Override - public boolean needsRedraw() { - return trackPathDescriptor.needsRedraw(); - } - - @Override - public Path getLastPath() { - Path path = new Path(); - for(int i = 0; i < coloredPaths.size(); ++i) { - path.addPath(coloredPaths.get(i).getPath()); + public void drawPath(Canvas canvas) { + for (int i = 0; i < coloredPaths.size(); i++) { + ColoredPath coloredPath = coloredPaths.get(i); + canvas.drawPath(coloredPath.getPath(), coloredPath.getPathPaint()); } - return path; } - + /** - * Returns coloredPaths. - * - * @return coloredPaths + * Gets the colored paths. */ @VisibleForTesting List getColoredPaths() { diff --git a/MyTracks/src/com/google/android/apps/mytracks/maps/FixedSpeedTrackPathDescriptor.java b/MyTracks/src/com/google/android/apps/mytracks/maps/FixedSpeedTrackPathDescriptor.java index 4034fec57..56d0f3a8f 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/maps/FixedSpeedTrackPathDescriptor.java +++ b/MyTracks/src/com/google/android/apps/mytracks/maps/FixedSpeedTrackPathDescriptor.java @@ -31,40 +31,36 @@ import android.content.SharedPreferences.OnSharedPreferenceChangeListener; */ public class FixedSpeedTrackPathDescriptor implements TrackPathDescriptor, OnSharedPreferenceChangeListener { + private final Context context; private int slowSpeed; private int normalSpeed; - private final Context context; public FixedSpeedTrackPathDescriptor(Context context) { this.context = context; - context.getSharedPreferences(Constants.SETTINGS_NAME, Context.MODE_PRIVATE) - .registerOnSharedPreferenceChangeListener(this); - slowSpeed = PreferencesUtils.getInt(context, R.string.track_color_mode_slow_key, PreferencesUtils.TRACK_COLOR_MODE_SLOW_DEFAULT); normalSpeed = PreferencesUtils.getInt(context, R.string.track_color_mode_medium_key, PreferencesUtils.TRACK_COLOR_MODE_MEDIUM_DEFAULT); + context.getSharedPreferences(Constants.SETTINGS_NAME, Context.MODE_PRIVATE) + .registerOnSharedPreferenceChangeListener(this); } - /** - * Gets the slow speed for reference. - * - * @return The speed limit considered as slow. - */ + @Override public int getSlowSpeed() { return slowSpeed; } - /** - * Gets the normal speed for reference. - * - * @return The speed limit considered as normal. - */ + @Override public int getNormalSpeed() { return normalSpeed; } + @Override + public boolean updateState() { + return false; + } + @Override public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { if (PreferencesUtils.getKey(context, R.string.track_color_mode_slow_key).equals(key)) { @@ -75,9 +71,4 @@ public class FixedSpeedTrackPathDescriptor PreferencesUtils.TRACK_COLOR_MODE_MEDIUM_DEFAULT); } } - - @Override - public boolean needsRedraw() { - return false; - } } \ No newline at end of file diff --git a/MyTracks/src/com/google/android/apps/mytracks/maps/SingleColorTrackPathPainter.java b/MyTracks/src/com/google/android/apps/mytracks/maps/SingleColorTrackPathPainter.java index 53e104575..d29f86edf 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/maps/SingleColorTrackPathPainter.java +++ b/MyTracks/src/com/google/android/apps/mytracks/maps/SingleColorTrackPathPainter.java @@ -40,88 +40,103 @@ public class SingleColorTrackPathPainter implements TrackPathPainter { private Path path; public SingleColorTrackPathPainter(Context context) { - selectedTrackPaint = TrackPathUtilities.getPaint(R.color.red, context); + selectedTrackPaint = TrackPathUtils.getPaint(context, R.color.red); } @Override - public void drawTrack(Canvas canvas) { - canvas.drawPath(path, selectedTrackPaint); + public boolean hasPath() { + return path != null; } @Override - public void updatePath(Projection projection, Rect viewRect, int startLocationIdx, - Boolean alwaysVisible, List points) { - path = newPath(); - updatePath(projection, viewRect, startLocationIdx, alwaysVisible, points, path); + public boolean updateState() { + return false; + } + @Override + public void updatePath( + Projection projection, Rect viewRect, int startIndex, List points) { + if (!hasPath()) { + path = newPath(); + } + updatePath(projection, viewRect, startIndex, points, path); + } + + @Override + public void clearPath() { + path = null; + } + + @Override + public void drawPath(Canvas canvas) { + if (path != null) { + canvas.drawPath(path, selectedTrackPaint); + } } /** * Updates the path. * - * @param projection The Canvas to draw upon. - * @param viewRect The Path to be drawn. - * @param startLocationIdx The start point from where update the path. - * @param alwaysVisible Flag for always visible. - * @param points The list of points used to update the path. - * @param pathToUpdate The path to be created. + * @param projection the projection + * @param viewRect the view rectangle + * @param startIndex the start index + * @param points the points + * @param pathToUpdate the path to update */ @VisibleForTesting - void updatePath(Projection projection, Rect viewRect, int startLocationIdx, - Boolean alwaysVisible, List points, Path pathToUpdate) { - pathToUpdate.incReserve(points.size()); - // Whether to start a new segment on new valid and visible point. - boolean newSegment = startLocationIdx <= 0 || !points.get(startLocationIdx - 1).valid; - boolean lastVisible = !newSegment; - final Point pt = new Point(); - // Loop over track points. - for (int i = startLocationIdx; i < points.size(); ++i) { - CachedLocation loc = points.get(i); + void updatePath(Projection projection, Rect viewRect, int startIndex, List points, + Path pathToUpdate) { + pathToUpdate.incReserve(points.size() - startIndex); - // Check if valid, if not then indicate a new segment. - if (!loc.valid) { + boolean hasLastPoint = startIndex != 0 && points.get(startIndex - 1).isValid(); + boolean newSegment = !hasLastPoint; + // Assume if last point exists, it is visible + boolean lastPointVisible = hasLastPoint; + Point point = new Point(); + + for (int i = startIndex; i < points.size(); i++) { + CachedLocation cachedLocation = points.get(i); + + // If not valid, start a new segment + if (!cachedLocation.isValid()) { newSegment = true; continue; } - final GeoPoint geoPoint = loc.geoPoint; + GeoPoint geoPoint = cachedLocation.getGeoPoint(); // Check if this breaks the existing segment. - boolean visible = alwaysVisible - || viewRect.contains(geoPoint.getLongitudeE6(), geoPoint.getLatitudeE6()); - if (!visible && !lastVisible) { - // This is a point outside view not connected to a visible one. + boolean pointVisible = viewRect.contains(geoPoint.getLongitudeE6(), geoPoint.getLatitudeE6()); + if (!pointVisible && !lastPointVisible) { + // This point and the last point are both outside visible area. newSegment = true; } - lastVisible = visible; + lastPointVisible = pointVisible; - // Either move to beginning of a new segment or continue the old one. - projection.toPixels(geoPoint, pt); + // Either update point or draw a line from the last point + projection.toPixels(geoPoint, point); if (newSegment) { - pathToUpdate.moveTo(pt.x, pt.y); + pathToUpdate.moveTo(point.x, point.y); newSegment = false; } else { - pathToUpdate.lineTo(pt.x, pt.y); + pathToUpdate.lineTo(point.x, point.y); } } } - @Override - public void clear() { - path = null; + /** + * Creates a new path. + */ + @VisibleForTesting + protected Path newPath() { + return new Path(); } - @Override - public boolean needsRedraw() { - return false; - } - - @Override - public Path getLastPath() { + /** + * Gets the path. + */ + @VisibleForTesting + public Path getPath() { return path; } - // Visible for testing - public Path newPath() { - return new Path(); - } } \ No newline at end of file diff --git a/MyTracks/src/com/google/android/apps/mytracks/maps/TrackPathDescriptor.java b/MyTracks/src/com/google/android/apps/mytracks/maps/TrackPathDescriptor.java index 389994e6b..d214e53e8 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/maps/TrackPathDescriptor.java +++ b/MyTracks/src/com/google/android/apps/mytracks/maps/TrackPathDescriptor.java @@ -13,26 +13,28 @@ * License for the specific language governing permissions and limitations under * the License. */ + package com.google.android.apps.mytracks.maps; /** - * An interface for classes which describe how to draw a track path. - * + * An interface for classes which describe how to draw a track path. + * * @author Vangelis S. */ public interface TrackPathDescriptor { + /** - * @return The maximum speed which is considered slow. + * Gets the maximum speed which is considered slow. */ - int getSlowSpeed(); - + public int getSlowSpeed(); + /** - * @return The maximum speed which is considered normal. + * Gets the maximum speed which is considered normal. */ - int getNormalSpeed(); - + public int getNormalSpeed(); + /** - * @return True if the path needs to be updated. + * Updates state. Returns true if the state is updated. */ - boolean needsRedraw(); + public boolean updateState(); } \ No newline at end of file diff --git a/MyTracks/src/com/google/android/apps/mytracks/maps/TrackPathPainter.java b/MyTracks/src/com/google/android/apps/mytracks/maps/TrackPathPainter.java index 1f1d8e31d..2f6b31fde 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/maps/TrackPathPainter.java +++ b/MyTracks/src/com/google/android/apps/mytracks/maps/TrackPathPainter.java @@ -13,56 +13,54 @@ * License for the specific language governing permissions and limitations under * the License. */ + package com.google.android.apps.mytracks.maps; import com.google.android.apps.mytracks.MapOverlay.CachedLocation; import com.google.android.maps.Projection; import android.graphics.Canvas; -import android.graphics.Path; import android.graphics.Rect; import java.util.List; /** - * An interface for classes which paint the track path. - * + * An interface for classes which paint the track path. + * * @author Vangelis S. */ public interface TrackPathPainter { - + /** - * Clears the related data. + * Returns true if has path. */ - void clear(); - + public boolean hasPath(); + /** - * Draws the path to the canvas. - * @param canvas The Canvas to draw upon + * Updates state. Returns true if the state is updated. */ - void drawTrack(Canvas canvas); - + public boolean updateState(); + /** - * Updates the path. - * @param projection The Canvas to draw upon. - * @param viewRect The Path to be drawn. - * @param startLocationIdx The start point from where update the path. - * @param alwaysVisible Flag for alwaysvisible. - * @param points The list of points used to update the path. + * Updates the path. Creates a new path if necessary + * + * @param projection the projection + * @param viewRect the view rectangle + * @param startIndex the start index + * @param points the points */ - void updatePath(Projection projection, Rect viewRect, int startLocationIdx, - Boolean alwaysVisible, List points); - + public void updatePath( + Projection projection, Rect viewRect, int startIndex, List points); + /** - * @return True if the path needs to be updated. + * Clears the path. */ - boolean needsRedraw(); - + public void clearPath(); + /** - * @return The path being used currently. + * Draws the path. + * + * @param canvas the canvas */ - Path getLastPath(); + public void drawPath(Canvas canvas); } - - - diff --git a/MyTracks/src/com/google/android/apps/mytracks/maps/TrackPathPainterFactory.java b/MyTracks/src/com/google/android/apps/mytracks/maps/TrackPathPainterFactory.java index f00d38ae1..cba98f20e 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/maps/TrackPathPainterFactory.java +++ b/MyTracks/src/com/google/android/apps/mytracks/maps/TrackPathPainterFactory.java @@ -13,6 +13,7 @@ * License for the specific language governing permissions and limitations under * the License. */ + package com.google.android.apps.mytracks.maps; import static com.google.android.apps.mytracks.Constants.TAG; @@ -24,32 +25,31 @@ import android.content.Context; import android.util.Log; /** - * A factory for TrackPathPainters. - * + * A factory for {@link TrackPathPainter}. + * * @author Vangelis S. */ public class TrackPathPainterFactory { - private TrackPathPainterFactory() { - } + private TrackPathPainterFactory() {} /** - * Get a new TrackPathPainter. - * @param context Context to fetch system preferences. - * @return The TrackPathPainter that corresponds to the track color mode setting. + * Get a new {@link TrackPathPainter}. + * + * @param context the context */ public static TrackPathPainter getTrackPathPainter(Context context) { String trackColorMode = PreferencesUtils.getString(context, R.string.track_color_mode_key, context.getString(R.string.settings_map_track_color_mode_single_value)); Log.i(TAG, "Creating track path painter of type: " + trackColorMode); - if (context.getString(R.string.settings_map_track_color_mode_fixed_value) - .equals(trackColorMode)) { - return new DynamicSpeedTrackPathPainter(context, new FixedSpeedTrackPathDescriptor(context)); - } else if (context.getString(R.string.settings_map_track_color_mode_dynamic_value) + if (context.getString(R.string.settings_map_track_color_mode_dynamic_value) .equals(trackColorMode)) { return new DynamicSpeedTrackPathPainter(context, new DynamicSpeedTrackPathDescriptor( context)); + } else if (context.getString(R.string.settings_map_track_color_mode_fixed_value) + .equals(trackColorMode)) { + return new DynamicSpeedTrackPathPainter(context, new FixedSpeedTrackPathDescriptor(context)); } else { return new SingleColorTrackPathPainter(context); } diff --git a/MyTracks/src/com/google/android/apps/mytracks/maps/TrackPathUtilities.java b/MyTracks/src/com/google/android/apps/mytracks/maps/TrackPathUtils.java similarity index 71% rename from MyTracks/src/com/google/android/apps/mytracks/maps/TrackPathUtilities.java rename to MyTracks/src/com/google/android/apps/mytracks/maps/TrackPathUtils.java index 956d2c8eb..a9cb03cf0 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/maps/TrackPathUtilities.java +++ b/MyTracks/src/com/google/android/apps/mytracks/maps/TrackPathUtils.java @@ -13,21 +13,30 @@ * License for the specific language governing permissions and limitations under * the License. */ + package com.google.android.apps.mytracks.maps; import android.content.Context; import android.graphics.Paint; /** - * Various utility functions for TrackPath painting. - * + * Various utility functions for track path painting. + * * @author Vangelis S. */ -public class TrackPathUtilities { - - public static Paint getPaint(int id, Context context) { +public class TrackPathUtils { + + private TrackPathUtils() {} + + /** + * Gets a paint. + * + * @param context the context + * @param colorId the color id + */ + public static Paint getPaint(Context context, int colorId) { Paint paint = new Paint(); - paint.setColor(context.getResources().getColor(id)); + paint.setColor(context.getResources().getColor(colorId)); paint.setStrokeWidth(3); paint.setStyle(Paint.Style.STROKE); paint.setAntiAlias(true); 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 626bb5b76..1d882bab6 100644 --- a/MyTracksLib/src/com/google/android/apps/mytracks/content/MyTracksProviderUtilsImpl.java +++ b/MyTracksLib/src/com/google/android/apps/mytracks/content/MyTracksProviderUtilsImpl.java @@ -681,7 +681,7 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { return waypoint; } - private ContentValues createContentValues(Waypoint waypoint) { + ContentValues createContentValues(Waypoint waypoint) { ContentValues values = new ContentValues(); // Value < 0 indicates no id is available @@ -824,7 +824,7 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { throw new IllegalArgumentException("locationFactory is null"); } return new LocationIterator() { - private long lastTrackPointId = startTrackPointId; + private long lastTrackPointId = -1L; private Cursor cursor = getCursor(startTrackPointId); private final CachedTrackPointsIndexes indexes = cursor != null ? new CachedTrackPointsIndexes(cursor) @@ -843,7 +843,8 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { * Advances the cursor to the next batch. Returns true if successful. */ private boolean advanceCursorToNextBatch() { - long trackPointId = lastTrackPointId + (descending ? -1 : 1); + long trackPointId = lastTrackPointId == -1L ? -1L : lastTrackPointId + (descending ? -1 + : 1); Log.d(TAG, "Advancing track point id: " + trackPointId); cursor.close(); cursor = getCursor(trackPointId); diff --git a/MyTracksTest/libs/robotium-solo-3.1.jar b/MyTracksTest/libs/robotium-solo-3.1.jar deleted file mode 100644 index 0ba05859b90a8c29ca519e228674db9aebe69d90..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 49691 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 $rk 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 segments = new LinkedList>(); - /** A list of disjoined path segments. */ - public final List> segments = new LinkedList>(); - /** The total number of points in this path. */ - public int totalPoints; + // The total number of points in this path. + private int totalPoints; private List currentSegment; @Override @@ -49,12 +48,20 @@ public class MockPath extends Path { currentSegment.add(new PointF(x, y)); totalPoints++; } - + @Override public void moveTo(float x, float y) { super.moveTo(x, y); - segments.add(currentSegment = - new ArrayList(Arrays.asList(new PointF(x, y)))); + currentSegment = new ArrayList(Arrays.asList(new PointF(x, y))); + segments.add(currentSegment); totalPoints++; } + + /** + * Gets the total number of points in this path. + */ + public int getTotalPoints() { + return totalPoints; + } + } \ No newline at end of file diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/MockProjection.java b/MyTracksTest/src/com/google/android/apps/mytracks/MockProjection.java index a1f36d90b..b1dbc5bdd 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/MockProjection.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/MockProjection.java @@ -13,6 +13,7 @@ * License for the specific language governing permissions and limitations under * the License. */ + package com.google.android.apps.mytracks; import com.google.android.maps.GeoPoint; @@ -21,18 +22,17 @@ import com.google.android.maps.Projection; import android.graphics.Point; /** - * Elements for Tests for the MyTracks map overlay. + * A mock {@link Projection}. Acts as an identity matrix. * * @author Bartlomiej Niechwiej * @author Vangelis S. * - * A mock {@code Projection} that acts as the identity matrix. */ public class MockProjection implements Projection { @Override - public Point toPixels(GeoPoint in, Point out) { - return out; + public Point toPixels(GeoPoint geoPoint, Point point) { + return point; } @Override 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 d36fd2baa..c73458920 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/content/MyTracksProviderUtilsImplTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/content/MyTracksProviderUtilsImplTest.java @@ -51,6 +51,9 @@ public class MyTracksProviderUtilsImplTest extends AndroidTestCase { private static final String MOCK_DESC = "Mock Next Waypoint Desc!"; private static final String TEST_DESC = "Test Desc!"; private static final String TEST_DESC_NEW = "Test Desc new!"; + private double INITIAL_LATITUDE = 37.0; + private double INITIAL_LONGITUDE = -57.0; + private double ALTITUDE_INTERVAL = 2.5; @Override protected void setUp() throws Exception { @@ -273,13 +276,13 @@ public class MyTracksProviderUtilsImplTest extends AndroidTestCase { // Check whether all have been deleted. tracksCursor = contentResolver.query(TracksColumns.CONTENT_URI, null, null, null, TracksColumns._ID); - assertTrue(tracksCursor.getCount() == 0); + assertEquals(0, tracksCursor.getCount()); tracksPointsCursor = contentResolver.query(TrackPointsColumns.CONTENT_URI, null, null, null, TrackPointsColumns._ID); - assertTrue(tracksPointsCursor.getCount() == 0); + assertEquals(0, tracksPointsCursor.getCount()); waypointCursor = contentResolver.query(WaypointsColumns.CONTENT_URI, null, null, null, WaypointsColumns._ID); - assertTrue(waypointCursor.getCount() == 0); + assertEquals(0, waypointCursor.getCount()); } /** @@ -328,9 +331,7 @@ public class MyTracksProviderUtilsImplTest extends AndroidTestCase { @UsesMocks(Cursor.class) public void testCreateTrack() { Cursor cursorMock = AndroidMock.createNiceMock(Cursor.class); - // The startColumnIndex can be any value, just make it to 2 in case it's - // hard coded to 0 or 1 in some where. - int startColumnIndex = 2; + int startColumnIndex = 1; int columnIndex = startColumnIndex; AndroidMock.expect(cursorMock.getColumnIndexOrThrow(TracksColumns._ID)) .andReturn(columnIndex++); @@ -359,7 +360,7 @@ public class MyTracksProviderUtilsImplTest extends AndroidTestCase { * Tests the method {@link MyTracksProviderUtilsImpl#createContentValues(Track)}. */ @UsesMocks(TripStatistics.class) - public void testCreateContentValues_Track() { + public void testCreateContentValues_track() { // ID long trackId = System.currentTimeMillis(); // Name @@ -645,9 +646,7 @@ public class MyTracksProviderUtilsImplTest extends AndroidTestCase { @UsesMocks(Cursor.class) public void testCreateWaypoint() { Cursor cursorMock = AndroidMock.createNiceMock(Cursor.class); - // The startColumnIndex can be any value, just make it to 2 in case it's - // hard coded to 0 or 1 in some where. - int startColumnIndex = 2; + int startColumnIndex = 1; int columnIndex = startColumnIndex; AndroidMock.expect(cursorMock.getColumnIndexOrThrow(WaypointsColumns._ID)) .andReturn(columnIndex++); @@ -683,7 +682,7 @@ public class MyTracksProviderUtilsImplTest extends AndroidTestCase { /** * Tests the method {@link MyTracksProviderUtilsImpl#createContentValues(Waypoint)}. */ - public void testCreateContentValues_Waypoint() { + public void testCreateContentValues_waypoint() { long trackId = System.currentTimeMillis(); Track track = getTrack(trackId, 10); providerUtils.insertTrack(track); @@ -819,10 +818,10 @@ public class MyTracksProviderUtilsImplTest extends AndroidTestCase { LocationIterator locationIterator = providerUtils.getLocationIterator(trackId, startTrackPointId, true, MyTracksProviderUtils.DEFAULT_LOCATION_FACTORY); - assertEquals(startTrackPointId, locationIterator.getLocationId()); for (int i = 1; i >= 0; i--) { assertTrue(locationIterator.hasNext()); Location location = locationIterator.next(); + assertEquals(2 + (i - 1), locationIterator.getLocationId()); checkLocation(i, location); } assertFalse(locationIterator.hasNext()); @@ -843,10 +842,11 @@ public class MyTracksProviderUtilsImplTest extends AndroidTestCase { LocationIterator locationIterator = providerUtils.getLocationIterator(trackId, startTrackPointId, false, MyTracksProviderUtils.DEFAULT_LOCATION_FACTORY); - assertEquals(startTrackPointId, locationIterator.getLocationId()); + for (int i = 1; i < 10; i++) { assertTrue(locationIterator.hasNext()); Location location = locationIterator.next(); + assertEquals(2 + (i - 1), locationIterator.getLocationId()); checkLocation(i, location); } assertFalse(locationIterator.hasNext()); @@ -871,6 +871,7 @@ public class MyTracksProviderUtilsImplTest extends AndroidTestCase { * test also covers the method * {@link MyTracksProviderUtilsImpl#createContentValues(Location, long)}. */ + @SuppressWarnings("javadoc") public void testInsertTrackPoint() { // Insert track, point at first. long trackId = System.currentTimeMillis(); @@ -888,6 +889,7 @@ public class MyTracksProviderUtilsImplTest extends AndroidTestCase { * {@link MyTracksProviderUtilsImpl#createContentValues(Location, long)} and * the inner class {@link MyTracksProviderUtilsImpl.CachedTrackPointsIndexes}. */ + @SuppressWarnings("javadoc") public void testBulkInsertTrackPoints() { // Insert track, point at first. long trackId = System.currentTimeMillis(); @@ -908,6 +910,7 @@ public class MyTracksProviderUtilsImplTest extends AndroidTestCase { * and the inner class * {@link MyTracksProviderUtilsImpl.CachedTrackPointsIndexes}. */ + @SuppressWarnings("javadoc") @UsesMocks(Cursor.class) public void testCreateLocation() { Cursor cursorMock = AndroidMock.createNiceMock(Cursor.class); @@ -984,7 +987,7 @@ public class MyTracksProviderUtilsImplTest extends AndroidTestCase { track.setId(id); track.setName("Test: " + id); track.setNumberOfPoints(numPoints); - for(int i=0; i < numPoints; i++) { + for(int i = 0; i < numPoints; i++) { track.addLocation(createLocation(i)); } return track; @@ -996,26 +999,27 @@ public class MyTracksProviderUtilsImplTest extends AndroidTestCase { * @return created location */ private Location createLocation(int i) { + Location loc = new Location("test"); - loc.setLatitude(37.0 + (double) i / 10000.0); - loc.setLongitude(57.0 - (double) i / 10000.0); + loc.setLatitude(INITIAL_LATITUDE + (double) i / 10000.0); + loc.setLongitude(INITIAL_LONGITUDE - (double) i / 10000.0); loc.setAccuracy((float) i / 100.0f); - loc.setAltitude(i * 2.5); + loc.setAltitude(i * ALTITUDE_INTERVAL); return loc; } /** * Checks the value of a location. * - * @param i the index of this location which created in the method {@link - * this#getTrack(long, int)} + * @param i the index of this location which created in the method + * {@link MyTracksProviderUtilsImplTest#getTrack(long, int)} * @param location the location to be checked */ private void checkLocation(int i, Location location) { - assertEquals(37.0 + (double) i / 10000.0, location.getLatitude()); - assertEquals(57.0 - (double) i / 10000.0, location.getLongitude()); + assertEquals(INITIAL_LATITUDE + (double) i / 10000.0, location.getLatitude()); + assertEquals(INITIAL_LONGITUDE - (double) i / 10000.0, location.getLongitude()); assertEquals((float) i / 100.0f, location.getAccuracy()); - assertEquals(i * 2.5, location.getAltitude()); + assertEquals(i * ALTITUDE_INTERVAL, location.getAltitude()); } /** diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/content/TrackDataHubTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/content/TrackDataHubTest.java index e6ab88ef0..119494743 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/content/TrackDataHubTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/content/TrackDataHubTest.java @@ -19,12 +19,11 @@ import static com.google.android.testing.mocking.AndroidMock.capture; import static com.google.android.testing.mocking.AndroidMock.eq; import static com.google.android.testing.mocking.AndroidMock.expect; import static com.google.android.testing.mocking.AndroidMock.isA; -import static com.google.android.testing.mocking.AndroidMock.leq; import com.google.android.apps.mytracks.Constants; import com.google.android.apps.mytracks.content.MyTracksProviderUtils.LocationFactory; import com.google.android.apps.mytracks.content.MyTracksProviderUtils.LocationIterator; -import com.google.android.apps.mytracks.content.TrackDataListener.ProviderState; +import com.google.android.apps.mytracks.content.TrackDataListener.LocationState; import com.google.android.apps.mytracks.services.TrackRecordingServiceTest.MockContext; import com.google.android.apps.mytracks.util.PreferencesUtils; import com.google.android.maps.mytracks.R; @@ -37,7 +36,6 @@ import android.content.SharedPreferences.OnSharedPreferenceChangeListener; import android.database.ContentObserver; import android.database.Cursor; import android.database.MatrixCursor; -import android.hardware.Sensor; import android.hardware.SensorEvent; import android.hardware.SensorEventListener; import android.location.Location; @@ -57,7 +55,7 @@ import org.easymock.IAnswer; /** * Tests for {@link TrackDataHub}. - * + * * @author Rodrigo Damazio */ public class TrackDataHubTest extends AndroidTestCase { @@ -65,53 +63,51 @@ public class TrackDataHubTest extends AndroidTestCase { private static final long TRACK_ID = 42L; private static final int TARGET_POINTS = 50; - private MyTracksProviderUtils providerUtils; - private TrackDataHub hub; - private TrackDataManager trackDataManager; - private DataSource dataSource; - private SharedPreferences sharedPreferences; - private TrackDataListener listener1; - private TrackDataListener listener2; - private Capture preferenceListenerCapture = - new Capture(); private MockContext context; + private SharedPreferences sharedPreferences; + private MyTracksProviderUtils myTracksProviderUtils; + private DataSource dataSource; + private TrackDataManager trackDataManager; + private TrackDataHub trackDataHub; + private TrackDataListener trackDataListener1; + private TrackDataListener trackDataListener2; + private Capture preferenceChangeListenerCapture = new Capture< + SharedPreferences.OnSharedPreferenceChangeListener>(); private float declination; - @UsesMocks({MyTracksProviderUtils.class, DataSource.class, TrackDataListener.class}) + @UsesMocks({ MyTracksProviderUtils.class, DataSource.class, TrackDataListener.class }) @Override protected void setUp() throws Exception { super.setUp(); - - MockContentResolver mockContentResolver = new MockContentResolver(); - RenamingDelegatingContext targetContext = new RenamingDelegatingContext( - getContext(), getContext(), "test."); - context = new MockContext(mockContentResolver, targetContext); - + context = new MockContext(new MockContentResolver(), new RenamingDelegatingContext( + getContext(), getContext(), "test.")); sharedPreferences = context.getSharedPreferences(Constants.SETTINGS_NAME, Context.MODE_PRIVATE); - providerUtils = AndroidMock.createMock("providerUtils", MyTracksProviderUtils.class); - dataSource = AndroidMock.createMock("dataSource", DataSource.class, context); - + myTracksProviderUtils = AndroidMock.createMock(MyTracksProviderUtils.class); + dataSource = AndroidMock.createMock(DataSource.class, context); trackDataManager = new TrackDataManager(); - hub = new TrackDataHub(context, trackDataManager, providerUtils, TARGET_POINTS) { - @Override + trackDataHub = new TrackDataHub( + context, trackDataManager, myTracksProviderUtils, TARGET_POINTS) { + @Override protected DataSource newDataSource() { return dataSource; } - @Override - protected void runInListenerThread(Runnable runnable) { - // Run everything in the same thread. + @Override + protected void runInHanderThread(Runnable runnable) { + // Run everything in the same thread runnable.run(); } - @Override - protected float getDeclinationFor(Location location, long timestamp) { + @Override + protected float getDeclination(Location location, long timestamp) { return declination; } }; - listener1 = AndroidMock.createStrictMock("listener1", TrackDataListener.class); - listener2 = AndroidMock.createStrictMock("listener2", TrackDataListener.class); + trackDataListener1 = AndroidMock.createStrictMock( + "trackDataListener1", TrackDataListener.class); + trackDataListener2 = AndroidMock.createStrictMock( + "trackDataListener2", TrackDataListener.class); PreferencesUtils.setLong(context, R.string.recording_track_id_key, TRACK_ID); PreferencesUtils.setLong(context, R.string.selected_track_id_key, TRACK_ID); } @@ -121,78 +117,563 @@ public class TrackDataHubTest extends AndroidTestCase { AndroidMock.reset(dataSource); // Expect everything to be unregistered. - if (preferenceListenerCapture.hasCaptured()) { - dataSource.unregisterOnSharedPreferenceChangeListener(preferenceListenerCapture.getValue()); - } - dataSource.unregisterLocationListener(isA(LocationListener.class)); - dataSource.unregisterCompassListener(isA(SensorEventListener.class)); dataSource.unregisterContentObserver(isA(ContentObserver.class)); AndroidMock.expectLastCall().times(3); - + dataSource.unregisterLocationListener(isA(LocationListener.class)); + dataSource.unregisterHeadingListener(isA(SensorEventListener.class)); + dataSource.unregisterOnSharedPreferenceChangeListener( + isA(OnSharedPreferenceChangeListener.class)); AndroidMock.replay(dataSource); - hub.stop(); - hub = null; - + trackDataHub.stop(); + trackDataHub = null; super.tearDown(); } - public void testTrackListen() { - Capture observerCapture = new Capture(); + /** + * Tests registering for tracks table update. + */ + public void testTracksTableUpdate() { + + // Register two listeners + Capture contentObserverCapture = new Capture(); Track track = new Track(); - expect(providerUtils.getTrack(TRACK_ID)).andStubReturn(track); - expectStart(); - dataSource.registerContentObserver(eq(TracksColumns.CONTENT_URI), capture(observerCapture)); - - // Expect the initial loading. - // Both listeners (registered before and after start) should get the same data. - listener1.onTrackUpdated(track); - listener2.onTrackUpdated(track); - + expect(myTracksProviderUtils.getTrack(TRACK_ID)).andStubReturn(track); + dataSource.registerOnSharedPreferenceChangeListener(capture(preferenceChangeListenerCapture)); + dataSource.registerContentObserver( + eq(TracksColumns.CONTENT_URI), capture(contentObserverCapture)); + trackDataListener1.onTrackUpdated(track); + trackDataListener2.onTrackUpdated(track); replay(); - hub.registerTrackDataListener(listener1, EnumSet.of(TrackDataType.TRACKS_TABLE)); - hub.start(); - hub.registerTrackDataListener(listener2, EnumSet.of(TrackDataType.TRACKS_TABLE)); - + trackDataHub.start(); + trackDataHub.registerTrackDataListener( + trackDataListener1, EnumSet.of(TrackDataType.TRACKS_TABLE)); + trackDataHub.registerTrackDataListener( + trackDataListener2, EnumSet.of(TrackDataType.TRACKS_TABLE)); verifyAndReset(); - ContentObserver observer = observerCapture.getValue(); - expect(providerUtils.getTrack(TRACK_ID)).andStubReturn(track); - - // Now expect an update. - listener1.onTrackUpdated(track); - listener2.onTrackUpdated(track); - + // Causes tracks table update + ContentObserver contentObserver = contentObserverCapture.getValue(); + expect(myTracksProviderUtils.getTrack(TRACK_ID)).andStubReturn(track); + trackDataListener1.onTrackUpdated(track); + trackDataListener2.onTrackUpdated(track); replay(); - observer.onChange(false); - + contentObserver.onChange(false); verifyAndReset(); - // Unregister one, get another update. - expect(providerUtils.getTrack(TRACK_ID)).andStubReturn(track); - listener2.onTrackUpdated(track); - + // Unregister one listener + expect(myTracksProviderUtils.getTrack(TRACK_ID)).andStubReturn(track); + trackDataListener2.onTrackUpdated(track); replay(); - hub.unregisterTrackDataListener(listener1); - - observer.onChange(false); - + trackDataHub.unregisterTrackDataListener(trackDataListener1); + contentObserver.onChange(false); verifyAndReset(); - // Unregister the other, expect internal unregistration - dataSource.unregisterContentObserver(observer); - + // Unregister the second listener + dataSource.unregisterContentObserver(contentObserver); replay(); - hub.unregisterTrackDataListener(listener2); - observer.onChange(false); - + trackDataHub.unregisterTrackDataListener(trackDataListener2); + contentObserver.onChange(false); verifyAndReset(); } + /** + * Tests registering for waypoints table update. + */ + public void testWaypointsTableUpdate() { + Waypoint waypoint1 = new Waypoint(); + Waypoint waypoint2 = new Waypoint(); + Waypoint waypoint3 = new Waypoint(); + Waypoint waypoint4 = new Waypoint(); + Location location = new Location("gps"); + location.setLatitude(10.0); + location.setLongitude(8.0); + waypoint1.setLocation(location); + waypoint2.setLocation(location); + waypoint3.setLocation(location); + waypoint4.setLocation(location); + + // Register two listeners + Capture contentObserverCapture = new Capture(); + expect(myTracksProviderUtils.getWaypointsCursor( + eq(TRACK_ID), AndroidMock.leq(0L), eq(Constants.MAX_DISPLAYED_WAYPOINTS_POINTS))) + .andStubAnswer(new FixedSizeCursorAnswer(2)); + expect(myTracksProviderUtils.createWaypoint(isA(Cursor.class))) + .andReturn(waypoint1).andReturn(waypoint2).andReturn(waypoint1).andReturn(waypoint2); + dataSource.registerOnSharedPreferenceChangeListener(capture(preferenceChangeListenerCapture)); + dataSource.registerContentObserver( + eq(WaypointsColumns.CONTENT_URI), capture(contentObserverCapture)); + trackDataListener1.clearWaypoints(); + trackDataListener2.clearWaypoints(); + trackDataListener1.onNewWaypoint(waypoint1); + trackDataListener2.onNewWaypoint(waypoint1); + trackDataListener1.onNewWaypoint(waypoint2); + trackDataListener2.onNewWaypoint(waypoint2); + trackDataListener1.onNewWaypointsDone(); + trackDataListener2.onNewWaypointsDone(); + replay(); + + trackDataHub.start(); + trackDataHub.registerTrackDataListener( + trackDataListener1, EnumSet.of(TrackDataType.WAYPOINTS_TABLE)); + trackDataHub.registerTrackDataListener( + trackDataListener2, EnumSet.of(TrackDataType.WAYPOINTS_TABLE)); + verifyAndReset(); + + // Cause waypoints table update + ContentObserver contentObserver = contentObserverCapture.getValue(); + expect(myTracksProviderUtils.getWaypointsCursor( + eq(TRACK_ID), AndroidMock.leq(0L), eq(Constants.MAX_DISPLAYED_WAYPOINTS_POINTS))) + .andStubAnswer(new FixedSizeCursorAnswer(3)); + expect(myTracksProviderUtils.createWaypoint(isA(Cursor.class))) + .andReturn(waypoint1).andReturn(waypoint2).andReturn(waypoint3); + trackDataListener1.clearWaypoints(); + trackDataListener2.clearWaypoints(); + trackDataListener1.onNewWaypoint(waypoint1); + trackDataListener2.onNewWaypoint(waypoint1); + trackDataListener1.onNewWaypoint(waypoint2); + trackDataListener2.onNewWaypoint(waypoint2); + trackDataListener1.onNewWaypoint(waypoint3); + trackDataListener2.onNewWaypoint(waypoint3); + trackDataListener1.onNewWaypointsDone(); + trackDataListener2.onNewWaypointsDone(); + replay(); + + contentObserver.onChange(false); + verifyAndReset(); + + // Unregister one listener + expect(myTracksProviderUtils.getWaypointsCursor( + eq(TRACK_ID), AndroidMock.leq(0L), eq(Constants.MAX_DISPLAYED_WAYPOINTS_POINTS))) + .andStubAnswer(new FixedSizeCursorAnswer(4)); + expect(myTracksProviderUtils.createWaypoint(isA(Cursor.class))) + .andReturn(waypoint1).andReturn(waypoint2).andReturn(waypoint3).andReturn(waypoint4); + trackDataListener2.clearWaypoints(); + trackDataListener2.onNewWaypoint(waypoint1); + trackDataListener2.onNewWaypoint(waypoint2); + trackDataListener2.onNewWaypoint(waypoint3); + trackDataListener2.onNewWaypoint(waypoint4); + trackDataListener2.onNewWaypointsDone(); + replay(); + + trackDataHub.unregisterTrackDataListener(trackDataListener1); + contentObserver.onChange(false); + verifyAndReset(); + + // Unregister the second listener + dataSource.unregisterContentObserver(contentObserver); + replay(); + + trackDataHub.unregisterTrackDataListener(trackDataListener2); + contentObserver.onChange(false); + verifyAndReset(); + } + + /** + * Tests track points table update. + */ + public void testTrackPointsTableUpdate() { + // Register one listener + Capture contentObserverCapture = new Capture(); + dataSource.registerOnSharedPreferenceChangeListener(capture(preferenceChangeListenerCapture)); + dataSource.registerContentObserver( + eq(TrackPointsColumns.CONTENT_URI), capture(contentObserverCapture)); + + FixedSizeLocationIterator locationIterator = new FixedSizeLocationIterator(1, 10, 5); + expect(myTracksProviderUtils.getLocationIterator( + eq(TRACK_ID), eq(0L), eq(false), isA(LocationFactory.class))).andReturn(locationIterator); + expect(myTracksProviderUtils.getLastLocationId(TRACK_ID)).andReturn(10L); + trackDataListener1.clearTrackPoints(); + locationIterator.expectLocationsDelivered(trackDataListener1); + trackDataListener1.onNewTrackPointsDone(); + replay(); + + trackDataHub.start(); + trackDataHub.registerTrackDataListener( + trackDataListener1, EnumSet.of(TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE)); + verifyAndReset(); + + // Register a second listener + locationIterator = new FixedSizeLocationIterator(1, 10, 5); + expect(myTracksProviderUtils.getLocationIterator( + eq(TRACK_ID), eq(0L), eq(false), isA(LocationFactory.class))).andReturn(locationIterator); + expect(myTracksProviderUtils.getLastLocationId(TRACK_ID)).andReturn(10L); + trackDataListener2.clearTrackPoints(); + locationIterator.expectLocationsDelivered(trackDataListener2); + trackDataListener2.onNewTrackPointsDone(); + replay(); + + trackDataHub.registerTrackDataListener( + trackDataListener2, EnumSet.of(TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE)); + verifyAndReset(); + + // Deliver more points - should go to both listeners without clearing + ContentObserver contentObserver = contentObserverCapture.getValue(); + locationIterator = new FixedSizeLocationIterator(11, 10, 1); + expect(myTracksProviderUtils.getLocationIterator( + eq(TRACK_ID), eq(11L), eq(false), isA(LocationFactory.class))).andReturn(locationIterator); + expect(myTracksProviderUtils.getLastLocationId(TRACK_ID)).andReturn(20L); + locationIterator.expectLocationsDelivered(trackDataListener1); + locationIterator.expectLocationsDelivered(trackDataListener2); + trackDataListener1.onNewTrackPointsDone(); + trackDataListener2.onNewTrackPointsDone(); + replay(); + + contentObserver.onChange(false); + verifyAndReset(); + + // Unregister one listener and change track + locationIterator = new FixedSizeLocationIterator(101, 10); + expect(myTracksProviderUtils.getLocationIterator( + eq(TRACK_ID + 1), eq(0L), eq(false), isA(LocationFactory.class))) + .andReturn(locationIterator); + expect(myTracksProviderUtils.getLastLocationId(TRACK_ID + 1)).andReturn(110L); + trackDataListener2.clearTrackPoints(); + locationIterator.expectLocationsDelivered(trackDataListener2); + trackDataListener2.onNewTrackPointsDone(); + replay(); + + trackDataHub.unregisterTrackDataListener(trackDataListener1); + trackDataHub.loadTrack(TRACK_ID + 1); + verifyAndReset(); + } + + /** + * Tests track points table update with registering the same listener. + */ + public void testTrackPointsTableUpdate_reRegister() { + + // Register one listener + Capture contentObserverCapture = new Capture(); + dataSource.registerOnSharedPreferenceChangeListener(capture(preferenceChangeListenerCapture)); + dataSource.registerContentObserver( + eq(TrackPointsColumns.CONTENT_URI), capture(contentObserverCapture)); + + FixedSizeLocationIterator locationIterator = new FixedSizeLocationIterator(1, 10, 5); + expect(myTracksProviderUtils.getLocationIterator( + eq(TRACK_ID), eq(0L), eq(false), isA(LocationFactory.class))).andReturn(locationIterator); + expect(myTracksProviderUtils.getLastLocationId(TRACK_ID)).andReturn(10L); + + trackDataListener1.clearTrackPoints(); + locationIterator.expectLocationsDelivered(trackDataListener1); + trackDataListener1.onNewTrackPointsDone(); + replay(); + + trackDataHub.start(); + trackDataHub.registerTrackDataListener( + trackDataListener1, EnumSet.of(TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE)); + verifyAndReset(); + + // Unregister the listener + ContentObserver observer = contentObserverCapture.getValue(); + dataSource.unregisterContentObserver(observer); + replay(); + + trackDataHub.unregisterTrackDataListener(trackDataListener1); + verifyAndReset(); + + // Register again + dataSource.registerContentObserver( + eq(TrackPointsColumns.CONTENT_URI), capture(contentObserverCapture)); + locationIterator = new FixedSizeLocationIterator(1, 10, 5); + expect(myTracksProviderUtils.getLocationIterator( + eq(TRACK_ID), eq(0L), eq(false), isA(LocationFactory.class))).andReturn(locationIterator); + expect(myTracksProviderUtils.getLastLocationId(TRACK_ID)).andReturn(10L); + trackDataListener1.clearTrackPoints(); + locationIterator.expectLocationsDelivered(trackDataListener1); + trackDataListener1.onNewTrackPointsDone(); + replay(); + + trackDataHub.registerTrackDataListener( + trackDataListener1, EnumSet.of(TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE)); + verifyAndReset(); + } + + /** + * Tests tracks point able change. Register a listener after a track change. + */ + public void testTrackPointsTableUpdate_reRegisterAfterTrackChange() { + + // Register one listener + Capture observerCapture = new Capture(); + dataSource.registerOnSharedPreferenceChangeListener(capture(preferenceChangeListenerCapture)); + dataSource.registerContentObserver( + eq(TrackPointsColumns.CONTENT_URI), capture(observerCapture)); + + FixedSizeLocationIterator locationIterator = new FixedSizeLocationIterator(1, 10, 5); + expect(myTracksProviderUtils.getLocationIterator( + eq(TRACK_ID), eq(0L), eq(false), isA(LocationFactory.class))).andReturn(locationIterator); + expect(myTracksProviderUtils.getLastLocationId(TRACK_ID)).andReturn(10L); + trackDataListener1.clearTrackPoints(); + locationIterator.expectLocationsDelivered(trackDataListener1); + trackDataListener1.onNewTrackPointsDone(); + replay(); + + trackDataHub.start(); + trackDataHub.registerTrackDataListener( + trackDataListener1, EnumSet.of(TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE)); + verifyAndReset(); + + // Unregister the listener + ContentObserver observer = observerCapture.getValue(); + dataSource.unregisterContentObserver(observer); + replay(); + + trackDataHub.unregisterTrackDataListener(trackDataListener1); + verifyAndReset(); + + // Register the listener after a new track + dataSource.registerContentObserver( + eq(TrackPointsColumns.CONTENT_URI), capture(observerCapture)); + locationIterator = new FixedSizeLocationIterator(1, 10); + expect(myTracksProviderUtils.getLocationIterator( + eq(TRACK_ID + 1), eq(0L), eq(false), isA(LocationFactory.class))) + .andReturn(locationIterator); + expect(myTracksProviderUtils.getLastLocationId(TRACK_ID + 1)).andReturn(10L); + trackDataListener1.clearTrackPoints(); + locationIterator.expectLocationsDelivered(trackDataListener1); + trackDataListener1.onNewTrackPointsDone(); + replay(); + + trackDataHub.loadTrack(TRACK_ID + 1); + trackDataHub.registerTrackDataListener( + trackDataListener1, EnumSet.of(TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE)); + verifyAndReset(); + } + + /** + * Tests track points table update with large track sampling. + */ + public void testTrackPointsTableUpdate_largeTrackSampling() { + Capture contentObserverCapture = new Capture(); + dataSource.registerOnSharedPreferenceChangeListener(capture(preferenceChangeListenerCapture)); + dataSource.registerContentObserver( + eq(TrackPointsColumns.CONTENT_URI), capture(contentObserverCapture)); + + expect(myTracksProviderUtils.getLastLocationId(TRACK_ID)).andReturn(200L); + AndroidMock.expectLastCall().anyTimes(); + FixedSizeLocationIterator locationIterator1 = new FixedSizeLocationIterator( + 1, 200, 4, 25, 71, 120); + expect(myTracksProviderUtils.getLocationIterator( + eq(TRACK_ID), eq(0L), eq(false), isA(LocationFactory.class))).andReturn(locationIterator1); + FixedSizeLocationIterator locationIterator2 = new FixedSizeLocationIterator( + 1, 200, 4, 25, 71, 120); + expect(myTracksProviderUtils.getLocationIterator( + eq(TRACK_ID), eq(0L), eq(false), isA(LocationFactory.class))).andReturn(locationIterator2); + + trackDataListener1.clearTrackPoints(); + locationIterator1.expectSampledLocationsDelivered(trackDataListener1, 4, false); + trackDataListener1.onNewTrackPointsDone(); + trackDataListener2.clearTrackPoints(); + locationIterator2.expectSampledLocationsDelivered(trackDataListener2, 4, true); + trackDataListener2.onNewTrackPointsDone(); + replay(); + + trackDataHub.start(); + trackDataHub.registerTrackDataListener( + trackDataListener1, EnumSet.of(TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE)); + trackDataHub.registerTrackDataListener(trackDataListener2, EnumSet.of( + TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE, TrackDataType.SAMPLED_OUT_TRACK_POINTS_TABLE)); + verifyAndReset(); + } + + /** + * Tests track points table update with resampling. + */ + public void testTrackPointsTableUpdate_resampling() { + Capture observerCapture = new Capture(); + dataSource.registerOnSharedPreferenceChangeListener(capture(preferenceChangeListenerCapture)); + dataSource.registerContentObserver( + eq(TrackPointsColumns.CONTENT_URI), capture(observerCapture)); + + // Deliver 30 points (no sampling happens) + FixedSizeLocationIterator locationIterator = new FixedSizeLocationIterator(1, 30, 5); + expect(myTracksProviderUtils.getLocationIterator( + eq(TRACK_ID), eq(0L), eq(false), isA(LocationFactory.class))).andReturn(locationIterator); + expect(myTracksProviderUtils.getLastLocationId(TRACK_ID)).andReturn(30L); + + trackDataListener1.clearTrackPoints(); + locationIterator.expectLocationsDelivered(trackDataListener1); + trackDataListener1.onNewTrackPointsDone(); + replay(); + + trackDataHub.start(); + trackDataHub.registerTrackDataListener( + trackDataListener1, EnumSet.of(TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE)); + verifyAndReset(); + + // Now deliver 30 more (incrementally sampled) + ContentObserver observer = observerCapture.getValue(); + locationIterator = new FixedSizeLocationIterator(31, 30); + expect(myTracksProviderUtils.getLocationIterator( + eq(TRACK_ID), eq(31L), eq(false), isA(LocationFactory.class))).andReturn(locationIterator); + expect(myTracksProviderUtils.getLastLocationId(TRACK_ID)).andReturn(60L); + locationIterator.expectSampledLocationsDelivered(trackDataListener1, 2, false); + trackDataListener1.onNewTrackPointsDone(); + replay(); + + observer.onChange(false); + verifyAndReset(); + + // Now another 30 (triggers resampling) + locationIterator = new FixedSizeLocationIterator(1, 90); + expect(myTracksProviderUtils.getLocationIterator( + eq(TRACK_ID), eq(0L), eq(false), isA(LocationFactory.class))).andReturn(locationIterator); + expect(myTracksProviderUtils.getLastLocationId(TRACK_ID)).andReturn(90L); + trackDataListener1.clearTrackPoints(); + locationIterator.expectSampledLocationsDelivered(trackDataListener1, 2, false); + trackDataListener1.onNewTrackPointsDone(); + replay(); + + observer.onChange(false); + verifyAndReset(); + } + + /** + * Tests headings change. + */ + public void testHeadingsChange() throws Exception { + dataSource.registerOnSharedPreferenceChangeListener(capture(preferenceChangeListenerCapture)); + Capture sensorEventListenerCapture = new Capture(); + dataSource.registerHeadingListener(capture(sensorEventListenerCapture)); + Capture locationListenerCapture = new Capture(); + dataSource.registerLocationListener(capture(locationListenerCapture)); + + SensorEvent event = newSensorEvent(); + + // Expect location state changed + trackDataListener1.onLocationStateChanged(isA(LocationState.class)); + AndroidMock.expectLastCall().anyTimes(); + + // First, get a dummy heading update + trackDataListener1.onHeadingChanged(0.0); + + // Second, get a heading update without known location + trackDataListener1.onHeadingChanged(42.0f); + replay(); + + // Register one listener and update heading value + trackDataHub.start(); + trackDataHub.registerTrackDataListener( + trackDataListener1, EnumSet.of(TrackDataType.HEADING, TrackDataType.LOCATION)); + SensorEventListener sensorListener = sensorEventListenerCapture.getValue(); + LocationListener locationListener = locationListenerCapture.getValue(); + event.values[0] = 42.0f; + sensorListener.onSensorChanged(event); + verifyAndReset(); + + // Expect location state changed + trackDataListener1.onLocationStateChanged(isA(LocationState.class)); + AndroidMock.expectLastCall().anyTimes(); + + // Expect location changed + trackDataListener1.onLocationChanged(isA(Location.class)); + AndroidMock.expectLastCall().anyTimes(); + + // Expect a heading update with declination + trackDataListener1.onHeadingChanged(52.0); + replay(); + + // Update location and sensor + Location location = new Location("gps"); + location.setLatitude(10.0); + location.setLongitude(20.0); + location.setAltitude(30.0); + declination = 10.0f; + locationListener.onLocationChanged(location); + sensorListener.onSensorChanged(event); + verifyAndReset(); + + trackDataListener1.onHeadingChanged(52.0); + replay(); + + /* + * Change declination. Should still get the old value since the declination + * is only updated once an hour. + */ + declination = 20.0f; + sensorListener.onSensorChanged(event); + verifyAndReset(); + } + + /** + * Tests preferences change. + */ + public void testPreferencesChange() throws Exception { + + // Register two listeners + PreferencesUtils.setBoolean(context, R.string.report_speed_key, true); + PreferencesUtils.setBoolean(context, R.string.metric_units_key, true); + + dataSource.registerOnSharedPreferenceChangeListener(capture(preferenceChangeListenerCapture)); + expect(trackDataListener1.onMetricUnitsChanged(true)).andReturn(false); + expect(trackDataListener1.onReportSpeedChanged(true)).andReturn(false); + expect(trackDataListener2.onMetricUnitsChanged(true)).andReturn(false); + expect(trackDataListener2.onReportSpeedChanged(true)).andReturn(false); + replay(); + + trackDataHub.start(); + trackDataHub.registerTrackDataListener( + trackDataListener1, EnumSet.of(TrackDataType.PREFERENCE)); + trackDataHub.registerTrackDataListener( + trackDataListener2, EnumSet.of(TrackDataType.PREFERENCE)); + verifyAndReset(); + + // Change report speed to false + expect(trackDataListener1.onReportSpeedChanged(false)).andReturn(false); + expect(trackDataListener2.onReportSpeedChanged(false)).andReturn(false); + replay(); + + PreferencesUtils.setBoolean(context, R.string.report_speed_key, false); + OnSharedPreferenceChangeListener listener = preferenceChangeListenerCapture.getValue(); + listener.onSharedPreferenceChanged( + sharedPreferences, PreferencesUtils.getKey(context, R.string.report_speed_key)); + verifyAndReset(); + + // Change metric units to false + expect(trackDataListener1.onMetricUnitsChanged(false)).andReturn(false); + expect(trackDataListener2.onMetricUnitsChanged(false)).andReturn(false); + replay(); + + PreferencesUtils.setBoolean(context, R.string.metric_units_key, false); + listener.onSharedPreferenceChanged( + sharedPreferences, PreferencesUtils.getKey(context, R.string.metric_units_key)); + verifyAndReset(); + } + + /** + * Creates a new sensor event. + */ + private SensorEvent newSensorEvent() throws Exception { + Constructor constructor = SensorEvent.class.getDeclaredConstructor(int.class); + constructor.setAccessible(true); + return constructor.newInstance(3); + } + + /** + * Replays mocks. + */ + private void replay() { + AndroidMock.replay(myTracksProviderUtils, dataSource, trackDataListener1, trackDataListener2); + } + + /** + * Verifies and resets mocks. + */ + private void verifyAndReset() { + AndroidMock.verify(myTracksProviderUtils, dataSource, trackDataListener1, trackDataListener2); + AndroidMock.reset(myTracksProviderUtils, dataSource, trackDataListener1, trackDataListener2); + } + + /** + * Fixed size cursor answer. + * + * @author Jimmy Shih + */ private static class FixedSizeCursorAnswer implements IAnswer { private final int size; @@ -203,79 +684,86 @@ public class TrackDataHubTest extends AndroidTestCase { @Override public Cursor answer() throws Throwable { MatrixCursor cursor = new MatrixCursor(new String[] { BaseColumns._ID }); - for (long i = 1; i <= size; i++) { + for (long i = 0; i < size; i++) { cursor.addRow(new Object[] { i }); } return cursor; } } + /** + * Fixed size location iterator. + * + * @author Jimmy Shih + */ private static class FixedSizeLocationIterator implements LocationIterator { private final long startId; - private final Location[] locs; + private final Location[] locations; private final Set splitIndexSet = new HashSet(); - private int currentIdx = -1; + private int currentIndex = -1; public FixedSizeLocationIterator(long startId, int size) { this(startId, size, null); } - public FixedSizeLocationIterator(long startId, int size, int... splitIndices) { + public FixedSizeLocationIterator(long startId, int size, int... splitIndexes) { this.startId = startId; - this.locs = new Location[size]; + this.locations = new Location[size]; for (int i = 0; i < size; i++) { - Location loc = new Location("gps"); - loc.setLatitude(-15.0 + i / 1000.0); - loc.setLongitude(37 + i / 1000.0); - loc.setAltitude(i); - - locs[i] = loc; + Location location = new Location("gps"); + location.setLatitude(-15.0 + i / 1000.0); + location.setLongitude(37 + i / 1000.0); + location.setAltitude(i); + locations[i] = location; } - if (splitIndices != null) { - for (int splitIdx : splitIndices) { - splitIndexSet.add(splitIdx); + if (splitIndexes != null) { + for (int splitIndex : splitIndexes) { + splitIndexSet.add(splitIndex); - Location splitLoc = locs[splitIdx]; - splitLoc.setLatitude(100.0); - splitLoc.setLongitude(200.0); + Location splitLocation = locations[splitIndex]; + splitLocation.setLatitude(100.0); + splitLocation.setLongitude(200.0); } } } public void expectLocationsDelivered(TrackDataListener listener) { - for (int i = 0; i < locs.length; i++) { + for (int i = 0; i < locations.length; i++) { if (splitIndexSet.contains(i)) { listener.onSegmentSplit(); } else { - listener.onNewTrackPoint(locs[i]); + listener.onSampledInTrackPoint(locations[i]); } } } public void expectSampledLocationsDelivered( TrackDataListener listener, int sampleFrequency, boolean includeSampledOut) { - for (int i = 0; i < locs.length; i++) { + boolean includeNext = false; + for (int i = 0; i < locations.length; i++) { if (splitIndexSet.contains(i)) { listener.onSegmentSplit(); - } else if (i % sampleFrequency == 0) { - listener.onNewTrackPoint(locs[i]); + includeNext = true; + } else if (includeNext || (i % sampleFrequency == 0)) { + listener.onSampledInTrackPoint(locations[i]); + includeNext = false; } else if (includeSampledOut) { - listener.onSampledOutTrackPoint(locs[i]); + listener.onSampledOutTrackPoint(locations[i]); } } } @Override public boolean hasNext() { - return currentIdx < (locs.length - 1); + return currentIndex < locations.length - 1; } @Override public Location next() { - currentIdx++; - return locs[currentIdx]; + currentIndex++; + return locations[currentIndex]; } @Override @@ -285,7 +773,7 @@ public class TrackDataHubTest extends AndroidTestCase { @Override public long getLocationId() { - return startId + currentIdx; + return startId + currentIndex; } @Override @@ -293,544 +781,4 @@ public class TrackDataHubTest extends AndroidTestCase { // Do nothing } } - - public void testWaypointListen() { - Capture observerCapture = new Capture(); - - Waypoint wpt1 = new Waypoint(), - wpt2 = new Waypoint(), - wpt3 = new Waypoint(), - wpt4 = new Waypoint(); - Location loc = new Location("gps"); - loc.setLatitude(10.0); - loc.setLongitude(8.0); - wpt1.setLocation(loc); - wpt2.setLocation(loc); - wpt3.setLocation(loc); - wpt4.setLocation(loc); - - expect(providerUtils.getWaypointsCursor( - eq(TRACK_ID), leq(0L), eq(Constants.MAX_DISPLAYED_WAYPOINTS_POINTS))) - .andStubAnswer(new FixedSizeCursorAnswer(2)); - expect(providerUtils.createWaypoint(isA(Cursor.class))) - .andReturn(wpt1) - .andReturn(wpt2) - .andReturn(wpt1) - .andReturn(wpt2); - - expectStart(); - dataSource.registerContentObserver(eq(WaypointsColumns.CONTENT_URI), capture(observerCapture)); - - // Expect the initial loading. - // Both listeners (registered before and after start) should get the same data. - listener1.clearWaypoints(); - listener1.onNewWaypoint(wpt1); - listener1.onNewWaypoint(wpt2); - listener1.onNewWaypointsDone(); - listener2.clearWaypoints(); - listener2.onNewWaypoint(wpt1); - listener2.onNewWaypoint(wpt2); - listener2.onNewWaypointsDone(); - - replay(); - - hub.registerTrackDataListener(listener1, EnumSet.of(TrackDataType.WAYPOINTS_TABLE)); - hub.start(); - hub.registerTrackDataListener(listener2, EnumSet.of(TrackDataType.WAYPOINTS_TABLE)); - - verifyAndReset(); - - ContentObserver observer = observerCapture.getValue(); - - expect(providerUtils.getWaypointsCursor( - eq(TRACK_ID), leq(0L), eq(Constants.MAX_DISPLAYED_WAYPOINTS_POINTS))) - .andStubAnswer(new FixedSizeCursorAnswer(3)); - expect(providerUtils.createWaypoint(isA(Cursor.class))) - .andReturn(wpt1) - .andReturn(wpt2) - .andReturn(wpt3); - - // Now expect an update. - listener1.clearWaypoints(); - listener2.clearWaypoints(); - listener1.onNewWaypoint(wpt1); - listener2.onNewWaypoint(wpt1); - listener1.onNewWaypoint(wpt2); - listener2.onNewWaypoint(wpt2); - listener1.onNewWaypoint(wpt3); - listener2.onNewWaypoint(wpt3); - listener1.onNewWaypointsDone(); - listener2.onNewWaypointsDone(); - - replay(); - - observer.onChange(false); - - verifyAndReset(); - - // Unregister one, get another update. - expect(providerUtils.getWaypointsCursor( - eq(TRACK_ID), leq(0L), eq(Constants.MAX_DISPLAYED_WAYPOINTS_POINTS))) - .andStubAnswer(new FixedSizeCursorAnswer(4)); - expect(providerUtils.createWaypoint(isA(Cursor.class))) - .andReturn(wpt1) - .andReturn(wpt2) - .andReturn(wpt3) - .andReturn(wpt4); - - // Now expect an update. - listener2.clearWaypoints(); - listener2.onNewWaypoint(wpt1); - listener2.onNewWaypoint(wpt2); - listener2.onNewWaypoint(wpt3); - listener2.onNewWaypoint(wpt4); - listener2.onNewWaypointsDone(); - - replay(); - - hub.unregisterTrackDataListener(listener1); - - observer.onChange(false); - - verifyAndReset(); - - // Unregister the other, expect internal unregistration - dataSource.unregisterContentObserver(observer); - - replay(); - - hub.unregisterTrackDataListener(listener2); - observer.onChange(false); - - verifyAndReset(); - } - - public void testPointsListen() { - Capture observerCapture = new Capture(); - - expectStart(); - dataSource.registerContentObserver( - eq(TrackPointsColumns.CONTENT_URI), capture(observerCapture)); - - FixedSizeLocationIterator locationIterator = new FixedSizeLocationIterator(1, 10, 5); - expect(providerUtils.getLocationIterator( - eq(TRACK_ID), eq(0L), eq(false), isA(LocationFactory.class))) - .andReturn(locationIterator); - expect(providerUtils.getLastLocationId(TRACK_ID)).andReturn(10L); - - listener1.clearTrackPoints(); - locationIterator.expectLocationsDelivered(listener1); - listener1.onNewTrackPointsDone(); - - replay(); - - hub.start(); - hub.registerTrackDataListener(listener1, EnumSet.of(TrackDataType.TRACK_POINTS_TABLE)); - - verifyAndReset(); - - // Register a second listener - it will get the same points as the previous one - locationIterator = new FixedSizeLocationIterator(1, 10, 5); - expect(providerUtils.getLocationIterator( - eq(TRACK_ID), eq(0L), eq(false), isA(LocationFactory.class))) - .andReturn(locationIterator); - expect(providerUtils.getLastLocationId(TRACK_ID)).andReturn(10L); - - listener2.clearTrackPoints(); - locationIterator.expectLocationsDelivered(listener2); - listener2.onNewTrackPointsDone(); - - replay(); - - hub.start(); - hub.registerTrackDataListener(listener2, EnumSet.of(TrackDataType.TRACK_POINTS_TABLE)); - - verifyAndReset(); - - // Deliver more points - should go to both listeners, without clearing. - ContentObserver observer = observerCapture.getValue(); - - locationIterator = new FixedSizeLocationIterator(11, 10, 1); - expect(providerUtils.getLocationIterator( - eq(TRACK_ID), eq(11L), eq(false), isA(LocationFactory.class))) - .andReturn(locationIterator); - expect(providerUtils.getLastLocationId(TRACK_ID)).andReturn(20L); - - locationIterator.expectLocationsDelivered(listener1); - locationIterator.expectLocationsDelivered(listener2); - listener1.onNewTrackPointsDone(); - listener2.onNewTrackPointsDone(); - - replay(); - - observer.onChange(false); - - verifyAndReset(); - - // Unregister listener1, switch tracks to ensure data is cleared/reloaded. - locationIterator = new FixedSizeLocationIterator(101, 10); - expect(providerUtils.getLocationIterator( - eq(TRACK_ID + 1), eq(0L), eq(false), isA(LocationFactory.class))) - .andReturn(locationIterator); - expect(providerUtils.getLastLocationId(TRACK_ID + 1)).andReturn(110L); - - listener2.clearTrackPoints(); - locationIterator.expectLocationsDelivered(listener2); - listener2.onNewTrackPointsDone(); - - replay(); - - hub.unregisterTrackDataListener(listener1); - hub.loadTrack(TRACK_ID + 1); - - verifyAndReset(); - } - - public void testPointsListen_beforeStart() { - - } - - public void testPointsListen_reRegister() { - Capture observerCapture = new Capture(); - - expectStart(); - dataSource.registerContentObserver( - eq(TrackPointsColumns.CONTENT_URI), capture(observerCapture)); - - FixedSizeLocationIterator locationIterator = new FixedSizeLocationIterator(1, 10, 5); - expect(providerUtils.getLocationIterator( - eq(TRACK_ID), eq(0L), eq(false), isA(LocationFactory.class))) - .andReturn(locationIterator); - expect(providerUtils.getLastLocationId(TRACK_ID)).andReturn(10L); - - listener1.clearTrackPoints(); - locationIterator.expectLocationsDelivered(listener1); - listener1.onNewTrackPointsDone(); - - replay(); - - hub.start(); - hub.registerTrackDataListener(listener1, EnumSet.of(TrackDataType.TRACK_POINTS_TABLE)); - - verifyAndReset(); - - // Unregister - ContentObserver observer = observerCapture.getValue(); - dataSource.unregisterContentObserver(observer); - - replay(); - - hub.unregisterTrackDataListener(listener1); - - verifyAndReset(); - - // Register again, except only points since unregistered. - dataSource.registerContentObserver( - eq(TrackPointsColumns.CONTENT_URI), capture(observerCapture)); - - locationIterator = new FixedSizeLocationIterator(1, 10, 5); - expect(providerUtils.getLocationIterator( - eq(TRACK_ID), eq(0L), eq(false), isA(LocationFactory.class))) - .andReturn(locationIterator); - expect(providerUtils.getLastLocationId(TRACK_ID)).andReturn(10L); - - listener1.clearTrackPoints(); - locationIterator.expectLocationsDelivered(listener1); - listener1.onNewTrackPointsDone(); - - replay(); - - hub.registerTrackDataListener(listener1, EnumSet.of(TrackDataType.TRACK_POINTS_TABLE)); - - verifyAndReset(); - } - - public void testPointsListen_reRegisterTrackChanged() { - Capture observerCapture = new Capture(); - - expectStart(); - dataSource.registerContentObserver( - eq(TrackPointsColumns.CONTENT_URI), capture(observerCapture)); - - FixedSizeLocationIterator locationIterator = new FixedSizeLocationIterator(1, 10, 5); - expect(providerUtils.getLocationIterator( - eq(TRACK_ID), eq(0L), eq(false), isA(LocationFactory.class))) - .andReturn(locationIterator); - expect(providerUtils.getLastLocationId(TRACK_ID)).andReturn(10L); - - listener1.clearTrackPoints(); - locationIterator.expectLocationsDelivered(listener1); - listener1.onNewTrackPointsDone(); - - replay(); - - hub.start(); - hub.registerTrackDataListener(listener1, EnumSet.of(TrackDataType.TRACK_POINTS_TABLE)); - - verifyAndReset(); - - // Unregister - ContentObserver observer = observerCapture.getValue(); - dataSource.unregisterContentObserver(observer); - - replay(); - - hub.unregisterTrackDataListener(listener1); - - verifyAndReset(); - - // Register again after track changed, expect all points. - dataSource.registerContentObserver( - eq(TrackPointsColumns.CONTENT_URI), capture(observerCapture)); - - locationIterator = new FixedSizeLocationIterator(1, 10); - expect(providerUtils.getLocationIterator( - eq(TRACK_ID + 1), eq(0L), eq(false), isA(LocationFactory.class))) - .andReturn(locationIterator); - expect(providerUtils.getLastLocationId(TRACK_ID + 1)).andReturn(10L); - - listener1.clearTrackPoints(); - locationIterator.expectLocationsDelivered(listener1); - listener1.onNewTrackPointsDone(); - - replay(); - - hub.loadTrack(TRACK_ID + 1); - hub.registerTrackDataListener(listener1, EnumSet.of(TrackDataType.TRACK_POINTS_TABLE)); - - verifyAndReset(); - } - - public void testPointsListen_largeTrackSampling() { - Capture observerCapture = new Capture(); - - expectStart(); - dataSource.registerContentObserver( - eq(TrackPointsColumns.CONTENT_URI), capture(observerCapture)); - - FixedSizeLocationIterator locationIterator = new FixedSizeLocationIterator(1, 200, 4, 25, 71, 120); - expect(providerUtils.getLocationIterator( - eq(TRACK_ID), eq(0L), eq(false), isA(LocationFactory.class))) - .andReturn(locationIterator); - expect(providerUtils.getLastLocationId(TRACK_ID)).andReturn(200L); - - listener1.clearTrackPoints(); - listener2.clearTrackPoints(); - locationIterator.expectSampledLocationsDelivered(listener1, 4, false); - locationIterator.expectSampledLocationsDelivered(listener2, 4, true); - listener1.onNewTrackPointsDone(); - listener2.onNewTrackPointsDone(); - - replay(); - - hub.registerTrackDataListener(listener1, - EnumSet.of(TrackDataType.TRACK_POINTS_TABLE)); - hub.registerTrackDataListener(listener2, - EnumSet.of(TrackDataType.TRACK_POINTS_TABLE, TrackDataType.SAMPLED_OUT_TRACK_POINTS)); - hub.start(); - - verifyAndReset(); - } - - public void testPointsListen_resampling() { - Capture observerCapture = new Capture(); - - expectStart(); - dataSource.registerContentObserver( - eq(TrackPointsColumns.CONTENT_URI), capture(observerCapture)); - - // Deliver 30 points (no sampling happens) - FixedSizeLocationIterator locationIterator = new FixedSizeLocationIterator(1, 30, 5); - expect(providerUtils.getLocationIterator( - eq(TRACK_ID), eq(0L), eq(false), isA(LocationFactory.class))) - .andReturn(locationIterator); - expect(providerUtils.getLastLocationId(TRACK_ID)).andReturn(30L); - - listener1.clearTrackPoints(); - locationIterator.expectLocationsDelivered(listener1); - listener1.onNewTrackPointsDone(); - - replay(); - - hub.start(); - hub.registerTrackDataListener(listener1, EnumSet.of(TrackDataType.TRACK_POINTS_TABLE)); - - verifyAndReset(); - - // Now deliver 30 more (incrementally sampled) - ContentObserver observer = observerCapture.getValue(); - locationIterator = new FixedSizeLocationIterator(31, 30); - expect(providerUtils.getLocationIterator( - eq(TRACK_ID), eq(31L), eq(false), isA(LocationFactory.class))) - .andReturn(locationIterator); - expect(providerUtils.getLastLocationId(TRACK_ID)).andReturn(60L); - - locationIterator.expectSampledLocationsDelivered(listener1, 2, false); - listener1.onNewTrackPointsDone(); - - replay(); - - observer.onChange(false); - - verifyAndReset(); - - // Now another 30 (triggers resampling) - locationIterator = new FixedSizeLocationIterator(1, 90); - expect(providerUtils.getLocationIterator( - eq(TRACK_ID), eq(0L), eq(false), isA(LocationFactory.class))) - .andReturn(locationIterator); - expect(providerUtils.getLastLocationId(TRACK_ID)).andReturn(90L); - - listener1.clearTrackPoints(); - locationIterator.expectSampledLocationsDelivered(listener1, 2, false); - listener1.onNewTrackPointsDone(); - - replay(); - - observer.onChange(false); - - verifyAndReset(); - } - - public void testLocationListen() { - // TODO - } - - public void testCompassListen() throws Exception { - AndroidMock.resetToDefault(listener1); - - expectStart(); - Capture listenerCapture = new Capture(); - dataSource.registerCompassListener(capture(listenerCapture)); - - Capture locationListenerCapture = new Capture(); - dataSource.registerLocationListener(capture(locationListenerCapture)); - - SensorEvent event = newSensorEvent(); - - // First, get a dummy heading update. - listener1.onCurrentHeadingChanged(0.0); - - // Then, get a heading update without a known location (thus can't calculate declination). - listener1.onCurrentHeadingChanged(42.0f); - - // Also expect location updates which are not relevant to us. - listener1.onProviderStateChange(isA(ProviderState.class)); - AndroidMock.expectLastCall().anyTimes(); - - replay(); - - hub.registerTrackDataListener(listener1, - EnumSet.of(TrackDataType.COMPASS, TrackDataType.LOCATION)); - hub.start(); - - SensorEventListener sensorListener = listenerCapture.getValue(); - LocationListener locationListener = locationListenerCapture.getValue(); - event.values[0] = 42.0f; - sensorListener.onSensorChanged(event); - - verifyAndReset(); - - // Expect the heading update to include declination. - listener1.onCurrentHeadingChanged(52.0); - - // Also expect location updates which are not relevant to us. - listener1.onProviderStateChange(isA(ProviderState.class)); - AndroidMock.expectLastCall().anyTimes(); - listener1.onCurrentLocationChanged(isA(Location.class)); - AndroidMock.expectLastCall().anyTimes(); - - replay(); - - // Now try injecting a location update, triggering a declination update. - Location location = new Location("gps"); - location.setLatitude(10.0); - location.setLongitude(20.0); - location.setAltitude(30.0); - declination = 10.0f; - locationListener.onLocationChanged(location); - sensorListener.onSensorChanged(event); - - verifyAndReset(); - - listener1.onCurrentHeadingChanged(52.0); - - replay(); - - // Now try changing the known declination - it should still return the old declination, since - // updates only happen sparsely. - declination = 20.0f; - sensorListener.onSensorChanged(event); - - verifyAndReset(); - } - - private Sensor newSensor() throws Exception { - Constructor constructor = Sensor.class.getDeclaredConstructor(); - constructor.setAccessible(true); - return constructor.newInstance(); - } - - private SensorEvent newSensorEvent() throws Exception { - Constructor constructor = SensorEvent.class.getDeclaredConstructor(int.class); - constructor.setAccessible(true); - return constructor.newInstance(3); - } - - public void testDisplayPreferencesListen() throws Exception { - PreferencesUtils.setBoolean(context, R.string.report_speed_key, true); - PreferencesUtils.setBoolean(context, R.string.metric_units_key, true); - - expectStart(); - - expect(listener1.onUnitsChanged(true)).andReturn(false); - expect(listener2.onUnitsChanged(true)).andReturn(false); - expect(listener1.onReportSpeedChanged(true)).andReturn(false); - expect(listener2.onReportSpeedChanged(true)).andReturn(false); - - replay(); - - hub.registerTrackDataListener(listener1, EnumSet.of(TrackDataType.PREFERENCE)); - hub.start(); - hub.registerTrackDataListener(listener2, EnumSet.of(TrackDataType.PREFERENCE)); - - verifyAndReset(); - - expect(listener1.onReportSpeedChanged(false)).andReturn(false); - expect(listener2.onReportSpeedChanged(false)).andReturn(false); - - replay(); - - PreferencesUtils.setBoolean(context, R.string.report_speed_key, false); - OnSharedPreferenceChangeListener listener = preferenceListenerCapture.getValue(); - listener.onSharedPreferenceChanged( - sharedPreferences, PreferencesUtils.getKey(context, R.string.report_speed_key)); - - AndroidMock.verify(dataSource, providerUtils, listener1, listener2); - AndroidMock.reset(dataSource, providerUtils, listener1, listener2); - - expect(listener1.onUnitsChanged(false)).andReturn(false); - expect(listener2.onUnitsChanged(false)).andReturn(false); - - replay(); - - PreferencesUtils.setBoolean(context, R.string.metric_units_key, false); - listener.onSharedPreferenceChanged( - sharedPreferences, PreferencesUtils.getKey(context, R.string.metric_units_key)); - - verifyAndReset(); - } - - private void expectStart() { - dataSource.registerOnSharedPreferenceChangeListener(capture(preferenceListenerCapture)); - } - - private void replay() { - AndroidMock.replay(dataSource, providerUtils, listener1, listener2); - } - - private void verifyAndReset() { - AndroidMock.verify(listener1, listener2, dataSource, providerUtils); - AndroidMock.reset(listener1, listener2, dataSource, providerUtils); - } } 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 e2146c527..4d16814aa 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/endtoendtest/CreateAndSendTrackTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/endtoendtest/CreateAndSendTrackTest.java @@ -273,24 +273,25 @@ public class CreateAndSendTrackTest extends ActivityInstrumentationTestCase2 0); EndToEndTestUtils.SOLO.goBack(); - + EndToEndTestUtils.stopRecording(true); } diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/endtoendtest/EndToEndTestUtils.java b/MyTracksTest/src/com/google/android/apps/mytracks/endtoendtest/EndToEndTestUtils.java index 6f283d0be..4c467d5d9 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/endtoendtest/EndToEndTestUtils.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/endtoendtest/EndToEndTestUtils.java @@ -77,18 +77,20 @@ public class EndToEndTestUtils { // Following is some check strings in English and Chinese private static final HashMap RELATIVE_STARTTIME_POSTFIX_MULTILINGUAL = new HashMap(); private static final HashMap VIEW_MODE_MULTILINGUAL = new HashMap(); - private static final HashMap ONE_KM_MULTILINGUAL = new HashMap(); - private static final HashMap ONE_MILE_MULTILINGUAL = new HashMap(); + private static final HashMap KM_MULTILINGUAL = new HashMap(); + private static final HashMap MILE_MULTILINGUAL = new HashMap(); public static String RELATIVE_STARTTIME_POSTFIX = ""; public static String VIEW_MODE = ""; - public static String ONE_KM = ""; - public static String ONE_MILE = ""; + public static String KM = ""; + public static String MILE = ""; public static int SHORT_WAIT_TIME = 2000; public static int NORMAL_WAIT_TIME = 8000; public static int LONG_WAIT_TIME = 15000; public static int SUPER_LONG_WAIT_TIME = 100000; + public static String deviceLanguage = ""; + static { RELATIVE_STARTTIME_POSTFIX_MULTILINGUAL.put("es", "mins ago"); RELATIVE_STARTTIME_POSTFIX_MULTILINGUAL.put("de", "Minuten"); @@ -102,17 +104,17 @@ public class EndToEndTestUtils { VIEW_MODE_MULTILINGUAL.put("ar", "وضع"); VIEW_MODE_MULTILINGUAL.put("zh", "模式"); - ONE_KM_MULTILINGUAL.put("es", "1 km"); - ONE_KM_MULTILINGUAL.put("de", "1 km"); - ONE_KM_MULTILINGUAL.put("fr", "1 km"); - ONE_KM_MULTILINGUAL.put("ar", "١ كم"); - ONE_KM_MULTILINGUAL.put("zh", "1 公里"); + KM_MULTILINGUAL.put("es", "km"); + KM_MULTILINGUAL.put("de", "km"); + KM_MULTILINGUAL.put("fr", "km"); + KM_MULTILINGUAL.put("ar", "كم"); + KM_MULTILINGUAL.put("zh", "公里"); - ONE_MILE_MULTILINGUAL.put("es", "1 mi"); - ONE_MILE_MULTILINGUAL.put("de", "1 mi"); - ONE_MILE_MULTILINGUAL.put("fr", "1 mile"); - ONE_MILE_MULTILINGUAL.put("ar", "١ ميل"); - ONE_MILE_MULTILINGUAL.put("zh", "1 英里"); + MILE_MULTILINGUAL.put("es", "mi"); + MILE_MULTILINGUAL.put("de", "mi"); + MILE_MULTILINGUAL.put("fr", "mile"); + MILE_MULTILINGUAL.put("ar", "ميل"); + MILE_MULTILINGUAL.put("zh", "英里"); } static Solo SOLO; @@ -130,17 +132,17 @@ public class EndToEndTestUtils { * Checks the language, then sets the fields with right string. */ private static void checkLanguage() { - String deviceLanguage = instrumentation.getContext().getResources().getConfiguration().locale.getLanguage(); + deviceLanguage = instrumentation.getContext().getResources().getConfiguration().locale.getLanguage(); if (RELATIVE_STARTTIME_POSTFIX_MULTILINGUAL.get(deviceLanguage) != null) { RELATIVE_STARTTIME_POSTFIX = RELATIVE_STARTTIME_POSTFIX_MULTILINGUAL.get(deviceLanguage); VIEW_MODE = VIEW_MODE_MULTILINGUAL.get(deviceLanguage); - ONE_KM = ONE_KM_MULTILINGUAL.get(deviceLanguage); - ONE_MILE = ONE_MILE_MULTILINGUAL.get(deviceLanguage); + KM = KM_MULTILINGUAL.get(deviceLanguage); + MILE = MILE_MULTILINGUAL.get(deviceLanguage); } else { RELATIVE_STARTTIME_POSTFIX = RELATIVE_STARTTIME_POSTFIX_MULTILINGUAL.get("es"); VIEW_MODE = VIEW_MODE_MULTILINGUAL.get("es"); - ONE_KM = ONE_KM_MULTILINGUAL.get("es"); - ONE_MILE = ONE_MILE_MULTILINGUAL.get("es"); + KM = KM_MULTILINGUAL.get("es"); + MILE = MILE_MULTILINGUAL.get("es"); } } diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/maps/DynamicSpeedTrackPathDescriptorTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/maps/DynamicSpeedTrackPathDescriptorTest.java index 6d00a0162..c788497f1 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/maps/DynamicSpeedTrackPathDescriptorTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/maps/DynamicSpeedTrackPathDescriptorTest.java @@ -57,35 +57,6 @@ public class DynamicSpeedTrackPathDescriptorTest extends AndroidTestCase { } } - /** - * Tests {@link DynamicSpeedTrackPathDescriptor#onSharedPreferenceChanged(SharedPreferences, - * String)} when the key is null. - */ - public void testOnSharedPreferenceChanged_nullKey() { - DynamicSpeedTrackPathDescriptor dynamicSpeedTrackPathDescriptor = new DynamicSpeedTrackPathDescriptor( - context); - int speedMargin = dynamicSpeedTrackPathDescriptor.getSpeedMargin(); - // Change value in shared preferences. - PreferencesUtils.setInt(context, R.string.track_color_mode_percentage_key, speedMargin + 2); - - dynamicSpeedTrackPathDescriptor.onSharedPreferenceChanged(sharedPreferences, null); - assertEquals(speedMargin, dynamicSpeedTrackPathDescriptor.getSpeedMargin()); - } - - /** - * Tests {@link DynamicSpeedTrackPathDescriptor#onSharedPreferenceChanged(SharedPreferences, - * String)} when the key is not null, and not trackColorModeDynamicVariation. - */ - public void testOnSharedPreferenceChanged_otherKey() { - DynamicSpeedTrackPathDescriptor dynamicSpeedTrackPathDescriptor = new DynamicSpeedTrackPathDescriptor( - context); - int speedMargin = dynamicSpeedTrackPathDescriptor.getSpeedMargin(); - // Change value in shared preferences. - PreferencesUtils.setInt(context, R.string.track_color_mode_percentage_key, speedMargin + 2); - dynamicSpeedTrackPathDescriptor.onSharedPreferenceChanged(sharedPreferences, "anyKey"); - assertEquals(speedMargin, dynamicSpeedTrackPathDescriptor.getSpeedMargin()); - } - /** * Tests {@link DynamicSpeedTrackPathDescriptor#onSharedPreferenceChanged(SharedPreferences, * String)} when the key is trackColorModeDynamicVariation. @@ -120,7 +91,7 @@ public class DynamicSpeedTrackPathDescriptorTest extends AndroidTestCase { } /** - * Tests {@link DynamicSpeedTrackPathDescriptor#needsRedraw()} by wrong track + * Tests {@link DynamicSpeedTrackPathDescriptor#updateState()} by wrong track * id. */ public void testNeedsRedraw_WrongTrackId() { @@ -128,11 +99,11 @@ public class DynamicSpeedTrackPathDescriptorTest extends AndroidTestCase { context, R.string.selected_track_id_key, PreferencesUtils.SELECTED_TRACK_ID_DEFAULT); DynamicSpeedTrackPathDescriptor dynamicSpeedTrackPathDescriptor = new DynamicSpeedTrackPathDescriptor( context); - assertEquals(false, dynamicSpeedTrackPathDescriptor.needsRedraw()); + assertEquals(false, dynamicSpeedTrackPathDescriptor.updateState()); } /** - * Tests {@link DynamicSpeedTrackPathDescriptor#needsRedraw()} by different + * Tests {@link DynamicSpeedTrackPathDescriptor#updateState()} by different * averageMovingSpeed. */ public void testIsDiffereceSignificant() { @@ -145,8 +116,7 @@ public class DynamicSpeedTrackPathDescriptorTest extends AndroidTestCase { // Difference is more than CRITICAL_DIFFERENCE_PERCENTAGE 30 * (1 + (DynamicSpeedTrackPathDescriptor.CRITICAL_DIFFERENCE_PERCENTAGE / 100.00) * 2) }; boolean[] expectedValues = { true, false, false, true }; - double[] expectedAverageMovingSpeed = { 20, 30, 30, - 30 * (1 + (DynamicSpeedTrackPathDescriptor.CRITICAL_DIFFERENCE_PERCENTAGE / 100.00) * 2) }; + double[] expectedAverageMovingSpeed = { 0, 30, 30, 30 }; // Test for (int i = 0; i < newAverageMovingSpeed.length; i++) { dynamicSpeedTrackPathDescriptor.setAverageMovingSpeed(averageMovingSpeeds[i]); diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/maps/DynamicSpeedTrackPathPainterTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/maps/DynamicSpeedTrackPathPainterTest.java index a583861fe..d381d3cb7 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/maps/DynamicSpeedTrackPathPainterTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/maps/DynamicSpeedTrackPathPainterTest.java @@ -62,7 +62,7 @@ public class DynamicSpeedTrackPathPainterTest extends TrackPathPainterTestCase { public void testUpdatePath_AllInvalidLocation() { List points = createCachedLocations(NUMBER_OF_LOCATIONS, INVALID_LATITUDE, -1); dynamicSpeedTrackPathPainter.updatePath(myTracksOverlay.getMapProjection(mockView), - myTracksOverlay.getMapViewRect(mockView), 1, true, points); + myTracksOverlay.getMapViewRect(mockView), 1, points); AndroidMock.verify(trackPathDescriptor); // Should be zero for there is no valid locations. assertEquals(0, dynamicSpeedTrackPathPainter.getColoredPaths().size()); @@ -80,7 +80,7 @@ public class DynamicSpeedTrackPathPainterTest extends TrackPathPainterTestCase { // Gets a number as the start index of points. int startLocationIdx = NUMBER_OF_LOCATIONS / 2; dynamicSpeedTrackPathPainter.updatePath(myTracksOverlay.getMapProjection(mockView), - myTracksOverlay.getMapViewRect(mockView), startLocationIdx, true, points); + myTracksOverlay.getMapViewRect(mockView), startLocationIdx, points); AndroidMock.verify(trackPathDescriptor); assertEquals(NUMBER_OF_LOCATIONS - startLocationIdx, dynamicSpeedTrackPathPainter @@ -123,7 +123,7 @@ public class DynamicSpeedTrackPathPainterTest extends TrackPathPainterTestCase { slowSpeed)); dynamicSpeedTrackPathPainter.updatePath(myTracksOverlay.getMapProjection(mockView), - myTracksOverlay.getMapViewRect(mockView), startLocationIdx, true, points); + myTracksOverlay.getMapViewRect(mockView), startLocationIdx, points); AndroidMock.verify(trackPathDescriptor); List coloredPath = dynamicSpeedTrackPathPainter.getColoredPaths(); diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/maps/SingleColorTrackPathPainterTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/maps/SingleColorTrackPathPainterTest.java index 1679c547a..a47cc51c0 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/maps/SingleColorTrackPathPainterTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/maps/SingleColorTrackPathPainterTest.java @@ -31,9 +31,10 @@ import java.util.List; */ public class SingleColorTrackPathPainterTest extends TrackPathPainterTestCase { + private static final int NUMBER_OF_LOCATIONS = 10; private SingleColorTrackPathPainter singleColorTrackPathPainter; private Path pathMock; - private static final int NUMBER_OF_LOCATIONS = 100; + /** * Initials a mocked TrackPathDescriptor object and @@ -44,8 +45,8 @@ public class SingleColorTrackPathPainterTest extends TrackPathPainterTestCase { protected void setUp() throws Exception { super.setUp(); - pathMock = AndroidMock.createStrictMock(Path.class); singleColorTrackPathPainter = new SingleColorTrackPathPainter(getContext()); + pathMock = AndroidMock.createStrictMock(Path.class); } /** @@ -54,11 +55,12 @@ public class SingleColorTrackPathPainterTest extends TrackPathPainterTestCase { * method when all locations are valid. */ public void testUpdatePath_AllValidLocation() { - pathMock.incReserve(NUMBER_OF_LOCATIONS); - List points = createCachedLocations(NUMBER_OF_LOCATIONS, - TrackStubUtils.INITIAL_LATITUDE, -1); // Gets a number as the start index of points. int startLocationIdx = NUMBER_OF_LOCATIONS / 2; + + pathMock.incReserve(NUMBER_OF_LOCATIONS - startLocationIdx); + List points = createCachedLocations(NUMBER_OF_LOCATIONS, + TrackStubUtils.INITIAL_LATITUDE, -1); for (int i = startLocationIdx; i < NUMBER_OF_LOCATIONS; i++) { pathMock.lineTo(0, 0); @@ -66,7 +68,7 @@ public class SingleColorTrackPathPainterTest extends TrackPathPainterTestCase { AndroidMock.replay(pathMock); singleColorTrackPathPainter.updatePath(myTracksOverlay.getMapProjection(mockView), - myTracksOverlay.getMapViewRect(mockView), startLocationIdx, true, points, pathMock); + myTracksOverlay.getMapViewRect(mockView), startLocationIdx, points, pathMock); AndroidMock.verify(pathMock); } @@ -76,20 +78,20 @@ public class SingleColorTrackPathPainterTest extends TrackPathPainterTestCase { * method when all locations are invalid. */ public void testUpdatePath_AllInvalidLocation() { - pathMock.incReserve(NUMBER_OF_LOCATIONS); + int startLocationIdx = NUMBER_OF_LOCATIONS / 2; + pathMock.incReserve(NUMBER_OF_LOCATIONS - startLocationIdx); List points = createCachedLocations(NUMBER_OF_LOCATIONS, INVALID_LATITUDE, -1); // Gets a random number from 1 to numberOfLocations. - int startLocationIdx = NUMBER_OF_LOCATIONS / 2; AndroidMock.replay(pathMock); singleColorTrackPathPainter.updatePath(myTracksOverlay.getMapProjection(mockView), - myTracksOverlay.getMapViewRect(mockView), startLocationIdx, true, points, pathMock); + myTracksOverlay.getMapViewRect(mockView), startLocationIdx, points, pathMock); AndroidMock.verify(pathMock); } /** - * Tests the - * {@link SingleColorTrackPathPainter#updatePath(com.google.android.maps.Projection, android.graphics.Rect, int, Boolean, List, Path)} - * method when there are three segments. + * Tests the {@link SingleColorTrackPathPainter#updatePath(com.google.android.maps.Projection, + * android.graphics.Rect, int, Boolean, List, Path)} method when there are + * three segments. */ public void testUpdatePath_ThreeSegments() { // First segment. @@ -103,7 +105,7 @@ public class SingleColorTrackPathPainterTest extends TrackPathPainterTestCase { points.addAll(createCachedLocations(NUMBER_OF_LOCATIONS, TrackStubUtils.INITIAL_LATITUDE, -1)); // Gets a random number from 1 to numberOfLocations. int startLocationIdx = NUMBER_OF_LOCATIONS / 2; - pathMock.incReserve(NUMBER_OF_LOCATIONS *3 + 1 +1); + pathMock.incReserve(NUMBER_OF_LOCATIONS *3 + 1 +1 - startLocationIdx); for (int i = 0; i < NUMBER_OF_LOCATIONS - startLocationIdx; i++) { pathMock.lineTo(0, 0); } @@ -118,7 +120,7 @@ public class SingleColorTrackPathPainterTest extends TrackPathPainterTestCase { AndroidMock.replay(pathMock); singleColorTrackPathPainter.updatePath(myTracksOverlay.getMapProjection(mockView), - myTracksOverlay.getMapViewRect(mockView), startLocationIdx, true, points, pathMock); + myTracksOverlay.getMapViewRect(mockView), startLocationIdx, points, pathMock); AndroidMock.verify(pathMock); } } diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/maps/TrackPathDescriptorDynamicSpeedTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/maps/TrackPathDescriptorDynamicSpeedTest.java index 53c42dd21..ec3361dbf 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/maps/TrackPathDescriptorDynamicSpeedTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/maps/TrackPathDescriptorDynamicSpeedTest.java @@ -40,13 +40,12 @@ public class TrackPathDescriptorDynamicSpeedTest extends TrackPathPainterTestCas myTracksOverlay.setTrackPathPainter(painter); int startLocationIdx = 0; - Boolean alwaysVisible = true; - + assertNotNull(painter); painter.updatePath(myTracksOverlay.getMapProjection(mockView), - myTracksOverlay.getMapViewRect(mockView), startLocationIdx, alwaysVisible, + myTracksOverlay.getMapViewRect(mockView), startLocationIdx, myTracksOverlay.getPoints()); - assertNotNull(myTracksOverlay.getTrackPathPainter().getLastPath()); - painter.drawTrack(canvas); + assertNotNull(myTracksOverlay.getTrackPathPainter().hasPath()); + painter.drawPath(canvas); } } diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/maps/TrackPathDescriptorFixedSpeedTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/maps/TrackPathDescriptorFixedSpeedTest.java index 48c0d8686..668d1d741 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/maps/TrackPathDescriptorFixedSpeedTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/maps/TrackPathDescriptorFixedSpeedTest.java @@ -40,13 +40,11 @@ public class TrackPathDescriptorFixedSpeedTest extends TrackPathPainterTestCase myTracksOverlay.setTrackPathPainter(painter); int startLocationIdx = 0; - Boolean alwaysVisible = true; - + assertNotNull(painter); painter.updatePath(myTracksOverlay.getMapProjection(mockView), - myTracksOverlay.getMapViewRect(mockView), startLocationIdx, alwaysVisible, - myTracksOverlay.getPoints()); - assertNotNull(myTracksOverlay.getTrackPathPainter().getLastPath()); - painter.drawTrack(canvas); + myTracksOverlay.getMapViewRect(mockView), startLocationIdx, myTracksOverlay.getPoints()); + assertNotNull(myTracksOverlay.getTrackPathPainter().hasPath()); + painter.drawPath(canvas); } } diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/maps/TrackPathPainterFactoryTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/maps/TrackPathPainterFactoryTest.java index 36bab1029..9d9622b6a 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/maps/TrackPathPainterFactoryTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/maps/TrackPathPainterFactoryTest.java @@ -55,8 +55,7 @@ public class TrackPathPainterFactoryTest extends TrackPathPainterTestCase { context, R.string.track_color_mode_key, context.getString(track_color_mode)); int startLocationIdx = 0; - Boolean alwaysVisible = true; - + TrackPathPainter painter = TrackPathPainterFactory.getTrackPathPainter(context); myTracksOverlay.setTrackPathPainter(painter); @@ -64,9 +63,9 @@ public class TrackPathPainterFactoryTest extends TrackPathPainterTestCase { assertTrue(c.isInstance(painter)); painter.updatePath(myTracksOverlay.getMapProjection(mockView), - myTracksOverlay.getMapViewRect(mockView), startLocationIdx, alwaysVisible, + myTracksOverlay.getMapViewRect(mockView), startLocationIdx, myTracksOverlay.getPoints()); - assertNotNull(myTracksOverlay.getTrackPathPainter().getLastPath()); - painter.drawTrack(canvas); + assertNotNull(myTracksOverlay.getTrackPathPainter().hasPath()); + painter.drawPath(canvas); } } diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/maps/TrackPathPainterSingleColorTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/maps/TrackPathPainterSingleColorTest.java index ffd50c0bb..32cce7438 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/maps/TrackPathPainterSingleColorTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/maps/TrackPathPainterSingleColorTest.java @@ -39,13 +39,12 @@ public class TrackPathPainterSingleColorTest extends TrackPathPainterTestCase { myTracksOverlay.setTrackPathPainter(painter); int startLocationIdx = 0; - Boolean alwaysVisible = true; assertNotNull(painter); painter.updatePath(myTracksOverlay.getMapProjection(mockView), - myTracksOverlay.getMapViewRect(mockView), startLocationIdx, alwaysVisible, + myTracksOverlay.getMapViewRect(mockView), startLocationIdx, myTracksOverlay.getPoints()); - assertNotNull(myTracksOverlay.getTrackPathPainter().getLastPath()); - painter.drawTrack(canvas); + assertNotNull(myTracksOverlay.getTrackPathPainter().hasPath()); + painter.drawPath(canvas); } }