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 0ba05859b..000000000 Binary files a/MyTracksTest/libs/robotium-solo-3.1.jar and /dev/null differ diff --git a/MyTracksTest/libs/robotium-solo-3.3.jar b/MyTracksTest/libs/robotium-solo-3.3.jar new file mode 100644 index 000000000..0fd942110 Binary files /dev/null and b/MyTracksTest/libs/robotium-solo-3.3.jar differ diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/MapOverlayTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/MapOverlayTest.java index 9bb02ff35..bcc24231f 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/MapOverlayTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/MapOverlayTest.java @@ -25,121 +25,137 @@ import android.location.Location; import android.test.AndroidTestCase; /** - * Tests for the MyTracks map overlay. + * Tests {@link MapOverlay}. * * @author Bartlomiej Niechwiej * @author Vangelis S. */ public class MapOverlayTest extends AndroidTestCase { private Canvas canvas; - private MockMyTracksOverlay myTracksOverlay; - private MapView mockView; + private MockMyTracksOverlay mockMyTracksOverlay; + private MapView mapView; @Override protected void setUp() throws Exception { super.setUp(); canvas = new Canvas(); - myTracksOverlay = new MockMyTracksOverlay(getContext()); + mockMyTracksOverlay = new MockMyTracksOverlay(getContext()); // Enable drawing. - myTracksOverlay.setTrackDrawingEnabled(true); + mockMyTracksOverlay.setTrackDrawingEnabled(true); // Set a TrackPathPainter with a MockPath. - myTracksOverlay.setTrackPathPainter(new SingleColorTrackPathPainter(getContext()) { - @Override + mockMyTracksOverlay.setTrackPathPainter(new SingleColorTrackPathPainter(getContext()) { + @Override public Path newPath() { return new MockPath(); - } + } }); - - mockView = null; + + mapView = null; } - - public void testAddLocation() throws Exception { + + /** + * Tests {@link MapOverlay#addLocation(Location)}. + */ + public void testAddLocation() throws Exception { Location location = new Location("gps"); location.setLatitude(10); location.setLongitude(20); - myTracksOverlay.addLocation(location); - assertEquals(1, myTracksOverlay.getNumLocations()); - assertEquals(0, myTracksOverlay.getNumWaypoints()); - + mockMyTracksOverlay.addLocation(location); + assertEquals(1, mockMyTracksOverlay.getNumLocations()); + assertEquals(0, mockMyTracksOverlay.getNumWaypoints()); + location.setLatitude(20); location.setLongitude(30); - myTracksOverlay.addLocation(location); - assertEquals(2, myTracksOverlay.getNumLocations()); - assertEquals(0, myTracksOverlay.getNumWaypoints()); - assertNull(myTracksOverlay.getTrackPathPainter().getLastPath()); - + mockMyTracksOverlay.addLocation(location); + assertEquals(2, mockMyTracksOverlay.getNumLocations()); + assertEquals(0, mockMyTracksOverlay.getNumWaypoints()); + assertFalse(mockMyTracksOverlay.getTrackPathPainter().hasPath()); + // Draw and make sure that we don't lose any point. - myTracksOverlay.draw(canvas, mockView, false); - assertEquals(2, myTracksOverlay.getNumLocations()); - assertEquals(0, myTracksOverlay.getNumWaypoints()); - assertNotNull(myTracksOverlay.getTrackPathPainter().getLastPath()); - assertTrue(myTracksOverlay.getTrackPathPainter().getLastPath() instanceof MockPath); - MockPath path = (MockPath) myTracksOverlay.getTrackPathPainter().getLastPath(); - assertEquals(2, path.totalPoints); - - myTracksOverlay.draw(canvas, mockView, true); - assertEquals(2, myTracksOverlay.getNumLocations()); - assertEquals(0, myTracksOverlay.getNumWaypoints()); - assertNotNull(myTracksOverlay.getTrackPathPainter().getLastPath()); + mockMyTracksOverlay.draw(canvas, mapView, false); + assertEquals(2, mockMyTracksOverlay.getNumLocations()); + assertEquals(0, mockMyTracksOverlay.getNumWaypoints()); + assertTrue(mockMyTracksOverlay.getTrackPathPainter().hasPath()); + SingleColorTrackPathPainter trackPathPainter = (SingleColorTrackPathPainter) mockMyTracksOverlay + .getTrackPathPainter(); + MockPath path = (MockPath) trackPathPainter.getPath(); + assertEquals(2, path.getTotalPoints()); + + mockMyTracksOverlay.draw(canvas, mapView, true); + assertEquals(2, mockMyTracksOverlay.getNumLocations()); + assertEquals(0, mockMyTracksOverlay.getNumWaypoints()); + assertTrue(mockMyTracksOverlay.getTrackPathPainter().hasPath()); } - + + /** + * Tests {@link MapOverlay#clearPoints()}. + */ public void testClearPoints() throws Exception { Location location = new Location("gps"); location.setLatitude(10); location.setLongitude(20); - myTracksOverlay.addLocation(location); - assertEquals(1, myTracksOverlay.getNumLocations()); - myTracksOverlay.clearPoints(); - assertEquals(0, myTracksOverlay.getNumLocations()); + mockMyTracksOverlay.addLocation(location); + assertEquals(1, mockMyTracksOverlay.getNumLocations()); + mockMyTracksOverlay.clearPoints(); + assertEquals(0, mockMyTracksOverlay.getNumLocations()); - // Same after drawing on canvas. + // Test after drawing on canvas final int locations = 100; for (int i = 0; i < locations; ++i) { - myTracksOverlay.addLocation(location); + mockMyTracksOverlay.addLocation(location); } - assertEquals(locations, myTracksOverlay.getNumLocations()); - myTracksOverlay.draw(canvas, mockView, false); - myTracksOverlay.draw(canvas, mockView, true); - myTracksOverlay.clearPoints(); - assertEquals(0, myTracksOverlay.getNumLocations()); + assertEquals(locations, mockMyTracksOverlay.getNumLocations()); + mockMyTracksOverlay.draw(canvas, mapView, false); + mockMyTracksOverlay.draw(canvas, mapView, true); + mockMyTracksOverlay.clearPoints(); + assertEquals(0, mockMyTracksOverlay.getNumLocations()); } + /** + * Tests {@link MapOverlay#addWaypoint(Waypoint)}. + */ public void testAddWaypoint() throws Exception { Location location = new Location("gps"); location.setLatitude(10); location.setLongitude(20); Waypoint waypoint = new Waypoint(); waypoint.setLocation(location); - myTracksOverlay.addWaypoint(waypoint); - assertEquals(1, myTracksOverlay.getNumWaypoints()); - assertEquals(0, myTracksOverlay.getNumLocations()); - assertNull(myTracksOverlay.getTrackPathPainter().getLastPath()); - + mockMyTracksOverlay.addWaypoint(waypoint); + assertEquals(1, mockMyTracksOverlay.getNumWaypoints()); + assertEquals(0, mockMyTracksOverlay.getNumLocations()); + assertFalse(mockMyTracksOverlay.getTrackPathPainter().hasPath()); + final int waypoints = 10; for (int i = 0; i < waypoints; ++i) { waypoint = new Waypoint(); waypoint.setLocation(location); - myTracksOverlay.addWaypoint(waypoint); + mockMyTracksOverlay.addWaypoint(waypoint); } - assertEquals(1 + waypoints, myTracksOverlay.getNumWaypoints()); - assertEquals(0, myTracksOverlay.getNumLocations()); - assertNull(myTracksOverlay.getTrackPathPainter().getLastPath()); + assertEquals(1 + waypoints, mockMyTracksOverlay.getNumWaypoints()); + assertEquals(0, mockMyTracksOverlay.getNumLocations()); + assertFalse(mockMyTracksOverlay.getTrackPathPainter().hasPath()); } + /** + * Tests {@link MapOverlay#clearWaypoints()}. + */ public void testClearWaypoints() throws Exception { Location location = new Location("gps"); location.setLatitude(10); location.setLongitude(20); Waypoint waypoint = new Waypoint(); waypoint.setLocation(location); - myTracksOverlay.addWaypoint(waypoint); - assertEquals(1, myTracksOverlay.getNumWaypoints()); - myTracksOverlay.clearWaypoints(); - assertEquals(0, myTracksOverlay.getNumWaypoints()); + mockMyTracksOverlay.addWaypoint(waypoint); + assertEquals(1, mockMyTracksOverlay.getNumWaypoints()); + mockMyTracksOverlay.clearWaypoints(); + assertEquals(0, mockMyTracksOverlay.getNumWaypoints()); } - + + /** + * Tests {@link MapOverlay#draw(Canvas, MapView, boolean)}. + */ public void testDrawing() { Location location = new Location("gps"); location.setLatitude(10); @@ -147,30 +163,30 @@ public class MapOverlayTest extends AndroidTestCase { location.setLongitude(20 + i); Waypoint waypoint = new Waypoint(); waypoint.setLocation(location); - myTracksOverlay.addWaypoint(waypoint); + mockMyTracksOverlay.addWaypoint(waypoint); } for (int i = 0; i < 100; ++i) { location = new Location("gps"); location.setLatitude(20 + i / 2); location.setLongitude(150 - i); - myTracksOverlay.addLocation(location); + mockMyTracksOverlay.addLocation(location); } - + // Shadow. - myTracksOverlay.draw(canvas, mockView, true); - // We don't expect to do anything if - assertNull(myTracksOverlay.getTrackPathPainter().getLastPath()); - assertEquals(40, myTracksOverlay.getNumWaypoints()); - assertEquals(100, myTracksOverlay.getNumLocations()); + mockMyTracksOverlay.draw(canvas, mapView, true); + // We don't expect to do anything if + assertFalse(mockMyTracksOverlay.getTrackPathPainter().hasPath()); + assertEquals(40, mockMyTracksOverlay.getNumWaypoints()); + assertEquals(100, mockMyTracksOverlay.getNumLocations()); // No shadow. - myTracksOverlay.draw(canvas, mockView, false); - assertNotNull(myTracksOverlay.getTrackPathPainter().getLastPath()); - assertTrue(myTracksOverlay.getTrackPathPainter().getLastPath() instanceof MockPath); - MockPath path = (MockPath) myTracksOverlay.getTrackPathPainter().getLastPath(); - assertEquals(40, myTracksOverlay.getNumWaypoints()); - assertEquals(100, myTracksOverlay.getNumLocations()); - assertEquals(100, path.totalPoints); - // TODO: Check the points from the path (and the segments). + mockMyTracksOverlay.draw(canvas, mapView, false); + assertTrue(mockMyTracksOverlay.getTrackPathPainter().hasPath()); + SingleColorTrackPathPainter trackPathPainter = (SingleColorTrackPathPainter) mockMyTracksOverlay + .getTrackPathPainter(); + MockPath path = (MockPath) trackPathPainter.getPath(); + assertEquals(40, mockMyTracksOverlay.getNumWaypoints()); + assertEquals(100, mockMyTracksOverlay.getNumLocations()); + assertEquals(100, path.getTotalPoints()); } } diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/MockMyTracksOverlay.java b/MyTracksTest/src/com/google/android/apps/mytracks/MockMyTracksOverlay.java index 74daea1de..d9a393498 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/MockMyTracksOverlay.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/MockMyTracksOverlay.java @@ -22,30 +22,28 @@ import android.content.Context; import android.graphics.Rect; /** - * Elements for Tests for the MyTracks map overlay. + * A mock {@link MapOverlay} for testing. * * @author Bartlomiej Niechwiej * @author Vangelis S. * - * A mock version of {@code MapOverlay} that does not use - * {@class MapView}. */ public class MockMyTracksOverlay extends MapOverlay { - private Projection mockProjection; + private Projection projection; public MockMyTracksOverlay(Context context) { super(context); - mockProjection = new MockProjection(); + projection = new MockProjection(); } @Override public Projection getMapProjection(MapView mapView) { - return mockProjection; + return projection; } @Override public Rect getMapViewRect(MapView mapView) { - return new Rect(0, 0, 100, 100); + return new Rect(0, 0, (int) (100 * 1E6), (int) (100 * 1E6)); } } \ No newline at end of file diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/MockPath.java b/MyTracksTest/src/com/google/android/apps/mytracks/MockPath.java index ba9592c08..0a7ed511c 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/MockPath.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/MockPath.java @@ -26,20 +26,19 @@ import java.util.List; import junit.framework.Assert; /** - * Elements for Tests for the MyTracks map overlay. + * A mock {@link Path}. Records calls to {@link Path#lineTo(float, float)} and + * {@link Path#moveTo(float, float)}. * * @author Bartlomiej Niechwiej * @author Vangelis S. - * - * A mock class that intercepts {@code Path}'s and records calls to - * {@code #moveTo()} and {@code #lineTo()}. */ public class MockPath extends Path { + + // A list of disjointed path segments. + private final List> 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); } }