diff --git a/MyTracks/.settings/org.eclipse.jdt.core.prefs b/MyTracks/.settings/org.eclipse.jdt.core.prefs index 60e490024..8e0fed2d0 100644 --- a/MyTracks/.settings/org.eclipse.jdt.core.prefs +++ b/MyTracks/.settings/org.eclipse.jdt.core.prefs @@ -1,4 +1,4 @@ -#Fri Oct 21 20:06:28 BRST 2011 +#Tue Jul 17 10:23:26 PDT 2012 eclipse.preferences.version=1 org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 @@ -76,29 +76,66 @@ org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning org.eclipse.jdt.core.compiler.source=1.6 org.eclipse.jdt.core.formatter.align_type_members_on_columns=false +org.eclipse.jdt.core.formatter.alignment_for_annotations_on_field=1585 +org.eclipse.jdt.core.formatter.alignment_for_annotations_on_field.count_dependent=1585|-1|1585 +org.eclipse.jdt.core.formatter.alignment_for_annotations_on_local_variable=1585 +org.eclipse.jdt.core.formatter.alignment_for_annotations_on_local_variable.count_dependent=1585|-1|1585 +org.eclipse.jdt.core.formatter.alignment_for_annotations_on_method=1585 +org.eclipse.jdt.core.formatter.alignment_for_annotations_on_method.count_dependent=1585|-1|1585 +org.eclipse.jdt.core.formatter.alignment_for_annotations_on_package=1585 +org.eclipse.jdt.core.formatter.alignment_for_annotations_on_package.count_dependent=1585|-1|1585 +org.eclipse.jdt.core.formatter.alignment_for_annotations_on_parameter=1040 +org.eclipse.jdt.core.formatter.alignment_for_annotations_on_parameter.count_dependent=1040|-1|1040 +org.eclipse.jdt.core.formatter.alignment_for_annotations_on_type=1585 +org.eclipse.jdt.core.formatter.alignment_for_annotations_on_type.count_dependent=1585|-1|1585 org.eclipse.jdt.core.formatter.alignment_for_arguments_in_allocation_expression=16 +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_allocation_expression.count_dependent=16|-1|16 org.eclipse.jdt.core.formatter.alignment_for_arguments_in_annotation=0 +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_annotation.count_dependent=16|-1|16 org.eclipse.jdt.core.formatter.alignment_for_arguments_in_enum_constant=48 +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_enum_constant.count_dependent=16|-1|16 org.eclipse.jdt.core.formatter.alignment_for_arguments_in_explicit_constructor_call=16 +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_explicit_constructor_call.count_dependent=16|-1|16 org.eclipse.jdt.core.formatter.alignment_for_arguments_in_method_invocation=16 +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_method_invocation.count_dependent=16|-1|16 org.eclipse.jdt.core.formatter.alignment_for_arguments_in_qualified_allocation_expression=16 +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_qualified_allocation_expression.count_dependent=16|-1|16 org.eclipse.jdt.core.formatter.alignment_for_assignment=0 org.eclipse.jdt.core.formatter.alignment_for_binary_expression=16 +org.eclipse.jdt.core.formatter.alignment_for_binary_expression.count_dependent=16|-1|16 +org.eclipse.jdt.core.formatter.alignment_for_cascading_method_invocation_with_arguments=16 +org.eclipse.jdt.core.formatter.alignment_for_cascading_method_invocation_with_arguments.count_dependent=16|-1|16 org.eclipse.jdt.core.formatter.alignment_for_compact_if=16 org.eclipse.jdt.core.formatter.alignment_for_conditional_expression=80 org.eclipse.jdt.core.formatter.alignment_for_enum_constants=48 +org.eclipse.jdt.core.formatter.alignment_for_enum_constants.count_dependent=0|-1|0 org.eclipse.jdt.core.formatter.alignment_for_expressions_in_array_initializer=16 +org.eclipse.jdt.core.formatter.alignment_for_expressions_in_array_initializer.count_dependent=16|-1|16 +org.eclipse.jdt.core.formatter.alignment_for_field_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_for_statement=16 +org.eclipse.jdt.core.formatter.alignment_for_generic_type_arguments=16 +org.eclipse.jdt.core.formatter.alignment_for_generic_type_arguments.count_dependent=16|-1|16 +org.eclipse.jdt.core.formatter.alignment_for_local_variable_declaration=16 org.eclipse.jdt.core.formatter.alignment_for_method_declaration=0 org.eclipse.jdt.core.formatter.alignment_for_multiple_fields=16 +org.eclipse.jdt.core.formatter.alignment_for_multiple_fields.count_dependent=16|-1|16 +org.eclipse.jdt.core.formatter.alignment_for_new_anonymous_class=0 org.eclipse.jdt.core.formatter.alignment_for_parameters_in_constructor_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_parameters_in_constructor_declaration.count_dependent=16|-1|16 org.eclipse.jdt.core.formatter.alignment_for_parameters_in_method_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_parameters_in_method_declaration.count_dependent=16|-1|16 org.eclipse.jdt.core.formatter.alignment_for_resources_in_try=80 org.eclipse.jdt.core.formatter.alignment_for_selector_in_method_invocation=16 +org.eclipse.jdt.core.formatter.alignment_for_selector_in_method_invocation.count_dependent=16|-1|16 org.eclipse.jdt.core.formatter.alignment_for_superclass_in_type_declaration=16 org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_enum_declaration=48 +org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_enum_declaration.count_dependent=16|-1|16 org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_type_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_type_declaration.count_dependent=16|-1|16 org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_constructor_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_constructor_declaration.count_dependent=16|-1|16 org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_method_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_method_declaration.count_dependent=16|-1|16 org.eclipse.jdt.core.formatter.alignment_for_union_type_in_multicatch=16 org.eclipse.jdt.core.formatter.blank_lines_after_imports=1 org.eclipse.jdt.core.formatter.blank_lines_after_package=1 @@ -138,12 +175,14 @@ org.eclipse.jdt.core.formatter.comment.line_length=80 org.eclipse.jdt.core.formatter.comment.new_lines_at_block_boundaries=true org.eclipse.jdt.core.formatter.comment.new_lines_at_javadoc_boundaries=true org.eclipse.jdt.core.formatter.comment.preserve_white_space_between_code_and_line_comments=false +org.eclipse.jdt.core.formatter.comment_new_line_at_start_of_html_paragraph=true org.eclipse.jdt.core.formatter.compact_else_if=true org.eclipse.jdt.core.formatter.continuation_indentation=2 org.eclipse.jdt.core.formatter.continuation_indentation_for_array_initializer=2 org.eclipse.jdt.core.formatter.disabling_tag=@formatter\:off org.eclipse.jdt.core.formatter.enabling_tag=@formatter\:on -org.eclipse.jdt.core.formatter.format_guardian_clause_on_one_line=true +org.eclipse.jdt.core.formatter.force_if_else_statement_brace=false +org.eclipse.jdt.core.formatter.format_guardian_clause_on_one_line=false org.eclipse.jdt.core.formatter.format_line_comment_starting_on_first_column=true org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_annotation_declaration_header=true org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_constant_header=true @@ -210,6 +249,7 @@ org.eclipse.jdt.core.formatter.insert_space_after_comma_in_parameterized_type_re org.eclipse.jdt.core.formatter.insert_space_after_comma_in_superinterfaces=insert org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_arguments=insert org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_parameters=insert +org.eclipse.jdt.core.formatter.insert_space_after_comment_prefix=insert org.eclipse.jdt.core.formatter.insert_space_after_ellipsis=insert org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_parameterized_type_reference=do not insert org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_arguments=do not insert @@ -340,7 +380,7 @@ org.eclipse.jdt.core.formatter.join_lines_in_comments=true org.eclipse.jdt.core.formatter.join_wrapped_lines=true org.eclipse.jdt.core.formatter.keep_else_statement_on_same_line=false org.eclipse.jdt.core.formatter.keep_empty_array_initializer_on_one_line=false -org.eclipse.jdt.core.formatter.keep_imple_if_on_one_line=true +org.eclipse.jdt.core.formatter.keep_imple_if_on_one_line=false org.eclipse.jdt.core.formatter.keep_then_statement_on_same_line=false org.eclipse.jdt.core.formatter.lineSplit=100 org.eclipse.jdt.core.formatter.never_indent_block_comments_on_first_column=false @@ -348,10 +388,22 @@ org.eclipse.jdt.core.formatter.never_indent_line_comments_on_first_column=false org.eclipse.jdt.core.formatter.number_of_blank_lines_at_beginning_of_method_body=0 org.eclipse.jdt.core.formatter.number_of_empty_lines_to_preserve=1 org.eclipse.jdt.core.formatter.put_empty_statement_on_new_line=true +org.eclipse.jdt.core.formatter.sort_local_variable_annotations=false +org.eclipse.jdt.core.formatter.sort_member_annotations=false +org.eclipse.jdt.core.formatter.sort_package_annotations=false +org.eclipse.jdt.core.formatter.sort_parameter_annotations=false +org.eclipse.jdt.core.formatter.sort_type_annotations=false org.eclipse.jdt.core.formatter.tabulation.char=space org.eclipse.jdt.core.formatter.tabulation.size=2 org.eclipse.jdt.core.formatter.use_on_off_tags=false org.eclipse.jdt.core.formatter.use_tabs_only_for_leading_indentations=false org.eclipse.jdt.core.formatter.wrap_before_binary_operator=true org.eclipse.jdt.core.formatter.wrap_before_or_operator_multicatch=true +org.eclipse.jdt.core.formatter.wrap_comment_inline_tags=false +org.eclipse.jdt.core.formatter.wrap_non_simple_local_variable_annotation=true +org.eclipse.jdt.core.formatter.wrap_non_simple_member_annotation=true +org.eclipse.jdt.core.formatter.wrap_non_simple_package_annotation=true +org.eclipse.jdt.core.formatter.wrap_non_simple_parameter_annotation=false +org.eclipse.jdt.core.formatter.wrap_non_simple_type_annotation=true org.eclipse.jdt.core.formatter.wrap_outer_expressions_when_nested=true +org.eclipse.jdt.core.formatter.wrap_prefer_two_fragments=false diff --git a/MyTracks/.settings/org.eclipse.jdt.ui.prefs b/MyTracks/.settings/org.eclipse.jdt.ui.prefs index 0ac0ec030..ef8d656cf 100644 --- a/MyTracks/.settings/org.eclipse.jdt.ui.prefs +++ b/MyTracks/.settings/org.eclipse.jdt.ui.prefs @@ -1,8 +1,8 @@ -#Thu Jul 21 03:04:13 BRT 2011 +#Tue Jul 17 10:22:06 PDT 2012 eclipse.preferences.version=1 editor_save_participant_org.eclipse.jdt.ui.postsavelistener.cleanup=true -formatter_profile=_MyTracks -formatter_settings_version=12 +formatter_profile=_My Tracks +formatter_settings_version=13 org.eclipse.jdt.ui.ignorelowercasenames=true org.eclipse.jdt.ui.importorder=com;android;java;javax;org; org.eclipse.jdt.ui.ondemandthreshold=99 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/MarkerEditActivity.java b/MyTracks/src/com/google/android/apps/mytracks/MarkerEditActivity.java index c8dab3243..98f3cc750 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/MarkerEditActivity.java +++ b/MyTracks/src/com/google/android/apps/mytracks/MarkerEditActivity.java @@ -165,7 +165,7 @@ public class MarkerEditActivity extends AbstractMyTracksActivity { waypointName.getText().toString(), waypointMarkerType.getText().toString(), waypointDescription.getText().toString(), - getString(R.string.marker_waypoint_icon_url)); + null); TrackRecordingServiceConnectionUtils.addMarker( this, trackRecordingServiceConnection, waypointCreationRequest); } 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/SearchListActivity.java b/MyTracks/src/com/google/android/apps/mytracks/SearchListActivity.java index 02f4bfd75..b54691de1 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/SearchListActivity.java +++ b/MyTracks/src/com/google/android/apps/mytracks/SearchListActivity.java @@ -323,9 +323,10 @@ public class SearchListActivity extends AbstractMyTracksActivity implements Dele String textQuery = intent.getStringExtra(SearchManager.QUERY); setTitle(textQuery); - Location currentLocation = myTracksLocationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER); - final SearchQuery query = new SearchQuery(textQuery, currentLocation, -1L, System - .currentTimeMillis()); + Location currentLocation = myTracksLocationManager.getLastKnownLocation( + LocationManager.GPS_PROVIDER); + final SearchQuery query = new SearchQuery( + textQuery, currentLocation, -1L, System.currentTimeMillis()); new Thread() { @Override public void run() { 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 new file mode 100644 index 000000000..a095a28ea --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/content/DataSource.java @@ -0,0 +1,209 @@ +/* + * 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 static com.google.android.apps.mytracks.Constants.MAX_LOCATION_AGE_MS; + +import com.google.android.apps.mytracks.Constants; +import com.google.android.apps.mytracks.services.MyTracksLocationManager; +import com.google.android.apps.mytracks.util.GoogleLocationUtils; +import com.google.android.maps.mytracks.R; + +import android.content.ContentResolver; +import android.content.Context; +import android.content.SharedPreferences; +import android.content.SharedPreferences.OnSharedPreferenceChangeListener; +import android.database.ContentObserver; +import android.hardware.Sensor; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; +import android.location.Location; +import android.location.LocationListener; +import android.location.LocationManager; +import android.net.Uri; +import android.util.Log; +import android.widget.Toast; + +/** + * Data source on the phone. + * + * @author Rodrigo Damazio + */ +public class DataSource { + + private static final int NETWORK_PROVIDER_MIN_TIME = 5 * 60 * 1000; // 5 minutes + private static final String TAG = DataSource.class.getSimpleName(); + + private final Context context; + private final ContentResolver contentResolver; + private final MyTracksLocationManager myTracksLocationManager; + private final SensorManager sensorManager; + private final SharedPreferences sharedPreferences; + + public DataSource(Context context) { + this.context = context; + contentResolver = context.getContentResolver(); + myTracksLocationManager = new MyTracksLocationManager(context); + sensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE); + sharedPreferences = context.getSharedPreferences(Constants.SETTINGS_NAME, Context.MODE_PRIVATE); + } + + public void close() { + myTracksLocationManager.close(); + } + + public boolean isAllowed() { + return myTracksLocationManager.isAllowed(); + } + + /** + * Registers a content observer. + * + * @param uri the uri + * @param observer the observer + */ + public void registerContentObserver(Uri uri, ContentObserver observer) { + contentResolver.registerContentObserver(uri, false, observer); + } + + /** + * Unregisters a content observer. + * + * @param observer the observer + */ + public void unregisterContentObserver(ContentObserver observer) { + contentResolver.unregisterContentObserver(observer); + } + + /** + * Registers a location listener. + * + * @param listener the listener + */ + public void registerLocationListener(LocationListener listener) { + // Check if the GPS provider exists + if (myTracksLocationManager.getProvider(LocationManager.GPS_PROVIDER) == null) { + listener.onProviderDisabled(LocationManager.GPS_PROVIDER); + unregisterLocationListener(listener); + return; + } + + // Listen for GPS location + myTracksLocationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0, listener); + + // Update the listener with the current provider state + if (myTracksLocationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) { + listener.onProviderEnabled(LocationManager.GPS_PROVIDER); + } else { + listener.onProviderDisabled(LocationManager.GPS_PROVIDER); + } + + // Listen for network location + try { + myTracksLocationManager.requestLocationUpdates( + LocationManager.NETWORK_PROVIDER, NETWORK_PROVIDER_MIN_TIME, 0, listener); + } catch (RuntimeException e) { + // Network location is optional, so just log the exception + Log.w(TAG, "Could not register for network location.", e); + } + } + + /** + * Unregisters a location listener. + * + * @param listener the listener + */ + public void unregisterLocationListener(LocationListener listener) { + myTracksLocationManager.removeUpdates(listener); + } + + /** + * Gets the last known location. + */ + public Location getLastKnownLocation() { + Location location = myTracksLocationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER); + if (!isLocationRecent(location)) { + // Try network location + location = myTracksLocationManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER); + String toast; + if (isLocationRecent(location)) { + toast = context.getString(R.string.my_location_approximate_location); + } else { + String setting = context.getString( + GoogleLocationUtils.isAvailable(context) ? R.string.gps_google_location_settings + : R.string.gps_location_access); + toast = context.getString(R.string.my_location_no_gps, setting); + } + Toast.makeText(context, toast, Toast.LENGTH_LONG).show(); + } + return location; + } + + /** + * Returns true if the location is recent. + * + * @param location the location + */ + private boolean isLocationRecent(Location location) { + if (location == null) { + return false; + } + return location.getTime() > System.currentTimeMillis() - MAX_LOCATION_AGE_MS; + } + + /** + * Registers a heading listener. + * + * @param listener the listener + */ + 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, heading, SensorManager.SENSOR_DELAY_UI); + } + + /** + * Unregisters a heading listener. + * + * @param listener the listener + */ + public void unregisterHeadingListener(SensorEventListener listener) { + sensorManager.unregisterListener(listener); + } + + /** + * Registers a shared preference change listener. + * + * @param listener the listener + */ + public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) { + sharedPreferences.registerOnSharedPreferenceChangeListener(listener); + } + + /** + * Unregisters a shared preference change listener. + * + * @param listener the listener + */ + public void unregisterOnSharedPreferenceChangeListener( + OnSharedPreferenceChangeListener listener) { + sharedPreferences.unregisterOnSharedPreferenceChangeListener(listener); + } +} \ No newline at end of file diff --git a/MyTracks/src/com/google/android/apps/mytracks/content/DataSourceListener.java b/MyTracks/src/com/google/android/apps/mytracks/content/DataSourceListener.java new file mode 100644 index 000000000..01cc1e7ef --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/content/DataSourceListener.java @@ -0,0 +1,78 @@ +/* + * 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 android.location.Location; + +/** + * Listener to be invoked when {@link DataSource} changes. + * + * @author Jimmy Shih + */ +public interface DataSourceListener { + + /** + * Notifies when the tracks table is updated. + */ + public void notifyTracksTableUpdated(); + + /** + * Notifies when the waypoints table is updated. + */ + public void notifyWaypointsTableUpdated(); + + /** + * Notifies when the track points table is updated. + */ + public void notifyTrackPointsTableUpdated(); + + /** + * Notifies when the current location changes. + * + * @param location the location + */ + public void notifyLocationChanged(Location location); + + + /** + * Notifies when the location provider enabled status changes. + * + * @param enabled true if enabled, false if not + */ + public void notifyLocationProviderEnabled(boolean enabled); + + /** + * Notifies when the location provider availability status changes. + * + * @param available true if available, false if not + */ + public void notifyLocationProviderAvailable(boolean available); + + /** + * Notifies when the compass heading changes. + * + * @param heading the new heading + */ + public void notifyHeadingChanged(float heading); + + /** + * Notifies when a preference changes. + * + * @param key the preference key + */ + public void notifyPreferenceChanged(String key); +} \ No newline at end of file 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 fe75b0874..48db3e8a1 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/content/DataSourceManager.java +++ b/MyTracks/src/com/google/android/apps/mytracks/content/DataSourceManager.java @@ -1,8 +1,24 @@ +/* + * 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 static com.google.android.apps.mytracks.Constants.TAG; -import com.google.android.apps.mytracks.content.TrackDataHub.ListenerDataType; +import com.google.common.annotations.VisibleForTesting; import android.content.SharedPreferences; import android.content.SharedPreferences.OnSharedPreferenceChangeListener; @@ -10,7 +26,6 @@ import android.database.ContentObserver; import android.hardware.Sensor; import android.hardware.SensorEvent; import android.hardware.SensorEventListener; -import android.hardware.SensorManager; import android.location.Location; import android.location.LocationListener; import android.location.LocationManager; @@ -23,87 +38,79 @@ import java.util.EnumSet; import java.util.Set; /** - * External data source manager, which converts system-level events into My Tracks data events. - * + * Data source manager. Creates observers/listeners and manages their + * registration with {@link DataSource}. The observers/listeners calls + * {@link DataSourceListener} when data changes. + * * @author Rodrigo Damazio */ -class DataSourceManager { +public class DataSourceManager { - /** Single interface for receiving system events that were registered for. */ - interface DataSourceListener { - void notifyTrackUpdated(); - void notifyWaypointUpdated(); - void notifyPointsUpdated(); - void notifyPreferenceChanged(String key); - void notifyLocationProviderEnabled(boolean enabled); - void notifyLocationProviderAvailable(boolean available); - void notifyLocationChanged(Location loc); - void notifyHeadingChanged(float heading); - } + /** + * Observer when the tracks table is updated. + * + * @author Jimmy Shih + */ + private class TracksTableObserver extends ContentObserver { - private final DataSourceListener listener; - - /** Observer for when the tracks table is updated. */ - private class TrackObserver extends ContentObserver { - public TrackObserver() { - super(contentHandler); + public TracksTableObserver() { + super(handler); } @Override public void onChange(boolean selfChange) { - listener.notifyTrackUpdated(); + dataSourceListener.notifyTracksTableUpdated(); } } - /** Observer for when the waypoints table is updated. */ - private class WaypointObserver extends ContentObserver { - public WaypointObserver() { - super(contentHandler); + /** + * Observer when the waypoints table is updated. + * + * @author Jimmy Shih + */ + private class WaypointsTableObserver extends ContentObserver { + + public WaypointsTableObserver() { + super(handler); } @Override public void onChange(boolean selfChange) { - listener.notifyWaypointUpdated(); + dataSourceListener.notifyWaypointsTableUpdated(); } } - /** Observer for when the points table is updated. */ - private class PointObserver extends ContentObserver { - public PointObserver() { - super(contentHandler); + /** + * Observer when the track points table is updated. + * + * @author Jimmy Shih + */ + private class TrackPointsTableObserver extends ContentObserver { + + public TrackPointsTableObserver() { + super(handler); } @Override public void onChange(boolean selfChange) { - listener.notifyPointsUpdated(); + dataSourceListener.notifyTrackPointsTableUpdated(); } } - /** Listener for when preferences change. */ - private class HubSharedPreferenceListener implements OnSharedPreferenceChangeListener { - @Override - public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { - listener.notifyPreferenceChanged(key); - } - } + /** + * Listener for location changes. + * + * @author Jimmy Shih + */ + @VisibleForTesting + class CurrentLocationListener implements LocationListener { - /** Listener for the current location (independent from track data). */ - private class CurrentLocationListener implements - LocationListener { @Override - public void onStatusChanged(String provider, int status, Bundle extras) { - if (!dataSources.isAllowed() || !LocationManager.GPS_PROVIDER.equals(provider)) { + public void onLocationChanged(Location location) { + if (!dataSource.isAllowed()) { return; } - listener.notifyLocationProviderAvailable(status == LocationProvider.AVAILABLE); - } - - @Override - public void onProviderEnabled(String provider) { - if (!dataSources.isAllowed() || !LocationManager.GPS_PROVIDER.equals(provider)) { - return; - } - listener.notifyLocationProviderEnabled(true); + dataSourceListener.notifyLocationChanged(location); } @Override @@ -111,161 +118,206 @@ class DataSourceManager { if (!LocationManager.GPS_PROVIDER.equals(provider)) { return; } - listener.notifyLocationProviderEnabled(false); + dataSourceListener.notifyLocationProviderEnabled(false); } @Override - public void onLocationChanged(Location location) { - if (!dataSources.isAllowed()) { + public void onProviderEnabled(String provider) { + if (!dataSource.isAllowed() || !LocationManager.GPS_PROVIDER.equals(provider)) { return; } - listener.notifyLocationChanged(location); + dataSourceListener.notifyLocationProviderEnabled(true); + } + + @Override + public void onStatusChanged(String provider, int status, Bundle extras) { + if (!dataSource.isAllowed() || !LocationManager.GPS_PROVIDER.equals(provider)) { + return; + } + dataSourceListener.notifyLocationProviderAvailable(status == LocationProvider.AVAILABLE); } } - /** Listener for compass readings. */ - private class CompassListener implements - SensorEventListener { - @Override - public void onSensorChanged(SensorEvent event) { - listener.notifyHeadingChanged(event.values[0]); - } + /** + * Listener for heading changes. + * + * @author Jimmy Shih + */ + @VisibleForTesting + class HeadingListener implements SensorEventListener { @Override public void onAccuracyChanged(Sensor sensor, int accuracy) { // Do nothing } + + @Override + public void onSensorChanged(SensorEvent event) { + dataSourceListener.notifyHeadingChanged(event.values[0]); + } } - /** Wrapper for registering internal listeners. */ - private final DataSourcesWrapper dataSources; + /** + * Listener for preference changes. + * + * @author Jimmy Shih + */ + private class PreferenceListener implements OnSharedPreferenceChangeListener { - // Internal listeners (to receive data from the system) - private final Set registeredListeners = - EnumSet.noneOf(ListenerDataType.class); - private final Handler contentHandler; - private final ContentObserver pointObserver; - private final ContentObserver waypointObserver; - private final ContentObserver trackObserver; - private final LocationListener locationListener; - private final OnSharedPreferenceChangeListener preferenceListener; - private final SensorEventListener compassListener; - - DataSourceManager(DataSourceListener listener, DataSourcesWrapper dataSources) { - this.listener = listener; - this.dataSources = dataSources; - - contentHandler = new Handler(); - pointObserver = new PointObserver(); - waypointObserver = new WaypointObserver(); - trackObserver = new TrackObserver(); - - compassListener = new CompassListener(); - locationListener = new CurrentLocationListener(); - preferenceListener = new HubSharedPreferenceListener(); + @Override + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { + dataSourceListener.notifyPreferenceChanged(key); + } } - /** Updates the internal (sensor, position, etc) listeners. */ - void updateAllListeners(EnumSet externallyNeededListeners) { - EnumSet neededListeners = EnumSet.copyOf(externallyNeededListeners); + private final DataSource dataSource; + private final DataSourceListener dataSourceListener; - // Special case - map sampled-out points type to points type since they - // correspond to the same internal listener. - if (neededListeners.contains(ListenerDataType.SAMPLED_OUT_POINT_UPDATES)) { - neededListeners.remove(ListenerDataType.SAMPLED_OUT_POINT_UPDATES); - neededListeners.add(ListenerDataType.POINT_UPDATES); + // Registered listeners + private final Set registeredListeners = EnumSet.noneOf(TrackDataType.class); + + private final Handler handler; + private final TracksTableObserver tracksTableObserver; + private final WaypointsTableObserver waypointsTableObserver; + private final TrackPointsTableObserver trackPointsTableObserver; + private final CurrentLocationListener currentLocationListener; + private final HeadingListener headingListener; + private final PreferenceListener preferenceListener; + + public DataSourceManager(DataSource dataSource, DataSourceListener dataSourceListener) { + this.dataSource = dataSource; + this.dataSourceListener = dataSourceListener; + + handler = new Handler(); + tracksTableObserver = new TracksTableObserver(); + waypointsTableObserver = new WaypointsTableObserver(); + trackPointsTableObserver = new TrackPointsTableObserver(); + currentLocationListener = new CurrentLocationListener(); + headingListener = new HeadingListener(); + preferenceListener = new PreferenceListener(); + } + + /** + * Updates listeners with data source. + * + * @param listeners the listeners + */ + public void updateListeners(EnumSet listeners) { + EnumSet neededListeners = EnumSet.copyOf(listeners); + + /* + * Map SAMPLED_OUT_POINT_UPDATES to POINT_UPDATES since they correspond to + * the same internal listener + */ + 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 internal listeners to types " + neededListeners); + Log.d(TAG, "Updating listeners " + neededListeners); // Unnecessary = registered - needed - Set unnecessaryListeners = EnumSet.copyOf(registeredListeners); + Set unnecessaryListeners = EnumSet.copyOf(registeredListeners); unnecessaryListeners.removeAll(neededListeners); // Missing = needed - registered - Set missingListeners = EnumSet.copyOf(neededListeners); + Set missingListeners = EnumSet.copyOf(neededListeners); missingListeners.removeAll(registeredListeners); - // Remove all unnecessary listeners. - for (ListenerDataType type : unnecessaryListeners) { - unregisterListener(type); + // Remove unnecessary listeners + for (TrackDataType trackDataType : unnecessaryListeners) { + unregisterListener(trackDataType); } - // Add all missing listeners. - for (ListenerDataType type : missingListeners) { - registerListener(type); + // Add missing listeners + for (TrackDataType trackDataType : missingListeners) { + registerListener(trackDataType); } - // Now all needed types are registered. + // Update registered listeners registeredListeners.clear(); registeredListeners.addAll(neededListeners); } - private void registerListener(ListenerDataType type) { - switch (type) { - case COMPASS_UPDATES: { - // Listen to compass - Sensor compass = dataSources.getSensor(Sensor.TYPE_ORIENTATION); - if (compass != null) { - Log.d(TAG, "TrackDataHub: Now registering sensor listener."); - dataSources.registerSensorListener(compassListener, compass, SensorManager.SENSOR_DELAY_UI); - } + /** + * Registers a listener with data source. + * + * @param trackDataType the listener data type + */ + private void registerListener(TrackDataType trackDataType) { + switch (trackDataType) { + case SELECTED_TRACK: + // Do nothing break; - } - case LOCATION_UPDATES: - dataSources.requestLocationUpdates(locationListener); + case TRACKS_TABLE: + dataSource.registerContentObserver(TracksColumns.CONTENT_URI, tracksTableObserver); break; - case POINT_UPDATES: - dataSources.registerContentObserver( - TrackPointsColumns.CONTENT_URI, false, pointObserver); + case WAYPOINTS_TABLE: + dataSource.registerContentObserver(WaypointsColumns.CONTENT_URI, waypointsTableObserver); break; - case TRACK_UPDATES: - dataSources.registerContentObserver(TracksColumns.CONTENT_URI, false, trackObserver); + case SAMPLED_IN_TRACK_POINTS_TABLE: + dataSource.registerContentObserver( + TrackPointsColumns.CONTENT_URI, trackPointsTableObserver); break; - case WAYPOINT_UPDATES: - dataSources.registerContentObserver( - WaypointsColumns.CONTENT_URI, false, waypointObserver); + case SAMPLED_OUT_TRACK_POINTS_TABLE: + // Do nothing. SAMPLED_OUT_POINT_UPDATES is mapped to POINT_UPDATES. break; - case DISPLAY_PREFERENCES: - dataSources.registerOnSharedPreferenceChangeListener(preferenceListener); + case LOCATION: + dataSource.registerLocationListener(currentLocationListener); + break; + case HEADING: + dataSource.registerHeadingListener(headingListener); + break; + case PREFERENCE: + dataSource.registerOnSharedPreferenceChangeListener(preferenceListener); + break; + default: break; - case SAMPLED_OUT_POINT_UPDATES: - throw new IllegalArgumentException("Should have been mapped to point updates"); } } - private void unregisterListener(ListenerDataType type) { - switch (type) { - case COMPASS_UPDATES: - dataSources.unregisterSensorListener(compassListener); + /** + * Unregisters a listener with data source. + * + * @param trackDataType listener data type + */ + private void unregisterListener(TrackDataType trackDataType) { + switch (trackDataType) { + case SELECTED_TRACK: + // Do nothing break; - case LOCATION_UPDATES: - dataSources.removeLocationUpdates(locationListener); + case TRACKS_TABLE: + dataSource.unregisterContentObserver(tracksTableObserver); break; - case POINT_UPDATES: - dataSources.unregisterContentObserver(pointObserver); + case WAYPOINTS_TABLE: + dataSource.unregisterContentObserver(waypointsTableObserver); break; - case TRACK_UPDATES: - dataSources.unregisterContentObserver(trackObserver); + case SAMPLED_IN_TRACK_POINTS_TABLE: + dataSource.unregisterContentObserver(trackPointsTableObserver); break; - case WAYPOINT_UPDATES: - dataSources.unregisterContentObserver(waypointObserver); + case SAMPLED_OUT_TRACK_POINTS_TABLE: + // Do nothing. SAMPLED_OUT_POINT_UPDATES is mapped to POINT_UPDATES. break; - case DISPLAY_PREFERENCES: - dataSources.unregisterOnSharedPreferenceChangeListener(preferenceListener); + case LOCATION: + dataSource.unregisterLocationListener(currentLocationListener); + break; + case HEADING: + dataSource.unregisterHeadingListener(headingListener); + break; + case PREFERENCE: + dataSource.unregisterOnSharedPreferenceChangeListener(preferenceListener); + break; + default: break; - case SAMPLED_OUT_POINT_UPDATES: - throw new IllegalArgumentException("Should have been mapped to point updates"); } } - /** Unregisters all internal (sensor, position, etc.) listeners. */ - void unregisterAllListeners() { - dataSources.removeLocationUpdates(locationListener); - dataSources.unregisterSensorListener(compassListener); - dataSources.unregisterContentObserver(trackObserver); - dataSources.unregisterContentObserver(waypointObserver); - dataSources.unregisterContentObserver(pointObserver); - dataSources.unregisterOnSharedPreferenceChangeListener(preferenceListener); + /** + * Unregisters all listeners with data source. + */ + public void unregisterAllListeners() { + for (TrackDataType trackDataType : TrackDataType.values()) { + unregisterListener(trackDataType); + } } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/content/DataSourcesWrapper.java b/MyTracks/src/com/google/android/apps/mytracks/content/DataSourcesWrapper.java deleted file mode 100644 index 1e0634313..000000000 --- a/MyTracks/src/com/google/android/apps/mytracks/content/DataSourcesWrapper.java +++ /dev/null @@ -1,56 +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 android.content.SharedPreferences.OnSharedPreferenceChangeListener; -import android.database.ContentObserver; -import android.hardware.Sensor; -import android.hardware.SensorEventListener; -import android.location.Location; -import android.location.LocationListener; -import android.net.Uri; - -/** - * Interface for abstracting registration of external data source listeners. - * - * @author Rodrigo Damazio - */ -interface DataSourcesWrapper { - // Preferences - void registerOnSharedPreferenceChangeListener( - OnSharedPreferenceChangeListener listener); - void unregisterOnSharedPreferenceChangeListener( - OnSharedPreferenceChangeListener listener); - - // Content provider - void registerContentObserver(Uri contentUri, boolean descendents, - ContentObserver observer); - void unregisterContentObserver(ContentObserver observer); - - // Sensors - Sensor getSensor(int type); - void registerSensorListener(SensorEventListener listener, - Sensor sensor, int sensorDelay); - void unregisterSensorListener(SensorEventListener listener); - - // Location - boolean isLocationProviderEnabled(String provider); - void requestLocationUpdates(LocationListener listener); - void removeLocationUpdates(LocationListener listener); - Location getLastKnownLocation(); - void close(); - boolean isAllowed(); -} \ No newline at end of file diff --git a/MyTracks/src/com/google/android/apps/mytracks/content/DataSourcesWrapperImpl.java b/MyTracks/src/com/google/android/apps/mytracks/content/DataSourcesWrapperImpl.java deleted file mode 100644 index 6e83041c4..000000000 --- a/MyTracks/src/com/google/android/apps/mytracks/content/DataSourcesWrapperImpl.java +++ /dev/null @@ -1,181 +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 static com.google.android.apps.mytracks.Constants.MAX_LOCATION_AGE_MS; -import static com.google.android.apps.mytracks.Constants.MAX_NETWORK_AGE_MS; - -import com.google.android.apps.mytracks.Constants; -import com.google.android.apps.mytracks.services.MyTracksLocationManager; -import com.google.android.apps.mytracks.util.GoogleLocationUtils; -import com.google.android.maps.mytracks.R; - -import android.content.ContentResolver; -import android.content.Context; -import android.content.SharedPreferences; -import android.content.SharedPreferences.OnSharedPreferenceChangeListener; -import android.database.ContentObserver; -import android.hardware.Sensor; -import android.hardware.SensorEventListener; -import android.hardware.SensorManager; -import android.location.Location; -import android.location.LocationListener; -import android.location.LocationManager; -import android.location.LocationProvider; -import android.net.Uri; -import android.util.Log; -import android.widget.Toast; - -/** - * Real implementation of the data sources, which talks to system services. - * - * @author Rodrigo Damazio - */ -class DataSourcesWrapperImpl implements DataSourcesWrapper { - // System services - private final SensorManager sensorManager; - private final MyTracksLocationManager myTracksLocationManager; - private final ContentResolver contentResolver; - private final SharedPreferences sharedPreferences; - private final Context context; - - DataSourcesWrapperImpl(Context context, SharedPreferences sharedPreferences) { - this.context = context; - this.sensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE); - this.myTracksLocationManager = new MyTracksLocationManager(context); - this.contentResolver = context.getContentResolver(); - this.sharedPreferences = sharedPreferences; - } - - @Override - public void close() { - myTracksLocationManager.close(); - } - - @Override - public boolean isAllowed() { - return myTracksLocationManager.isAllowed(); - } - - @Override - public void registerOnSharedPreferenceChangeListener( - OnSharedPreferenceChangeListener listener) { - sharedPreferences.registerOnSharedPreferenceChangeListener(listener); - } - - @Override - public void unregisterOnSharedPreferenceChangeListener( - OnSharedPreferenceChangeListener listener) { - sharedPreferences.unregisterOnSharedPreferenceChangeListener(listener); - } - - @Override - public void registerContentObserver(Uri contentUri, boolean descendents, - ContentObserver observer) { - contentResolver.registerContentObserver(contentUri, descendents, observer); - } - - @Override - public void unregisterContentObserver(ContentObserver observer) { - contentResolver.unregisterContentObserver(observer); - } - - @Override - public Sensor getSensor(int type) { - return sensorManager.getDefaultSensor(type); - } - - @Override - public void registerSensorListener(SensorEventListener listener, - Sensor sensor, int sensorDelay) { - sensorManager.registerListener(listener, sensor, sensorDelay); - } - - @Override - public void unregisterSensorListener(SensorEventListener listener) { - sensorManager.unregisterListener(listener); - } - - @Override - public boolean isLocationProviderEnabled(String provider) { - return myTracksLocationManager.isProviderEnabled(provider); - } - - @Override - public void requestLocationUpdates(LocationListener listener) { - // Check if the provider exists. - LocationProvider gpsProvider = myTracksLocationManager.getProvider(LocationManager.GPS_PROVIDER); - if (gpsProvider == null) { - listener.onProviderDisabled(LocationManager.GPS_PROVIDER); - myTracksLocationManager.removeUpdates(listener); - return; - } - - // Listen to GPS location. - String providerName = gpsProvider.getName(); - Log.d(Constants.TAG, "TrackDataHub: Using location provider " + providerName); - myTracksLocationManager.requestLocationUpdates(providerName, - 0 /*minTime*/, 0 /*minDist*/, listener); - - // Give an initial update on provider state. - if (myTracksLocationManager.isProviderEnabled(providerName)) { - listener.onProviderEnabled(providerName); - } else { - listener.onProviderDisabled(providerName); - } - - // Listen to network location - try { - myTracksLocationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, - 1000 * 60 * 5 /*minTime*/, 0 /*minDist*/, listener); - } catch (RuntimeException e) { - // If anything at all goes wrong with getting a cell location do not - // abort. Cell location is not essential to this app. - Log.w(Constants.TAG, "Could not register network location listener.", e); - } - } - - @Override - public Location getLastKnownLocation() { - // TODO: Let's look at more advanced algorithms to determine the best - // current location. - - Location loc = myTracksLocationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER); - final long now = System.currentTimeMillis(); - if (loc == null || loc.getTime() < now - MAX_LOCATION_AGE_MS) { - // We don't have a recent GPS fix, just use cell towers if available - loc = myTracksLocationManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER); - - String toast = context.getString(R.string.my_location_approximate_location); - if (loc == null || loc.getTime() < now - MAX_NETWORK_AGE_MS) { - // We don't have a recent cell tower location, let the user know: - String setting = context.getString( - GoogleLocationUtils.isAvailable(context) ? R.string.gps_google_location_settings - : R.string.gps_location_access); - toast = context.getString(R.string.my_location_no_gps, setting); - } - - // Let the user know we have only an approximate location: - Toast.makeText(context, toast, Toast.LENGTH_LONG).show(); - } - return loc; - } - - @Override - public void removeLocationUpdates(LocationListener listener) { - myTracksLocationManager.removeUpdates(listener); - } -} \ No newline at end of file diff --git a/MyTracks/src/com/google/android/apps/mytracks/content/DescriptionGeneratorImpl.java b/MyTracks/src/com/google/android/apps/mytracks/content/DescriptionGeneratorImpl.java index 916c7a40e..e647dc4f2 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/content/DescriptionGeneratorImpl.java +++ b/MyTracks/src/com/google/android/apps/mytracks/content/DescriptionGeneratorImpl.java @@ -76,8 +76,8 @@ public class DescriptionGeneratorImpl implements DescriptionGenerator { } @Override - public String generateWaypointDescription(Waypoint waypoint) { - return generateTripStatisticsDescription(waypoint.getTripStatistics(), false); + public String generateWaypointDescription(TripStatistics tripStatistics) { + return generateTripStatisticsDescription(tripStatistics, false); } /** diff --git a/MyTracks/src/com/google/android/apps/mytracks/content/MyTracksProvider.java b/MyTracks/src/com/google/android/apps/mytracks/content/MyTracksProvider.java index aba3f3157..29bf4ea4d 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/content/MyTracksProvider.java +++ b/MyTracks/src/com/google/android/apps/mytracks/content/MyTracksProvider.java @@ -16,9 +16,9 @@ package com.google.android.apps.mytracks.content; -import com.google.android.apps.mytracks.Constants; import com.google.android.apps.mytracks.util.PreferencesUtils; import com.google.android.maps.mytracks.R; +import com.google.common.annotations.VisibleForTesting; import android.content.ContentProvider; import android.content.ContentUris; @@ -38,64 +38,65 @@ import android.text.TextUtils; import android.util.Log; /** - * A provider that handles recorded (GPS) tracks and their track points. - * + * A {@link ContentProvider} that handles access to track points, tracks, and + * waypoints tables. + * * @author Leif Hendrik Wilden */ public class MyTracksProvider extends ContentProvider { - private static final String DATABASE_NAME = "mytracks.db"; - private static final int DATABASE_VERSION = 20; - private static final int TRACKPOINTS = 1; - private static final int TRACKPOINTS_ID = 2; - private static final int TRACKS = 3; - private static final int TRACKS_ID = 4; - private static final int WAYPOINTS = 5; - private static final int WAYPOINTS_ID = 6; private static final String TAG = MyTracksProvider.class.getSimpleName(); + @VisibleForTesting + static final String DATABASE_NAME = "mytracks.db"; + private static final int DATABASE_VERSION = 20; /** - * Helper which creates or upgrades the database if necessary. + * Database helper for creating and upgrading the database. */ - private static class DatabaseHelper extends SQLiteOpenHelper { - + @VisibleForTesting + static class DatabaseHelper extends SQLiteOpenHelper { + public DatabaseHelper(Context context) { - super(context, DATABASE_NAME, null, DATABASE_VERSION); + this(context, DATABASE_NAME); } - + + @VisibleForTesting + public DatabaseHelper(Context context, String databaseName) { + super(context, databaseName, null, DATABASE_VERSION); + } + @Override public void onCreate(SQLiteDatabase db) { db.execSQL(TrackPointsColumns.CREATE_TABLE); db.execSQL(TracksColumns.CREATE_TABLE); db.execSQL(WaypointsColumns.CREATE_TABLE); } - + @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { Log.w(TAG, "Upgrading database from version " + oldVersion + " to " + newVersion); if (oldVersion < 17) { - // Delete the old data - Log.w(TAG, "Delete all old data"); + Log.w(TAG, "Deleting all old data."); db.execSQL("DROP TABLE IF EXISTS " + TrackPointsColumns.TABLE_NAME); db.execSQL("DROP TABLE IF EXISTS " + TracksColumns.TABLE_NAME); db.execSQL("DROP TABLE IF EXISTS " + WaypointsColumns.TABLE_NAME); onCreate(db); } else { - // Incremental updates go here. For each DB version, add a corresponding if clause. - - // Track points sensor column + // Incremental upgrades. One if statement per DB version. + + // Add track points SENSOR column if (oldVersion <= 17) { Log.w(TAG, "Upgrade DB: Adding sensor column."); db.execSQL("ALTER TABLE " + TrackPointsColumns.TABLE_NAME + " ADD " + TrackPointsColumns.SENSOR + " BLOB"); } - // Tracks table id column + // Add tracks TABLEID column if (oldVersion <= 18) { Log.w(TAG, "Upgrade DB: Adding tableid column."); db.execSQL("ALTER TABLE " + TracksColumns.TABLE_NAME + " ADD " + TracksColumns.TABLEID + " STRING"); } - // Tracks table icon column + // Add tracks ICON column if (oldVersion <= 19) { Log.w(TAG, "Upgrade DB: Adding icon column."); db.execSQL( @@ -105,42 +106,55 @@ public class MyTracksProvider extends ContentProvider { } } - private final UriMatcher urlMatcher; + /** + * Types of url. + * + * @author Jimmy Shih + */ + @VisibleForTesting + enum UrlType { + TRACKPOINTS, TRACKPOINTS_ID, TRACKS, TRACKS_ID, WAYPOINTS, WAYPOINTS_ID + } + private final UriMatcher uriMatcher; private SQLiteDatabase db; public MyTracksProvider() { - urlMatcher = new UriMatcher(UriMatcher.NO_MATCH); - urlMatcher.addURI(MyTracksProviderUtils.AUTHORITY, - "trackpoints", TRACKPOINTS); - urlMatcher.addURI(MyTracksProviderUtils.AUTHORITY, - "trackpoints/#", TRACKPOINTS_ID); - urlMatcher.addURI(MyTracksProviderUtils.AUTHORITY, "tracks", TRACKS); - urlMatcher.addURI(MyTracksProviderUtils.AUTHORITY, "tracks/#", TRACKS_ID); - urlMatcher.addURI(MyTracksProviderUtils.AUTHORITY, "waypoints", WAYPOINTS); - urlMatcher.addURI(MyTracksProviderUtils.AUTHORITY, - "waypoints/#", WAYPOINTS_ID); - } - - private boolean canAccess() { - if (Binder.getCallingPid() == Process.myPid()) { - return true; - } else { - return PreferencesUtils.getBoolean( - getContext(), R.string.allow_access_key, PreferencesUtils.ALLOW_ACCESS_DEFAULT); - } + uriMatcher = new UriMatcher(UriMatcher.NO_MATCH); + uriMatcher.addURI(MyTracksProviderUtils.AUTHORITY, TrackPointsColumns.TABLE_NAME, + UrlType.TRACKPOINTS.ordinal()); + uriMatcher.addURI(MyTracksProviderUtils.AUTHORITY, TrackPointsColumns.TABLE_NAME + "/#", + UrlType.TRACKPOINTS_ID.ordinal()); + uriMatcher.addURI( + MyTracksProviderUtils.AUTHORITY, TracksColumns.TABLE_NAME, UrlType.TRACKS.ordinal()); + uriMatcher.addURI(MyTracksProviderUtils.AUTHORITY, TracksColumns.TABLE_NAME + "/#", + UrlType.TRACKS_ID.ordinal()); + uriMatcher.addURI( + MyTracksProviderUtils.AUTHORITY, WaypointsColumns.TABLE_NAME, UrlType.WAYPOINTS.ordinal()); + uriMatcher.addURI(MyTracksProviderUtils.AUTHORITY, WaypointsColumns.TABLE_NAME + "/#", + UrlType.WAYPOINTS_ID.ordinal()); } @Override public boolean onCreate() { + return onCreate(getContext()); + } + + /** + * Helper method to make onCreate is testable. + * @param context context to creates database + * @return true means run successfully + */ + @VisibleForTesting + boolean onCreate(Context context) { if (!canAccess()) { return false; } - DatabaseHelper dbHelper = new DatabaseHelper(getContext()); + DatabaseHelper databaseHelper = new DatabaseHelper(context); try { - db = dbHelper.getWritableDatabase(); + db = databaseHelper.getWritableDatabase(); } catch (SQLiteException e) { - Log.e(TAG, "Unable to open database for writing", e); + Log.e(TAG, "Unable to open database for writing.", e); } return db != null; } @@ -152,7 +166,7 @@ public class MyTracksProvider extends ContentProvider { } String table; boolean shouldVacuum = false; - switch (urlMatcher.match(url)) { + switch (getUrlType(url)) { case TRACKPOINTS: table = TrackPointsColumns.TABLE_NAME; break; @@ -166,17 +180,23 @@ public class MyTracksProvider extends ContentProvider { default: throw new IllegalArgumentException("Unknown URL " + url); } - - Log.w(MyTracksProvider.TAG, "provider delete in " + table + "!"); - int count = db.delete(table, where, selectionArgs); + + Log.w(MyTracksProvider.TAG, "Deleting table " + table); + int count; + try { + db.beginTransaction(); + count = db.delete(table, where, selectionArgs); + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } getContext().getContentResolver().notifyChange(url, null, true); - + if (shouldVacuum) { - // If a potentially large amount of data was deleted, we want to reclaim its space. - Log.i(TAG, "Vacuuming the database"); + // If a potentially large amount of data was deleted, reclaim its space. + Log.i(TAG, "Vacuuming the database."); db.execSQL("VACUUM"); } - return count; } @@ -185,7 +205,7 @@ public class MyTracksProvider extends ContentProvider { if (!canAccess()) { return null; } - switch (urlMatcher.match(url)) { + switch (getUrlType(url)) { case TRACKPOINTS: return TrackPointsColumns.CONTENT_TYPE; case TRACKPOINTS_ID: @@ -208,197 +228,240 @@ public class MyTracksProvider extends ContentProvider { if (!canAccess()) { return null; } - Log.d(MyTracksProvider.TAG, "MyTracksProvider.insert"); - ContentValues values; - if (initialValues != null) { - values = initialValues; - } else { - values = new ContentValues(); + if (initialValues == null) { + initialValues = new ContentValues(); } - - int urlMatchType = urlMatcher.match(url); - return insertType(url, urlMatchType, values); - } - - private Uri insertType(Uri url, int urlMatchType, ContentValues values) { - switch (urlMatchType) { - case TRACKPOINTS: - return insertTrackPoint(url, values); - case TRACKS: - return insertTrack(url, values); - case WAYPOINTS: - return insertWaypoint(url, values); - default: - throw new IllegalArgumentException("Unknown URL " + url); + Uri result = null; + try { + db.beginTransaction(); + result = insertContentValues(url, getUrlType(url), initialValues); + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); } + return result; } - @Override public int bulkInsert(Uri url, ContentValues[] valuesBulk) { if (!canAccess()) { return 0; } - Log.d(MyTracksProvider.TAG, "MyTracksProvider.bulkInsert"); int numInserted = 0; try { // Use a transaction in order to make the insertions run as a single batch db.beginTransaction(); - - int urlMatch = urlMatcher.match(url); + + UrlType urlType = getUrlType(url); for (numInserted = 0; numInserted < valuesBulk.length; numInserted++) { - ContentValues values = valuesBulk[numInserted]; - if (values == null) { values = new ContentValues(); } - - insertType(url, urlMatch, values); + ContentValues contentValues = valuesBulk[numInserted]; + if (contentValues == null) { + contentValues = new ContentValues(); + } + insertContentValues(url, urlType, contentValues); } - db.setTransactionSuccessful(); } finally { db.endTransaction(); } - return numInserted; } - private Uri insertTrackPoint(Uri url, ContentValues values) { - boolean hasLat = values.containsKey(TrackPointsColumns.LATITUDE); - boolean hasLong = values.containsKey(TrackPointsColumns.LONGITUDE); - boolean hasTime = values.containsKey(TrackPointsColumns.TIME); - if (!hasLat || !hasLong || !hasTime) { - throw new IllegalArgumentException( - "Latitude, longitude, and time values are required."); - } - long rowId = db.insert(TrackPointsColumns.TABLE_NAME, TrackPointsColumns._ID, values); - if (rowId >= 0) { - Uri uri = ContentUris.appendId( - TrackPointsColumns.CONTENT_URI.buildUpon(), rowId).build(); - getContext().getContentResolver().notifyChange(url, null, true); - return uri; - } - throw new SQLiteException("Failed to insert row into " + url); - } - - private Uri insertTrack(Uri url, ContentValues values) { - boolean hasStartTime = values.containsKey(TracksColumns.STARTTIME); - boolean hasStartId = values.containsKey(TracksColumns.STARTID); - if (!hasStartTime || !hasStartId) { - throw new IllegalArgumentException( - "Both start time and start id values are required."); - } - long rowId = db.insert(TracksColumns.TABLE_NAME, TracksColumns._ID, values); - if (rowId > 0) { - Uri uri = ContentUris.appendId( - TracksColumns.CONTENT_URI.buildUpon(), rowId).build(); - getContext().getContentResolver().notifyChange(url, null, true); - return uri; - } - throw new SQLException("Failed to insert row into " + url); - } - - private Uri insertWaypoint(Uri url, ContentValues values) { - long rowId = db.insert(WaypointsColumns.TABLE_NAME, WaypointsColumns._ID, values); - if (rowId > 0) { - Uri uri = ContentUris.appendId( - WaypointsColumns.CONTENT_URI.buildUpon(), rowId).build(); - getContext().getContentResolver().notifyChange(url, null, true); - return uri; - } - throw new SQLException("Failed to insert row into " + url); - } - @Override public Cursor query( - Uri url, String[] projection, String selection, String[] selectionArgs, - String sort) { + Uri url, String[] projection, String selection, String[] selectionArgs, String sort) { if (!canAccess()) { return null; } - SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); - int match = urlMatcher.match(url); + SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder(); String sortOrder = null; - if (match == TRACKPOINTS) { - qb.setTables(TrackPointsColumns.TABLE_NAME); - if (sort != null) { - sortOrder = sort; - } else { - sortOrder = TrackPointsColumns.DEFAULT_SORT_ORDER; - } - } else if (match == TRACKPOINTS_ID) { - qb.setTables(TrackPointsColumns.TABLE_NAME); - qb.appendWhere("_id=" + url.getPathSegments().get(1)); - } else if (match == TRACKS) { - qb.setTables(TracksColumns.TABLE_NAME); - if (sort != null) { - sortOrder = sort; - } else { - sortOrder = TracksColumns.DEFAULT_SORT_ORDER; - } - } else if (match == TRACKS_ID) { - qb.setTables(TracksColumns.TABLE_NAME); - qb.appendWhere("_id=" + url.getPathSegments().get(1)); - } else if (match == WAYPOINTS) { - qb.setTables(WaypointsColumns.TABLE_NAME); - if (sort != null) { - sortOrder = sort; - } else { - sortOrder = WaypointsColumns.DEFAULT_SORT_ORDER; - } - } else if (match == WAYPOINTS_ID) { - qb.setTables(WaypointsColumns.TABLE_NAME); - qb.appendWhere("_id=" + url.getPathSegments().get(1)); - } else { - throw new IllegalArgumentException("Unknown URL " + url); + switch (getUrlType(url)) { + case TRACKPOINTS: + queryBuilder.setTables(TrackPointsColumns.TABLE_NAME); + sortOrder = sort != null ? sort : TrackPointsColumns.DEFAULT_SORT_ORDER; + break; + case TRACKPOINTS_ID: + queryBuilder.setTables(TrackPointsColumns.TABLE_NAME); + queryBuilder.appendWhere("_id=" + url.getPathSegments().get(1)); + break; + case TRACKS: + queryBuilder.setTables(TracksColumns.TABLE_NAME); + sortOrder = sort != null ? sort : TracksColumns.DEFAULT_SORT_ORDER; + break; + case TRACKS_ID: + queryBuilder.setTables(TracksColumns.TABLE_NAME); + queryBuilder.appendWhere("_id=" + url.getPathSegments().get(1)); + break; + case WAYPOINTS: + queryBuilder.setTables(WaypointsColumns.TABLE_NAME); + sortOrder = sort != null ? sort : WaypointsColumns.DEFAULT_SORT_ORDER; + break; + case WAYPOINTS_ID: + queryBuilder.setTables(WaypointsColumns.TABLE_NAME); + queryBuilder.appendWhere("_id=" + url.getPathSegments().get(1)); + break; + default: + throw new IllegalArgumentException("Unknown url " + url); } - Log.i(Constants.TAG, "Build query: " - + qb.buildQuery(projection, selection, selectionArgs, null, null, sortOrder, null)); - - Cursor c = qb.query(db, projection, selection, selectionArgs, null, null, - sortOrder); - c.setNotificationUri(getContext().getContentResolver(), url); - return c; + Cursor cursor = queryBuilder.query( + db, projection, selection, selectionArgs, null, null, sortOrder); + cursor.setNotificationUri(getContext().getContentResolver(), url); + return cursor; } @Override - public int update(Uri url, ContentValues values, String where, - String[] selectionArgs) { + public int update(Uri url, ContentValues values, String where, String[] selectionArgs) { if (!canAccess()) { return 0; } + String table; + String whereClause; + switch (getUrlType(url)) { + case TRACKPOINTS: + table = TrackPointsColumns.TABLE_NAME; + whereClause = where; + break; + case TRACKPOINTS_ID: + table = TrackPointsColumns.TABLE_NAME; + whereClause = TrackPointsColumns._ID + "=" + url.getPathSegments().get(1); + if (!TextUtils.isEmpty(where)) { + whereClause += " AND (" + where + ")"; + } + break; + case TRACKS: + table = TracksColumns.TABLE_NAME; + whereClause = where; + break; + case TRACKS_ID: + table = TracksColumns.TABLE_NAME; + whereClause = TracksColumns._ID + "=" + url.getPathSegments().get(1); + if (!TextUtils.isEmpty(where)) { + whereClause += " AND (" + where + ")"; + } + break; + case WAYPOINTS: + table = WaypointsColumns.TABLE_NAME; + whereClause = where; + break; + case WAYPOINTS_ID: + table = WaypointsColumns.TABLE_NAME; + whereClause = WaypointsColumns._ID + "=" + url.getPathSegments().get(1); + if (!TextUtils.isEmpty(where)) { + whereClause += " AND (" + where + ")"; + } + break; + default: + throw new IllegalArgumentException("Unknown url " + url); + } int count; - int match = urlMatcher.match(url); - if (match == TRACKPOINTS) { - count = db.update(TrackPointsColumns.TABLE_NAME, values, where, selectionArgs); - } else if (match == TRACKPOINTS_ID) { - String segment = url.getPathSegments().get(1); - count = db.update(TrackPointsColumns.TABLE_NAME, values, "_id=" + segment - + (!TextUtils.isEmpty(where) - ? " AND (" + where + ')' - : ""), - selectionArgs); - } else if (match == TRACKS) { - count = db.update(TracksColumns.TABLE_NAME, values, where, selectionArgs); - } else if (match == TRACKS_ID) { - String segment = url.getPathSegments().get(1); - count = db.update(TracksColumns.TABLE_NAME, values, "_id=" + segment - + (!TextUtils.isEmpty(where) - ? " AND (" + where + ')' - : ""), - selectionArgs); - } else if (match == WAYPOINTS) { - count = db.update(WaypointsColumns.TABLE_NAME, values, where, selectionArgs); - } else if (match == WAYPOINTS_ID) { - String segment = url.getPathSegments().get(1); - count = db.update(WaypointsColumns.TABLE_NAME, values, "_id=" + segment - + (!TextUtils.isEmpty(where) - ? " AND (" + where + ')' - : ""), - selectionArgs); - } else { - throw new IllegalArgumentException("Unknown URL " + url); + try { + db.beginTransaction(); + count = db.update(table, values, whereClause, selectionArgs); + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); } getContext().getContentResolver().notifyChange(url, null, true); return count; } -} + /** + * Returns true if the caller can access the content provider. + */ + private boolean canAccess() { + if (Binder.getCallingPid() == Process.myPid()) { + return true; + } else { + return PreferencesUtils.getBoolean( + getContext(), R.string.allow_access_key, PreferencesUtils.ALLOW_ACCESS_DEFAULT); + } + } + + /** + * Gets the {@link UrlType} for a url. + * + * @param url the url + */ + private UrlType getUrlType(Uri url) { + return UrlType.values()[uriMatcher.match(url)]; + } + + /** + * Inserts a content based on the url type. + * + * @param url the content url + * @param urlType the url type + * @param contentValues the content values + */ + private Uri insertContentValues(Uri url, UrlType urlType, ContentValues contentValues) { + switch (urlType) { + case TRACKPOINTS: + return insertTrackPoint(url, contentValues); + case TRACKS: + return insertTrack(url, contentValues); + case WAYPOINTS: + return insertWaypoint(url, contentValues); + default: + throw new IllegalArgumentException("Unknown url " + url); + } + } + + /** + * Inserts a track point. + * + * @param url the content url + * @param values the content values + */ + private Uri insertTrackPoint(Uri url, ContentValues values) { + boolean hasLatitude = values.containsKey(TrackPointsColumns.LATITUDE); + boolean hasLongitude = values.containsKey(TrackPointsColumns.LONGITUDE); + boolean hasTime = values.containsKey(TrackPointsColumns.TIME); + if (!hasLatitude || !hasLongitude || !hasTime) { + throw new IllegalArgumentException("Latitude, longitude, and time values are required."); + } + long rowId = db.insert(TrackPointsColumns.TABLE_NAME, TrackPointsColumns._ID, values); + if (rowId >= 0) { + Uri uri = ContentUris.appendId(TrackPointsColumns.CONTENT_URI.buildUpon(), rowId).build(); + getContext().getContentResolver().notifyChange(url, null, true); + return uri; + } + throw new SQLiteException("Failed to insert a track point " + url); + } + + /** + * Inserts a track. + * + * @param url the content url + * @param contentValues the content values + */ + private Uri insertTrack(Uri url, ContentValues contentValues) { + boolean hasStartTime = contentValues.containsKey(TracksColumns.STARTTIME); + boolean hasStartId = contentValues.containsKey(TracksColumns.STARTID); + if (!hasStartTime || !hasStartId) { + throw new IllegalArgumentException("Both start time and start id values are required."); + } + long rowId = db.insert(TracksColumns.TABLE_NAME, TracksColumns._ID, contentValues); + if (rowId >= 0) { + Uri uri = ContentUris.appendId(TracksColumns.CONTENT_URI.buildUpon(), rowId).build(); + getContext().getContentResolver().notifyChange(url, null, true); + return uri; + } + throw new SQLException("Failed to insert a track " + url); + } + + /** + * Inserts a waypoint. + * + * @param url the content url + * @param contentValues the content values + */ + private Uri insertWaypoint(Uri url, ContentValues contentValues) { + long rowId = db.insert(WaypointsColumns.TABLE_NAME, WaypointsColumns._ID, contentValues); + if (rowId >= 0) { + Uri uri = ContentUris.appendId(WaypointsColumns.CONTENT_URI.buildUpon(), rowId).build(); + getContext().getContentResolver().notifyChange(url, null, true); + return uri; + } + throw new SQLException("Failed to insert a waypoint " + url); + } +} \ 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 599d8a463..1f99d4257 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/content/TrackDataHub.java +++ b/MyTracks/src/com/google/android/apps/mytracks/content/TrackDataHub.java @@ -13,26 +13,24 @@ * 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.DataSourceManager.DataSourceListener; 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.TrackDataListeners.ListenerRegistration; +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.content.SharedPreferences; import android.database.Cursor; import android.hardware.GeomagneticField; import android.location.Location; @@ -46,1062 +44,785 @@ 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(); - /** Types of data that we can expose. */ - public static enum ListenerDataType { - /** Listen to when the selected track changes. */ - SELECTED_TRACK_CHANGED, + // One hour in milliseconds + private static final int ONE_HOUR = 60 * 60 * 1000; - /** Listen to when the tracks change. */ - TRACK_UPDATES, - - /** Listen to when the waypoints change. */ - WAYPOINT_UPDATES, - - /** Listen to when the current track points change. */ - POINT_UPDATES, - - /** - * Listen to sampled-out points. - * Listening to this without listening to {@link #POINT_UPDATES} - * makes no sense and may yield unexpected results. - */ - SAMPLED_OUT_POINT_UPDATES, - - /** Listen to updates to the current location. */ - LOCATION_UPDATES, - - /** Listen to updates to the current heading. */ - COMPASS_UPDATES, - - /** Listens to changes in display preferences. */ - DISPLAY_PREFERENCES; - } - - /** Listener which receives events from the system. */ - private class HubDataSourceListener implements DataSourceListener { - @Override - public void notifyTrackUpdated() { - TrackDataHub.this.notifyTrackUpdated(getListenersFor(ListenerDataType.TRACK_UPDATES)); - } - - @Override - public void notifyWaypointUpdated() { - TrackDataHub.this.notifyWaypointUpdated(getListenersFor(ListenerDataType.WAYPOINT_UPDATES)); - } - - @Override - public void notifyPointsUpdated() { - TrackDataHub.this.notifyPointsUpdated(true, 0, 0, - getListenersFor(ListenerDataType.POINT_UPDATES), - getListenersFor(ListenerDataType.SAMPLED_OUT_POINT_UPDATES)); - } - - @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(ListenerDataType.LOCATION_UPDATES)); - } - - @Override - public void notifyHeadingChanged(float heading) { - lastSeenMagneticHeading = heading; - maybeUpdateDeclination(); - TrackDataHub.this.notifyHeadingChanged(getListenersFor(ListenerDataType.COMPASS_UPDATES)); - } - } - - // Application services private final Context context; - private final MyTracksProviderUtils providerUtils; - private final SharedPreferences preferences; + private final TrackDataManager trackDataManager; + private final MyTracksProviderUtils myTracksProviderUtils; + private final int targetNumPoints; + private final DoubleBufferedLocationFactory locationFactory; - // 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 TrackDataListeners dataListeners; - - /** Wrapper for interacting with system data managers. */ - private DataSourcesWrapper dataSources; - - /** Manager for system data listener registrations. */ + 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) { - SharedPreferences preferences = context.getSharedPreferences( - Constants.SETTINGS_NAME, Context.MODE_PRIVATE); - MyTracksProviderUtils providerUtils = MyTracksProviderUtils.Factory.get(context); - return new TrackDataHub(context, - new TrackDataListeners(), - preferences, 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, TrackDataListeners listeners, SharedPreferences preferences, - MyTracksProviderUtils providerUtils, int targetNumPoints) { - this.context = ctx; - this.dataListeners = listeners; - this.preferences = preferences; - this.providerUtils = providerUtils; + @VisibleForTesting + TrackDataHub(Context context, TrackDataManager trackDataManager, + MyTracksProviderUtils myTracksProviderUtils, int targetNumPoints) { + this.context = context; + this.trackDataManager = trackDataManager; + 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; + handlerThread = new HandlerThread("TrackDataHubHandlerThread"); + handlerThread.start(); + handler = new Handler(handlerThread.getLooper()); + dataSource = newDataSource(); + dataSourceManager = new DataSourceManager(dataSource, this); - listenerHandlerThread = new HandlerThread("trackDataContentThread"); - listenerHandlerThread.start(); - listenerHandler = new Handler(listenerHandlerThread.getLooper()); - dataSources = newDataSources(); - dataSourceManager = new DataSourceManager(dataSourceListener, dataSources); - - // This may or may not register internal listeners, depending on whether - // we already had external listeners. - dataSourceManager.updateAllListeners(getNeededListenerTypes()); - loadSharedPreferences(); - - // If there were listeners already registered, make sure they become up-to-date. - loadDataForAllListeners(); - } - - // @VisibleForTesting - protected DataSourcesWrapper newDataSources() { - return new DataSourcesWrapperImpl(context, preferences); + 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; - dataSources.close(); - dataSources = null; + dataSourceManager.unregisterAllListeners(); + handlerThread.getLooper().quit(); + + handlerThread = null; + handler = null; + dataSource.close(); + 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 = dataSources.getLastKnownLocation(); - if (loc != null) { - notifyLocationChanged(loc, getListenersFor(ListenerDataType.LOCATION_UPDATES)); - } + 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 (dataListeners) { - ListenerRegistration registration = - dataListeners.registerTrackDataListener(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(registration); - - dataSourceManager.updateAllListeners(getNeededListenerTypes()); - } - } - - public void unregisterTrackDataListener(TrackDataListener listener) { - synchronized (dataListeners) { - dataListeners.unregisterTrackDataListener(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.updateAllListeners(getNeededListenerTypes()); - } - } - - /** - * Reloads all track data received so far into the specified listeners. - */ - public void reloadDataForListener(TrackDataListener listener) { - ListenerRegistration registration; - synchronized (dataListeners) { - registration = dataListeners.getRegistration(listener); - registration.resetState(); - loadNewDataForListener(registration); - } - } - - /** - * Reloads all track data received so far into the specified listeners. - * - * Assumes it's called from a block that synchronizes on {@link #dataListeners}. - */ - private void loadNewDataForListener(final ListenerRegistration registration) { - if (!isStarted()) { - Log.w(TAG, "Not started, not reloading"); - return; - } - if (registration == null) { - Log.w(TAG, "Not reloading for null registration"); - 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 = (dataListeners.getNumListeners() == 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 = registration.lastTrackId != selectedTrackId || - registration.lastSamplingFrequency != lastSamplingFrequency; - Log.d(TAG, "Doing a " + (reloadAll ? "full" : "partial") + " reload for " + registration); + notifyTracksTableUpdate(trackDataManager.getListeners(TrackDataType.TRACKS_TABLE)); + } + }); + } - TrackDataListener listener = registration.listener; - Set listenerSet = Collections.singleton(listener); + @Override + public void notifyWaypointsTableUpdated() { + runInHanderThread(new Runnable() { + @Override + public void run() { + notifyWaypointsTableUpdate(trackDataManager.getListeners(TrackDataType.WAYPOINTS_TABLE)); + } + }); + } - if (registration.isInterestedIn(ListenerDataType.DISPLAY_PREFERENCES)) { - 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 && registration.isInterestedIn(ListenerDataType.SELECTED_TRACK_CHANGED)) { - notifySelectedTrackChanged(selectedTrackId, listenerSet); - } - - if (registration.isInterestedIn(ListenerDataType.TRACK_UPDATES)) { - notifyTrackUpdated(listenerSet); - } - - boolean interestedInPoints = - registration.isInterestedIn(ListenerDataType.POINT_UPDATES); - boolean interestedInSampledOutPoints = - registration.isInterestedIn(ListenerDataType.SAMPLED_OUT_POINT_UPDATES); - 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 = registration.lastPointId + 1; - previousNumPoints = registration.numLoadedPoints; + @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 (registration.isInterestedIn(ListenerDataType.WAYPOINT_UPDATES)) { - notifyWaypointUpdated(listenerSet); - } - - if (registration.isInterestedIn(ListenerDataType.LOCATION_UPDATES)) { - 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 (registration.isInterestedIn(ListenerDataType.COMPASS_UPDATES)) { - 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 (dataListeners) { - if (!dataListeners.hasListeners()) { - 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(ListenerDataType.DISPLAY_PREFERENCES)) { - listener.onUnitsChanged(metricUnits); - listener.onReportSpeedChanged(reportSpeed); - } + hasProviderEnabled = enabled; + notifyLocationStateChanged(trackDataManager.getListeners(TrackDataType.LOCATION)); + } + }); + } - notifySelectedTrackChanged(selectedTrackId, - getListenersFor(ListenerDataType.SELECTED_TRACK_CHANGED)); + @Override + public void notifyLocationProviderAvailable(final boolean available) { + runInHanderThread(new Runnable() { + @Override + public void run() { + hasFix = available; + notifyLocationStateChanged(trackDataManager.getListeners(TrackDataType.LOCATION)); + } + }); + } - notifyTrackUpdated(getListenersFor(ListenerDataType.TRACK_UPDATES)); + @Override + public void notifyLocationChanged(final Location location) { + runInHanderThread(new Runnable() { + @Override + public void run() { + notifyLocationChanged( + location, false, trackDataManager.getListeners(TrackDataType.LOCATION)); + } + }); + } - Set pointListeners = - getListenersFor(ListenerDataType.POINT_UPDATES); - Set sampledOutPointListeners = - getListenersFor(ListenerDataType.SAMPLED_OUT_POINT_UPDATES); - notifyPointsCleared(pointListeners); - notifyPointsUpdated(true, 0, 0, pointListeners, sampledOutPointListeners); - - notifyWaypointUpdated(getListenersFor(ListenerDataType.WAYPOINT_UPDATES)); + @Override + public void notifyHeadingChanged(final float heading) { + runInHanderThread(new Runnable() { + @Override + public void run() { + lastHeading = heading; if (lastSeenLocation != null) { - notifyLocationChanged(lastSeenLocation, true, - getListenersFor(ListenerDataType.LOCATION_UPDATES)); - } 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(ListenerDataType.COMPASS_UPDATES)); + 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(ListenerDataType.DISPLAY_PREFERENCES); + /** + * 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 (dataListeners) { - 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(ListenerDataType.DISPLAY_PREFERENCES); - - for (TrackDataListener listener : displayListeners) { - if (listener.onUnitsChanged(metricUnits)) { - synchronized (dataListeners) { - 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(ListenerDataType.LOCATION_UPDATES)) { - 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 (dataListeners) { - sampledListeners = getListenersFor(ListenerDataType.POINT_UPDATES); - sampledOutListeners = getListenersFor(ListenerDataType.SAMPLED_OUT_POINT_UPDATES); - } - 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.getLastTrackLocationId(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 - ListenerRegistration registration = dataListeners.getRegistration(listener); - if (registration != null) { - registration.lastTrackId = currentSelectedTrackId; - registration.lastPointId = localLastSeenLocationId; - registration.lastSamplingFrequency = pointSamplingFrequency; - registration.numLoadedPoints = 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(ListenerDataType type) { - synchronized (dataListeners) { - return dataListeners.getListenersFor(type); + for (TrackDataListener trackDataListener : trackDataListeners) { + trackDataListener.onLocationChanged(lastSeenLocation); } } - private EnumSet getNeededListenerTypes() { - EnumSet neededTypes = dataListeners.getAllRegisteredTypes(); - - // We always want preference updates. - neededTypes.add(ListenerDataType.DISPLAY_PREFERENCES); - - 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); + } + + /** + * Gets the value selectedTrackId. + * + * @return the selectedTrackId + */ + @VisibleForTesting + long getSelectedTrackId() { + return selectedTrackId; + } + + /** + * Gets the minRequiredAccuracy. + * + * @return the minRequiredAccuracy + */ + @VisibleForTesting + int getMinRequiredAccuracy() { + return minRequiredAccuracy; + } + + /** + * Gets the metricUnits. + * + * @return the metricUnits + */ + @VisibleForTesting + boolean isMetricUnits() { + return metricUnits; + } + + /** + * Gets the reportSpeed. + * + * @return the reportSpeed + */ + @VisibleForTesting + boolean isReportSpeed() { + return reportSpeed; + } + + /** + * Sets the value of lastSeenLocation. + * + * @param lastSeenLocation value of lastSeenLocation + */ + @VisibleForTesting + void setLastSeenLocation(Location lastSeenLocation) { + this.lastSeenLocation = lastSeenLocation; + } +} \ 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/TrackDataListeners.java b/MyTracks/src/com/google/android/apps/mytracks/content/TrackDataListeners.java deleted file mode 100644 index 7a85b9eef..000000000 --- a/MyTracks/src/com/google/android/apps/mytracks/content/TrackDataListeners.java +++ /dev/null @@ -1,159 +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 static com.google.android.apps.mytracks.Constants.TAG; - -import com.google.android.apps.mytracks.content.TrackDataHub.ListenerDataType; - -import android.util.Log; - -import java.util.EnumMap; -import java.util.EnumSet; -import java.util.HashMap; -import java.util.LinkedHashSet; -import java.util.Map; -import java.util.Set; - -/** - * Manager for the external data listeners and their listening types. - * - * @author Rodrigo Damazio - */ -class TrackDataListeners { - - /** Internal representation of a listener's registration. */ - static class ListenerRegistration { - final TrackDataListener listener; - final EnumSet types; - - // State that was last notified to the listener, for resuming after a pause. - long lastTrackId; - long lastPointId; - int lastSamplingFrequency; - int numLoadedPoints; - - public ListenerRegistration(TrackDataListener listener, - EnumSet types) { - this.listener = listener; - this.types = types; - } - - public boolean isInterestedIn(ListenerDataType type) { - return types.contains(type); - } - - public void resetState() { - lastTrackId = 0L; - lastPointId = 0L; - lastSamplingFrequency = 0; - numLoadedPoints = 0; - } - - @Override - public String toString() { - return "ListenerRegistration [listener=" + listener + ", types=" + types - + ", lastTrackId=" + lastTrackId + ", lastPointId=" + lastPointId - + ", lastSamplingFrequency=" + lastSamplingFrequency - + ", numLoadedPoints=" + numLoadedPoints + "]"; - } - } - - /** Map of external listener to its registration details. */ - private final Map registeredListeners = - new HashMap(); - - /** Map of data type to external listeners interested in it. */ - private final Map> listenerSetsPerType = - new EnumMap>(ListenerDataType.class); - - public TrackDataListeners() { - // Create sets for all data types at startup. - for (ListenerDataType type : ListenerDataType.values()) { - listenerSetsPerType.put(type, new LinkedHashSet()); - } - } - - /** - * Registers a listener to send data to. - * It is ok to call this method before {@link TrackDataHub#start}, and in that case - * the data will only be passed to listeners when {@link TrackDataHub#start} is called. - * - * @param listener the listener to register - * @param dataTypes the type of data that the listener is interested in - */ - public ListenerRegistration registerTrackDataListener(final TrackDataListener listener, EnumSet dataTypes) { - Log.d(TAG, "Registered track data listener: " + listener); - if (registeredListeners.containsKey(listener)) { - throw new IllegalStateException("Listener already registered"); - } - - ListenerRegistration registration = new ListenerRegistration(listener, dataTypes); - registeredListeners.put(listener, registration); - - for (ListenerDataType type : dataTypes) { - // This is guaranteed not to be null. - Set typeSet = listenerSetsPerType.get(type); - typeSet.add(listener); - } - - return registration; - } - - /** - * Unregisters a listener to send data to. - * - * @param listener the listener to unregister - */ - public void unregisterTrackDataListener(TrackDataListener listener) { - Log.d(TAG, "Unregistered track data listener: " + listener); - // Remove and keep the corresponding registration. - ListenerRegistration match = registeredListeners.remove(listener); - if (match == null) { - Log.w(TAG, "Tried to unregister listener which is not registered."); - return; - } - - // Remove it from the per-type sets - for (ListenerDataType type : match.types) { - listenerSetsPerType.get(type).remove(listener); - } - } - - public ListenerRegistration getRegistration(TrackDataListener listener) { - return registeredListeners.get(listener); - } - - public Set getListenersFor(ListenerDataType type) { - return listenerSetsPerType.get(type); - } - - public EnumSet getAllRegisteredTypes() { - EnumSet listeners = EnumSet.noneOf(ListenerDataType.class); - for (ListenerRegistration registration : this.registeredListeners.values()) { - listeners.addAll(registration.types); - } - return listeners; - } - - public boolean hasListeners() { - return !registeredListeners.isEmpty(); - } - - public int getNumListeners() { - return registeredListeners.size(); - } -} diff --git a/MyTracks/src/com/google/android/apps/mytracks/content/TrackDataManager.java b/MyTracks/src/com/google/android/apps/mytracks/content/TrackDataManager.java new file mode 100644 index 000000000..e816d2408 --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/content/TrackDataManager.java @@ -0,0 +1,124 @@ +/* + * 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 static com.google.android.apps.mytracks.Constants.TAG; + +import android.util.Log; + +import java.util.EnumMap; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +/** + * Manages register/unregister {@link TrackDataListener} and keeping the state + * for each registered listener. + * + * @author Rodrigo Damazio + */ +public class TrackDataManager { + + // Map of listener to its track data types + private final Map> + listenerToTypesMap = new HashMap>(); + + // Map of track data type to listeners + private final Map> + typeToListenersMap = new EnumMap>(TrackDataType.class); + + public TrackDataManager() { + for (TrackDataType trackDataType : TrackDataType.values()) { + typeToListenersMap.put(trackDataType, new LinkedHashSet()); + } + } + + /** + * Registers a listener. + * + * @param listener the listener + * @param trackDataTypes the track data types the listener is interested + */ + public void registerListener( + TrackDataListener listener, EnumSet trackDataTypes) { + if (listenerToTypesMap.containsKey(listener)) { + throw new IllegalStateException("Listener is already registered"); + } + listenerToTypesMap.put(listener, trackDataTypes); + for (TrackDataType trackDataType : trackDataTypes) { + typeToListenersMap.get(trackDataType).add(listener); + } + } + + /** + * Unregisters a listener. + * + * @param listener the listener + */ + public void unregisterListener(TrackDataListener listener) { + 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 : removedTypes) { + typeToListenersMap.get(trackDataType).remove(listener); + } + } + + /** + * Gets the number of {@link TrackDataListener}. + */ + public int getNumberOfListeners() { + return listenerToTypesMap.size(); + } + + /** + * Gets the track data types for a listener. + * + * @param listener the listener + */ + public EnumSet getTrackDataTypes(TrackDataListener listener) { + return listenerToTypesMap.get(listener); + } + + /** + * Gets the listeners for a {@link TrackDataType}. + * + * @param type the type + */ + public Set getListeners(TrackDataType type) { + return typeToListenersMap.get(type); + } + + /** + * Gets all the registered {@link TrackDataType}. + */ + public EnumSet getRegisteredTrackDataTypes() { + EnumSet types = EnumSet.noneOf(TrackDataType.class); + 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 new file mode 100644 index 000000000..87a5f9549 --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/content/TrackDataType.java @@ -0,0 +1,33 @@ +/* + * 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; + +/** + * Types of track data. + * + * @author Jimmy Shih + */ +public enum TrackDataType { + SELECTED_TRACK, // selected track changes + TRACKS_TABLE, // tracks table changes + WAYPOINTS_TABLE, // waypoints table 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 + 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 be9c1ff28..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.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.TrackDataHub.ListenerDataType; 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,14 +358,14 @@ 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( - ListenerDataType.SELECTED_TRACK_CHANGED, - ListenerDataType.TRACK_UPDATES, - ListenerDataType.WAYPOINT_UPDATES, - ListenerDataType.POINT_UPDATES, - ListenerDataType.SAMPLED_OUT_POINT_UPDATES, - ListenerDataType.DISPLAY_PREFERENCES)); + TrackDataType.SELECTED_TRACK, + TrackDataType.TRACKS_TABLE, + TrackDataType.WAYPOINTS_TABLE, + 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 b06f4bf2f..ee3acd5fe 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.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.TrackDataHub.ListenerDataType; 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; @@ -275,7 +274,7 @@ public class MapFragment extends Fragment } @Override - public void onProviderStateChange(ProviderState state) { + public void onLocationStateChanged(LocationState state) { final String message; final boolean isGpsDisabled; switch (state) { @@ -322,13 +321,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(); } @@ -369,7 +368,7 @@ public class MapFragment extends Fragment } @Override - public void onNewTrackPoint(Location location) { + public void onSampledInTrackPoint(Location location) { if (LocationUtils.isValidLocation(location)) { mapOverlay.addLocation(location); } @@ -409,7 +408,7 @@ public class MapFragment extends Fragment } @Override - public boolean onUnitsChanged(boolean metric) { + public boolean onMetricUnitsChanged(boolean metric) { // We don't care. return false; } @@ -425,13 +424,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( - ListenerDataType.SELECTED_TRACK_CHANGED, - ListenerDataType.WAYPOINT_UPDATES, - ListenerDataType.POINT_UPDATES, - ListenerDataType.LOCATION_UPDATES, - ListenerDataType.COMPASS_UPDATES)); + TrackDataType.SELECTED_TRACK, + TrackDataType.WAYPOINTS_TABLE, + TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE, + TrackDataType.LOCATION, + 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 8a00c3c2e..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.TrackDetailActivity; import com.google.android.apps.mytracks.content.Track; import com.google.android.apps.mytracks.content.TrackDataHub; -import com.google.android.apps.mytracks.content.TrackDataHub.ListenerDataType; 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,12 +255,12 @@ 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( - ListenerDataType.SELECTED_TRACK_CHANGED, - ListenerDataType.TRACK_UPDATES, - ListenerDataType.LOCATION_UPDATES, - ListenerDataType.DISPLAY_PREFERENCES)); + TrackDataType.SELECTED_TRACK, + TrackDataType.TRACKS_TABLE, + TrackDataType.LOCATION, + TrackDataType.PREFERENCE)); } /** @@ -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/io/docs/SendDocsUtils.java b/MyTracks/src/com/google/android/apps/mytracks/io/docs/SendDocsUtils.java index 906798471..af9af24b7 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/docs/SendDocsUtils.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/docs/SendDocsUtils.java @@ -56,26 +56,31 @@ import java.util.Locale; * @author Matthew Simmons */ public class SendDocsUtils { - private static final String GET_SPREADSHEET_BY_TITLE_URI = + @VisibleForTesting + public static final String GET_SPREADSHEET_BY_TITLE_URI = "https://docs.google.com/feeds/documents/private/full?" + "category=mine,spreadsheet&title=%s&title-exact=true"; private static final String CREATE_SPREADSHEET_URI = "https://docs.google.com/feeds/documents/private/full"; private static final String GET_WORKSHEETS_URI = "https://spreadsheets.google.com/feeds/worksheets/%s/private/full"; - private static final String GET_WORKSHEET_URI = + @VisibleForTesting + public static final String GET_WORKSHEET_URI = "https://spreadsheets.google.com/feeds/list/%s/%s/private/full"; - - private static final String SPREADSHEET_ID_PREFIX = + @VisibleForTesting + public static final String SPREADSHEET_ID_PREFIX = "https://docs.google.com/feeds/documents/private/full/spreadsheet%3A"; - - private static final String CONTENT_TYPE = "Content-Type"; - private static final String ATOM_FEED_MIME_TYPE = "application/atom+xml"; + @VisibleForTesting + public static final String CONTENT_TYPE = "Content-Type"; + @VisibleForTesting + public static final String ATOM_FEED_MIME_TYPE = "application/atom+xml"; private static final String OPENDOCUMENT_SPREADSHEET_MIME_TYPE = "application/x-vnd.oasis.opendocument.spreadsheet"; - private static final String AUTHORIZATION = "Authorization"; - private static final String AUTHORIZATION_PREFIX = "GoogleLogin auth="; + @VisibleForTesting + public static final String AUTHORIZATION = "Authorization"; + @VisibleForTesting + public static final String AUTHORIZATION_PREFIX = "GoogleLogin auth="; private static final String SLUG = "Slug"; diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/file/GpxImporter.java b/MyTracks/src/com/google/android/apps/mytracks/io/file/GpxImporter.java index 06f155df2..2bfa6c1a2 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/file/GpxImporter.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/file/GpxImporter.java @@ -306,7 +306,7 @@ public class GpxImporter extends DefaultHandler { tripStatisticsBuilder = new TripStatisticsBuilder(0); tripStatisticsBuilder.pauseAt(0); } - track.setTripStatistics(tripStatisticsBuilder.getStatistics()); + track.setTripStatistics(tripStatisticsBuilder.getTripStatistics()); track.setNumberOfPoints(numberOfLocations); myTracksProviderUtils.updateTrack(track); tracksIds.add(track.getId()); @@ -527,7 +527,7 @@ public class GpxImporter extends DefaultHandler { */ private long getLastPointId() { flushPoints(); - return myTracksProviderUtils.getLastLocationId(track.getId()); + return myTracksProviderUtils.getLastTrackLocationId(track.getId()); } /** diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/file/TcxTrackWriter.java b/MyTracks/src/com/google/android/apps/mytracks/io/file/TcxTrackWriter.java index e2c12a9de..eeaf457a6 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/file/TcxTrackWriter.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/file/TcxTrackWriter.java @@ -169,6 +169,7 @@ public class TcxTrackWriter implements TrackFormatWriter { public void writeEndTrack(Location lastPoint) { if (printWriter != null) { printWriter.println(""); + printWriter.println("" + StringUtils.formatCData(track.getDescription()) + ""); printWriter.println(""); printWriter.println("" + StringUtils.formatCData(context.getString(R.string.send_google_by_my_tracks, "", "")) diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/fusiontables/SendFusionTablesAsyncTask.java b/MyTracks/src/com/google/android/apps/mytracks/io/fusiontables/SendFusionTablesAsyncTask.java index ef128be09..31370e64a 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/fusiontables/SendFusionTablesAsyncTask.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/fusiontables/SendFusionTablesAsyncTask.java @@ -27,6 +27,7 @@ import com.google.api.client.http.HttpResponse; import com.google.api.client.http.HttpTransport; import com.google.api.client.http.InputStreamContent; import com.google.api.client.util.Strings; +import com.google.common.annotations.VisibleForTesting; import android.accounts.Account; import android.accounts.AccountManager; @@ -52,13 +53,17 @@ import java.util.Vector; */ public class SendFusionTablesAsyncTask extends AbstractSendAsyncTask { - private static final String APP_NAME_PREFIX = "Google-MyTracks-"; + @VisibleForTesting + public static final String APP_NAME_PREFIX = "Google-MyTracks-"; private static final String SQL_KEY = "sql="; - private static final String CONTENT_TYPE = "application/x-www-form-urlencoded"; - private static final String FUSION_TABLES_BASE_URL = + @VisibleForTesting + public static final String CONTENT_TYPE = "application/x-www-form-urlencoded"; + @VisibleForTesting + public static final String FUSION_TABLES_BASE_URL = "https://www.google.com/fusiontables/api/query"; private static final int MAX_POINTS_PER_UPLOAD = 2048; - private static final String GDATA_VERSION = "2"; + @VisibleForTesting + public static final String GDATA_VERSION = "2"; private static final int PROGRESS_CREATE_TABLE = 0; private static final int PROGRESS_UNLIST_TABLE = 5; @@ -248,7 +253,7 @@ public class SendFusionTablesAsyncTask extends AbstractSendAsyncTask { elevationBuffer.setNext(metricUnits ? location.getAltitude() : location.getAltitude() * UnitConversions.M_TO_FT); if (i % elevationSamplingFrequency == 0) { - distances.add(tripStatisticsBuilder.getStatistics().getTotalDistance()); + distances.add(tripStatisticsBuilder.getTripStatistics().getTotalDistance()); elevations.add(elevationBuffer.getAverage()); } lastLocation = location; @@ -274,7 +279,7 @@ public class SendFusionTablesAsyncTask extends AbstractSendAsyncTask { // Create an end marker if (lastLocation != null) { - distances.add(tripStatisticsBuilder.getStatistics().getTotalDistance()); + distances.add(tripStatisticsBuilder.getTripStatistics().getTotalDistance()); elevations.add(elevationBuffer.getAverage()); DescriptionGenerator descriptionGenerator = new DescriptionGeneratorImpl(context); track.setDescription( diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/maps/SendMapsAsyncTask.java b/MyTracks/src/com/google/android/apps/mytracks/io/maps/SendMapsAsyncTask.java index 660e0ead4..e46720b8c 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/maps/SendMapsAsyncTask.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/maps/SendMapsAsyncTask.java @@ -297,7 +297,7 @@ public class SendMapsAsyncTask extends AbstractSendAsyncTask { elevationBuffer.setNext(metricUnits ? location.getAltitude() : location.getAltitude() * UnitConversions.M_TO_FT); if (i % elevationSamplingFrequency == 0) { - distances.add(tripStatisticsBuilder.getStatistics().getTotalDistance()); + distances.add(tripStatisticsBuilder.getTripStatistics().getTotalDistance()); elevations.add(elevationBuffer.getAverage()); } lastLocation = location; @@ -323,7 +323,7 @@ public class SendMapsAsyncTask extends AbstractSendAsyncTask { // Create an end marker if (lastLocation != null) { - distances.add(tripStatisticsBuilder.getStatistics().getTotalDistance()); + distances.add(tripStatisticsBuilder.getTripStatistics().getTotalDistance()); elevations.add(elevationBuffer.getAverage()); track.setDescription(getTrackDescription(track, distances, elevations)); diff --git a/MyTracks/src/com/google/android/apps/mytracks/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/MyTracks/src/com/google/android/apps/mytracks/services/AbsoluteLocationListenerPolicy.java b/MyTracks/src/com/google/android/apps/mytracks/services/AbsoluteLocationListenerPolicy.java index 2ae2aa899..f9240baca 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/services/AbsoluteLocationListenerPolicy.java +++ b/MyTracks/src/com/google/android/apps/mytracks/services/AbsoluteLocationListenerPolicy.java @@ -19,41 +19,34 @@ package com.google.android.apps.mytracks.services; /** * This is a simple location listener policy that will always dictate the same * polling interval. - * + * * @author Sandor Dornbush */ public class AbsoluteLocationListenerPolicy implements LocationListenerPolicy { - /** - * The interval to request for gps signal. - */ private final long interval; /** - * @param interval The interval to request for gps signal + * Constructor. + * + * @param interval the interval to request for gps signal */ - public AbsoluteLocationListenerPolicy(final long interval) { + public AbsoluteLocationListenerPolicy(long interval) { this.interval = interval; } - /** - * @return The interval given in the constructor - */ + @Override public long getDesiredPollingInterval() { return interval; } - /** - * Discards the idle time. - */ - public void updateIdleTime(long idleTime) { - } - - /** - * Returns the minimum distance between updates. - * Get all updates to properly measure moving time. - */ + @Override public int getMinDistance() { return 0; } + + @Override + public void updateIdleTime(long idleTime) { + // Ignore + } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/AdaptiveLocationListenerPolicy.java b/MyTracks/src/com/google/android/apps/mytracks/services/AdaptiveLocationListenerPolicy.java index 7d630e995..64ba60ddc 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/services/AdaptiveLocationListenerPolicy.java +++ b/MyTracks/src/com/google/android/apps/mytracks/services/AdaptiveLocationListenerPolicy.java @@ -16,67 +16,58 @@ package com.google.android.apps.mytracks.services; - /** - * A LocationListenerPolicy that will change based on how long the user has been - * stationary. - * - * This policy will dictate a policy based on a min, max and idle time. - * The policy will dictate an interval bounded by min and max whic is half of - * the idle time. - * + * A {@link LocationListenerPolicy} that will change based on how long the user + * has been stationary. This policy will dictate a policy based on a min, max + * and idle time. The policy will dictate an interval bounded by min and max, + * and is half of the idle time. + * * @author Sandor Dornbush */ public class AdaptiveLocationListenerPolicy implements LocationListenerPolicy { - /** - * Smallest interval this policy will dictate, in milliseconds. - */ private final long minInterval; - - /** - * Largest interval this policy will dictate, in milliseconds. - */ private final long maxInterval; - private final int minDistance; - - /** - * The time the user has been at the current location, in milliseconds. - */ + + // The time the user has been idle at the current location, in milliseconds. private long idleTime; /** - * Creates a policy that will be bounded by the given min and max. - * - * @param min Smallest interval this policy will dictate, in milliseconds - * @param max Largest interval this policy will dictate, in milliseconds + * Creates a policy that will be bounded by the given minInterval and + * maxInterval. + * + * @param minInterval the smallest interval this policy will dictate, in + * milliseconds + * @param maxInterval the largest interval this policy will dictate, in + * milliseconds + * @param minDistance the minimum distance in meters */ - public AdaptiveLocationListenerPolicy(long min, long max, int minDistance) { - this.minInterval = min; - this.maxInterval = max; + public AdaptiveLocationListenerPolicy(long minInterval, long maxInterval, int minDistance) { + this.minInterval = minInterval; + this.maxInterval = maxInterval; this.minDistance = minDistance; } - /** - * @return An interval bounded by min and max which is half of the idle time + /* + * Returns an interval half of the idle time, but bounded by minInteval and + * maxInterval. */ + @Override public long getDesiredPollingInterval() { long desiredInterval = idleTime / 2; - // Round to avoid setting the interval too often. + // Round to second to avoid setting the interval too often desiredInterval = (desiredInterval / 1000) * 1000; - return Math.max(Math.min(maxInterval, desiredInterval), - minInterval); + return Math.max(Math.min(maxInterval, desiredInterval), minInterval); } - public void updateIdleTime(long newIdleTime) { - this.idleTime = newIdleTime; - } - - /** - * Returns the minimum distance between updates. - */ + @Override public int getMinDistance() { return minDistance; } + + @Override + public void updateIdleTime(long newIdleTime) { + idleTime = newIdleTime; + } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/LocationListenerPolicy.java b/MyTracks/src/com/google/android/apps/mytracks/services/LocationListenerPolicy.java index 1fab6102f..18250f3de 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/services/LocationListenerPolicy.java +++ b/MyTracks/src/com/google/android/apps/mytracks/services/LocationListenerPolicy.java @@ -17,19 +17,18 @@ package com.google.android.apps.mytracks.services; /** - * This is an interface for classes that will manage the location listener policy. - * Different policy options are: - * Absolute - * Addaptive - * + * This is an interface for classes that will manage the location listener + * policy. Different policy options are: {@link AbsoluteLocationListenerPolicy} + * and {@link AdaptiveLocationListenerPolicy}. + * * @author Sandor Dornbush */ public interface LocationListenerPolicy { /** - * Returns the polling time this policy would like at this time. - * - * @return The polling that this policy dictates + * Returns the polling interval this policy would like at this moment. + * + * @return the polling interval */ public long getDesiredPollingInterval(); @@ -39,10 +38,10 @@ public interface LocationListenerPolicy { public int getMinDistance(); /** - * Notifies the amount of time the user has been idle at their current - * location. - * - * @param idleTime The time that the user has been idle at this spot + * Notifies the amount of time the user has been idle at his current location. + * + * @param idleTime the time that the user has been idle at his current + * location */ public void updateIdleTime(long idleTime); } diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/PreferenceManager.java b/MyTracks/src/com/google/android/apps/mytracks/services/PreferenceManager.java deleted file mode 100644 index 1e92b11d8..000000000 --- a/MyTracks/src/com/google/android/apps/mytracks/services/PreferenceManager.java +++ /dev/null @@ -1,139 +0,0 @@ -/* - * Copyright 2010 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ -package com.google.android.apps.mytracks.services; - -import com.google.android.apps.mytracks.Constants; -import com.google.android.apps.mytracks.util.PreferencesUtils; -import com.google.android.maps.mytracks.R; - -import android.content.Context; -import android.content.SharedPreferences; -import android.content.SharedPreferences.OnSharedPreferenceChangeListener; -import android.util.Log; - -/** - * A class that manages reading the shared preferences for the service. - * - * @author Sandor Dornbush - */ -public class PreferenceManager implements OnSharedPreferenceChangeListener { - private TrackRecordingService service; - private SharedPreferences sharedPreferences; - - public PreferenceManager(TrackRecordingService service) { - this.service = service; - this.sharedPreferences = service.getSharedPreferences( - Constants.SETTINGS_NAME, Context.MODE_PRIVATE); - sharedPreferences.registerOnSharedPreferenceChangeListener(this); - - // Refresh all properties. - onSharedPreferenceChanged(sharedPreferences, null); - } - - /** - * Notifies that preferences have changed. Call this with key == null to - * update all preferences in one call. - * - * @param key the key that changed (may be null to update all preferences) - */ - @Override - public void onSharedPreferenceChanged(SharedPreferences preferences, String key) { - if (service == null) { - Log.w(Constants.TAG, "onSharedPreferenceChanged: a preference change (key = " + key - + ") after a call to shutdown()"); - return; - } - if (key == null - || PreferencesUtils.getKey(service, R.string.recording_track_id_key).equals(key)) { - long recordingTrackId = PreferencesUtils.getLong(service, R.string.recording_track_id_key); - /* - * Only set the id if it is valid. Setting it to -1L should only happen - * in TrackRecordingService.endCurrentTrack() - */ - if (recordingTrackId != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT) { - service.setRecordingTrackId(recordingTrackId); - } - } - if (key == null || PreferencesUtils.getKey(service, R.string.metric_units_key).equals(key)) { - service.setMetricUnits(PreferencesUtils.getBoolean( - service, R.string.metric_units_key, PreferencesUtils.METRIC_UNITS_DEFAULT)); - } - if (key == null - || PreferencesUtils.getKey(service, R.string.announcement_frequency_key).equals(key)) { - service.setAnnouncementFrequency(PreferencesUtils.getInt( - service, R.string.announcement_frequency_key, - PreferencesUtils.ANNOUNCEMENT_FREQUENCY_DEFAULT)); - } - if (key == null || PreferencesUtils.getKey(service, R.string.split_frequency_key).equals(key)) { - service.setSplitFrequency(PreferencesUtils.getInt( - service, R.string.split_frequency_key, PreferencesUtils.SPLIT_FREQUENCY_DEFAULT)); - } - if (key == null - || PreferencesUtils.getKey(service, R.string.min_recording_interval_key).equals(key)) { - int minRecordingInterval = PreferencesUtils.getInt(service, - R.string.min_recording_interval_key, PreferencesUtils.MIN_RECORDING_INTERVAL_DEFAULT); - switch (minRecordingInterval) { - case PreferencesUtils.MIN_RECORDING_INTERVAL_ADAPT_BATTERY_LIFE: - /* - * Choose battery life over moving time accuracy. min: 30 seconds, - * max: 5 minutes, inDist: 5 meters. - */ - service.setLocationListenerPolicy(new AdaptiveLocationListenerPolicy(30000, 300000, 5)); - break; - case PreferencesUtils.MIN_RECORDING_INTERVAL_ADAPT_ACCURACY: - /* - * Get all the updates. min: 1 second, max: 30 seconds, minDist: 0 - * meter. - */ - service.setLocationListenerPolicy(new AdaptiveLocationListenerPolicy(1000, 30000, 0)); - break; - default: - service.setLocationListenerPolicy( - new AbsoluteLocationListenerPolicy(minRecordingInterval * 1000)); - } - } - if (key == null - || PreferencesUtils.getKey(service, R.string.min_recording_distance_key).equals(key)) { - service.setMinRecordingDistance(PreferencesUtils.getInt( - service, R.string.min_recording_distance_key, - PreferencesUtils.MIN_RECORDING_DISTANCE_DEFAULT)); - } - if (key == null - || PreferencesUtils.getKey(service, R.string.max_recording_distance_key).equals(key)) { - service.setMaxRecordingDistance(PreferencesUtils.getInt( - service, R.string.max_recording_distance_key, - PreferencesUtils.MAX_RECORDING_DISTANCE_DEFAULT)); - } - - if (key == null - || PreferencesUtils.getKey(service, R.string.min_required_accuracy_key).equals(key)) { - service.setMinRequiredAccuracy(PreferencesUtils.getInt( - service, R.string.min_required_accuracy_key, - PreferencesUtils.MIN_REQUIRED_ACCURACY_DEFAULT)); - } - if (key == null - || PreferencesUtils.getKey(service, R.string.auto_resume_track_timeout_key).equals(key)) { - service.setAutoResumeTrackTimeout(PreferencesUtils.getInt( - service, R.string.auto_resume_track_timeout_key, - PreferencesUtils.AUTO_RESUME_TRACK_TIMEOUT_DEFAULT)); - } - } - - public void shutdown() { - sharedPreferences.unregisterOnSharedPreferenceChangeListener(this); - service = null; - } -} diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/TrackRecordingService.java b/MyTracks/src/com/google/android/apps/mytracks/services/TrackRecordingService.java index d53be4403..75c9765de 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/services/TrackRecordingService.java +++ b/MyTracks/src/com/google/android/apps/mytracks/services/TrackRecordingService.java @@ -13,30 +13,28 @@ * License for the specific language governing permissions and limitations under * the License. */ + package com.google.android.apps.mytracks.services; import static com.google.android.apps.mytracks.Constants.RESUME_TRACK_EXTRA_NAME; -import static com.google.android.apps.mytracks.Constants.TAG; import com.google.android.apps.mytracks.Constants; import com.google.android.apps.mytracks.TrackDetailActivity; -import com.google.android.apps.mytracks.content.DescriptionGenerator; import com.google.android.apps.mytracks.content.DescriptionGeneratorImpl; import com.google.android.apps.mytracks.content.MyTracksLocation; +import com.google.android.apps.mytracks.content.MyTracksProvider; import com.google.android.apps.mytracks.content.MyTracksProviderUtils; import com.google.android.apps.mytracks.content.Sensor; import com.google.android.apps.mytracks.content.Sensor.SensorDataSet; import com.google.android.apps.mytracks.content.Track; -import com.google.android.apps.mytracks.content.TracksColumns; import com.google.android.apps.mytracks.content.Waypoint; import com.google.android.apps.mytracks.content.WaypointCreationRequest; import com.google.android.apps.mytracks.content.WaypointCreationRequest.WaypointType; -import com.google.android.apps.mytracks.content.WaypointsColumns; import com.google.android.apps.mytracks.services.sensors.SensorManager; import com.google.android.apps.mytracks.services.sensors.SensorManagerFactory; +import com.google.android.apps.mytracks.services.tasks.AnnouncementPeriodicTaskFactory; import com.google.android.apps.mytracks.services.tasks.PeriodicTaskExecutor; -import com.google.android.apps.mytracks.services.tasks.SplitTask; -import com.google.android.apps.mytracks.services.tasks.StatusAnnouncerFactory; +import com.google.android.apps.mytracks.services.tasks.SplitPeriodicTaskFactory; import com.google.android.apps.mytracks.stats.TripStatistics; import com.google.android.apps.mytracks.stats.TripStatisticsBuilder; import com.google.android.apps.mytracks.util.IntentUtils; @@ -49,9 +47,10 @@ import com.google.common.annotations.VisibleForTesting; import android.app.Notification; import android.app.PendingIntent; import android.app.Service; -import android.content.ContentValues; import android.content.Context; import android.content.Intent; +import android.content.SharedPreferences; +import android.content.SharedPreferences.OnSharedPreferenceChangeListener; import android.database.Cursor; import android.database.sqlite.SQLiteException; import android.location.Location; @@ -69,7 +68,6 @@ import android.support.v4.app.NotificationCompat; import android.support.v4.app.TaskStackBuilder; import android.util.Log; -import java.util.Locale; import java.util.Timer; import java.util.TimerTask; import java.util.concurrent.ExecutorService; @@ -77,90 +75,175 @@ import java.util.concurrent.Executors; /** * A background service that registers a location listener and records track - * points. Track points are saved to the MyTracksProvider. - * + * points. Track points are saved to the {@link MyTracksProvider}. + * * @author Leif Hendrik Wilden */ public class TrackRecordingService extends Service { + private static final String TAG = TrackRecordingService.class.getSimpleName(); + // One second in milliseconds + private static final long ONE_SECOND = 1000; + // One minute in milliseconds + private static final long ONE_MINUTE = 60 * ONE_SECOND; + @VisibleForTesting static final int MAX_AUTO_RESUME_TRACK_RETRY_ATTEMPTS = 3; + // The following variables are set in onCreate: + private Context context; + private MyTracksProviderUtils myTracksProviderUtils; private MyTracksLocationManager myTracksLocationManager; - private WakeLock wakeLock; + private PeriodicTaskExecutor announcementExecutor; + private PeriodicTaskExecutor splitExecutor; + private ExecutorService executorService; + private SharedPreferences sharedPreferences; + private long recordingTrackId; + private LocationListenerPolicy locationListenerPolicy; + private int minRecordingDistance; + private int maxRecordingDistance; + private int minRequiredAccuracy; + private int autoResumeTrackTimeout; + private long currentRecordingInterval; + private Track recordingTrack; - private int minRecordingDistance = PreferencesUtils.MIN_RECORDING_DISTANCE_DEFAULT; - private int maxRecordingDistance = PreferencesUtils.MAX_RECORDING_DISTANCE_DEFAULT; - private int minRequiredAccuracy = PreferencesUtils.MIN_REQUIRED_ACCURACY_DEFAULT; - private int autoResumeTrackTimeout = PreferencesUtils.AUTO_RESUME_TRACK_TIMEOUT_DEFAULT; + // The following variables are set when recording: - private long recordingTrackId = -1; - - private long currentWaypointId = -1; - - /** The timer posts a runnable to the main thread via this handler. */ - private final Handler handler = new Handler(); - - /** - * Utilities to deal with the database. - */ - private MyTracksProviderUtils providerUtils; - - private TripStatisticsBuilder statsBuilder; - private TripStatisticsBuilder waypointStatsBuilder; - - /** - * Current length of the recorded track. This length is calculated from the - * recorded points (as compared to each location fix). It's used to overlay - * waypoints precisely in the elevation profile chart. + /* + * Track length. Calculated from the recorded points to overlay waypoints + * precisely on the elevation chart. */ private double length; - /** - * Status announcer executor. - */ - private PeriodicTaskExecutor announcementExecutor; - private PeriodicTaskExecutor splitExecutor; + // Used for length calculation + private Location lastLengthLocation; + private TripStatisticsBuilder trackTripStatisticsBuilder; + private TripStatisticsBuilder markerTripStatisticsBuilder; + private WakeLock wakeLock; + private Location lastLocation; + private boolean isMoving; private SensorManager sensorManager; - private PreferenceManager prefereceManager; + // Timer to periodically invoke checkLocationListener + private final Timer timer = new Timer(); - /** - * The interval in milliseconds that we have requested to be notified of gps - * readings. - */ - private long currentRecordingInterval; + // Handler for the timer to post a runnable to the main thread + private final Handler handler = new Handler(); - /** - * The policy used to decide how often we should request gps updates. + private ServiceBinder binder = new ServiceBinder(this); + + /* + * Note that sharedPreferenceChangeListener cannot be an anonymous inner + * class. Anonymous inner class will get garbage collected. */ - private LocationListenerPolicy locationListenerPolicy = - new AbsoluteLocationListenerPolicy(0); + private final OnSharedPreferenceChangeListener + sharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() { + @Override + public void onSharedPreferenceChanged(SharedPreferences preferences, String key) { + if (key == null + || PreferencesUtils.getKey(context, R.string.recording_track_id_key).equals(key)) { + long id = PreferencesUtils.getLong(context, R.string.recording_track_id_key); + /* + * Only the TrackRecordingService can stop a recording and set the + * recordingTrackId to -1L. + */ + if (id != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT) { + recordingTrackId = id; + } + } + if (key == null + || PreferencesUtils.getKey(context, R.string.metric_units_key).equals(key)) { + boolean metricUnits = PreferencesUtils.getBoolean( + context, R.string.metric_units_key, PreferencesUtils.METRIC_UNITS_DEFAULT); + announcementExecutor.setMetricUnits(metricUnits); + splitExecutor.setMetricUnits(metricUnits); + } + if (key == null || PreferencesUtils.getKey(context, R.string.announcement_frequency_key) + .equals(key)) { + announcementExecutor.setTaskFrequency(PreferencesUtils.getInt( + context, R.string.announcement_frequency_key, + PreferencesUtils.ANNOUNCEMENT_FREQUENCY_DEFAULT)); + } + if (key == null + || PreferencesUtils.getKey(context, R.string.split_frequency_key).equals(key)) { + splitExecutor.setTaskFrequency(PreferencesUtils.getInt( + context, R.string.split_frequency_key, PreferencesUtils.SPLIT_FREQUENCY_DEFAULT)); + } + if (key == null || PreferencesUtils.getKey(context, R.string.min_recording_interval_key) + .equals(key)) { + int minRecordingInterval = PreferencesUtils.getInt(context, + R.string.min_recording_interval_key, + PreferencesUtils.MIN_RECORDING_INTERVAL_DEFAULT); + switch (minRecordingInterval) { + case PreferencesUtils.MIN_RECORDING_INTERVAL_ADAPT_BATTERY_LIFE: + // Choose battery life over moving time accuracy. + locationListenerPolicy = new AdaptiveLocationListenerPolicy( + 30 * ONE_SECOND, 5 * ONE_MINUTE, 5); + break; + case PreferencesUtils.MIN_RECORDING_INTERVAL_ADAPT_ACCURACY: + // Get all the updates. + locationListenerPolicy = new AdaptiveLocationListenerPolicy( + ONE_SECOND, 30 * ONE_SECOND, 0); + break; + default: + locationListenerPolicy = new AbsoluteLocationListenerPolicy( + minRecordingInterval * ONE_SECOND); + } + } + if (key == null || PreferencesUtils.getKey(context, R.string.min_recording_distance_key) + .equals(key)) { + minRecordingDistance = PreferencesUtils.getInt(context, + R.string.min_recording_distance_key, + PreferencesUtils.MIN_RECORDING_DISTANCE_DEFAULT); + if (trackTripStatisticsBuilder != null && markerTripStatisticsBuilder != null) { + trackTripStatisticsBuilder.setMinRecordingDistance(minRecordingDistance); + markerTripStatisticsBuilder.setMinRecordingDistance(minRecordingDistance); + } + } + if (key == null || PreferencesUtils.getKey(context, R.string.max_recording_distance_key) + .equals(key)) { + maxRecordingDistance = PreferencesUtils.getInt(context, + R.string.max_recording_distance_key, + PreferencesUtils.MAX_RECORDING_DISTANCE_DEFAULT); + } + if (key == null + || PreferencesUtils.getKey(context, R.string.min_required_accuracy_key).equals(key)) { + minRequiredAccuracy = PreferencesUtils.getInt(context, + R.string.min_required_accuracy_key, PreferencesUtils.MIN_REQUIRED_ACCURACY_DEFAULT); + } + if (key == null || PreferencesUtils.getKey( + context, R.string.auto_resume_track_timeout_key).equals(key)) { + autoResumeTrackTimeout = PreferencesUtils.getInt(context, + R.string.auto_resume_track_timeout_key, + PreferencesUtils.AUTO_RESUME_TRACK_TIMEOUT_DEFAULT); + } + } + }; private LocationListener locationListener = new LocationListener() { - @Override + @Override public void onProviderDisabled(String provider) { // Do nothing } - @Override + @Override public void onProviderEnabled(String provider) { // Do nothing } - @Override + @Override public void onStatusChanged(String provider, int status, Bundle extras) { // Do nothing } - @Override + @Override public void onLocationChanged(final Location location) { if (!myTracksLocationManager.isAllowed() || executorService.isShutdown() || executorService.isTerminated()) { return; } executorService.submit(new Runnable() { - @Override + @Override public void run() { onLocationChangedAsync(location); } @@ -168,20 +251,12 @@ public class TrackRecordingService extends Service { } }; - /** - * Task invoked by a timer periodically to make sure the location listener is - * still registered. - */ private TimerTask checkLocationListener = new TimerTask() { - @Override + @Override public void run() { - // It's always safe to assume that if isRecording() is true, it implies - // that onCreate() has finished. if (isRecording()) { handler.post(new Runnable() { public void run() { - Log.d(Constants.TAG, - "Re-registering location listener with TrackRecordingService."); unregisterLocationListener(); registerLocationListener(); } @@ -190,90 +265,51 @@ public class TrackRecordingService extends Service { } }; - /** - * This timer invokes periodically the checkLocationListener timer task. - */ - private final Timer timer = new Timer(); - - /** - * Is the phone currently moving? - */ - private boolean isMoving = true; - - /** - * The most recent recording track. - */ - private Track recordingTrack; - - /** - * Is the service currently recording a track? - */ - private boolean isRecording; - - /** - * Last good location the service has received from the location listener - */ - private Location lastLocation; - - /** - * Last valid location (i.e. not a marker) that was recorded. - */ - private Location lastValidLocation; - - /** - * A service to run tasks outside of the main thread. - */ - private ExecutorService executorService; - - private ServiceBinder binder = new ServiceBinder(this); - - /* - * Application lifetime events: - */ - /* * Note that this service, through the AndroidManifest.xml, is configured to * allow both MyTracks and third party apps to invoke it. For the onCreate * callback, we cannot tell whether the caller is MyTracks or a third party * app, thus it cannot start/stop a recording or write/update MyTracks - * database. However, it can resume a recording. + * database. */ @Override public void onCreate() { super.onCreate(); - Log.d(TAG, "TrackRecordingService.onCreate"); - providerUtils = MyTracksProviderUtils.Factory.get(this); + context = this; + myTracksProviderUtils = MyTracksProviderUtils.Factory.get(this); myTracksLocationManager = new MyTracksLocationManager(this); - - setUpTaskExecutors(); + announcementExecutor = new PeriodicTaskExecutor(this, new AnnouncementPeriodicTaskFactory()); + splitExecutor = new PeriodicTaskExecutor(this, new SplitPeriodicTaskFactory()); executorService = Executors.newSingleThreadExecutor(); + sharedPreferences = getSharedPreferences(Constants.SETTINGS_NAME, Context.MODE_PRIVATE); + sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener); - prefereceManager = new PreferenceManager(this); + // onSharedPreferenceChanged might not set recordingTrackId. + recordingTrackId = PreferencesUtils.RECORDING_TRACK_ID_DEFAULT; + // Require announcementExecutor and splitExecutor to be created. + sharedPreferenceChangeListener.onSharedPreferenceChanged(sharedPreferences, null); + + // Require locationManager and all preferences set. registerLocationListener(); - /* - * After 5 min, check every minute that location listener still is - * registered and spit out additional debugging info to the logs: - */ - timer.schedule(checkLocationListener, 1000 * 60 * 5, 1000 * 60); + timer.schedule(checkLocationListener, 5 * ONE_MINUTE, ONE_MINUTE); - // Try to restore previous recording state in case this service has been - // restarted by the system, which can sometimes happen. - recordingTrack = getRecordingTrack(); + /* + * Try to restart the previous recording track in case the service has been + * restarted by the system, which can sometimes happen. + */ + recordingTrack = myTracksProviderUtils.getTrack(recordingTrackId); if (recordingTrack != null) { - restoreStats(recordingTrack); - isRecording = true; + restartTrack(recordingTrack); } else { - if (recordingTrackId != -1L) { - // Make sure we have consistent state in shared preferences. - Log.w(TAG, "TrackRecordingService.onCreate: " - + "Resetting an orphaned recording track = " + recordingTrackId); + if (recordingTrackId != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT) { + Log.w(TAG, "recordingTrackId not -1L, but recordingTrack is null. " + recordingTrackId); + recordingTrackId = PreferencesUtils.RECORDING_TRACK_ID_DEFAULT; + PreferencesUtils.setLong(this, R.string.recording_track_id_key, recordingTrackId); } - recordingTrackId = -1L; - PreferencesUtils.setLong(this, R.string.recording_track_id_key, recordingTrackId); + showNotification(); } - showNotification(); } /* @@ -281,7 +317,7 @@ public class TrackRecordingService extends Service { * allow both MyTracks and third party apps to invoke it. For the onStart * callback, we cannot tell whether the caller is MyTracks or a third party * app, thus it cannot start/stop a recording or write/update MyTracks - * database. However, it can resume a recording. + * database. */ @Override public void onStart(Intent intent, int startId) { @@ -293,7 +329,7 @@ public class TrackRecordingService extends Service { * allow both MyTracks and third party apps to invoke it. For the * onStartCommand callback, we cannot tell whether the caller is MyTracks or a * third party app, thus it cannot start/stop a recording or write/update - * MyTracks database. However, it can resume a recording. + * MyTracks database. */ @Override public int onStartCommand(Intent intent, int flags, int startId) { @@ -301,175 +337,610 @@ public class TrackRecordingService extends Service { return START_STICKY; } - private void handleStartCommand(Intent intent, int startId) { - Log.d(TAG, "TrackRecordingService.handleStartCommand: " + startId); - - if (intent == null) { - return; - } - - // Check if called on phone reboot with resume intent. - if (intent.getBooleanExtra(RESUME_TRACK_EXTRA_NAME, false)) { - resumeTrack(startId); - } - } - - private boolean isTrackInProgress() { - return recordingTrackId != -1 || isRecording; - } - - private void resumeTrack(int startId) { - Log.d(TAG, "TrackRecordingService: requested resume"); - - // Make sure that the current track exists and is fresh enough. - if (recordingTrack == null || !shouldResumeTrack(recordingTrack)) { - Log.i(TAG, - "TrackRecordingService: Not resuming, because the previous track (" - + recordingTrack + ") doesn't exist or is too old"); - isRecording = false; - recordingTrackId = -1L; - PreferencesUtils.setLong(this, R.string.recording_track_id_key, recordingTrackId); - stopSelfResult(startId); - return; - } - - Log.i(TAG, "TrackRecordingService: resuming"); - } - @Override public IBinder onBind(Intent intent) { - Log.d(TAG, "TrackRecordingService.onBind"); return binder; } - @Override - public boolean onUnbind(Intent intent) { - Log.d(TAG, "TrackRecordingService.onUnbind"); - return super.onUnbind(intent); - } - @Override public void onDestroy() { - Log.d(TAG, "TrackRecordingService.onDestroy"); - - isRecording = false; showNotification(); - prefereceManager.shutdown(); + + sharedPreferences.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener); checkLocationListener.cancel(); checkLocationListener = null; timer.cancel(); timer.purge(); unregisterLocationListener(); - shutdownTaskExecutors(); + + try { + announcementExecutor.shutdown(); + } finally { + announcementExecutor = null; + } + + try { + splitExecutor.shutdown(); + } finally { + splitExecutor = null; + } + if (sensorManager != null) { SensorManagerFactory.releaseSystemSensorManager(); sensorManager = null; } // Make sure we have no indirect references to this service. + myTracksProviderUtils = null; myTracksLocationManager.close(); myTracksLocationManager = null; - providerUtils = null; binder.detachFromService(); binder = null; - // This should be the last operation. + // This should be the next to last operation releaseWakeLock(); - // Shutdown the executor service last to avoid sending events to a dead executor. + /* + * Shutdown the executor service last to avoid sending events to a dead + * executor. + */ executorService.shutdown(); super.onDestroy(); } - private void setAutoResumeTrackRetries(int retryAttempts) { - Log.d(TAG, "Updating auto-resume retry attempts to: " + retryAttempts); - PreferencesUtils.setInt(this, R.string.auto_resume_track_current_retry_key, retryAttempts); + /** + * Returns true if the service is recording. + */ + public boolean isRecording() { + return recordingTrackId != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT; } - private boolean shouldResumeTrack(Track track) { - Log.d(TAG, "shouldResumeTrack: autoResumeTrackTimeout = " - + autoResumeTrackTimeout); + /** + * Gets the trip statistics. + */ + public TripStatistics getTripStatistics() { + if (trackTripStatisticsBuilder == null) { + return null; + } + return trackTripStatisticsBuilder.getTripStatistics(); + } - // Check if we haven't exceeded the maximum number of retry attempts. - int retries = PreferencesUtils.getInt(this, R.string.auto_resume_track_current_retry_key, - PreferencesUtils.AUTO_RESUME_TRACK_CURRENT_RETRY_DEFAULT); - Log.d(TAG, - "shouldResumeTrack: Attempting to auto-resume the track (" - + (retries + 1) + "/" + MAX_AUTO_RESUME_TRACK_RETRY_ATTEMPTS + ")"); - if (retries >= MAX_AUTO_RESUME_TRACK_RETRY_ATTEMPTS) { - Log.i(TAG, - "shouldResumeTrack: Not resuming because exceeded the maximum " - + "number of auto-resume retries"); - return false; + /** + * Inserts a waypoint. + * + * @param waypointCreationRequest the waypoint creation request + * @return the waypoint id + */ + public long insertWaypoint(WaypointCreationRequest waypointCreationRequest) { + if (!isRecording()) { + throw new IllegalStateException("Cannot insert marker when not recording!"); + } + boolean isStatistics = waypointCreationRequest.getType() == WaypointType.STATISTICS; + + String name; + if (waypointCreationRequest.getName() != null) { + name = waypointCreationRequest.getName(); + } else { + int nextMarkerNumber = myTracksProviderUtils.getNextMarkerNumber( + recordingTrackId, isStatistics); + if (nextMarkerNumber == -1) { + nextMarkerNumber = 0; + } + name = getString( + isStatistics ? R.string.marker_split_name_format : R.string.marker_name_format, + nextMarkerNumber); } - // Increase number of retry attempts. - setAutoResumeTrackRetries(retries + 1); + TripStatistics tripStatistics; + String description; + if (isStatistics) { + long now = System.currentTimeMillis(); + markerTripStatisticsBuilder.pauseAt(now); + tripStatistics = markerTripStatisticsBuilder.getTripStatistics(); + markerTripStatisticsBuilder = new TripStatisticsBuilder(now); + description = new DescriptionGeneratorImpl(this).generateWaypointDescription(tripStatistics); + } else { + tripStatistics = null; + description = waypointCreationRequest.getDescription() != null ? waypointCreationRequest + .getDescription() + : ""; + } + + String category = waypointCreationRequest.getCategory() != null ? waypointCreationRequest + .getCategory() + : ""; + String icon = getString( + isStatistics ? R.string.marker_statistics_icon_url : R.string.marker_waypoint_icon_url); + int type = isStatistics ? Waypoint.TYPE_STATISTICS : Waypoint.TYPE_WAYPOINT; + + long duration; + Location location = myTracksProviderUtils.getLastTrackLocation(recordingTrackId); + if (location != null && trackTripStatisticsBuilder != null + && trackTripStatisticsBuilder.getTripStatistics() != null) { + duration = location.getTime() - trackTripStatisticsBuilder.getTripStatistics().getStartTime(); + } else { + if (!waypointCreationRequest.isTrackStatistics()) { + return -1L; + } + // For track statistics, make it an impossible location + location = new Location(""); + location.setLatitude(100); + location.setLongitude(180); + duration = 0; + } + Waypoint waypoint = new Waypoint(name, description, category, icon, recordingTrackId, type, + length, duration, -1L, -1L, location, tripStatistics); + Uri uri = myTracksProviderUtils.insertWaypoint(waypoint); + return Long.parseLong(uri.getLastPathSegment()); + } + + /** + * Starts the service as a foreground service. + * + * @param notification the notification for the foreground service + */ + @VisibleForTesting + protected void startForegroundService(Notification notification) { + startForeground(1, notification); + } + + /** + * Stops the service as a foreground service. + */ + @VisibleForTesting + protected void stopForegroundService() { + stopForeground(true); + } + + /** + * Handles start command. + * + * @param intent the intent + * @param startId the start id + */ + private void handleStartCommand(Intent intent, int startId) { + // Check if the service is called to resume track (from phone reboot) + if (intent != null && intent.getBooleanExtra(RESUME_TRACK_EXTRA_NAME, false)) { + if (!shouldResumeTrack(recordingTrack)) { + Log.i(TAG, "Stop resume track."); + stopRecording(); + stopSelfResult(startId); + return; + } + } + } + + /** + * Returns true if should resume. + * + * @param track the track + */ + private boolean shouldResumeTrack(Track track) { + if (track == null) { + Log.d(TAG, "Not resuming. Track is null."); + return false; + } + int retries = PreferencesUtils.getInt(this, R.string.auto_resume_track_current_retry_key, + PreferencesUtils.AUTO_RESUME_TRACK_CURRENT_RETRY_DEFAULT); + if (retries >= MAX_AUTO_RESUME_TRACK_RETRY_ATTEMPTS) { + Log.d(TAG, "Not resuming. Exceeded maximum retry attempts."); + return false; + } + PreferencesUtils.setInt(this, R.string.auto_resume_track_current_retry_key, retries + 1); - // Check for special cases. if (autoResumeTrackTimeout == PreferencesUtils.AUTO_RESUME_TRACK_TIMEOUT_NEVER) { - // Never resume. - Log.d(TAG, - "shouldResumeTrack: Auto-resume disabled (never resume)"); + Log.d(TAG, "Not resuming. Auto-resume track timeout set to never."); return false; } else if (autoResumeTrackTimeout == PreferencesUtils.AUTO_RESUME_TRACK_TIMEOUT_ALWAYS) { - // Always resume. - Log.d(TAG, - "shouldResumeTrack: Auto-resume forced (always resume)"); + Log.d(TAG, "Resuming. Auto-resume track timeout set to always."); return true; } - // Check if the last modified time is within the acceptable range. - long lastModified = - track.getTripStatistics() != null ? track.getTripStatistics().getStopTime() : 0; - Log.d(TAG, - "shouldResumeTrack: lastModified = " + lastModified - + ", autoResumeTrackTimeout: " + autoResumeTrackTimeout); - return lastModified > 0 && System.currentTimeMillis() - lastModified <= - autoResumeTrackTimeout * 60L * 1000L; + if (track.getTripStatistics() == null) { + Log.d(TAG, "Not resuming. No trip statistics."); + return false; + } + long stopTime = track.getTripStatistics().getStopTime(); + return stopTime > 0 + && (System.currentTimeMillis() - stopTime) <= autoResumeTrackTimeout * ONE_MINUTE; } - /* - * Setup/shutdown methods. + /** + * Starts a new track. + * + * @return the track id */ + private long startNewTrack() { + if (isRecording()) { + Log.d(TAG, "Ignore startNewTrack. Already recording."); + return -1L; + } + long now = System.currentTimeMillis(); + length = 0; + lastLengthLocation = null; + trackTripStatisticsBuilder = new TripStatisticsBuilder(now); + trackTripStatisticsBuilder.setMinRecordingDistance(minRecordingDistance); + markerTripStatisticsBuilder = new TripStatisticsBuilder(now); + markerTripStatisticsBuilder.setMinRecordingDistance(minRecordingDistance); + + Track track = new Track(); + track.setStartId(-1L); + TripStatistics tripStatistics = track.getTripStatistics(); + tripStatistics.setStartTime(now); + Uri uri = myTracksProviderUtils.insertTrack(track); + + recordingTrackId = Long.parseLong(uri.getLastPathSegment()); + track.setId(recordingTrackId); + track.setName(TrackNameUtils.getTrackName(this, recordingTrackId, now, null)); + track.setCategory(PreferencesUtils.getString( + this, R.string.default_activity_key, PreferencesUtils.DEFAULT_ACTIVITY_DEFAULT)); + myTracksProviderUtils.updateTrack(track); + + insertWaypoint(WaypointCreationRequest.DEFAULT_START_TRACK); + PreferencesUtils.setLong(this, R.string.recording_track_id_key, recordingTrackId); + PreferencesUtils.setInt(this, R.string.auto_resume_track_current_retry_key, 0); + + registerLocationListener(); + startRecording(); + return recordingTrackId; + } /** - * Tries to acquire a partial wake lock if not already acquired. Logs errors - * and gives up trying in case the wake lock cannot be acquired. + * Restart a track. + * + * @param track the track + */ + private void restartTrack(Track track) { + Log.d(TAG, "Restarting track: " + track.getId()); + + length = 0; + lastLengthLocation = null; + TripStatistics tripStatistics = track.getTripStatistics(); + trackTripStatisticsBuilder = new TripStatisticsBuilder(tripStatistics.getStartTime()); + trackTripStatisticsBuilder.setMinRecordingDistance(minRecordingDistance); + + long markerStartTime; + Waypoint waypoint = myTracksProviderUtils.getLastStatisticsWaypoint(recordingTrackId); + if (waypoint != null && waypoint.getTripStatistics() != null) { + markerStartTime = waypoint.getTripStatistics().getStopTime(); + } else { + markerStartTime = tripStatistics.getStartTime(); + } + markerTripStatisticsBuilder = new TripStatisticsBuilder(markerStartTime); + markerTripStatisticsBuilder.setMinRecordingDistance(minRecordingDistance); + + Cursor cursor = null; + try { + cursor = myTracksProviderUtils.getLocationsCursor( + recordingTrackId, -1, Constants.MAX_LOADED_TRACK_POINTS, true); + if (cursor == null) { + Log.e(TAG, "Cursor is null."); + } else { + if (cursor.moveToLast()) { + do { + Location location = myTracksProviderUtils.createLocation(cursor); + if (LocationUtils.isValidLocation(location)) { + trackTripStatisticsBuilder.addLocation(location, location.getTime()); + if (location.getTime() > markerStartTime) { + markerTripStatisticsBuilder.addLocation(location, location.getTime()); + } + if (lastLengthLocation != null) { + length += location.distanceTo(lastLengthLocation); + } + lastLengthLocation = location; + } + } while (cursor.moveToPrevious()); + } + trackTripStatisticsBuilder.pauseAt(tripStatistics.getStopTime()); + trackTripStatisticsBuilder.resumeAt(System.currentTimeMillis()); + } + } catch (RuntimeException e) { + Log.e(TAG, "RuntimeException", e); + } finally { + if (cursor != null) { + cursor.close(); + } + } + startRecording(); + } + + /** + * Common code for starting a recording, new track or restart track. + */ + private void startRecording() { + acquireWakeLock(); + + lastLocation = null; + isMoving = true; + sensorManager = SensorManagerFactory.getSystemSensorManager(this); + + showNotification(); + sendTrackBroadcast(R.string.track_started_broadcast_action, recordingTrackId); + announcementExecutor.restore(); + splitExecutor.restore(); + } + + /** + * Ends the current track. + */ + private void endCurrentTrack() { + if (!isRecording()) { + Log.d(TAG, "Ignore endCurrentTrack. Not recording."); + return; + } + announcementExecutor.shutdown(); + splitExecutor.shutdown(); + Track track = myTracksProviderUtils.getTrack(recordingTrackId); + if (track != null) { + long lastLocationId = myTracksProviderUtils.getLastTrackLocationId(recordingTrackId); + if (lastLocationId >= 0 && track.getStopId() >= 0) { + track.setStopId(lastLocationId); + } + updateTripStatisticsToTime(track.getTripStatistics(), System.currentTimeMillis()); + myTracksProviderUtils.updateTrack(track); + } + // Need to remember the trackId before calling stopRecording + long trackId = recordingTrackId; + stopRecording(); + + if (sensorManager != null) { + SensorManagerFactory.releaseSystemSensorManager(); + sensorManager = null; + } + + releaseWakeLock(); + showNotification(); + sendTrackBroadcast(R.string.track_stopped_broadcast_action, trackId); + stopSelf(); + } + + /** + * Common code for stopping a recording. + */ + private void stopRecording() { + recordingTrackId = PreferencesUtils.RECORDING_TRACK_ID_DEFAULT; + PreferencesUtils.setLong(this, R.string.recording_track_id_key, recordingTrackId); + } + + /** + * Updates a {@link TripStatistics} to a time. + * + * @param tripStatistics the trip statistics + * @param time the time + */ + private void updateTripStatisticsToTime(TripStatistics tripStatistics, long time) { + tripStatistics.setStopTime(time); + tripStatistics.setTotalTime(time - tripStatistics.getStartTime()); + } + + /** + * Called when location changed. + * + * @param location the location + */ + private void onLocationChangedAsync(Location location) { + try { + if (location == null) { + Log.w(TAG, "Ignore onLocationChangedAsync. location is null."); + return; + } + + if (!isRecording()) { + Log.w(TAG, "Ignore onLocationChangedAsync. Not recording."); + return; + } + + if (location.getAccuracy() > minRequiredAccuracy) { + Log.d(TAG, "Ignore onLocationChangedAsync. Poor accuracy."); + return; + } + + recordingTrack = myTracksProviderUtils.getTrack(recordingTrackId); + if (recordingTrack == null) { + Log.d(TAG, "Ignore onLocationChangedAsync. recodingTrack is null."); + return; + } + + locationListenerPolicy.updateIdleTime(trackTripStatisticsBuilder.getIdleTime()); + + if (LocationUtils.isValidLocation(location)) { + long now = System.currentTimeMillis(); + trackTripStatisticsBuilder.addLocation(location, now); + markerTripStatisticsBuilder.addLocation(location, now); + } + + if (currentRecordingInterval != locationListenerPolicy.getDesiredPollingInterval()) { + registerLocationListener(); + } + + Location lastRecordedLocation = myTracksProviderUtils.getLastLocation(); + double distanceToLastRecordedLocation = lastRecordedLocation != null ? location.distanceTo( + lastRecordedLocation) + : Double.POSITIVE_INFINITY; + double distanceToLastLocation = lastLocation != null ? location.distanceTo(lastLocation) + : Double.POSITIVE_INFINITY; + boolean hasSensorData = sensorManager != null && sensorManager.isEnabled() + && sensorManager.getSensorDataSet() != null && sensorManager.isSensorDataSetValid(); + + // If stationary for two locations, make sure the first one is recorded + if (distanceToLastLocation == 0 && !hasSensorData) { + if (isMoving) { + isMoving = false; + if (lastLocation != null && !lastLocation.equals(lastRecordedLocation)) { + if (!insertLocation(lastLocation, lastRecordedLocation, recordingTrackId)) { + return; + } + } + } + lastLocation = location; + } else if (distanceToLastRecordedLocation >= minRecordingDistance || hasSensorData) { + if (!isMoving) { + isMoving = true; + // Make sure the lastLocation is added. + if (lastLocation != null && !lastLocation.equals(lastRecordedLocation)) { + if (!insertLocation(lastLocation, lastRecordedLocation, recordingTrackId)) { + return; + } + } + } + + boolean startNewSegment = LocationUtils.isValidLocation(lastRecordedLocation) + && distanceToLastRecordedLocation > maxRecordingDistance + && recordingTrack.getStartId() >= 0; + if (startNewSegment) { + Log.d(TAG, "Inserting a separator track point."); + Location separator = new Location(LocationManager.GPS_PROVIDER); + separator.setLongitude(0); + separator.setLatitude(100); + separator.setTime(lastRecordedLocation.getTime()); + myTracksProviderUtils.insertTrackPoint(separator, recordingTrackId); + } + + if (!insertLocation(location, lastRecordedLocation, recordingTrackId)) { + return; + } + lastLocation = location; + } else { + Log.d(TAG, "Ignore location. distanceToLastRecordedLocation < minRecordingDistance."); + } + } catch (Error e) { + Log.e(TAG, "Error in onLocationChangedAsync", e); + throw e; + } catch (RuntimeException e) { + Log.e(TAG, "RuntimeException in onLocationChangedAsync", e); + throw e; + } + } + + /** + * Inserts a new location in the track points db and updates the corresponding + * track in the track db. + * + * @param location the location to be inserted + * @param lastRecordedLocation the last recorded location before this one (or + * null if none) + * @param trackId the id of the track + * @return true if successful. False if SQLite3 threw an exception. + */ + private boolean insertLocation(Location location, Location lastRecordedLocation, long trackId) { + + // Keep track of length along recorded track + if (LocationUtils.isValidLocation(location)) { + if (lastLengthLocation != null) { + length += location.distanceTo(lastLengthLocation); + } + lastLengthLocation = location; + } + + try { + Location locationToInsert = location; + if (sensorManager != null && sensorManager.isEnabled()) { + SensorDataSet sensorDataSet = sensorManager.getSensorDataSet(); + if (sensorDataSet != null && sensorManager.isSensorDataSetValid()) { + locationToInsert = new MyTracksLocation(location, sensorDataSet); + } + } + Uri uri = myTracksProviderUtils.insertTrackPoint(locationToInsert, trackId); + int pointId = Integer.parseInt(uri.getLastPathSegment()); + + // Update the current track + if (lastRecordedLocation != null && lastRecordedLocation.getLatitude() <= 90) { + if (recordingTrack.getStartId() < 0) { + recordingTrack.setStartId(pointId); + } + recordingTrack.setStopId(pointId); + recordingTrack.setNumberOfPoints(recordingTrack.getNumberOfPoints() + 1); + + long now = System.currentTimeMillis(); + TripStatistics tripStatistics = trackTripStatisticsBuilder.getTripStatistics(); + updateTripStatisticsToTime(tripStatistics, now); + recordingTrack.setTripStatistics(tripStatistics); + + myTracksProviderUtils.updateTrack(recordingTrack); + + // Update the first waypoint + Waypoint waypoint = myTracksProviderUtils.getFirstWaypoint(recordingTrackId); + if (waypoint != null) { + TripStatistics stats = trackTripStatisticsBuilder.getTripStatistics(); + waypoint.setLength(length); + waypoint.setDuration(now - stats.getStartTime()); + waypoint.setTripStatistics(stats); + myTracksProviderUtils.updateWaypoint(waypoint); + } + } + } catch (SQLiteException e) { + /* + * Insert failed, most likely because of SqlLite error code 5 + * (SQLite_BUSY). This is expected to happen extremely rarely (if our + * listener gets invoked twice at about the same time). + */ + Log.w(TAG, "SQLiteException", e); + return false; + } + announcementExecutor.update(); + splitExecutor.update(); + return true; + } + + /** + * Registers the location listener. + */ + private void registerLocationListener() { + if (myTracksLocationManager == null) { + Log.e(TAG, "locationManager is null."); + return; + } + try { + long interval = locationListenerPolicy.getDesiredPollingInterval(); + myTracksLocationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, interval, + locationListenerPolicy.getMinDistance(), locationListener); + currentRecordingInterval = interval; + } catch (RuntimeException e) { + Log.e(TAG, "Could not register location listener.", e); + } + } + + /** + * Unregisters the location manager. + */ + private void unregisterLocationListener() { + if (myTracksLocationManager == null) { + Log.e(TAG, "locationManager is null."); + return; + } + myTracksLocationManager.removeUpdates(locationListener); + } + + /** + * Acquires the wake lock. */ private void acquireWakeLock() { try { - PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); - if (pm == null) { - Log.e(TAG, - "TrackRecordingService: Power manager not found!"); + PowerManager powerManager = (PowerManager) getSystemService(Context.POWER_SERVICE); + if (powerManager == null) { + Log.e(TAG, "powerManager is null."); return; } if (wakeLock == null) { - wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, - TAG); + wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG); if (wakeLock == null) { - Log.e(TAG, - "TrackRecordingService: Could not create wake lock (null)."); + Log.e(TAG, "wakeLock is null."); return; } } if (!wakeLock.isHeld()) { wakeLock.acquire(); if (!wakeLock.isHeld()) { - Log.e(TAG, - "TrackRecordingService: Could not acquire wake lock."); + Log.e(TAG, "Unable to hold wakeLock."); } } } catch (RuntimeException e) { - Log.e(TAG, - "TrackRecordingService: Caught unexpected exception: " - + e.getMessage(), e); + Log.e(TAG, "Caught unexpected exception", e); } } /** - * Releases the wake lock if it's currently held. + * Releases the wake lock. */ private void releaseWakeLock() { if (wakeLock != null && wakeLock.isHeld()) { @@ -479,698 +950,62 @@ public class TrackRecordingService extends Service { } /** - * Shows the notification message and icon in the notification bar. + * Shows the notification. */ private void showNotification() { - if (isRecording) { + if (isRecording()) { Intent intent = IntentUtils.newIntent(this, TrackDetailActivity.class) .putExtra(TrackDetailActivity.EXTRA_TRACK_ID, recordingTrackId); TaskStackBuilder taskStackBuilder = TaskStackBuilder.from(this); taskStackBuilder.addNextIntent(intent); - NotificationCompat.Builder builder = new NotificationCompat.Builder(this) - .setContentIntent(taskStackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT)) + NotificationCompat.Builder builder = new NotificationCompat.Builder(this).setContentIntent( + taskStackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT)) .setContentText(getString(R.string.track_record_notification)) - .setContentTitle(getString(R.string.my_tracks_app_name)) - .setOngoing(true) - .setSmallIcon(R.drawable.my_tracks_notification_icon) - .setWhen(System.currentTimeMillis()); + .setContentTitle(getString(R.string.my_tracks_app_name)).setOngoing(true) + .setSmallIcon(R.drawable.my_tracks_notification_icon).setWhen(System.currentTimeMillis()); startForegroundService(builder.getNotification()); } else { stopForegroundService(); } } - @VisibleForTesting - protected void startForegroundService(Notification notification) { - startForeground(1, notification); - } - - @VisibleForTesting - protected void stopForegroundService() { - stopForeground(true); - } - - private void setUpTaskExecutors() { - announcementExecutor = new PeriodicTaskExecutor(this, new StatusAnnouncerFactory()); - splitExecutor = new PeriodicTaskExecutor(this, new SplitTask.Factory()); - } - - private void shutdownTaskExecutors() { - Log.d(TAG, "TrackRecordingService.shutdownExecuters"); - try { - announcementExecutor.shutdown(); - } finally { - announcementExecutor = null; - } - try { - splitExecutor.shutdown(); - } finally { - splitExecutor = null; - } - } - - private void registerLocationListener() { - if (myTracksLocationManager == null) { - Log.e(TAG, - "TrackRecordingService: Do not have any location manager."); - return; - } - Log.d(TAG, - "Preparing to register location listener w/ TrackRecordingService..."); - try { - long desiredInterval = locationListenerPolicy.getDesiredPollingInterval(); - myTracksLocationManager.requestLocationUpdates( - LocationManager.GPS_PROVIDER, desiredInterval, - locationListenerPolicy.getMinDistance(), - // , 0 /* minDistance, get all updates to properly time pauses */ - locationListener); - currentRecordingInterval = desiredInterval; - Log.d(TAG, - "...location listener now registered w/ TrackRecordingService @ " - + currentRecordingInterval); - } catch (RuntimeException e) { - Log.e(TAG, - "Could not register location listener: " + e.getMessage(), e); - } - } - - private void unregisterLocationListener() { - if (myTracksLocationManager == null) { - Log.e(TAG, - "TrackRecordingService: Do not have any location manager."); - return; - } - myTracksLocationManager.removeUpdates(locationListener); - Log.d(TAG, - "Location listener now unregistered w/ TrackRecordingService."); - } - - /* - * Recording lifecycle. - */ - - private long startNewTrack() { - Log.d(TAG, "TrackRecordingService.startNewTrack"); - if (isTrackInProgress()) { - return -1L; - } - - long startTime = System.currentTimeMillis(); - acquireWakeLock(); - - Track track = new Track(); - TripStatistics trackStats = track.getTripStatistics(); - trackStats.setStartTime(startTime); - track.setStartId(-1); - Uri trackUri = providerUtils.insertTrack(track); - recordingTrackId = Long.parseLong(trackUri.getLastPathSegment()); - track.setId(recordingTrackId); - track.setName(TrackNameUtils.getTrackName(this, recordingTrackId, startTime, null)); - track.setCategory(PreferencesUtils.getString( - this, R.string.default_activity_key, PreferencesUtils.DEFAULT_ACTIVITY_DEFAULT)); - isRecording = true; - isMoving = true; - - providerUtils.updateTrack(track); - statsBuilder = new TripStatisticsBuilder(startTime); - statsBuilder.setMinRecordingDistance(minRecordingDistance); - waypointStatsBuilder = new TripStatisticsBuilder(startTime); - waypointStatsBuilder.setMinRecordingDistance(minRecordingDistance); - currentWaypointId = insertWaypoint(WaypointCreationRequest.DEFAULT_START_TRACK); - length = 0; - showNotification(); - registerLocationListener(); - sensorManager = SensorManagerFactory.getSystemSensorManager(this); - - // Reset the number of auto-resume retries. - setAutoResumeTrackRetries(0); - // Persist the current recording track. - PreferencesUtils.setLong(this, R.string.recording_track_id_key, recordingTrackId); - - // Notify the world that we're now recording. - sendTrackBroadcast( - R.string.track_started_broadcast_action, recordingTrackId); - announcementExecutor.restore(); - splitExecutor.restore(); - - return recordingTrackId; - } - - private void restoreStats(Track track) { - Log.d(TAG, - "Restoring stats of track with ID: " + track.getId()); - - TripStatistics stats = track.getTripStatistics(); - statsBuilder = new TripStatisticsBuilder(stats.getStartTime()); - statsBuilder.setMinRecordingDistance(minRecordingDistance); - - length = 0; - lastValidLocation = null; - - Waypoint waypoint = providerUtils.getFirstWaypoint(recordingTrackId); - if (waypoint != null && waypoint.getTripStatistics() != null) { - currentWaypointId = waypoint.getId(); - waypointStatsBuilder = new TripStatisticsBuilder( - waypoint.getTripStatistics()); - } else { - // This should never happen, but we got to do something so life goes on: - waypointStatsBuilder = new TripStatisticsBuilder(stats.getStartTime()); - currentWaypointId = -1; - } - waypointStatsBuilder.setMinRecordingDistance(minRecordingDistance); - - Cursor cursor = null; - try { - cursor = providerUtils.getLocationsCursor( - recordingTrackId, -1, Constants.MAX_LOADED_TRACK_POINTS, - true); - if (cursor != null) { - if (cursor.moveToLast()) { - do { - Location location = providerUtils.createLocation(cursor); - if (LocationUtils.isValidLocation(location)) { - statsBuilder.addLocation(location, location.getTime()); - if (lastValidLocation != null) { - length += location.distanceTo(lastValidLocation); - } - lastValidLocation = location; - } - } while (cursor.moveToPrevious()); - } - statsBuilder.getStatistics().setMovingTime(stats.getMovingTime()); - statsBuilder.pauseAt(stats.getStopTime()); - statsBuilder.resumeAt(System.currentTimeMillis()); - } else { - Log.e(TAG, "Could not get track points cursor."); - } - } catch (RuntimeException e) { - Log.e(TAG, "Error while restoring track.", e); - } finally { - if (cursor != null) { - cursor.close(); - } - } - - announcementExecutor.restore(); - splitExecutor.restore(); - } - - private void onLocationChangedAsync(Location location) { - Log.d(TAG, "TrackRecordingService.onLocationChanged"); - - try { - // Don't record if the service has been asked to pause recording: - if (!isRecording) { - Log.w(TAG, - "Not recording because recording has been paused."); - return; - } - - // This should never happen, but just in case (we really don't want the - // service to crash): - if (location == null) { - Log.w(TAG, - "Location changed, but location is null."); - return; - } - - // Don't record if the accuracy is too bad: - if (location.getAccuracy() > minRequiredAccuracy) { - Log.d(TAG, - "Not recording. Bad accuracy."); - return; - } - - // At least one track must be available for appending points: - recordingTrack = getRecordingTrack(); - if (recordingTrack == null) { - Log.d(TAG, - "Not recording. No track to append to available."); - return; - } - - // Update the idle time if needed. - locationListenerPolicy.updateIdleTime(statsBuilder.getIdleTime()); - addLocationToStats(location); - if (currentRecordingInterval != - locationListenerPolicy.getDesiredPollingInterval()) { - registerLocationListener(); - } - - Location lastRecordedLocation = providerUtils.getLastLocation(); - double distanceToLastRecorded = Double.POSITIVE_INFINITY; - if (lastRecordedLocation != null) { - distanceToLastRecorded = location.distanceTo(lastRecordedLocation); - } - double distanceToLast = Double.POSITIVE_INFINITY; - if (lastLocation != null) { - distanceToLast = location.distanceTo(lastLocation); - } - boolean hasSensorData = sensorManager != null - && sensorManager.isEnabled() - && sensorManager.getSensorDataSet() != null - && sensorManager.isSensorDataSetValid(); - - // If the user has been stationary for two recording just record the first - // two and ignore the rest. This code will only have an effect if the - // maxRecordingDistance = 0 - if (distanceToLast == 0 && !hasSensorData) { - if (isMoving) { - Log.d(TAG, "Found two identical locations."); - isMoving = false; - if (lastLocation != null && lastRecordedLocation != null - && !lastRecordedLocation.equals(lastLocation)) { - // Need to write the last location. This will happen when - // lastRecordedLocation.distance(lastLocation) < - // minRecordingDistance - if (!insertLocation(lastLocation, lastRecordedLocation, recordingTrackId)) { - return; - } - } - } else { - Log.d(TAG, - "Not recording. More than two identical locations."); - } - } else if (distanceToLastRecorded > minRecordingDistance - || hasSensorData) { - if (lastLocation != null && !isMoving) { - // Last location was the last stationary location. Need to go back and - // add it. - if (!insertLocation(lastLocation, lastRecordedLocation, recordingTrackId)) { - return; - } - isMoving = true; - } - - // If separation from last recorded point is too large insert a - // separator to indicate end of a segment: - boolean startNewSegment = - lastRecordedLocation != null - && lastRecordedLocation.getLatitude() < 90 - && distanceToLastRecorded > maxRecordingDistance - && recordingTrack.getStartId() >= 0; - if (startNewSegment) { - // Insert a separator point to indicate start of new track: - Log.d(TAG, "Inserting a separator."); - Location separator = new Location(LocationManager.GPS_PROVIDER); - separator.setLongitude(0); - separator.setLatitude(100); - separator.setTime(lastRecordedLocation.getTime()); - providerUtils.insertTrackPoint(separator, recordingTrackId); - } - - if (!insertLocation(location, lastRecordedLocation, recordingTrackId)) { - return; - } - } else { - Log.d(TAG, String.format(Locale.US, - "Not recording. Distance to last recorded point (%f m) is less than %d m.", - distanceToLastRecorded, minRecordingDistance)); - // Return here so that the location is NOT recorded as the last location. - return; - } - } catch (Error e) { - // Probably important enough to rethrow. - Log.e(TAG, "Error in onLocationChanged", e); - throw e; - } catch (RuntimeException e) { - // Safe usually to trap exceptions. - Log.e(TAG, - "Trapping exception in onLocationChanged", e); - throw e; - } - lastLocation = location; - } - /** - * Inserts a new location in the track points db and updates the corresponding - * track in the track db. - * - * @param location the location to be inserted - * @param lastRecordedLocation the last recorded location before this one (or - * null if none) - * @param trackId the id of the track - * @return true if successful. False if SQLite3 threw an exception. + * Sends track broadcast. + * + * @param actionId the intent action id + * @param trackId the track id */ - private boolean insertLocation(Location location, Location lastRecordedLocation, long trackId) { - - // Keep track of length along recorded track (needed when a waypoint is - // inserted): - if (LocationUtils.isValidLocation(location)) { - if (lastValidLocation != null) { - length += location.distanceTo(lastValidLocation); - } - lastValidLocation = location; - } - - // Insert the new location: - try { - Location locationToInsert = location; - if (sensorManager != null && sensorManager.isEnabled()) { - SensorDataSet sd = sensorManager.getSensorDataSet(); - if (sd != null && sensorManager.isSensorDataSetValid()) { - locationToInsert = new MyTracksLocation(location, sd); - } - } - Uri pointUri = providerUtils.insertTrackPoint(locationToInsert, trackId); - int pointId = Integer.parseInt(pointUri.getLastPathSegment()); - - // Update the current track: - if (lastRecordedLocation != null - && lastRecordedLocation.getLatitude() < 90) { - ContentValues values = new ContentValues(); - TripStatistics stats = statsBuilder.getStatistics(); - if (recordingTrack.getStartId() < 0) { - values.put(TracksColumns.STARTID, pointId); - recordingTrack.setStartId(pointId); - } - values.put(TracksColumns.STOPID, pointId); - values.put(TracksColumns.STOPTIME, System.currentTimeMillis()); - values.put(TracksColumns.NUMPOINTS, - recordingTrack.getNumberOfPoints() + 1); - values.put(TracksColumns.MINLAT, stats.getBottom()); - values.put(TracksColumns.MAXLAT, stats.getTop()); - values.put(TracksColumns.MINLON, stats.getLeft()); - values.put(TracksColumns.MAXLON, stats.getRight()); - values.put(TracksColumns.TOTALDISTANCE, stats.getTotalDistance()); - values.put(TracksColumns.TOTALTIME, stats.getTotalTime()); - values.put(TracksColumns.MOVINGTIME, stats.getMovingTime()); - values.put(TracksColumns.AVGSPEED, stats.getAverageSpeed()); - values.put(TracksColumns.AVGMOVINGSPEED, stats.getAverageMovingSpeed()); - values.put(TracksColumns.MAXSPEED, stats.getMaxSpeed()); - values.put(TracksColumns.MINELEVATION, stats.getMinElevation()); - values.put(TracksColumns.MAXELEVATION, stats.getMaxElevation()); - values.put(TracksColumns.ELEVATIONGAIN, stats.getTotalElevationGain()); - values.put(TracksColumns.MINGRADE, stats.getMinGrade()); - values.put(TracksColumns.MAXGRADE, stats.getMaxGrade()); - getContentResolver().update(TracksColumns.CONTENT_URI, - values, "_id=" + recordingTrack.getId(), null); - updateCurrentWaypoint(); - } - } catch (SQLiteException e) { - // Insert failed, most likely because of SqlLite error code 5 - // (SQLite_BUSY). This is expected to happen extremely rarely (if our - // listener gets invoked twice at about the same time). - Log.w(TAG, - "Caught SQLiteException: " + e.getMessage(), e); - return false; - } - announcementExecutor.update(); - splitExecutor.update(); - return true; - } - - private void updateCurrentWaypoint() { - if (currentWaypointId >= 0) { - ContentValues values = new ContentValues(); - TripStatistics waypointStats = waypointStatsBuilder.getStatistics(); - values.put(WaypointsColumns.STARTTIME, waypointStats.getStartTime()); - values.put(WaypointsColumns.LENGTH, length); - values.put(WaypointsColumns.DURATION, System.currentTimeMillis() - - statsBuilder.getStatistics().getStartTime()); - values.put(WaypointsColumns.TOTALDISTANCE, - waypointStats.getTotalDistance()); - values.put(WaypointsColumns.TOTALTIME, waypointStats.getTotalTime()); - values.put(WaypointsColumns.MOVINGTIME, waypointStats.getMovingTime()); - values.put(WaypointsColumns.AVGSPEED, waypointStats.getAverageSpeed()); - values.put(WaypointsColumns.AVGMOVINGSPEED, - waypointStats.getAverageMovingSpeed()); - values.put(WaypointsColumns.MAXSPEED, waypointStats.getMaxSpeed()); - values.put(WaypointsColumns.MINELEVATION, - waypointStats.getMinElevation()); - values.put(WaypointsColumns.MAXELEVATION, - waypointStats.getMaxElevation()); - values.put(WaypointsColumns.ELEVATIONGAIN, - waypointStats.getTotalElevationGain()); - values.put(WaypointsColumns.MINGRADE, waypointStats.getMinGrade()); - values.put(WaypointsColumns.MAXGRADE, waypointStats.getMaxGrade()); - getContentResolver().update(WaypointsColumns.CONTENT_URI, - values, "_id=" + currentWaypointId, null); - } - } - - private void addLocationToStats(Location location) { - if (LocationUtils.isValidLocation(location)) { - long now = System.currentTimeMillis(); - statsBuilder.addLocation(location, now); - waypointStatsBuilder.addLocation(location, now); - } - } - - /* - * Application lifetime events: ============================ - */ - - public long insertWaypoint(WaypointCreationRequest request) { - if (!isRecording()) { - throw new IllegalStateException("Unable to insert marker while not recording!"); - } - Waypoint waypoint = new Waypoint(); - if (request.getType() == WaypointType.WAYPOINT) { - buildWaypointMarker(waypoint, request); - } else { - buildStatisticsMarker(waypoint, request); - } - waypoint.setTrackId(recordingTrackId); - waypoint.setLength(length); - if (lastLocation == null || statsBuilder == null || statsBuilder.getStatistics() == null) { - if (!request.isTrackStatistics()) { - return -1L; - } - /* - * For track statistics, a null location is OK. Make it an impossible - * location. - */ - Location location = new Location(""); - location.setLatitude(100); - location.setLongitude(180); - waypoint.setLocation(location); - } else { - waypoint.setLocation(lastLocation); - waypoint.setDuration(lastLocation.getTime() - statsBuilder.getStatistics().getStartTime()); - } - Uri uri = providerUtils.insertWaypoint(waypoint); - return Long.parseLong(uri.getLastPathSegment()); - } - - private void buildWaypointMarker(Waypoint wpt, WaypointCreationRequest request) { - wpt.setType(Waypoint.TYPE_WAYPOINT); - if (request.getIconUrl() == null) { - wpt.setIcon(getString(R.string.marker_waypoint_icon_url)); - } else { - wpt.setIcon(request.getIconUrl()); - } - String name; - if (request.getName() != null) { - name = request.getName(); - } else { - int nextMarkerNumber = providerUtils.getNextMarkerNumber(recordingTrackId, false); - if (nextMarkerNumber == -1) { - nextMarkerNumber = 0; - } - name = getString(R.string.marker_name_format, nextMarkerNumber); - } - wpt.setName(name); - if (request.getCategory() != null) { - wpt.setCategory(request.getCategory()); - } - if (request.getDescription() != null) { - wpt.setDescription(request.getDescription()); - } - } - - /** - * Build a statistics marker. - * A statistics marker holds the stats for the* last segment up to this marker. - * - * @param waypoint The waypoint which will be populated with stats data - * @param request The waypoint creation request - */ - private void buildStatisticsMarker(Waypoint waypoint, WaypointCreationRequest request) { - DescriptionGenerator descriptionGenerator = new DescriptionGeneratorImpl(this); - - // Set stop and total time in the stats data - final long time = System.currentTimeMillis(); - waypointStatsBuilder.pauseAt(time); - - // Override the duration - it's not the duration from the last waypoint, but - // the duration from the beginning of the whole track - waypoint.setDuration(time - statsBuilder.getStatistics().getStartTime()); - - // Set the rest of the waypoint data - waypoint.setType(Waypoint.TYPE_STATISTICS); - String name; - if (request.getName() != null) { - name = request.getName(); - } else { - int nextMarkerNumber = providerUtils.getNextMarkerNumber(recordingTrackId, true); - if (nextMarkerNumber == -1) { - nextMarkerNumber = 0; - } - name = getString(R.string.marker_split_name_format, nextMarkerNumber); - } - waypoint.setName(name); - waypoint.setTripStatistics(waypointStatsBuilder.getStatistics()); - waypoint.setDescription(descriptionGenerator.generateWaypointDescription(waypoint)); - waypoint.setIcon(getString(R.string.marker_statistics_icon_url)); - - waypoint.setStartId(providerUtils.getLastLocationId(recordingTrackId)); - - // Create a new stats keeper for the next marker. - waypointStatsBuilder = new TripStatisticsBuilder(time); - } - - private void endCurrentTrack() { - Log.d(TAG, "TrackRecordingService.endCurrentTrack"); - if (!isTrackInProgress()) { - return; - } - - announcementExecutor.shutdown(); - splitExecutor.shutdown(); - isRecording = false; - Track recordedTrack = providerUtils.getTrack(recordingTrackId); - if (recordedTrack != null) { - TripStatistics stats = recordedTrack.getTripStatistics(); - stats.setStopTime(System.currentTimeMillis()); - stats.setTotalTime(stats.getStopTime() - stats.getStartTime()); - long lastRecordedLocationId = - providerUtils.getLastLocationId(recordingTrackId); - ContentValues values = new ContentValues(); - if (lastRecordedLocationId >= 0 && recordedTrack.getStopId() >= 0) { - values.put(TracksColumns.STOPID, lastRecordedLocationId); - } - values.put(TracksColumns.STOPTIME, stats.getStopTime()); - values.put(TracksColumns.TOTALTIME, stats.getTotalTime()); - getContentResolver().update(TracksColumns.CONTENT_URI, values, - "_id=" + recordedTrack.getId(), null); - } - showNotification(); - long recordedTrackId = recordingTrackId; - recordingTrackId = -1L; - PreferencesUtils.setLong(this, R.string.recording_track_id_key, recordingTrackId); - - if (sensorManager != null) { - SensorManagerFactory.releaseSystemSensorManager(); - sensorManager = null; - } - - releaseWakeLock(); - - // Notify the world that we're no longer recording. - sendTrackBroadcast( - R.string.track_stopped_broadcast_action, recordedTrackId); - - stopSelf(); - } - - private void sendTrackBroadcast(int actionResId, long trackId) { - Intent broadcastIntent = new Intent() - .setAction(getString(actionResId)) + private void sendTrackBroadcast(int actionId, long trackId) { + Intent intent = new Intent().setAction(getString(actionId)) .putExtra(getString(R.string.track_id_broadcast_extra), trackId); - sendBroadcast(broadcastIntent, getString(R.string.permission_notification_value)); + sendBroadcast(intent, getString(R.string.permission_notification_value)); if (PreferencesUtils.getBoolean( this, R.string.allow_access_key, PreferencesUtils.ALLOW_ACCESS_DEFAULT)) { - sendBroadcast(broadcastIntent, getString(R.string.broadcast_notifications_permission)); + sendBroadcast(intent, getString(R.string.broadcast_notifications_permission)); } } - /* - * Data/state access. - */ - - private Track getRecordingTrack() { - if (recordingTrackId < 0) { - return null; - } - - return providerUtils.getTrack(recordingTrackId); - } - - public boolean isRecording() { - return isRecording; - } - - public TripStatistics getTripStatistics() { - return statsBuilder.getStatistics(); - } - - Location getLastLocation() { - return lastLocation; - } - - long getRecordingTrackId() { - return recordingTrackId; - } - - void setRecordingTrackId(long recordingTrackId) { - this.recordingTrackId = recordingTrackId; - } - - void setMaxRecordingDistance(int maxRecordingDistance) { - this.maxRecordingDistance = maxRecordingDistance; - } - - void setMinRecordingDistance(int minRecordingDistance) { - this.minRecordingDistance = minRecordingDistance; - if (statsBuilder != null && waypointStatsBuilder != null) { - statsBuilder.setMinRecordingDistance(minRecordingDistance); - waypointStatsBuilder.setMinRecordingDistance(minRecordingDistance); - } - } - - void setMinRequiredAccuracy(int minRequiredAccuracy) { - this.minRequiredAccuracy = minRequiredAccuracy; - } - - void setLocationListenerPolicy(LocationListenerPolicy locationListenerPolicy) { - this.locationListenerPolicy = locationListenerPolicy; - } - - void setAutoResumeTrackTimeout(int autoResumeTrackTimeout) { - this.autoResumeTrackTimeout = autoResumeTrackTimeout; - } - - void setAnnouncementFrequency(int announcementFrequency) { - announcementExecutor.setTaskFrequency(announcementFrequency); - } - - void setSplitFrequency(int frequency) { - splitExecutor.setTaskFrequency(frequency); - } - - void setMetricUnits(boolean metric) { - announcementExecutor.setMetricUnits(metric); - splitExecutor.setMetricUnits(metric); - } - - /** - * TODO: There is a bug in Android that leaks Binder instances. This bug is + * TODO: There is a bug in Android that leaks Binder instances. This bug is * especially visible if we have a non-static class, as there is no way to - * nullify reference to the outer class (the service). - * A workaround is to use a static class and explicitly clear service - * and detach it from the underlying Binder. With this approach, we minimize - * the leak to 24 bytes per each service instance. - * - * For more details, see the following bug: + * nullify reference to the outer class (the service). A workaround is to use + * a static class and explicitly clear service and detach it from the + * underlying Binder. With this approach, we minimize the leak to 24 bytes per + * each service instance. For more details, see the following bug: * http://code.google.com/p/android/issues/detail?id=6426. */ private static class ServiceBinder extends ITrackRecordingService.Stub { - private TrackRecordingService service; + private TrackRecordingService trackRecordingService; private DeathRecipient deathRecipient; - public ServiceBinder(TrackRecordingService service) { - this.service = service; + public ServiceBinder(TrackRecordingService trackRecordingService) { + this.trackRecordingService = trackRecordingService; } - // Logic for letting the actual service go up and down. - @Override public boolean isBinderAlive() { - // Pretend dead if the service went down. - return service != null; + return trackRecordingService != null; } @Override @@ -1188,31 +1023,84 @@ public class TrackRecordingService extends Service { if (!isBinderAlive()) { return false; } - deathRecipient = null; return true; } - /** - * Clears the reference to the outer class to minimize the leak. - */ - private void detachFromService() { - this.service = null; - attachInterface(null, null); - - if (deathRecipient != null) { - deathRecipient.binderDied(); + @Override + public boolean isRecording() { + if (!canAccess()) { + return false; } + return trackRecordingService.isRecording(); } - /** - * Checks if the service is available. If not, throws an - * {@link IllegalStateException}. - */ - private void checkService() { - if (service == null) { - throw new IllegalStateException("The service has been already detached!"); + @Override + public long getRecordingTrackId() { + if (!canAccess()) { + return -1L; } + return trackRecordingService.recordingTrackId; + } + + @Override + public long startNewTrack() { + if (!canAccess()) { + return -1L; + } + return trackRecordingService.startNewTrack(); + } + + @Override + public long insertWaypoint(WaypointCreationRequest waypointCreationRequest) { + if (!canAccess()) { + return -1L; + } + return trackRecordingService.insertWaypoint(waypointCreationRequest); + } + + @Override + public void endCurrentTrack() { + if (!canAccess()) { + return; + } + trackRecordingService.endCurrentTrack(); + } + + @Override + public void recordLocation(Location location) { + if (!canAccess()) { + return; + } + trackRecordingService.locationListener.onLocationChanged(location); + } + + @Override + public byte[] getSensorData() { + if (!canAccess()) { + return null; + } + if (trackRecordingService.sensorManager == null) { + Log.d(TAG, "sensorManager is null."); + return null; + } + if (trackRecordingService.sensorManager.getSensorDataSet() == null) { + Log.d(TAG, "Sensor data set is null."); + return null; + } + return trackRecordingService.sensorManager.getSensorDataSet().toByteArray(); + } + + @Override + public int getSensorState() { + if (!canAccess()) { + return Sensor.SensorState.NONE.getNumber(); + } + if (trackRecordingService.sensorManager == null) { + Log.d(TAG, "sensorManager is null."); + return Sensor.SensorState.NONE.getNumber(); + } + return trackRecordingService.sensorManager.getSensorState().getNumber(); } /** @@ -1222,97 +1110,28 @@ public class TrackRecordingService extends Service { */ private boolean canAccess() { // As a precondition for access, must check if the service is available. - checkService(); + if (trackRecordingService == null) { + throw new IllegalStateException("The track recording service has been detached!"); + } if (Process.myPid() == Binder.getCallingPid()) { return true; } else { - return PreferencesUtils.getBoolean( - service, R.string.allow_access_key, PreferencesUtils.ALLOW_ACCESS_DEFAULT); + return PreferencesUtils.getBoolean(trackRecordingService, R.string.allow_access_key, + PreferencesUtils.ALLOW_ACCESS_DEFAULT); } } - // Service method delegates. - - @Override - public boolean isRecording() { - if (!canAccess()) { - return false; - } - return service.isRecording(); - } - - @Override - public long getRecordingTrackId() { - if (!canAccess()) { - return -1L; - } - return service.recordingTrackId; - } - - @Override - public long startNewTrack() { - if (!canAccess()) { - return -1L; - } - return service.startNewTrack(); - } - /** - * Inserts a waypoint marker in the track being recorded. - * - * @param request Details of the waypoint to insert - * @return the unique ID of the inserted marker + * Detaches from the track recording service. Clears the reference to the + * outer class to minimize the leak. */ - public long insertWaypoint(WaypointCreationRequest request) { - if (!canAccess()) { - return -1L; - } - return service.insertWaypoint(request); - } + private void detachFromService() { + trackRecordingService = null; + attachInterface(null, null); - @Override - public void endCurrentTrack() { - if (!canAccess()) { - return; + if (deathRecipient != null) { + deathRecipient.binderDied(); } - service.endCurrentTrack(); - } - - @Override - public void recordLocation(Location loc) { - if (!canAccess()) { - return; - } - service.locationListener.onLocationChanged(loc); - } - - @Override - public byte[] getSensorData() { - if (!canAccess()) { - return null; - } - if (service.sensorManager == null) { - Log.d(TAG, "No sensor manager for data."); - return null; - } - if (service.sensorManager.getSensorDataSet() == null) { - Log.d(TAG, "Sensor data set is null."); - return null; - } - return service.sensorManager.getSensorDataSet().toByteArray(); - } - - @Override - public int getSensorState() { - if (!canAccess()) { - return Sensor.SensorState.NONE.getNumber(); - } - if (service.sensorManager == null) { - Log.d(TAG, "No sensor manager for data."); - return Sensor.SensorState.NONE.getNumber(); - } - return service.sensorManager.getSensorState().getNumber(); } } - } diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/tasks/AnnouncementPeriodicTask.java b/MyTracks/src/com/google/android/apps/mytracks/services/tasks/AnnouncementPeriodicTask.java new file mode 100644 index 000000000..6bc8086b9 --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/services/tasks/AnnouncementPeriodicTask.java @@ -0,0 +1,298 @@ +/* + * Copyright 2009 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.android.apps.mytracks.services.tasks; + +import com.google.android.apps.mytracks.services.TrackRecordingService; +import com.google.android.apps.mytracks.stats.TripStatistics; +import com.google.android.apps.mytracks.util.PreferencesUtils; +import com.google.android.apps.mytracks.util.StringUtils; +import com.google.android.apps.mytracks.util.UnitConversions; +import com.google.android.maps.mytracks.R; +import com.google.common.annotations.VisibleForTesting; + +import android.content.Context; +import android.speech.tts.TextToSpeech; +import android.speech.tts.TextToSpeech.OnInitListener; +import android.telephony.PhoneStateListener; +import android.telephony.TelephonyManager; +import android.util.Log; + +import java.util.Locale; + +/** + * This class will periodically announce the user's trip statistics. + * + * @author Sandor Dornbush + */ +public class AnnouncementPeriodicTask implements PeriodicTask { + + /** + * The rate at which announcements are spoken. + */ + @VisibleForTesting + static final float TTS_SPEECH_RATE = 0.9f; + + private static final String TAG = AnnouncementPeriodicTask.class.getSimpleName(); + private static final long HOUR_TO_MILLISECOND = 60 * 60 * 1000; + + private final Context context; + protected TextToSpeech tts; + + // Response from TTS after its initialization + private int initStatus = TextToSpeech.ERROR; + + // True if TTS engine is ready + private boolean ready = false; + + // True if speech is allowed + private boolean speechAllowed; + + /** + * Listener which updates {@link #speechAllowed} when the phone state changes. + */ + private final PhoneStateListener phoneStateListener = new PhoneStateListener() { + @Override + public void onCallStateChanged(int state, String incomingNumber) { + speechAllowed = state == TelephonyManager.CALL_STATE_IDLE; + if (!speechAllowed && tts != null && tts.isSpeaking()) { + // If we're already speaking, stop it. + tts.stop(); + } + } + }; + + public AnnouncementPeriodicTask(Context context) { + this.context = context; + } + + @Override + public void start() { + if (tts == null) { + tts = newTextToSpeech(context, new OnInitListener() { + @Override + public void onInit(int status) { + initStatus = status; + } + }); + } + speechAllowed = true; + listenToPhoneState(phoneStateListener, PhoneStateListener.LISTEN_CALL_STATE); + } + + @Override + public void run(TrackRecordingService trackRecordingService) { + if (trackRecordingService == null) { + Log.e(TAG, "TrackRecordingService is null."); + return; + } + announce(trackRecordingService.getTripStatistics()); + } + + /** + * Runs this task. + * + * @param tripStatistics the trip statistics + */ + @VisibleForTesting + void announce(TripStatistics tripStatistics) { + if (tripStatistics == null) { + Log.e(TAG, "TripStatistics is null."); + return; + } + + synchronized (this) { + if (!ready) { + ready = initStatus == TextToSpeech.SUCCESS; + if (ready) { + onTtsReady(); + } + } + if (!ready) { + Log.i(TAG, "TTS not ready."); + return; + } + } + + if (!speechAllowed) { + Log.i(TAG, "Speech is not allowed at this time."); + return; + } + speakAnnouncement(getAnnouncement(tripStatistics)); + } + + @Override + public void shutdown() { + listenToPhoneState(phoneStateListener, PhoneStateListener.LISTEN_NONE); + if (tts != null) { + tts.shutdown(); + tts = null; + } + } + + /** + * Called when TTS is ready. + */ + protected void onTtsReady() { + Locale locale = Locale.getDefault(); + int languageAvailability = tts.isLanguageAvailable(locale); + if (languageAvailability == TextToSpeech.LANG_MISSING_DATA + || languageAvailability == TextToSpeech.LANG_NOT_SUPPORTED) { + Log.w(TAG, "Default locale not available, use English."); + locale = Locale.ENGLISH; + /* + * TODO: instead of using english, load the language if missing and show a + * toast if not supported. Not able to change the resource strings to + * English. + */ + } + tts.setLanguage(locale); + + // Slow down the speed just a bit as it is hard to hear when exercising. + tts.setSpeechRate(TTS_SPEECH_RATE); + } + + /** + * Speaks the announcement. + * + * @param announcement the announcement + */ + protected void speakAnnouncement(String announcement) { + tts.speak(announcement, TextToSpeech.QUEUE_FLUSH, null); + } + + /** + * Create a new {@link TextToSpeech}. + * + * @param aContext a context + * @param onInitListener an on init listener + */ + @VisibleForTesting + protected TextToSpeech newTextToSpeech(Context aContext, OnInitListener onInitListener) { + return new TextToSpeech(aContext, onInitListener); + } + + /** + * Gets the announcement. + * + * @param tripStatistics the trip statistics + */ + @VisibleForTesting + protected String getAnnouncement(TripStatistics tripStatistics) { + boolean metricUnits = PreferencesUtils.getBoolean( + context, R.string.metric_units_key, PreferencesUtils.METRIC_UNITS_DEFAULT); + boolean reportSpeed = PreferencesUtils.getBoolean( + context, R.string.report_speed_key, PreferencesUtils.REPORT_SPEED_DEFAULT); + double distance = tripStatistics.getTotalDistance() * UnitConversions.M_TO_KM; + double speed = tripStatistics.getAverageMovingSpeed() * UnitConversions.MS_TO_KMH; + + if (distance == 0) { + return context.getString(R.string.voice_total_distance_zero); + } + + if (!metricUnits) { + distance *= UnitConversions.KM_TO_MI; + speed *= UnitConversions.KM_TO_MI; + } + + String rate; + if (reportSpeed) { + int speedId = metricUnits ? R.plurals.voiceSpeedKilometersPerHour + : R.plurals.voiceSpeedMilesPerHour; + rate = context.getResources().getQuantityString(speedId, getQuantityCount(speed), speed); + } else { + speed = speed == 0 ? 0.0 : 1 / speed; + int paceId = metricUnits ? R.string.voice_pace_per_kilometer : R.string.voice_pace_per_mile; + rate = context.getString(paceId, getAnnounceTime((long) (speed * HOUR_TO_MILLISECOND))); + } + + int totalDistanceId = metricUnits ? R.plurals.voiceTotalDistanceKilometers + : R.plurals.voiceTotalDistanceMiles; + String totalDistance = context.getResources() + .getQuantityString(totalDistanceId, getQuantityCount(distance), distance); + + return context.getString(R.string.voice_template, totalDistance, getAnnounceTime( + tripStatistics.getMovingTime()), rate); + } + + /** + * Listens to phone state. + * + * @param listener the listener + * @param events the interested events + */ + @VisibleForTesting + protected void listenToPhoneState(PhoneStateListener listener, int events) { + TelephonyManager telephony = (TelephonyManager) context.getSystemService( + Context.TELEPHONY_SERVICE); + if (telephony != null) { + telephony.listen(listener, events); + } + } + + /** + * Gets the announce time. + * + * @param time the time + */ + @VisibleForTesting + String getAnnounceTime(long time) { + int[] parts = StringUtils.getTimeParts(time); + String seconds = context.getResources() + .getQuantityString(R.plurals.voiceSeconds, parts[0], parts[0]); + String minutes = context.getResources() + .getQuantityString(R.plurals.voiceMinutes, parts[1], parts[1]); + String hours = context.getResources() + .getQuantityString(R.plurals.voiceHours, parts[2], parts[2]); + StringBuilder sb = new StringBuilder(); + if (parts[2] != 0) { + sb.append(hours); + sb.append(" "); + sb.append(minutes); + } else { + sb.append(minutes); + sb.append(" "); + sb.append(seconds); + } + return sb.toString(); + } + + /** + * Gets the plural count to be used by getQuantityString. getQuantityString + * only supports integer quantities, not a double quantity like "2.2". + *

+ * As a temporary workaround, we convert a double quantity to an integer + * quantity. If the double quantity is exactly 0, 1, or 2, then we can return + * these integer quantities. Otherwise, we cast the double quantity to an + * integer quantity. However, we need to make sure that if the casted value is + * 0, 1, or 2, we don't return those, instead, return the next biggest integer + * 3. + * + * @param d the double value + */ + private int getQuantityCount(double d) { + if (d == 0) { + return 0; + } else if (d == 1) { + return 1; + } else if (d == 2) { + return 2; + } else { + int count = (int) d; + return count < 3 ? 3 : count; + } + } +} diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/tasks/StatusAnnouncerFactory.java b/MyTracks/src/com/google/android/apps/mytracks/services/tasks/AnnouncementPeriodicTaskFactory.java similarity index 74% rename from MyTracks/src/com/google/android/apps/mytracks/services/tasks/StatusAnnouncerFactory.java rename to MyTracks/src/com/google/android/apps/mytracks/services/tasks/AnnouncementPeriodicTaskFactory.java index 7e8b56b37..0c6c7e87b 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/services/tasks/StatusAnnouncerFactory.java +++ b/MyTracks/src/com/google/android/apps/mytracks/services/tasks/AnnouncementPeriodicTaskFactory.java @@ -20,18 +20,14 @@ import com.google.android.apps.mytracks.util.ApiAdapterFactory; import android.content.Context; /** - * Factory which wraps construction and setup of text-to-speech announcements in - * an API-level-safe way. - * + * A {@link PeriodicTaskFactory} for text-to-speech announcement periodic task. + * * @author Rodrigo Damazio */ -public class StatusAnnouncerFactory implements PeriodicTaskFactory { - - public StatusAnnouncerFactory() { - } +public class AnnouncementPeriodicTaskFactory implements PeriodicTaskFactory { @Override public PeriodicTask create(Context context) { - return ApiAdapterFactory.getApiAdapter().getStatusAnnouncerTask(context); + return ApiAdapterFactory.getApiAdapter().getAnnouncementPeriodicTask(context); } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/tasks/Api8StatusAnnouncerTask.java b/MyTracks/src/com/google/android/apps/mytracks/services/tasks/Api8AnnouncementPeriodicTask.java similarity index 68% rename from MyTracks/src/com/google/android/apps/mytracks/services/tasks/Api8StatusAnnouncerTask.java rename to MyTracks/src/com/google/android/apps/mytracks/services/tasks/Api8AnnouncementPeriodicTask.java index fbc640998..d09312886 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/services/tasks/Api8StatusAnnouncerTask.java +++ b/MyTracks/src/com/google/android/apps/mytracks/services/tasks/Api8AnnouncementPeriodicTask.java @@ -16,8 +16,6 @@ package com.google.android.apps.mytracks.services.tasks; -import static com.google.android.apps.mytracks.Constants.TAG; - import android.annotation.TargetApi; import android.content.Context; import android.media.AudioManager; @@ -27,57 +25,57 @@ import android.util.Log; import java.util.HashMap; - /** * This class will periodically announce the user's trip statistics. This class - * will request and release audio focus.
- * For API Level 8 or higher. - * + * will request and release audio focus. + * * @author Sandor Dornbush */ @TargetApi(8) -public class Api8StatusAnnouncerTask extends StatusAnnouncerTask { - private final static HashMap SPEECH_PARAMS = new HashMap(); +public class Api8AnnouncementPeriodicTask extends AnnouncementPeriodicTask { + + private static final String TAG = Api8AnnouncementPeriodicTask.class.getSimpleName(); + private static final HashMap SPEECH_PARAMS = new HashMap(); static { SPEECH_PARAMS.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, "not_used"); } - private final AudioManager audioManager; - private final OnUtteranceCompletedListener utteranceListener = - new OnUtteranceCompletedListener() { - @Override + private final OnUtteranceCompletedListener + utteranceListener = new OnUtteranceCompletedListener() { + @Override public void onUtteranceCompleted(String utteranceId) { int result = audioManager.abandonAudioFocus(null); if (result == AudioManager.AUDIOFOCUS_REQUEST_FAILED) { - Log.w(TAG, "FroyoStatusAnnouncerTask: Failed to relinquish audio focus"); + Log.w(TAG, "Failed to relinquish audio focus."); } } }; - public Api8StatusAnnouncerTask(Context context) { - super(context); + private final AudioManager audioManager; + public Api8AnnouncementPeriodicTask(Context context) { + super(context); audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); } @Override protected void onTtsReady() { super.onTtsReady(); - tts.setOnUtteranceCompletedListener(utteranceListener); } @Override protected synchronized void speakAnnouncement(String announcement) { - int result = audioManager.requestAudioFocus(null, - TextToSpeech.Engine.DEFAULT_STREAM, - AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK); + int result = audioManager.requestAudioFocus( + null, TextToSpeech.Engine.DEFAULT_STREAM, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK); if (result == AudioManager.AUDIOFOCUS_REQUEST_FAILED) { - Log.w(TAG, "FroyoStatusAnnouncerTask: Request for audio focus failed."); + Log.w(TAG, "Failed to request audio focus."); } - // We don't care about the utterance id. - // It is supplied here to force onUtteranceCompleted to be called. + /* + * We don't care about the utterance id. It is supplied here to force + * onUtteranceCompleted to be called. + */ tts.speak(announcement, TextToSpeech.QUEUE_FLUSH, SPEECH_PARAMS); } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/tasks/PeriodicTask.java b/MyTracks/src/com/google/android/apps/mytracks/services/tasks/PeriodicTask.java index 1d351bec4..640e7bc1f 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/services/tasks/PeriodicTask.java +++ b/MyTracks/src/com/google/android/apps/mytracks/services/tasks/PeriodicTask.java @@ -20,7 +20,7 @@ import com.google.android.apps.mytracks.services.TrackRecordingService; /** * This is interface for a task that will be executed on some schedule. - * + * * @author Sandor Dornbush */ public interface PeriodicTask { @@ -28,11 +28,13 @@ public interface PeriodicTask { * Sets up this task for subsequent calls to the run method. */ public void start(); - + /** * This method will be called periodically. + * + * @param trackRecordingService the track recording service */ - public void run(TrackRecordingService service); + public void run(TrackRecordingService trackRecordingService); /** * Shuts down this task and clean up resources. diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/tasks/PeriodicTaskExecutor.java b/MyTracks/src/com/google/android/apps/mytracks/services/tasks/PeriodicTaskExecutor.java index 228ca27c8..14a92c684 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/services/tasks/PeriodicTaskExecutor.java +++ b/MyTracks/src/com/google/android/apps/mytracks/services/tasks/PeriodicTaskExecutor.java @@ -15,119 +15,167 @@ */ package com.google.android.apps.mytracks.services.tasks; -import static com.google.android.apps.mytracks.Constants.TAG; - import com.google.android.apps.mytracks.services.TrackRecordingService; +import com.google.android.apps.mytracks.stats.TripStatistics; import com.google.android.apps.mytracks.util.PreferencesUtils; import com.google.android.apps.mytracks.util.UnitConversions; import android.util.Log; /** - * Execute a task on a time or distance schedule. - * + * Execute a periodic task on a time or distance schedule. + * * @author Sandor Dornbush */ public class PeriodicTaskExecutor { + private static final String TAG = PeriodicTaskExecutor.class.getSimpleName(); + private static final long MINUTE_TO_MILLISECONDS = 60000L; + + private final TrackRecordingService trackRecordingService; + private final PeriodicTaskFactory periodicTaskFactory; + /** - * The frequency of the task. - * A value greater than zero is a frequency in time. - * A value less than zero is considered a frequency in distance. + * The task frequency. A positive value is a time frequency (minutes). A + * negative value is a distance frequency (km or mi). A zero value is to turn + * off periodic task. */ private int taskFrequency = PreferencesUtils.FREQUENCY_OFF; - /** - * The next distance when the task should execute. - */ - private double nextTaskDistance = 0; + private PeriodicTask periodicTask; - /** - * Time based executor. - */ - private TimerTaskExecutor timerExecutor = null; + // Time periodic task executor + private TimerTaskExecutor timerTaskExecutor = null; private boolean metricUnits; - private final TrackRecordingService service; + // The next distance for the distance periodic task + private double nextTaskDistance = Double.MAX_VALUE; - private final PeriodicTaskFactory factory; - - private PeriodicTask task; - - public PeriodicTaskExecutor(TrackRecordingService service, PeriodicTaskFactory factory) { - this.service = service; - this.factory = factory; + public PeriodicTaskExecutor( + TrackRecordingService trackRecordingService, PeriodicTaskFactory periodicTaskFactory) { + this.trackRecordingService = trackRecordingService; + this.periodicTaskFactory = periodicTaskFactory; } /** - * Restores the manager. + * Restores the executor. */ public void restore() { - // TODO: Decouple service from this class once and forever. - if (!service.isRecording()) { + if (!trackRecordingService.isRecording()) { + Log.d(TAG, "Not recording."); return; } if (!isTimeFrequency()) { - if (timerExecutor != null) { - timerExecutor.shutdown(); - timerExecutor = null; + if (timerTaskExecutor != null) { + timerTaskExecutor.shutdown(); + timerTaskExecutor = null; } } if (taskFrequency == PreferencesUtils.FREQUENCY_OFF) { + Log.d(TAG, "Task frequency is off."); return; } - // Try to make the task. - task = factory.create(service); - // Returning null is ok. - if (task == null) { + periodicTask = periodicTaskFactory.create(trackRecordingService); + + // Returning null is ok + if (periodicTask == null) { + Log.d(TAG, "Peridoic task is null."); return; } - task.start(); + periodicTask.start(); if (isTimeFrequency()) { - if (timerExecutor == null) { - timerExecutor = new TimerTaskExecutor(task, service); + if (timerTaskExecutor == null) { + timerTaskExecutor = new TimerTaskExecutor(periodicTask, trackRecordingService); } - timerExecutor.scheduleTask(taskFrequency * 60000L); + timerTaskExecutor.scheduleTask(taskFrequency * MINUTE_TO_MILLISECONDS); } else { - // For distance based splits. + // For distance periodic task calculateNextTaskDistance(); } } /** - * Shuts down the manager. + * Shuts down the executor. */ public void shutdown() { - if (task != null) { - task.shutdown(); - task = null; + if (periodicTask != null) { + periodicTask.shutdown(); + periodicTask = null; } - if (timerExecutor != null) { - timerExecutor.shutdown(); - timerExecutor = null; + if (timerTaskExecutor != null) { + timerTaskExecutor.shutdown(); + timerTaskExecutor = null; } } /** - * Calculates the next distance when the task should execute. + * Updates the executor. */ - void calculateNextTaskDistance() { - // TODO: Decouple service from this class once and forever. - if (!service.isRecording() || task == null) { + public void update() { + if (!isDistanceFrequency() || periodicTask == null) { + return; + } + TripStatistics tripStatistics = trackRecordingService.getTripStatistics(); + if (tripStatistics == null) { + return; + } + double distance = tripStatistics.getTotalDistance() + * UnitConversions.M_TO_KM; + if (!metricUnits) { + distance *= UnitConversions.KM_TO_MI; + } + + if (distance > nextTaskDistance) { + periodicTask.run(trackRecordingService); + calculateNextTaskDistance(); + } + } + + /** + * Sets task frequency. + * + * @param taskFrequency the task frequency + */ + public void setTaskFrequency(int taskFrequency) { + this.taskFrequency = taskFrequency; + restore(); + } + + /** + * Sets metricUnits. + * + * @param metricUnits true to use metric units + */ + public void setMetricUnits(boolean metricUnits) { + this.metricUnits = metricUnits; + calculateNextTaskDistance(); + } + + /** + * Calculates the next distance for the distance periodic task. + */ + private void calculateNextTaskDistance() { + if (!trackRecordingService.isRecording() || periodicTask == null) { return; } + TripStatistics tripStatistics = trackRecordingService.getTripStatistics(); + if (tripStatistics == null) { + return; + } + if (!isDistanceFrequency()) { nextTaskDistance = Double.MAX_VALUE; Log.d(TAG, "SplitManager: Distance splits disabled."); return; } - double distance = service.getTripStatistics().getTotalDistance() * UnitConversions.M_TO_KM; + double distance = tripStatistics.getTotalDistance() + * UnitConversions.M_TO_KM; if (!metricUnits) { distance *= UnitConversions.KM_TO_MI; } @@ -135,56 +183,19 @@ public class PeriodicTaskExecutor { int index = (int) (distance / taskFrequency); index -= 1; nextTaskDistance = taskFrequency * index; - Log.d(TAG, "SplitManager: Next split distance: " + nextTaskDistance); } /** - * Updates executer with new trip statistics. + * True if time frequency. */ - public void update() { - if (!isDistanceFrequency() || task == null) { - return; - } - // Convert the distance in meters to km or mi. - double distance = service.getTripStatistics().getTotalDistance() * UnitConversions.M_TO_KM; - if (!metricUnits) { - distance *= UnitConversions.KM_TO_MI; - } - - if (distance > nextTaskDistance) { - task.run(service); - calculateNextTaskDistance(); - } - } - private boolean isTimeFrequency() { return taskFrequency > 0; } + /** + * True if distance frequency. + */ private boolean isDistanceFrequency() { return taskFrequency < 0; } - - /** - * Sets the task frequency. - * < 0 Use the absolute value as a distance in the current measurement km - * or mi - * 0 Turn off the task - * > 0 Use the value as a time in minutes - * @param taskFrequency The frequency in time or distance - */ - public void setTaskFrequency(int taskFrequency) { - Log.d(TAG, "setTaskFrequency: taskFrequency = " + taskFrequency); - this.taskFrequency = taskFrequency; - restore(); - } - - public void setMetricUnits(boolean metricUnits) { - this.metricUnits = metricUnits; - calculateNextTaskDistance(); - } - - double getNextTaskDistance() { - return nextTaskDistance; - } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/tasks/PeriodicTaskFactory.java b/MyTracks/src/com/google/android/apps/mytracks/services/tasks/PeriodicTaskFactory.java index f3f8be445..32e191022 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/services/tasks/PeriodicTaskFactory.java +++ b/MyTracks/src/com/google/android/apps/mytracks/services/tasks/PeriodicTaskFactory.java @@ -13,21 +13,22 @@ * License for the specific language governing permissions and limitations under * the License. */ + package com.google.android.apps.mytracks.services.tasks; import android.content.Context; /** - * An interface for classes that can create periodic tasks. - * + * An interface for classes that can create {@link PeriodicTask}. + * * @author Sandor Dornbush */ public interface PeriodicTaskFactory { /** - * Creates a periodic task which does voice announcements. - * - * @return the task, or null if task is not supported + * Creates a {@link PeriodicTask}. + * + * @return the task, or null if the task is not supported */ - PeriodicTask create(Context context); + public PeriodicTask create(Context context); } \ No newline at end of file diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/tasks/SplitTask.java b/MyTracks/src/com/google/android/apps/mytracks/services/tasks/SplitPeriodicTask.java similarity index 65% rename from MyTracks/src/com/google/android/apps/mytracks/services/tasks/SplitTask.java rename to MyTracks/src/com/google/android/apps/mytracks/services/tasks/SplitPeriodicTask.java index 763a922be..7d291df77 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/services/tasks/SplitTask.java +++ b/MyTracks/src/com/google/android/apps/mytracks/services/tasks/SplitPeriodicTask.java @@ -19,39 +19,21 @@ package com.google.android.apps.mytracks.services.tasks; import com.google.android.apps.mytracks.content.WaypointCreationRequest; import com.google.android.apps.mytracks.services.TrackRecordingService; -import android.content.Context; - - /** * A simple task to insert statistics markers periodically. + * * @author Sandor Dornbush */ -public class SplitTask implements PeriodicTask { +public class SplitPeriodicTask implements PeriodicTask { - private SplitTask() { + @Override + public void start() {} + + @Override + public void run(TrackRecordingService trackRecordingService) { + trackRecordingService.insertWaypoint(WaypointCreationRequest.DEFAULT_STATISTICS); } @Override - public void run(TrackRecordingService service) { - service.insertWaypoint(WaypointCreationRequest.DEFAULT_STATISTICS); - } - - @Override - public void shutdown() { - } - - @Override - public void start() { - } - - /** - * Create new SplitTasks. - */ - public static class Factory implements PeriodicTaskFactory { - - @Override - public PeriodicTask create(Context context) { - return new SplitTask(); - } - } + public void shutdown() {} } diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/tasks/SplitPeriodicTaskFactory.java b/MyTracks/src/com/google/android/apps/mytracks/services/tasks/SplitPeriodicTaskFactory.java new file mode 100644 index 000000000..93c0f6cc5 --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/services/tasks/SplitPeriodicTaskFactory.java @@ -0,0 +1,32 @@ +/* + * Copyright 2012 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.android.apps.mytracks.services.tasks; + +import android.content.Context; + +/** + * A {@link PeriodicTaskFactory} for {@link SplitPeriodicTask}. + * + * @author Jimmy Shih + */ +public class SplitPeriodicTaskFactory implements PeriodicTaskFactory { + + @Override + public PeriodicTask create(Context context) { + return new SplitPeriodicTask(); + } +} \ No newline at end of file diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/tasks/StatusAnnouncerTask.java b/MyTracks/src/com/google/android/apps/mytracks/services/tasks/StatusAnnouncerTask.java deleted file mode 100644 index 36667ae80..000000000 --- a/MyTracks/src/com/google/android/apps/mytracks/services/tasks/StatusAnnouncerTask.java +++ /dev/null @@ -1,356 +0,0 @@ -/* - * Copyright 2009 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.android.apps.mytracks.services.tasks; - -import static com.google.android.apps.mytracks.Constants.TAG; - -import com.google.android.apps.mytracks.Constants; -import com.google.android.apps.mytracks.services.TrackRecordingService; -import com.google.android.apps.mytracks.stats.TripStatistics; -import com.google.android.apps.mytracks.util.PreferencesUtils; -import com.google.android.apps.mytracks.util.StringUtils; -import com.google.android.apps.mytracks.util.UnitConversions; -import com.google.android.maps.mytracks.R; -import com.google.common.annotations.VisibleForTesting; - -import android.content.Context; -import android.speech.tts.TextToSpeech; -import android.speech.tts.TextToSpeech.OnInitListener; -import android.telephony.PhoneStateListener; -import android.telephony.TelephonyManager; -import android.util.Log; - -import java.util.Locale; - -/** - * This class will periodically announce the user's trip statistics. - * - * @author Sandor Dornbush - */ -public class StatusAnnouncerTask implements PeriodicTask { - - /** - * The rate at which announcements are spoken. - */ - // @VisibleForTesting - static final float TTS_SPEECH_RATE = 0.9f; - - /** - * A pointer to the service context. - */ - private final Context context; - - /** - * The interface to the text to speech engine. - */ - protected TextToSpeech tts; - - /** - * The response received from the TTS engine after initialization. - */ - private int initStatus = TextToSpeech.ERROR; - - /** - * Whether the TTS engine is ready. - */ - private boolean ready = false; - - /** - * Whether we're allowed to speak right now. - */ - private boolean speechAllowed; - - /** - * Listener which updates {@link #speechAllowed} when the phone state changes. - */ - private final PhoneStateListener phoneListener = new PhoneStateListener() { - @Override - public void onCallStateChanged(int state, String incomingNumber) { - speechAllowed = state == TelephonyManager.CALL_STATE_IDLE; - - if (!speechAllowed && tts != null && tts.isSpeaking()) { - // If we're already speaking, stop it. - tts.stop(); - } - } - }; - - public StatusAnnouncerTask(Context context) { - this.context = context; - } - - /** - * {@inheritDoc} - * - * Announces the trip status. - */ - @Override - public void run(TrackRecordingService service) { - if (service == null) { - Log.e(TAG, "StatusAnnouncer TrackRecordingService not initialized"); - return; - } - - runWithStatistics(service.getTripStatistics()); - } - - /** - * This method exists as a convenience for testing code, allowing said code - * to avoid needing to instantiate an entire {@link TrackRecordingService} - * just to test the announcer. - */ - // @VisibleForTesting - void runWithStatistics(TripStatistics statistics) { - if (statistics == null) { - Log.e(TAG, "StatusAnnouncer stats not initialized."); - return; - } - - synchronized (this) { - checkReady(); - if (!ready) { - Log.e(TAG, "StatusAnnouncer Tts not ready."); - return; - } - } - - if (!speechAllowed) { - Log.i(Constants.TAG, - "Not making announcement - not allowed at this time"); - return; - } - - String announcement = getAnnouncement(statistics); - Log.d(Constants.TAG, "Announcement: " + announcement); - speakAnnouncement(announcement); - } - - protected void speakAnnouncement(String announcement) { - tts.speak(announcement, TextToSpeech.QUEUE_FLUSH, null); - } - - /** - * Builds the announcement string. - * - * @return The string that will be read to the user - */ - // @VisibleForTesting - protected String getAnnouncement(TripStatistics stats) { - boolean metricUnits = PreferencesUtils.getBoolean( - context, R.string.metric_units_key, PreferencesUtils.METRIC_UNITS_DEFAULT); - boolean reportSpeed = PreferencesUtils.getBoolean( - context, R.string.report_speed_key, PreferencesUtils.REPORT_SPEED_DEFAULT); - double d = stats.getTotalDistance() * UnitConversions.M_TO_KM; - double s = stats.getAverageMovingSpeed() * UnitConversions.MS_TO_KMH; - - if (d == 0) { - return context.getString(R.string.voice_total_distance_zero); - } - - if (!metricUnits) { - d *= UnitConversions.KM_TO_MI; - s *= UnitConversions.KM_TO_MI; - } - - if (!reportSpeed) { - s = 3600000.0 / s; // converts from speed to pace - } - - // Makes sure s is not NaN. - if (Double.isNaN(s)) { - s = 0; - } - - String speed; - if (reportSpeed) { - int speedId = metricUnits ? R.plurals.voiceSpeedKilometersPerHour - : R.plurals.voiceSpeedMilesPerHour; - speed = context.getResources().getQuantityString(speedId, getQuantityCount(s), s); - } else { - int paceId = metricUnits ? R.string.voice_pace_per_kilometer : R.string.voice_pace_per_mile; - speed = context.getString(paceId, getAnnounceTime((long) s)); - } - - int totalDistanceId = metricUnits ? R.plurals.voiceTotalDistanceKilometers - : R.plurals.voiceTotalDistanceMiles; - String totalDistance = context.getResources().getQuantityString( - totalDistanceId, getQuantityCount(d), d); - - return context.getString( - R.string.voice_template, totalDistance, getAnnounceTime(stats.getMovingTime()), speed); - } - - /** - * Gets the plural count to be used by getQuantityString. getQuantityString - * only supports integer quantities, not a double quantity like "2.2". - *

- * As a temporary workaround, we convert a double quantity to an integer - * quantity. If the double quantity is exactly 0, 1, or 2, then we can return - * these integer quantities. Otherwise, we cast the double quantity to an - * integer quantity. However, we need to make sure that if the casted value is - * 0, 1, or 2, we don't return those, instead, return the next biggest integer - * 3. - * - * @param d the double value - */ - private int getQuantityCount(double d) { - if (d == 0) { - return 0; - } else if (d == 1) { - return 1; - } else if (d == 2) { - return 2; - } else { - int count = (int) d; - return count < 3 ? 3 : count; - } - } - - @Override - public void start() { - Log.i(Constants.TAG, "Starting TTS"); - if (tts == null) { - // We can't have this class also be the listener, otherwise it's unsafe to - // reference it in Cupcake (even if we don't instantiate it). - tts = newTextToSpeech(context, new OnInitListener() { - @Override - public void onInit(int status) { - onTtsInit(status); - } - }); - } - speechAllowed = true; - - // Register ourselves as a listener so we won't speak during a call. - listenToPhoneState(phoneListener, PhoneStateListener.LISTEN_CALL_STATE); - } - - /** - * Called when the TTS engine is initialized. - */ - private void onTtsInit(int status) { - Log.i(TAG, "TrackRecordingService.TTS init: " + status); - synchronized (this) { - // TTS should be valid here but NPE exceptions were reported to the market. - initStatus = status; - checkReady(); - } - } - - /** - * Ensures that the TTS is ready (finishing its initialization if needed). - */ - private void checkReady() { - synchronized (this) { - if (ready) { - // Already done; - return; - } - - ready = initStatus == TextToSpeech.SUCCESS && tts != null; - Log.d(TAG, "Status announcer ready: " + ready); - - if (ready) { - onTtsReady(); - } - } - } - - /** - * Finishes the TTS engine initialization. - * Called once (and only once) when the TTS engine is ready. - */ - protected void onTtsReady() { - // Force the language to be the same as the string we will be speaking, - // if that's available. - Locale speechLanguage = Locale.getDefault(); - int languageAvailability = tts.isLanguageAvailable(speechLanguage); - if (languageAvailability == TextToSpeech.LANG_MISSING_DATA || - languageAvailability == TextToSpeech.LANG_NOT_SUPPORTED) { - // English is probably supported. - // TODO: Somehow use announcement strings from English too. - Log.w(TAG, "Default language not available, using English."); - speechLanguage = Locale.ENGLISH; - } - tts.setLanguage(speechLanguage); - - // Slow down the speed just a bit as it is hard to hear when exercising. - tts.setSpeechRate(TTS_SPEECH_RATE); - } - - @Override - public void shutdown() { - // Stop listening to phone state. - listenToPhoneState(phoneListener, PhoneStateListener.LISTEN_NONE); - - if (tts != null) { - tts.shutdown(); - tts = null; - } - - Log.i(Constants.TAG, "TTS shut down"); - } - - /** - * Wrapper for instantiating a {@link TextToSpeech} object, which causes - * several issues during testing. - */ - // @VisibleForTesting - protected TextToSpeech newTextToSpeech(Context ctx, OnInitListener onInitListener) { - return new TextToSpeech(ctx, onInitListener); - } - - /** - * Wrapper for calls to the 100%-unmockable {@link TelephonyManager#listen}. - */ - // @VisibleForTesting - protected void listenToPhoneState(PhoneStateListener listener, int events) { - TelephonyManager telephony = - (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); - if (telephony != null) { - telephony.listen(listener, events); - } - } - - /** - * Gets a string to announce the time. - * - * @param time the time - */ - @VisibleForTesting - String getAnnounceTime(long time) { - int[] parts = StringUtils.getTimeParts(time); - String seconds = context.getResources().getQuantityString( - R.plurals.voiceSeconds, parts[0], parts[0]); - String minutes = context.getResources().getQuantityString( - R.plurals.voiceMinutes, parts[1], parts[1]); - String hours = context.getResources().getQuantityString( - R.plurals.voiceHours, parts[2], parts[2]); - - StringBuilder sb = new StringBuilder(); - if (parts[2] != 0) { - sb.append(hours); - sb.append(" "); - sb.append(minutes); - } else { - sb.append(minutes); - sb.append(" "); - sb.append(seconds); - } - return sb.toString(); - } -} diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/tasks/TimerTaskExecutor.java b/MyTracks/src/com/google/android/apps/mytracks/services/tasks/TimerTaskExecutor.java index ec7d66c05..4d9364de7 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/services/tasks/TimerTaskExecutor.java +++ b/MyTracks/src/com/google/android/apps/mytracks/services/tasks/TimerTaskExecutor.java @@ -16,46 +16,46 @@ package com.google.android.apps.mytracks.services.tasks; -import static com.google.android.apps.mytracks.Constants.TAG; - -import android.util.Log; +import com.google.android.apps.mytracks.services.TrackRecordingService; +import com.google.android.apps.mytracks.stats.TripStatistics; import java.util.Date; import java.util.Timer; import java.util.TimerTask; -import com.google.android.apps.mytracks.services.TrackRecordingService; - /** * This class will periodically perform a task. - * + * * @author Sandor Dornbush */ public class TimerTaskExecutor { - private final PeriodicTask task; - private final TrackRecordingService service; + private final PeriodicTask periodicTask; + private final TrackRecordingService trackRecordingService; /** - * A timer to schedule the announcements. - * This is non-null if the task is in started (scheduled) state. + * A timer to schedule the announcements. This is non-null if the task is in + * started (scheduled) state. */ private Timer timer; - public TimerTaskExecutor(PeriodicTask task, - TrackRecordingService service) { - this.task = task; - this.service = service; + public TimerTaskExecutor(PeriodicTask periodicTask, TrackRecordingService trackRecordingService) { + this.periodicTask = periodicTask; + this.trackRecordingService = trackRecordingService; } /** - * Schedules the task at the given interval. - * - * @param interval The interval in milliseconds + * Schedules the periodic task at an interval. + * + * @param interval the interval in milliseconds */ public void scheduleTask(long interval) { - // TODO: Decouple service from this class once and forever. - if (!service.isRecording()) { + if (!trackRecordingService.isRecording()) { + return; + } + + TripStatistics tripStatistics = trackRecordingService.getTripStatistics(); + if (tripStatistics == null) { return; } @@ -64,7 +64,7 @@ public class TimerTaskExecutor { timer.purge(); } else { // First start, or we were previously shut down. - task.start(); + periodicTask.start(); } timer = new Timer(); @@ -73,27 +73,22 @@ public class TimerTaskExecutor { } long now = System.currentTimeMillis(); - long next = service.getTripStatistics().getStartTime(); + long next = tripStatistics.getStartTime(); if (next < now) { next = now + interval - ((now - next) % interval); } - - Date start = new Date(next); - Log.i(TAG, task.getClass().getSimpleName() + " scheduled to start at " + start - + " every " + interval + " milliseconds."); - timer.scheduleAtFixedRate(new PeriodicTimerTask(), start, interval); + timer.scheduleAtFixedRate(new PeriodicTimerTask(), new Date(next), interval); } /** - * Cleans up this object. + * Shuts down. */ public void shutdown() { - Log.i(TAG, task.getClass().getSimpleName() + " shutting down."); if (timer != null) { timer.cancel(); timer.purge(); timer = null; - task.shutdown(); + periodicTask.shutdown(); } } @@ -101,9 +96,10 @@ public class TimerTaskExecutor { * The timer task to announce the trip status. */ private class PeriodicTimerTask extends TimerTask { + @Override public void run() { - task.run(service); + periodicTask.run(trackRecordingService); } } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/stats/DoubleBuffer.java b/MyTracks/src/com/google/android/apps/mytracks/stats/DoubleBuffer.java index 356664617..7c86f0ba8 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/stats/DoubleBuffer.java +++ b/MyTracks/src/com/google/android/apps/mytracks/stats/DoubleBuffer.java @@ -20,66 +20,35 @@ package com.google.android.apps.mytracks.stats; * This class maintains a buffer of doubles. This buffer is a convenient class * for storing a series of doubles and calculating information about them. This * is a FIFO buffer. - * + * * @author Sandor Dornbush */ public class DoubleBuffer { - /** - * The location that the next write will occur at. - */ + // The location that the next write will occur at. private int index; - /** - * The sliding buffer of doubles. - */ + // The sliding buffer of doubles. private final double[] buffer; - /** - * Have all of the slots in the buffer been filled? - */ + // True if the buffer is full private boolean isFull; /** - * Creates a buffer with size elements. - * - * @param size the number of elements in the buffer - * @throws IllegalArgumentException if the size is not a positive value + * Creates a buffer with a certain size. + * + * @param size the size */ public DoubleBuffer(int size) { if (size < 1) { - throw new IllegalArgumentException("The buffer size must be positive."); + throw new IllegalArgumentException("The buffer size must be greater than 1."); } buffer = new double[size]; reset(); } /** - * Adds a double to the buffer. If the buffer is full the oldest element is - * overwritten. - * - * @param d the double to add - */ - public void setNext(double d) { - if (index == buffer.length) { - index = 0; - } - buffer[index] = d; - index++; - if (index == buffer.length) { - isFull = true; - } - } - - /** - * Are all of the entries in the buffer used? - */ - public boolean isFull() { - return isFull; - } - - /** - * Resets the buffer to the initial state. + * Resets the buffer. */ public void reset() { index = 0; @@ -87,16 +56,20 @@ public class DoubleBuffer { } /** - * Gets the average of values from the buffer. - * - * @return The average of the buffer + * Returns true if the buffer is full. + */ + public boolean isFull() { + return isFull; + } + + /** + * Gets the average of the buffer. */ public double getAverage() { int numberOfEntries = isFull ? buffer.length : index; if (numberOfEntries == 0) { return 0; } - double sum = 0; for (int i = 0; i < numberOfEntries; i++) { sum += buffer[i]; @@ -105,15 +78,15 @@ public class DoubleBuffer { } /** - * Gets the average and standard deviation of the buffer. - * - * @return An array of two elements - the first is the average, and the second - * is the variance + * Gets the average and the variance of the buffer. + * + * @return an array of two elements - the first is the average, the second is + * the variance */ public double[] getAverageAndVariance() { int numberOfEntries = isFull ? buffer.length : index; if (numberOfEntries == 0) { - return new double[]{0, 0}; + return new double[] { 0, 0 }; } double sum = 0; @@ -124,8 +97,25 @@ public class DoubleBuffer { } double average = sum / numberOfEntries; - return new double[]{average, - sumSquares / numberOfEntries - Math.pow(average, 2)}; + double variance = sumSquares / numberOfEntries - Math.pow(average, 2); + return new double[] { average, variance }; + } + + /** + * Adds a double to the buffer. If the buffer is full the oldest element is + * overwritten. + * + * @param value the double to add + */ + public void setNext(double value) { + if (index == buffer.length) { + index = 0; + } + buffer[index] = value; + index++; + if (index == buffer.length) { + isFull = true; + } } @Override diff --git a/MyTracks/src/com/google/android/apps/mytracks/stats/TripStatisticsBuilder.java b/MyTracks/src/com/google/android/apps/mytracks/stats/TripStatisticsBuilder.java index 3f6f7e770..18f7103b9 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/stats/TripStatisticsBuilder.java +++ b/MyTracks/src/com/google/android/apps/mytracks/stats/TripStatisticsBuilder.java @@ -20,352 +20,307 @@ import static com.google.android.apps.mytracks.Constants.TAG; import com.google.android.apps.mytracks.Constants; import com.google.android.apps.mytracks.util.PreferencesUtils; +import com.google.common.annotations.VisibleForTesting; import android.location.Location; import android.util.Log; /** - * Statistics keeper for a trip. + * Builder for {@link TripStatistics}. For keeping statistics as a track is + * paused/resumed and new locations are added. * * @author Sandor Dornbush * @author Rodrigo Damazio */ public class TripStatisticsBuilder { - /** - * Statistical data about the trip, which can be displayed to the user. - */ - private final TripStatistics data; - /** - * The last location that the gps reported. - */ - private Location lastLocation; - - /** - * The last location that contributed to the stats. It is also the last - * location the user was found to be moving. - */ - private Location lastMovingLocation; - - /** - * The current speed in meters/second as reported by the gps. - */ - private double currentSpeed; - - /** - * The current grade. This value is very noisy and not reported to the user. - */ - private double currentGrade; - - /** - * Is the trip currently paused? - * All trips start paused. - */ - private boolean paused = true; - - /** - * A buffer of the last speed readings in meters/second. - */ - private final DoubleBuffer speedBuffer = - new DoubleBuffer(Constants.SPEED_SMOOTHING_FACTOR); - - /** - * A buffer of the recent elevation readings in meters. - */ - private final DoubleBuffer elevationBuffer = - new DoubleBuffer(Constants.ELEVATION_SMOOTHING_FACTOR); - - /** - * A buffer of the distance between recent gps readings in meters. - */ - private final DoubleBuffer distanceBuffer = - new DoubleBuffer(Constants.DISTANCE_SMOOTHING_FACTOR); - - /** - * A buffer of the recent grade calculations. - */ - private final DoubleBuffer gradeBuffer = - new DoubleBuffer(Constants.GRADE_SMOOTHING_FACTOR); - - /** - * The total number of locations in this trip. - */ - private long totalLocations = 0; + // The trip statistics. + private final TripStatistics tripStatistics; + // The minimum recording distance. private int minRecordingDistance = PreferencesUtils.MIN_RECORDING_DISTANCE_DEFAULT; + // True if the trip is paused. All trips start as paused. + private boolean paused = true; + + // The last location as reported by GPS. + private Location lastLocation; + + // The last moving location that contributed to the moving statistics. + private Location lastMovingLocation; + + // A buffer of the recent speed readings (m/s) for calculating max speed. + private final DoubleBuffer speedBuffer = new DoubleBuffer(Constants.SPEED_SMOOTHING_FACTOR); + + // A buffer of the recent elevation readings (m). + private final DoubleBuffer elevationBuffer = new DoubleBuffer( + Constants.ELEVATION_SMOOTHING_FACTOR); + + // A buffer of the recent distance readings for calculating grade. + private final DoubleBuffer distanceBuffer = new DoubleBuffer(Constants.DISTANCE_SMOOTHING_FACTOR); + + // A buffer of the recent grade calculations + private final DoubleBuffer gradeBuffer = new DoubleBuffer(Constants.GRADE_SMOOTHING_FACTOR); + /** - * Creates a new trip starting at the given time. + * Creates a new trip starting at a start time. * - * @param startTime the start time. + * @param startTime the start time */ public TripStatisticsBuilder(long startTime) { - data = new TripStatistics(); + tripStatistics = new TripStatistics(); resumeAt(startTime); } /** - * Creates a new trip, starting with existing statistics data. - * - * @param statsData the statistics data to copy and start from - */ - public TripStatisticsBuilder(TripStatistics statsData) { - data = new TripStatistics(statsData); - if (data.getStartTime() > 0) { - resumeAt(data.getStartTime()); - } - } - - /** - * Adds a location to the current trip. This will update all of the internal - * variables with this new location. + * Creates a new trip, starting with an existing {@link TripStatistics}. * - * @param currentLocation the current gps location - * @param systemTime the time used for calculation of totalTime. This should - * be the phone's time (not GPS time) - * @return true if the person is moving + * @param other the existing {@link TripStatistics} */ - public boolean addLocation(Location currentLocation, long systemTime) { - if (paused) { - Log.w(TAG, - "Tried to account for location while track is paused"); - return false; + public TripStatisticsBuilder(TripStatistics other) { + tripStatistics = new TripStatistics(other); + if (tripStatistics.getStartTime() > 0) { + resumeAt(tripStatistics.getStartTime()); } - - totalLocations++; - - double elevationDifference = updateElevation(currentLocation.getAltitude()); - - // Update the "instant" values: - data.setTotalTime(systemTime - data.getStartTime()); - currentSpeed = currentLocation.getSpeed(); - - // This was the 1st location added, remember it and do nothing else: - if (lastLocation == null) { - lastLocation = currentLocation; - lastMovingLocation = currentLocation; - return false; - } - - updateBounds(currentLocation); - - // Don't do anything if we didn't move since last fix: - double distance = lastLocation.distanceTo(currentLocation); - if (distance < minRecordingDistance && - currentSpeed < Constants.MAX_NO_MOVEMENT_SPEED) { - lastLocation = currentLocation; - return false; - } - - data.addTotalDistance(lastMovingLocation.distanceTo(currentLocation)); - updateSpeed(currentLocation.getTime(), currentSpeed, - lastLocation.getTime(), lastLocation.getSpeed()); - - updateGrade(distance, elevationDifference); - lastLocation = currentLocation; - lastMovingLocation = currentLocation; - return true; } /** - * Updates the track's bounding box to include the given location. - */ - private void updateBounds(Location location) { - data.updateLatitudeExtremities(location.getLatitude()); - data.updateLongitudeExtremities(location.getLongitude()); - } - - /** - * Updates the elevation measurements. + * Sets the min recording distance. * - * @param elevation the current elevation + * @param minRecordingDistance the min recording distance */ - // @VisibleForTesting - double updateElevation(double elevation) { - double oldSmoothedElevation = getSmoothedElevation(); - elevationBuffer.setNext(elevation); - double smoothedElevation = getSmoothedElevation(); - data.updateElevationExtremities(smoothedElevation); - double elevationDifference = elevationBuffer.isFull() - ? smoothedElevation - oldSmoothedElevation - : 0.0; - if (elevationDifference > 0) { - data.addTotalElevationGain(elevationDifference); - } - return elevationDifference; + public void setMinRecordingDistance(int minRecordingDistance) { + this.minRecordingDistance = minRecordingDistance; } /** - * Updates the speed measurements. + * Resumes the current track at a given time. * - * @param updateTime the time of the speed update - * @param speed the current speed - * @param lastLocationTime the time of the last speed update - * @param lastLocationSpeed the speed of the last update + * @param time the time */ - // @VisibleForTesting - void updateSpeed(long updateTime, double speed, long lastLocationTime, - double lastLocationSpeed) { - // We are now sure the user is moving. - long timeDifference = updateTime - lastLocationTime; - if (timeDifference < 0) { - Log.e(TAG, - "Found negative time change: " + timeDifference); - } - data.addMovingTime(timeDifference); - - if (isValidSpeed(updateTime, speed, lastLocationTime, lastLocationSpeed, - speedBuffer)) { - speedBuffer.setNext(speed); - if (speed > data.getMaxSpeed()) { - data.setMaxSpeed(speed); - } - double movingSpeed = data.getAverageMovingSpeed(); - if (speedBuffer.isFull() && (movingSpeed > data.getMaxSpeed())) { - data.setMaxSpeed(movingSpeed); - } - } else { - Log.d(TAG, - "TripStatistics ignoring big change: Raw Speed: " + speed - + " old: " + lastLocationSpeed + " [" + toString() + "]"); - } - } - - /** - * Checks to see if this is a valid speed. - * - * @param updateTime The time at the current reading - * @param speed The current speed - * @param lastLocationTime The time at the last location - * @param lastLocationSpeed Speed at the last location - * @param speedBuffer A buffer of recent readings - * @return True if this is likely a valid speed - */ - public static boolean isValidSpeed(long updateTime, double speed, - long lastLocationTime, double lastLocationSpeed, - DoubleBuffer speedBuffer) { - - // We don't want to count 0 towards the speed. - if (speed == 0) { - return false; - } - // We are now sure the user is moving. - long timeDifference = updateTime - lastLocationTime; - - // There are a lot of noisy speed readings. - // Do the cheapest checks first, most expensive last. - // The following code will ignore unlikely to be real readings. - // - 128 m/s seems to be an internal android error code. - if (Math.abs(speed - 128) < 1) { - return false; - } - - // Another check for a spurious reading. See if the path seems physically - // likely. Ignore any speeds that imply accelaration greater than 2g's - // Really who can accelerate faster? - double speedDifference = Math.abs(lastLocationSpeed - speed); - if (speedDifference > Constants.MAX_ACCELERATION * timeDifference) { - return false; - } - - // There are three additional checks if the reading gets this far: - // - Only use the speed if the buffer is full - // - Check that the current speed is less than 10x the recent smoothed speed - // - Double check that the current speed does not imply crazy acceleration - double smoothedSpeed = speedBuffer.getAverage(); - double smoothedDiff = Math.abs(smoothedSpeed - speed); - return !speedBuffer.isFull() || - (speed < smoothedSpeed * 10 - && smoothedDiff < Constants.MAX_ACCELERATION * timeDifference); - } - - /** - * Updates the grade measurements. - * - * @param distance the distance the user just traveled - * @param elevationDifference the elevation difference between the current - * reading and the previous reading - */ - // @VisibleForTesting - void updateGrade(double distance, double elevationDifference) { - distanceBuffer.setNext(distance); - double smoothedDistance = distanceBuffer.getAverage(); - - // With the error in the altitude measurement it is dangerous to divide - // by anything less than 5. - if (!elevationBuffer.isFull() || !distanceBuffer.isFull() - || smoothedDistance < 5.0) { + public void resumeAt(long time) { + if (!paused) { return; } - currentGrade = elevationDifference / smoothedDistance; - gradeBuffer.setNext(currentGrade); - data.updateGradeExtremities(gradeBuffer.getAverage()); + + tripStatistics.setStartTime(time); + tripStatistics.setStopTime(-1L); + paused = false; + lastLocation = null; + lastMovingLocation = null; + speedBuffer.reset(); + elevationBuffer.reset(); + distanceBuffer.reset(); + gradeBuffer.reset(); } /** - * Pauses the track at the given time. + * Pauses the track at a given time. * * @param time the time to pause at */ public void pauseAt(long time) { - if (paused) { return; } - - data.setStopTime(time); - data.setTotalTime(time - data.getStartTime()); - lastLocation = null; // Make sure the counter restarts. + if (paused) { + return; + } + tripStatistics.setStopTime(time); + // TODO: total time needs to take into account pauses + tripStatistics.setTotalTime(time - tripStatistics.getStartTime()); paused = true; } /** - * Resumes the current track at the given time. - * - * @param time the time to resume at + * Gets the trip statistics. */ - public void resumeAt(long time) { - if (!paused) { return; } - - // TODO: The times are bogus if the track is paused then resumed again - data.setStartTime(time); - data.setStopTime(-1); - paused = false; - } - - @Override - public String toString() { - return "TripStatistics { Data: " + data.toString() - + "; Total Locations: " + totalLocations - + "; Paused: " + paused - + "; Current speed: " + currentSpeed - + "; Current grade: " + currentGrade - + "}"; + public TripStatistics getTripStatistics() { + // Take a snapshot - we don't want anyone messing with our internals + return new TripStatistics(tripStatistics); } /** - * Returns the amount of time the user has been idle or 0 if they are moving. + * Returns the amount of time the user has been idle or 0 if he is moving. */ public long getIdleTime() { - if (lastLocation == null || lastMovingLocation == null) + if (lastLocation == null || lastMovingLocation == null) { return 0; + } return lastLocation.getTime() - lastMovingLocation.getTime(); } /** - * Gets the current elevation smoothed over several readings. The elevation - * data is very noisy so it is better to use the smoothed elevation than the - * raw elevation for many tasks. - * - * @return The elevation smoothed over several readings + * Gets the smoothed elevation over several readings. The elevation readings + * is noisy so the smoothed elevation is better than the raw elevation for + * many tasks. */ public double getSmoothedElevation() { return elevationBuffer.getAverage(); } - public TripStatistics getStatistics() { - // Take a snapshot - we don't want anyone messing with our internals - return new TripStatistics(data); + /** + * Adds a location. This will update all of the internal variables with this + * new location. + * + * @param location the location + * @param systemTime the system time for calculating totalTime. This should be + * the phone's system time (not GPS time) + * @return true if the person is moving + */ + public boolean addLocation(Location location, long systemTime) { + if (paused) { + Log.w(TAG, "Track is paused. Ignore addLocation."); + return false; + } + + tripStatistics.setTotalTime(systemTime - tripStatistics.getStartTime()); + + double elevationDifference = updateElevation(location.getAltitude()); + tripStatistics.updateLatitudeExtremities(location.getLatitude()); + tripStatistics.updateLongitudeExtremities(location.getLongitude()); + + // If this is the first location, remember it and return. + if (lastLocation == null || lastMovingLocation == null) { + lastLocation = location; + lastMovingLocation = location; + return false; + } + + // Don't do anything more if we didn't move since the last location. + double distance = lastLocation.distanceTo(location); + if (distance < minRecordingDistance && location.getSpeed() < Constants.MAX_NO_MOVEMENT_SPEED) { + lastLocation = location; + return false; + } + + long timeDifference = location.getTime() - lastLocation.getTime(); + if (timeDifference < 0) { + Log.e(TAG, "Negative time difference: " + timeDifference); + lastLocation = location; + return false; + } + + tripStatistics.addTotalDistance(lastMovingLocation.distanceTo(location)); + tripStatistics.addMovingTime(timeDifference); + updateSpeed( + location.getTime(), location.getSpeed(), lastLocation.getTime(), lastLocation.getSpeed()); + updateGrade(distance, elevationDifference); + lastLocation = location; + lastMovingLocation = location; + return true; } - public void setMinRecordingDistance(int minRecordingDistance) { - this.minRecordingDistance = minRecordingDistance; + /** + * Updates a speed reading. Assumes the user is moving. + * + * @param time the time + * @param speed the speed + * @param lastLocationTime the last location time + * @param lastLocationSpeed the last location speed + */ + @VisibleForTesting + void updateSpeed(long time, double speed, long lastLocationTime, double lastLocationSpeed) { + if (!isValidSpeed(time, speed, lastLocationTime, lastLocationSpeed, speedBuffer)) { + Log.d(TAG, "Invalid speed. speed: " + speed + " lastLocationSpeed: " + lastLocationSpeed); + return; + } + speedBuffer.setNext(speed); + if (speed > tripStatistics.getMaxSpeed()) { + tripStatistics.setMaxSpeed(speed); + } + double movingSpeed = tripStatistics.getAverageMovingSpeed(); + if (speedBuffer.isFull() && movingSpeed > tripStatistics.getMaxSpeed()) { + tripStatistics.setMaxSpeed(movingSpeed); + } + } + + /** + * Updates an elevation reading. + * + * @param elevation the elevation + */ + @VisibleForTesting + double updateElevation(double elevation) { + double oldAverage = elevationBuffer.getAverage(); + elevationBuffer.setNext(elevation); + double newAverage = elevationBuffer.getAverage(); + tripStatistics.updateElevationExtremities(newAverage); + double elevationDifference = elevationBuffer.isFull() ? newAverage - oldAverage : 0.0; + if (elevationDifference > 0) { + tripStatistics.addTotalElevationGain(elevationDifference); + } + return elevationDifference; + } + + /** + * Updates a grade reading. + * + * @param distance the distance the user just traveled + * @param elevationDifference the elevation difference between the current + * reading and the previous reading + */ + @VisibleForTesting + void updateGrade(double distance, double elevationDifference) { + distanceBuffer.setNext(distance); + double smoothedDistance = distanceBuffer.getAverage(); + + /* + * With the error in the altitude measurement it is dangerous to divide by + * anything less than 5. + */ + if (!elevationBuffer.isFull() || !distanceBuffer.isFull() || smoothedDistance < 5.0) { + return; + } + gradeBuffer.setNext(elevationDifference / smoothedDistance); + tripStatistics.updateGradeExtremities(gradeBuffer.getAverage()); + } + + /** + * Returns true if the speed is valid. + * + * @param time the time + * @param speed the speed + * @param lastLocationTime the last location time + * @param lastLocationSpeed the last location speed + * @param speedBuffer a buffer of speed readings + */ + public static boolean isValidSpeed(long time, double speed, long lastLocationTime, + double lastLocationSpeed, DoubleBuffer speedBuffer) { + + /* + * There are a lot of noisy speed readings. Do the cheapest checks first, + * most expensive last. + */ + if (speed == 0) { + return false; + } + + /* + * The following code will ignore unlikely readings. 128 m/s seems to be an + * internal android error code. + */ + if (Math.abs(speed - 128) < 1) { + return false; + } + + /* + * See if the speed seems physically likely. Ignore any speeds that imply + * acceleration greater than 2g. + */ + long timeDifference = time - lastLocationTime; + double speedDifference = Math.abs(lastLocationSpeed - speed); + if (speedDifference > Constants.MAX_ACCELERATION * timeDifference) { + return false; + } + + /* + * Only check if the speed buffer is full. Check that the speed is less than + * 10X the smoothed average and the speed difference doesn't imply 2g + * acceleration. + */ + if (!speedBuffer.isFull()) { + return true; + } + double average = speedBuffer.getAverage(); + double diff = Math.abs(average - speed); + return (speed < average * 10) && (diff < Constants.MAX_ACCELERATION * timeDifference); } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/util/Api7Adapter.java b/MyTracks/src/com/google/android/apps/mytracks/util/Api7Adapter.java index 1ac9f28d2..a863b278a 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/util/Api7Adapter.java +++ b/MyTracks/src/com/google/android/apps/mytracks/util/Api7Adapter.java @@ -19,8 +19,8 @@ import com.google.android.apps.mytracks.Constants; import com.google.android.apps.mytracks.ContextualActionModeCallback; import com.google.android.apps.mytracks.io.backup.BackupPreferencesListener; import com.google.android.apps.mytracks.services.sensors.BluetoothConnectionManager; +import com.google.android.apps.mytracks.services.tasks.AnnouncementPeriodicTask; import com.google.android.apps.mytracks.services.tasks.PeriodicTask; -import com.google.android.apps.mytracks.services.tasks.StatusAnnouncerTask; import com.google.api.client.http.HttpTransport; import com.google.api.client.http.apache.ApacheHttpTransport; @@ -50,8 +50,8 @@ import java.util.List; public class Api7Adapter implements ApiAdapter { @Override - public PeriodicTask getStatusAnnouncerTask(Context context) { - return new StatusAnnouncerTask(context); + public PeriodicTask getAnnouncementPeriodicTask(Context context) { + return new AnnouncementPeriodicTask(context); } @Override diff --git a/MyTracks/src/com/google/android/apps/mytracks/util/Api8Adapter.java b/MyTracks/src/com/google/android/apps/mytracks/util/Api8Adapter.java index 26ccf5b2c..39a01f394 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/util/Api8Adapter.java +++ b/MyTracks/src/com/google/android/apps/mytracks/util/Api8Adapter.java @@ -15,9 +15,9 @@ */ package com.google.android.apps.mytracks.util; -import com.google.android.apps.mytracks.io.backup.BackupPreferencesListener; import com.google.android.apps.mytracks.io.backup.Api8BackupPreferencesListener; -import com.google.android.apps.mytracks.services.tasks.Api8StatusAnnouncerTask; +import com.google.android.apps.mytracks.io.backup.BackupPreferencesListener; +import com.google.android.apps.mytracks.services.tasks.Api8AnnouncementPeriodicTask; import com.google.android.apps.mytracks.services.tasks.PeriodicTask; import android.content.Context; @@ -30,8 +30,8 @@ import android.content.Context; public class Api8Adapter extends Api7Adapter { @Override - public PeriodicTask getStatusAnnouncerTask(Context context) { - return new Api8StatusAnnouncerTask(context); + public PeriodicTask getAnnouncementPeriodicTask(Context context) { + return new Api8AnnouncementPeriodicTask(context); } @Override diff --git a/MyTracks/src/com/google/android/apps/mytracks/util/ApiAdapter.java b/MyTracks/src/com/google/android/apps/mytracks/util/ApiAdapter.java index c80285794..71d4aaf71 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/util/ApiAdapter.java +++ b/MyTracks/src/com/google/android/apps/mytracks/util/ApiAdapter.java @@ -42,13 +42,13 @@ import java.util.List; public interface ApiAdapter { /** - * Gets a status announcer task. + * Gets an announcement periodic task. *

* Due to changes in API level 8. * * @param context the context */ - public PeriodicTask getStatusAnnouncerTask(Context context); + public PeriodicTask getAnnouncementPeriodicTask(Context context); /** * Gets a {@link BackupPreferencesListener}. diff --git a/MyTracksLib/.settings/org.eclipse.jdt.core.prefs b/MyTracksLib/.settings/org.eclipse.jdt.core.prefs index 60e490024..a46bd1b8b 100644 --- a/MyTracksLib/.settings/org.eclipse.jdt.core.prefs +++ b/MyTracksLib/.settings/org.eclipse.jdt.core.prefs @@ -1,4 +1,4 @@ -#Fri Oct 21 20:06:28 BRST 2011 +#Tue Jul 17 10:23:25 PDT 2012 eclipse.preferences.version=1 org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 @@ -76,29 +76,66 @@ org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning org.eclipse.jdt.core.compiler.source=1.6 org.eclipse.jdt.core.formatter.align_type_members_on_columns=false +org.eclipse.jdt.core.formatter.alignment_for_annotations_on_field=1585 +org.eclipse.jdt.core.formatter.alignment_for_annotations_on_field.count_dependent=1585|-1|1585 +org.eclipse.jdt.core.formatter.alignment_for_annotations_on_local_variable=1585 +org.eclipse.jdt.core.formatter.alignment_for_annotations_on_local_variable.count_dependent=1585|-1|1585 +org.eclipse.jdt.core.formatter.alignment_for_annotations_on_method=1585 +org.eclipse.jdt.core.formatter.alignment_for_annotations_on_method.count_dependent=1585|-1|1585 +org.eclipse.jdt.core.formatter.alignment_for_annotations_on_package=1585 +org.eclipse.jdt.core.formatter.alignment_for_annotations_on_package.count_dependent=1585|-1|1585 +org.eclipse.jdt.core.formatter.alignment_for_annotations_on_parameter=1040 +org.eclipse.jdt.core.formatter.alignment_for_annotations_on_parameter.count_dependent=1040|-1|1040 +org.eclipse.jdt.core.formatter.alignment_for_annotations_on_type=1585 +org.eclipse.jdt.core.formatter.alignment_for_annotations_on_type.count_dependent=1585|-1|1585 org.eclipse.jdt.core.formatter.alignment_for_arguments_in_allocation_expression=16 +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_allocation_expression.count_dependent=16|-1|16 org.eclipse.jdt.core.formatter.alignment_for_arguments_in_annotation=0 +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_annotation.count_dependent=16|-1|16 org.eclipse.jdt.core.formatter.alignment_for_arguments_in_enum_constant=48 +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_enum_constant.count_dependent=16|-1|16 org.eclipse.jdt.core.formatter.alignment_for_arguments_in_explicit_constructor_call=16 +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_explicit_constructor_call.count_dependent=16|-1|16 org.eclipse.jdt.core.formatter.alignment_for_arguments_in_method_invocation=16 +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_method_invocation.count_dependent=16|-1|16 org.eclipse.jdt.core.formatter.alignment_for_arguments_in_qualified_allocation_expression=16 +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_qualified_allocation_expression.count_dependent=16|-1|16 org.eclipse.jdt.core.formatter.alignment_for_assignment=0 org.eclipse.jdt.core.formatter.alignment_for_binary_expression=16 +org.eclipse.jdt.core.formatter.alignment_for_binary_expression.count_dependent=16|-1|16 +org.eclipse.jdt.core.formatter.alignment_for_cascading_method_invocation_with_arguments=16 +org.eclipse.jdt.core.formatter.alignment_for_cascading_method_invocation_with_arguments.count_dependent=16|-1|16 org.eclipse.jdt.core.formatter.alignment_for_compact_if=16 org.eclipse.jdt.core.formatter.alignment_for_conditional_expression=80 org.eclipse.jdt.core.formatter.alignment_for_enum_constants=48 +org.eclipse.jdt.core.formatter.alignment_for_enum_constants.count_dependent=0|-1|0 org.eclipse.jdt.core.formatter.alignment_for_expressions_in_array_initializer=16 +org.eclipse.jdt.core.formatter.alignment_for_expressions_in_array_initializer.count_dependent=16|-1|16 +org.eclipse.jdt.core.formatter.alignment_for_field_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_for_statement=16 +org.eclipse.jdt.core.formatter.alignment_for_generic_type_arguments=16 +org.eclipse.jdt.core.formatter.alignment_for_generic_type_arguments.count_dependent=16|-1|16 +org.eclipse.jdt.core.formatter.alignment_for_local_variable_declaration=16 org.eclipse.jdt.core.formatter.alignment_for_method_declaration=0 org.eclipse.jdt.core.formatter.alignment_for_multiple_fields=16 +org.eclipse.jdt.core.formatter.alignment_for_multiple_fields.count_dependent=16|-1|16 +org.eclipse.jdt.core.formatter.alignment_for_new_anonymous_class=0 org.eclipse.jdt.core.formatter.alignment_for_parameters_in_constructor_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_parameters_in_constructor_declaration.count_dependent=16|-1|16 org.eclipse.jdt.core.formatter.alignment_for_parameters_in_method_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_parameters_in_method_declaration.count_dependent=16|-1|16 org.eclipse.jdt.core.formatter.alignment_for_resources_in_try=80 org.eclipse.jdt.core.formatter.alignment_for_selector_in_method_invocation=16 +org.eclipse.jdt.core.formatter.alignment_for_selector_in_method_invocation.count_dependent=16|-1|16 org.eclipse.jdt.core.formatter.alignment_for_superclass_in_type_declaration=16 org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_enum_declaration=48 +org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_enum_declaration.count_dependent=16|-1|16 org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_type_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_type_declaration.count_dependent=16|-1|16 org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_constructor_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_constructor_declaration.count_dependent=16|-1|16 org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_method_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_method_declaration.count_dependent=16|-1|16 org.eclipse.jdt.core.formatter.alignment_for_union_type_in_multicatch=16 org.eclipse.jdt.core.formatter.blank_lines_after_imports=1 org.eclipse.jdt.core.formatter.blank_lines_after_package=1 @@ -138,12 +175,14 @@ org.eclipse.jdt.core.formatter.comment.line_length=80 org.eclipse.jdt.core.formatter.comment.new_lines_at_block_boundaries=true org.eclipse.jdt.core.formatter.comment.new_lines_at_javadoc_boundaries=true org.eclipse.jdt.core.formatter.comment.preserve_white_space_between_code_and_line_comments=false +org.eclipse.jdt.core.formatter.comment_new_line_at_start_of_html_paragraph=true org.eclipse.jdt.core.formatter.compact_else_if=true org.eclipse.jdt.core.formatter.continuation_indentation=2 org.eclipse.jdt.core.formatter.continuation_indentation_for_array_initializer=2 org.eclipse.jdt.core.formatter.disabling_tag=@formatter\:off org.eclipse.jdt.core.formatter.enabling_tag=@formatter\:on -org.eclipse.jdt.core.formatter.format_guardian_clause_on_one_line=true +org.eclipse.jdt.core.formatter.force_if_else_statement_brace=false +org.eclipse.jdt.core.formatter.format_guardian_clause_on_one_line=false org.eclipse.jdt.core.formatter.format_line_comment_starting_on_first_column=true org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_annotation_declaration_header=true org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_constant_header=true @@ -210,6 +249,7 @@ org.eclipse.jdt.core.formatter.insert_space_after_comma_in_parameterized_type_re org.eclipse.jdt.core.formatter.insert_space_after_comma_in_superinterfaces=insert org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_arguments=insert org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_parameters=insert +org.eclipse.jdt.core.formatter.insert_space_after_comment_prefix=insert org.eclipse.jdt.core.formatter.insert_space_after_ellipsis=insert org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_parameterized_type_reference=do not insert org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_arguments=do not insert @@ -340,7 +380,7 @@ org.eclipse.jdt.core.formatter.join_lines_in_comments=true org.eclipse.jdt.core.formatter.join_wrapped_lines=true org.eclipse.jdt.core.formatter.keep_else_statement_on_same_line=false org.eclipse.jdt.core.formatter.keep_empty_array_initializer_on_one_line=false -org.eclipse.jdt.core.formatter.keep_imple_if_on_one_line=true +org.eclipse.jdt.core.formatter.keep_imple_if_on_one_line=false org.eclipse.jdt.core.formatter.keep_then_statement_on_same_line=false org.eclipse.jdt.core.formatter.lineSplit=100 org.eclipse.jdt.core.formatter.never_indent_block_comments_on_first_column=false @@ -348,10 +388,22 @@ org.eclipse.jdt.core.formatter.never_indent_line_comments_on_first_column=false org.eclipse.jdt.core.formatter.number_of_blank_lines_at_beginning_of_method_body=0 org.eclipse.jdt.core.formatter.number_of_empty_lines_to_preserve=1 org.eclipse.jdt.core.formatter.put_empty_statement_on_new_line=true +org.eclipse.jdt.core.formatter.sort_local_variable_annotations=false +org.eclipse.jdt.core.formatter.sort_member_annotations=false +org.eclipse.jdt.core.formatter.sort_package_annotations=false +org.eclipse.jdt.core.formatter.sort_parameter_annotations=false +org.eclipse.jdt.core.formatter.sort_type_annotations=false org.eclipse.jdt.core.formatter.tabulation.char=space org.eclipse.jdt.core.formatter.tabulation.size=2 org.eclipse.jdt.core.formatter.use_on_off_tags=false org.eclipse.jdt.core.formatter.use_tabs_only_for_leading_indentations=false org.eclipse.jdt.core.formatter.wrap_before_binary_operator=true org.eclipse.jdt.core.formatter.wrap_before_or_operator_multicatch=true +org.eclipse.jdt.core.formatter.wrap_comment_inline_tags=false +org.eclipse.jdt.core.formatter.wrap_non_simple_local_variable_annotation=true +org.eclipse.jdt.core.formatter.wrap_non_simple_member_annotation=true +org.eclipse.jdt.core.formatter.wrap_non_simple_package_annotation=true +org.eclipse.jdt.core.formatter.wrap_non_simple_parameter_annotation=false +org.eclipse.jdt.core.formatter.wrap_non_simple_type_annotation=true org.eclipse.jdt.core.formatter.wrap_outer_expressions_when_nested=true +org.eclipse.jdt.core.formatter.wrap_prefer_two_fragments=false diff --git a/MyTracksLib/.settings/org.eclipse.jdt.ui.prefs b/MyTracksLib/.settings/org.eclipse.jdt.ui.prefs index 4a0ee86f7..189d09e17 100644 --- a/MyTracksLib/.settings/org.eclipse.jdt.ui.prefs +++ b/MyTracksLib/.settings/org.eclipse.jdt.ui.prefs @@ -1,8 +1,8 @@ -#Thu Jul 21 03:05:33 BRT 2011 +#Tue Jul 17 10:22:31 PDT 2012 eclipse.preferences.version=1 editor_save_participant_org.eclipse.jdt.ui.postsavelistener.cleanup=true -formatter_profile=_MyTracks -formatter_settings_version=12 +formatter_profile=_My Tracks +formatter_settings_version=13 org.eclipse.jdt.ui.ignorelowercasenames=true org.eclipse.jdt.ui.importorder=com;android;java;javax;org; org.eclipse.jdt.ui.ondemandthreshold=99 diff --git a/MyTracksLib/src/com/google/android/apps/mytracks/content/DescriptionGenerator.java b/MyTracksLib/src/com/google/android/apps/mytracks/content/DescriptionGenerator.java index 7cc3cb5c0..6a489d40e 100644 --- a/MyTracksLib/src/com/google/android/apps/mytracks/content/DescriptionGenerator.java +++ b/MyTracksLib/src/com/google/android/apps/mytracks/content/DescriptionGenerator.java @@ -15,6 +15,8 @@ */ package com.google.android.apps.mytracks.content; +import com.google.android.apps.mytracks.stats.TripStatistics; + import java.util.Vector; /** @@ -37,9 +39,9 @@ public interface DescriptionGenerator { Track track, Vector distances, Vector elevations, boolean html); /** - * Generate a waypoint description. + * Generate a waypoint description from a trip statistics. * - * @param waypoint the waypoint + * @param tripStatistics the trip statistics */ - public String generateWaypointDescription(Waypoint waypoint); + public String generateWaypointDescription(TripStatistics tripStatistics); } diff --git a/MyTracksLib/src/com/google/android/apps/mytracks/content/MyTracksProviderUtils.java b/MyTracksLib/src/com/google/android/apps/mytracks/content/MyTracksProviderUtils.java index 005129874..8eb6f94e1 100644 --- a/MyTracksLib/src/com/google/android/apps/mytracks/content/MyTracksProviderUtils.java +++ b/MyTracksLib/src/com/google/android/apps/mytracks/content/MyTracksProviderUtils.java @@ -13,458 +13,427 @@ * License for the specific language governing permissions and limitations under * the License. */ + package com.google.android.apps.mytracks.content; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.location.Location; +import android.location.LocationManager; import android.net.Uri; import java.util.Iterator; import java.util.List; /** - * Utility to access data from the mytracks content provider. - * + * Utilities to access data from the My Tracks content provider. + * * @author Rodrigo Damazio */ public interface MyTracksProviderUtils { /** - * Authority (first part of URI) for the MyTracks content provider: + * The authority (the first part of the URI) for the My Tracks content + * provider. */ public static final String AUTHORITY = "com.google.android.maps.mytracks"; /** - * Deletes all tracks (including track points) from the provider. - */ - void deleteAllTracks(); - - /** - * Deletes a track with the given track id. - * - * @param trackId the unique track id - */ - void deleteTrack(long trackId); - - /** - * Deletes a way point with the given way point id. - * This will also correct the next statistics way point after the deleted one - * to reflect the deletion. - * The generator is needed to stitch together statistics waypoints. - * - * @param waypointId the unique way point id - * @param descriptionGenerator the class to generate descriptions - */ - void deleteWaypoint(long waypointId, - DescriptionGenerator descriptionGenerator); - - /** - * Finds the next statistics waypoint after the given waypoint. - * - * @param waypoint a given waypoint - * @return the next statistics waypoint, or null if none found. - */ - Waypoint getNextStatisticsWaypointAfter(Waypoint waypoint); - - /** - * Updates the waypoint in the provider. - * - * @param waypoint - * @return true if successful - */ - boolean updateWaypoint(Waypoint waypoint); - - /** - * Finds the first recorded location from the location provider. + * Gets a track cursor. The caller owns the returned cursor and is responsible + * for closing it. * - * @return the first location, or null if no locations available + * @param selection the selection + * @param selectionArgs the selection arguments + * @param sortOrder the sort order */ - Location getFirstLocation(); + public Cursor getTracksCursor(String selection, String[] selectionArgs, String sortOrder); /** - * Finds the last recorded location from the location provider. - * - * @return the last location, or null if no locations available + * Gets all the tracks. If no track exists, an empty list is returned. + *

+ * Note that the returned tracks do not have any track points attached. */ - Location getLastLocation(); + public List getAllTracks(); /** - * Finds the first recorded waypoint for a given track from the location - * provider. - * This is a special waypoint that holds the stats for current segment. - * - * @param trackId the id of the track the waypoint belongs to - * @return the first waypoint, or null if no waypoints available + * Gets a track by a track id. Returns null if not found. + *

+ * Note that the returned track doesn't have any track points attached. + * + * @param trackId the track id. */ - Waypoint getFirstWaypoint(long trackId); + public Track getTrack(long trackId); /** - * Finds the given waypoint from the location provider. - * - * @param waypointId - * @return the waypoint, or null if it does not exist + * Gets the last track. Returns null if doesn't exist. */ - Waypoint getWaypoint(long waypointId); + public Track getLastTrack(); /** - * Finds the last recorded location id from the track points provider. - * - * @param trackId find last location on this track - * @return the location id, or -1 if no locations available + * Gets the last track id. Returns -1L if doesn't exist. */ - long getLastLocationId(long trackId); + public long getLastTrackId(); /** - * Finds the id of the 1st waypoint for a given track. - * The 1st waypoint is special as it contains the stats for the current - * segment. - * - * @param trackId find last location on this track - * @return the waypoint id, or -1 if no waypoints are available - */ - long getFirstWaypointId(long trackId); - - /** - * Finds the id of the 1st waypoint for a given track. - * The 1st waypoint is special as it contains the stats for the current - * segment. - * - * @param trackId find last location on this track - * @return the waypoint id, or -1 if no waypoints are available - */ - long getLastWaypointId(long trackId); - - /** - * Gets the next marker number. - * + * Returns true if a track exists. + * * @param trackId the track id - * @param statistics true for statistics marker, false for waypoint marker - * @return the next number or -1 if unable to get the value */ - int getNextMarkerNumber(long trackId, boolean statistics); + public boolean trackExists(long trackId); + + /** + * Inserts a track. + *

+ * Note: This doesn't insert any track points. + * + * @param track the track + * @return the content provider URI of the inserted track. + */ + public Uri insertTrack(Track track); + + /** + * Updates a track. + *

+ * Note: This doesn't update any track points. + * + * @param track the track + */ + public void updateTrack(Track track); + + /** + * Deletes all tracks (including waypoints and track points). + */ + public void deleteAllTracks(); + + /** + * Deletes a track. + * + * @param trackId the track id + */ + public void deleteTrack(long trackId); + + /** + * Creates a {@link Track} from a cursor. + * + * @param cursor the cursor pointing to the track + */ + public Track createTrack(Cursor cursor); + + /** + * Creates a {@link ContentValues} from a track. + * + * @param track the track + */ + public ContentValues createContentValues(Track track); + + /** + * Gets a waypoint cursor. The caller owns the returned cursor and is + * responsible for closing it. + * + * @param selection the selection + * @param selectionArgs the selection arguments + * @param sortOrder the sort order + * @param maxWaypoints the maximum number of waypoints to return + */ + public Cursor getWaypointsCursor( + String selection, String[] selectionArgs, String sortOrder, int maxWaypoints); + + /** + * Gets a waypoint cursor for a track. The caller owns the returned cursor and + * is responsible for closing it. + * + * @param trackId the track id + * @param minWaypointId the minimum waypoint id + * @param maxWaypoints the maximum number of waypoints to return + */ + public Cursor getWaypointsCursor(long trackId, long minWaypointId, int maxWaypoints); + + /** + * Gets the first recorded waypoint for a track. The first waypoint is special + * as it contains the stats for the current segment. Returns null if it + * doesn't exist. + * + * @param trackId the track id + */ + public Waypoint getFirstWaypoint(long trackId); + + /** + * Gets the first waypoint id for a track. The first waypoint is special as it + * contains the stats for the current segment. Returns -1L if it doesn't + * exist. + * + * @param trackId the track id + */ + public long getFirstWaypointId(long trackId); + + /** + * Gets the last waypoint id for a track. Returns -1L if it doesn't exist. + * + * @param trackId the track id + */ + public long getLastWaypointId(long trackId); + + /** + * Gets the last statistics waypoint for a track. Returns null if it doesn't exist. + * + * @param trackId the track id + */ + public Waypoint getLastStatisticsWaypoint(long trackId); /** - * Finds the last recorded track from the track provider. - * - * @return the last track, or null if no tracks available + * Gets the next marker number. Returns -1 if not able to get the next marker + * number. + * + * @param trackId the track id + * @param statistics true for statistics marker, false for waypoint marker */ - Track getLastTrack(); + public int getNextMarkerNumber(long trackId, boolean statistics); /** - * Finds the last recorded track id from the tracks provider. - * - * @return the track id, or -1 if no tracks available + * Gets the next statistics waypoint after the given waypoint. Returns null if + * it doesn't exists. + * + * @param waypoint the given waypoint */ - long getLastTrackId(); + public Waypoint getNextStatisticsWaypointAfter(Waypoint waypoint); /** - * Finds a location by given unique id. - * - * @param id the desired id - * @return a Location object, or null if not found + * Gets a waypoint from a waypoint id. Returns null if not found. + * + * @param waypointId the waypoint id */ - Location getLocation(long id); + public Waypoint getWaypoint(long waypointId); /** - * Creates a cursor over the locations in the track points provider which - * iterates over a given range of unique ids. - * Caller owns the returned cursor and is responsible for closing it. - * - * @param trackId the id of the track for which to get the points - * @param minTrackPointId the minimum id for the track points - * @param maxLocations maximum number of locations retrieved - * @param descending if true the results will be returned in descending id - * order (latest location first) - * @return A cursor over the selected range of locations + * Inserts a waypoint. + * + * @param waypoint the waypoint + * @return the content provider URI of the inserted waypoint. */ - Cursor getLocationsCursor(long trackId, long minTrackPointId, - int maxLocations, boolean descending); + public Uri insertWaypoint(Waypoint waypoint); /** - * Creates a cursor over the waypoints of a track. - * Caller owns the returned cursor and is responsible for closing it. - * - * @param trackId the id of the track for which to get the points - * @param minWaypointId the minimum id for the track points - * @param maxWaypoints the maximum number of waypoints to return - * @return A cursor over the selected range of locations + * Updates a waypoint. Returns true if successful. + * + * @param waypoint the waypoint */ - Cursor getWaypointsCursor(long trackId, long minWaypointId, - int maxWaypoints); + public boolean updateWaypoint(Waypoint waypoint); /** - * Creates a cursor over waypoints with the given selection. - * Caller owns the returned cursor and is responsible for closing it. - * - * @param selection a given selection - * @param selectionArgs arguments for the given selection - * @param order the order in which to return results - * @param maxWaypoints the maximum number of waypoints to return - * @return a cursor of the selected waypoints + * Deletes a waypoint. If deleting a statistics waypoint, this will also + * correct the next statistics waypoint after the deleted one to reflect the + * deletion. The generator is used to update the next statistics waypoint. + * + * @param waypointId the waypoint id + * @param descriptionGenerator the description generator */ - Cursor getWaypointsCursor(String selection, String[] selectionArgs, String order, int maxWaypoints); + public void deleteWaypoint(long waypointId, DescriptionGenerator descriptionGenerator); /** - * Finds a track by given unique track id. - * Note that the returned track object does not have any track points attached. - * Use {@link #getLocationIterator(long, long, boolean, LocationFactory)} to load - * the track points. - * - * @param id desired unique track id - * @return a Track object, or null if not found + * Creates a waypoint from a cursor. + * + * @param cursor the cursor pointing to the waypoint */ - Track getTrack(long id); + public Waypoint createWaypoint(Cursor cursor); /** - * Retrieves all tracks without track points. If no tracks exist, an empty - * list will be returned. Use {@link #getLocationIterator(long, long, boolean, LocationFactory)} - * to load the track points. - * - * @return a list of all the recorded tracks + * Gets the first recorded location. Returns null if it doesn't exist. */ - List getAllTracks(); + public Location getFirstLocation(); /** - * Creates a cursor over the tracks provider with a given selection. - * Caller owns the returned cursor and is responsible for closing it. - * - * @param selection a given selection - * @param selectionArgs parameters for the given selection - * @param order the order to return results in - * @return a cursor of the selected tracks + * Gets the last recorded location. Returns null if it doesn't exist. */ - Cursor getTracksCursor(String selection, String[] selectionArgs, String order); + public Location getLastLocation(); /** - * Inserts a track in the tracks provider. - * Note: This does not insert any track points. - * Use {@link #insertTrackPoint(Location, long)} to insert them. - * - * @param track the track to insert - * @return the content provider URI for the inserted track + * Gets a location by track point id. Returns null if not found. + * + * @param trackPointId the track point id */ - Uri insertTrack(Track track); + public Location getLocation(long trackPointId); /** - * Inserts a track point in the tracks provider. - * - * @param location the location to insert - * @return the content provider URI for the inserted track point + * Creates a location cursor. The caller owns the returned cursor and is + * responsible for closing it. + * + * @param trackId the track id + * @param startTrackPointId the starting track point id + * @param maxLocations maximum number of locations to return + * @param descending true to sort the result in descending order (latest + * location first) */ - Uri insertTrackPoint(Location location, long trackId); + public Cursor getLocationsCursor( + long trackId, long startTrackPointId, int maxLocations, boolean descending); /** - * Inserts multiple track points in a single operation. - * - * @param locations an array of locations to insert - * @param length the number of locations (from the beginning of the array) - * to actually insert, or -1 for all of them - * @param trackId the ID of the track to insert the points into + * Creates a new read-only iterator over a given track's points. It provides a + * lightweight way of iterating over long tracks without failing due to the + * underlying cursor limitations. Since it's a read-only iterator, + * {@link Iterator#remove()} always throws + * {@link UnsupportedOperationException}. Each call to + * {@link LocationIterator#next()} may advance to the next DB record, and if + * so, the iterator calls {@link LocationFactory#createLocation()} and + * populates it with information retrieved from the record. When done with + * iteration, {@link LocationIterator#close()} must be called. + * + * @param trackId the track id + * @param startTrackPointId the start track point id or -1L to start from the + * first point + * @param descending true to sort the result in descending order (latest + * location first) + * @param locationFactory the location factory + */ + public LocationIterator getLocationIterator( + long trackId, long startTrackPointId, boolean descending, LocationFactory locationFactory); + + /** + * Gets the last location id for a track. Returns -1L if it doesn't exist. + * + * @param trackId the track id + */ + public long getLastTrackLocationId(long trackId); + + /** + * Gets the last location for a track. Returns -1L if it doesn't exist. + * + * @param trackId the track id + */ + public Location getLastTrackLocation(long trackId); + + /** + * Inserts a track point. + * + * @param location the location + * @param trackId the track id + * @return the content provider URI of the inserted track point + */ + public Uri insertTrackPoint(Location location, long trackId); + + /** + * Inserts multiple track points. + * + * @param locations an array of locations + * @param length the number of locations (from the beginning of the array) to + * insert, or -1 for all of them + * @param trackId the track id * @return the number of points inserted */ - int bulkInsertTrackPoints(Location[] locations, int length, long trackId); + public int bulkInsertTrackPoints(Location[] locations, int length, long trackId); /** - * Inserts a waypoint in the provider. - * - * @param waypoint the waypoint to insert - * @return the content provider URI for the inserted track + * Fills a location from a cursor. + * + * @param cursor the cursor pointing to the location + * @param location the location to be overwritten */ - Uri insertWaypoint(Waypoint waypoint); + public void fillLocation(Cursor cursor, Location location); /** - * Tests if a track with given id exists. - * - * @param id the unique id - * @return true if track exists + * Creates a location object from a cursor. + * + * @param cursor the cursor pointing to the location */ - boolean trackExists(long id); + public Location createLocation(Cursor cursor); /** - * Updates a track in the content provider. - * Note: This will not update any track points. - * - * @param track a given track + * A lightweight wrapper around the original {@link Cursor} with a method to + * clean up. */ - void updateTrack(Track track); - - /** - * Creates a Track object from a given cursor. - * - * @param cursor a cursor pointing at a db or provider with tracks - * @return a new Track object - */ - Track createTrack(Cursor cursor); - - /** - * Creates the ContentValues for a given Track object. - * - * Note: If the track has an id<0 the id column will not be filled. - * - * @param track a given track object - * @return a filled in ContentValues object - */ - ContentValues createContentValues(Track track); - - /** - * Creates a location object from a given cursor. - * - * @param cursor a cursor pointing at a db or provider with locations - * @return a new location object - */ - Location createLocation(Cursor cursor); - - /** - * Fill a location object with values from a given cursor. - * - * @param cursor a cursor pointing at a db or provider with locations - * @param location a location object to be overwritten - */ - void fillLocation(Cursor cursor, Location location); - - /** - * Creates a waypoint object from a given cursor. - * - * @param cursor a cursor pointing at a db or provider with waypoints. - * @return a new waypoint object - */ - Waypoint createWaypoint(Cursor cursor); - - /** - * A lightweight wrapper around the original {@link Cursor} with a method to clean up. - */ - interface LocationIterator extends Iterator { - /** - * Returns ID of the most recently retrieved track point through a call to {@link #next()}. - * - * @return the ID of the most recent track point ID. - */ - long getLocationId(); + public interface LocationIterator extends Iterator { /** - * Should be called in case the underlying iterator hasn't reached the last record. - * Calling it if it has reached the last record is a no-op. + * Gets the most recently retrieved track point id by {@link #next()}. */ - void close(); + public long getLocationId(); + + /** + * Closes the iterator. + */ + public void close(); } /** - * A factory for creating new {@class Location}s. + * A factory for creating new {@link Location}. */ - interface LocationFactory { + public interface LocationFactory { + /** - * Creates a new {@link Location} object to be populated from the underlying database record. - * It's up to the implementing class to decide whether to create a new instance or reuse - * existing to optimize for speed. - * - * @return a {@link Location} to be populated from the database. + * Creates a new {@link Location}. An implementation can create new + * instances or reuse existing instances for optimization. */ - Location createLocation(); + public Location createLocation(); } /** - * The default {@class Location}s factory, which creates a new location of 'gps' type. + * The default {@link LocationFactory} which creates a location each time. */ - LocationFactory DEFAULT_LOCATION_FACTORY = new LocationFactory() { - @Override + public LocationFactory DEFAULT_LOCATION_FACTORY = new LocationFactory() { + @Override public Location createLocation() { - return new Location("gps"); + return new Location(LocationManager.GPS_PROVIDER); } }; /** - * A location factory which uses two location instances (one for the current location, - * and one for the previous), useful when we need to keep the last location. + * A {@link LocationFactory} which uses two location instances (one for the + * current location and one for the previous), useful when needing to keep the + * last location. */ public class DoubleBufferedLocationFactory implements LocationFactory { - private final Location locs[] = new MyTracksLocation[] { - new MyTracksLocation("gps"), - new MyTracksLocation("gps") - }; - private int lastLoc = 0; + + private final Location locations[] = new MyTracksLocation[] { + new MyTracksLocation(LocationManager.GPS_PROVIDER), + new MyTracksLocation(LocationManager.GPS_PROVIDER) }; + private int lastLocation = 0; @Override public Location createLocation() { - lastLoc = (lastLoc + 1) % locs.length; - return locs[lastLoc]; + lastLocation = (lastLocation + 1) % locations.length; + return locations[lastLocation]; } } /** - * Creates a new read-only iterator over all track points for the given track. It provides - * a lightweight way of iterating over long tracks without failing due to the underlying cursor - * limitations. Since it's a read-only iterator, {@link Iterator#remove()} always throws - * {@class UnsupportedOperationException}. - * - * Each call to {@link LocationIterator#next()} may advance to the next DB record, and if so, - * the iterator calls {@link LocationFactory#createLocation()} and populates it with information - * retrieved from the record. - * - * When done with iteration, you must call {@link LocationIterator#close()} to make sure that all - * resources are properly deallocated. - * - * Example use: - * - * ... - * LocationIterator it = providerUtils.getLocationIterator( - * 1, MyTracksProviderUtils.DEFAULT_LOCATION_FACTORY); - * try { - * for (Location loc : it) { - * ... // Do something useful with the location. - * } - * } finally { - * it.close(); - * } - * ... - * - * - * @param trackId the ID of a track to retrieve locations for. - * @param startTrackPointId the ID of the first track point to load, or -1 to start from - * the first point. - * @param descending if true the results will be returned in descending ID - * order (latest location first). - * @param locationFactory the factory for creating new locations. - * - * @return the read-only iterator over the given track's points. - */ - LocationIterator getLocationIterator(long trackId, long startTrackPointId, boolean descending, - LocationFactory locationFactory); - - /** - * A factory which can produce instances of {@link MyTracksProviderUtils}, - * and can be overridden in tests (a.k.a. poor man's guice). + * A factory which can produce instances of {@link MyTracksProviderUtils}, and + * can be overridden for testing. */ public static class Factory { + private static Factory instance = new Factory(); /** - * Creates and returns an instance of {@link MyTracksProviderUtils} which - * uses the given context to access its data. + * Creates an instance of {@link MyTracksProviderUtils}. + * + * @param context the context */ public static MyTracksProviderUtils get(Context context) { return instance.newForContext(context); } /** - * Returns the global instance of this factory. + * Returns the factory instance. */ public static Factory getInstance() { return instance; } /** - * Overrides the global instance for this factory, to be used for testing. - * If used, don't forget to set it back to the original value after the - * test is run. + * Overrides the factory instance for testing. Don't forget to set it back + * to the original value after testing. + * + * @param factory the factory */ public static void overrideInstance(Factory factory) { instance = factory; } /** - * Creates an instance of {@link MyTracksProviderUtils}. + * Creates an instance of {@link MyTracksProviderUtils}. Allows subclasses + * to override for testing. + * + * @param context the context */ protected MyTracksProviderUtils newForContext(Context context) { return new MyTracksProviderUtilsImpl(context.getContentResolver()); 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 0b1becc95..e3910bb9b 100644 --- a/MyTracksLib/src/com/google/android/apps/mytracks/content/MyTracksProviderUtilsImpl.java +++ b/MyTracksLib/src/com/google/android/apps/mytracks/content/MyTracksProviderUtilsImpl.java @@ -13,9 +13,8 @@ * 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.lib.MyTracksLibConstants.TAG; +package com.google.android.apps.mytracks.content; import com.google.android.apps.mytracks.content.Sensor.SensorDataSet; import com.google.android.apps.mytracks.stats.TripStatistics; @@ -33,15 +32,15 @@ import java.util.List; import java.util.NoSuchElementException; /** - * Helper class providing easy access to locations and tracks in the - * MyTracksProvider. All static members. - * + * {@link MyTracksProviderUtils} implementation. + * * @author Leif Hendrik Wilden */ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { - private final ContentResolver contentResolver; + private static final String TAG = MyTracksProviderUtilsImpl.class.getSimpleName(); + private final ContentResolver contentResolver; private int defaultCursorBatchSize = 2000; public MyTracksProviderUtilsImpl(ContentResolver contentResolver) { @@ -49,50 +48,262 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { } /** - * Creates the ContentValues for a given location object. - * - * @param location a given location - * @param trackId the id of the track it belongs to - * @return a filled in ContentValues object + * Gets a track cursor. + * + * @param projection the projection + * @param selection the selection + * @param selectionArgs the selection arguments + * @param sortOrder the sort oder */ - private static ContentValues createContentValues(Location location, long trackId) { - ContentValues values = new ContentValues(); - values.put(TrackPointsColumns.TRACKID, trackId); - values.put(TrackPointsColumns.LONGITUDE, (int) (location.getLongitude() * 1E6)); - values.put(TrackPointsColumns.LATITUDE, (int) (location.getLatitude() * 1E6)); + private Cursor getTracksCursor( + String[] projection, String selection, String[] selectionArgs, String sortOrder) { + return contentResolver.query( + TracksColumns.CONTENT_URI, projection, selection, selectionArgs, sortOrder); + } - // This is an ugly hack for Samsung phones that don't properly populate the - // time field. - values.put(TrackPointsColumns.TIME, - (location.getTime() == 0) ? System.currentTimeMillis() : location.getTime()); - if (location.hasAltitude()) { - values.put(TrackPointsColumns.ALTITUDE, location.getAltitude()); - } - if (location.hasAccuracy()) { - values.put(TrackPointsColumns.ACCURACY, location.getAccuracy()); - } - if (location.hasSpeed()) { - values.put(TrackPointsColumns.SPEED, location.getSpeed()); - } - if (location.hasBearing()) { - values.put(TrackPointsColumns.BEARING, location.getBearing()); - } + @Override + public Cursor getTracksCursor(String selection, String[] selectionArgs, String sortOrder) { + return getTracksCursor(null, selection, selectionArgs, sortOrder); + } - if (location instanceof MyTracksLocation) { - MyTracksLocation mtLocation = (MyTracksLocation) location; - if (mtLocation.getSensorDataSet() != null) { - values.put(TrackPointsColumns.SENSOR, mtLocation.getSensorDataSet().toByteArray()); + @Override + public List getAllTracks() { + Cursor cursor = getTracksCursor(null, null, null, TracksColumns._ID); + ArrayList tracks = new ArrayList(); + if (cursor != null) { + tracks.ensureCapacity(cursor.getCount()); + if (cursor.moveToFirst()) { + do { + tracks.add(createTrack(cursor)); + } while (cursor.moveToNext()); + } + cursor.close(); + } + return tracks; + } + + @Override + public Track getTrack(long trackId) { + if (trackId < 0) { + return null; + } + Cursor cursor = null; + try { + cursor = getTracksCursor(null, TracksColumns._ID + "=?", + new String[] { Long.toString(trackId) }, TracksColumns._ID); + if (cursor != null && cursor.moveToNext()) { + return createTrack(cursor); + } + } finally { + if (cursor != null) { + cursor.close(); } } - return values; + return null; + } + + @Override + public Track getLastTrack() { + Cursor cursor = null; + try { + String selection = TracksColumns._ID + "=(select max(" + TracksColumns._ID + ") from " + + TracksColumns.TABLE_NAME + ")"; + cursor = getTracksCursor(null, selection, null, TracksColumns._ID); + if (cursor != null && cursor.moveToNext()) { + return createTrack(cursor); + } + } finally { + if (cursor != null) { + cursor.close(); + } + } + return null; + } + + @Override + public long getLastTrackId() { + Cursor cursor = null; + try { + String selection = TracksColumns._ID + "=(select max(" + TracksColumns._ID + ") from " + + TracksColumns.TABLE_NAME + ")"; + cursor = getTracksCursor( + new String[] { TracksColumns._ID }, selection, null, TracksColumns._ID); + if (cursor != null && cursor.moveToFirst()) { + return cursor.getLong(cursor.getColumnIndexOrThrow(TracksColumns._ID)); + } + } finally { + if (cursor != null) { + cursor.close(); + } + } + return -1L; + } + + @Override + public boolean trackExists(long trackId) { + if (trackId < 0) { + return false; + } + Cursor cursor = null; + try { + cursor = getTracksCursor(new String[] { TracksColumns._ID }, TracksColumns._ID + "=?", + new String[] { Long.toString(trackId) }, TracksColumns._ID); + if (cursor != null && cursor.moveToNext()) { + return true; + } + } finally { + if (cursor != null) { + cursor.close(); + } + } + return false; + } + + @Override + public Uri insertTrack(Track track) { + return contentResolver.insert(TracksColumns.CONTENT_URI, createContentValues(track)); + } + + @Override + public void updateTrack(Track track) { + contentResolver.update(TracksColumns.CONTENT_URI, createContentValues(track), + TracksColumns._ID + "=?", new String[] { Long.toString(track.getId()) }); + } + + @Override + public void deleteAllTracks() { + contentResolver.delete(TrackPointsColumns.CONTENT_URI, null, null); + contentResolver.delete(WaypointsColumns.CONTENT_URI, null, null); + // Delete tracks last since it triggers a database vaccum call + contentResolver.delete(TracksColumns.CONTENT_URI, null, null); + } + + @Override + public void deleteTrack(long trackId) { + Track track = getTrack(trackId); + if (track != null) { + String where = TrackPointsColumns._ID + ">=? AND " + TrackPointsColumns._ID + "<=?"; + String[] selectionArgs = new String[] { Long.toString(track.getStartId()), Long.toString( + track.getStopId()) }; + contentResolver.delete(TrackPointsColumns.CONTENT_URI, where, selectionArgs); + } + contentResolver.delete(WaypointsColumns.CONTENT_URI, WaypointsColumns.TRACKID + "=?", + new String[] { Long.toString(trackId) }); + // Delete tracks last since it triggers a database vaccum call + contentResolver.delete(TracksColumns.CONTENT_URI, TracksColumns._ID + "=?", new String[] { + Long.toString(trackId) }); + } + + @Override + public Track createTrack(Cursor cursor) { + int idIndex = cursor.getColumnIndexOrThrow(TracksColumns._ID); + int nameIndex = cursor.getColumnIndexOrThrow(TracksColumns.NAME); + int descriptionIndex = cursor.getColumnIndexOrThrow(TracksColumns.DESCRIPTION); + int categoryIndex = cursor.getColumnIndexOrThrow(TracksColumns.CATEGORY); + int startIdIndex = cursor.getColumnIndexOrThrow(TracksColumns.STARTID); + int stopIdIndex = cursor.getColumnIndexOrThrow(TracksColumns.STOPID); + int startTimeIndex = cursor.getColumnIndexOrThrow(TracksColumns.STARTTIME); + int stopTimeIndex = cursor.getColumnIndexOrThrow(TracksColumns.STOPTIME); + int numPointsIndex = cursor.getColumnIndexOrThrow(TracksColumns.NUMPOINTS); + int totalDistanceIndex = cursor.getColumnIndexOrThrow(TracksColumns.TOTALDISTANCE); + int totalTimeIndex = cursor.getColumnIndexOrThrow(TracksColumns.TOTALTIME); + int movingTimeIndex = cursor.getColumnIndexOrThrow(TracksColumns.MOVINGTIME); + int minLatIndex = cursor.getColumnIndexOrThrow(TracksColumns.MINLAT); + int maxLatIndex = cursor.getColumnIndexOrThrow(TracksColumns.MAXLAT); + int minLonIndex = cursor.getColumnIndexOrThrow(TracksColumns.MINLON); + int maxLonIndex = cursor.getColumnIndexOrThrow(TracksColumns.MAXLON); + int maxSpeedIndex = cursor.getColumnIndexOrThrow(TracksColumns.MAXSPEED); + int minElevationIndex = cursor.getColumnIndexOrThrow(TracksColumns.MINELEVATION); + int maxElevationIndex = cursor.getColumnIndexOrThrow(TracksColumns.MAXELEVATION); + int elevationGainIndex = cursor.getColumnIndexOrThrow(TracksColumns.ELEVATIONGAIN); + int minGradeIndex = cursor.getColumnIndexOrThrow(TracksColumns.MINGRADE); + int maxGradeIndex = cursor.getColumnIndexOrThrow(TracksColumns.MAXGRADE); + int mapIdIndex = cursor.getColumnIndexOrThrow(TracksColumns.MAPID); + int tableIdIndex = cursor.getColumnIndexOrThrow(TracksColumns.TABLEID); + int iconIndex = cursor.getColumnIndexOrThrow(TracksColumns.ICON); + + Track track = new Track(); + TripStatistics tripStatistics = track.getTripStatistics(); + if (!cursor.isNull(idIndex)) { + track.setId(cursor.getLong(idIndex)); + } + if (!cursor.isNull(nameIndex)) { + track.setName(cursor.getString(nameIndex)); + } + if (!cursor.isNull(descriptionIndex)) { + track.setDescription(cursor.getString(descriptionIndex)); + } + if (!cursor.isNull(categoryIndex)) { + track.setCategory(cursor.getString(categoryIndex)); + } + if (!cursor.isNull(startIdIndex)) { + track.setStartId(cursor.getLong(startIdIndex)); + } + if (!cursor.isNull(stopIdIndex)) { + track.setStopId(cursor.getLong(stopIdIndex)); + } + if (!cursor.isNull(startTimeIndex)) { + tripStatistics.setStartTime(cursor.getLong(startTimeIndex)); + } + if (!cursor.isNull(stopTimeIndex)) { + tripStatistics.setStopTime(cursor.getLong(stopTimeIndex)); + } + if (!cursor.isNull(numPointsIndex)) { + track.setNumberOfPoints(cursor.getInt(numPointsIndex)); + } + if (!cursor.isNull(totalDistanceIndex)) { + tripStatistics.setTotalDistance(cursor.getFloat(totalDistanceIndex)); + } + if (!cursor.isNull(totalTimeIndex)) { + tripStatistics.setTotalTime(cursor.getLong(totalTimeIndex)); + } + if (!cursor.isNull(movingTimeIndex)) { + tripStatistics.setMovingTime(cursor.getLong(movingTimeIndex)); + } + if (!cursor.isNull(minLatIndex) && !cursor.isNull(maxLatIndex) && !cursor.isNull(minLonIndex) + && !cursor.isNull(maxLonIndex)) { + int bottom = cursor.getInt(minLatIndex); + int top = cursor.getInt(maxLatIndex); + int left = cursor.getInt(minLonIndex); + int right = cursor.getInt(maxLonIndex); + tripStatistics.setBounds(left, top, right, bottom); + } + if (!cursor.isNull(maxSpeedIndex)) { + tripStatistics.setMaxSpeed(cursor.getFloat(maxSpeedIndex)); + } + if (!cursor.isNull(minElevationIndex)) { + tripStatistics.setMinElevation(cursor.getFloat(minElevationIndex)); + } + if (!cursor.isNull(maxElevationIndex)) { + tripStatistics.setMaxElevation(cursor.getFloat(maxElevationIndex)); + } + if (!cursor.isNull(elevationGainIndex)) { + tripStatistics.setTotalElevationGain(cursor.getFloat(elevationGainIndex)); + } + if (!cursor.isNull(minGradeIndex)) { + tripStatistics.setMinGrade(cursor.getFloat(minGradeIndex)); + } + if (!cursor.isNull(maxGradeIndex)) { + tripStatistics.setMaxGrade(cursor.getFloat(maxGradeIndex)); + } + if (!cursor.isNull(mapIdIndex)) { + track.setMapId(cursor.getString(mapIdIndex)); + } + if (!cursor.isNull(tableIdIndex)) { + track.setTableId(cursor.getString(tableIdIndex)); + } + if (!cursor.isNull(iconIndex)) { + track.setIcon(cursor.getString(iconIndex)); + } + return track; } @Override public ContentValues createContentValues(Track track) { ContentValues values = new ContentValues(); - TripStatistics stats = track.getTripStatistics(); + TripStatistics tripStatistics = track.getTripStatistics(); - // Values id < 0 indicate no id is available: + // Value < 0 indicates no id is available if (track.getId() >= 0) { values.put(TracksColumns._ID, track.getId()); } @@ -101,34 +312,403 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { values.put(TracksColumns.CATEGORY, track.getCategory()); values.put(TracksColumns.STARTID, track.getStartId()); values.put(TracksColumns.STOPID, track.getStopId()); - values.put(TracksColumns.STARTTIME, stats.getStartTime()); - values.put(TracksColumns.STOPTIME, stats.getStopTime()); + values.put(TracksColumns.STARTTIME, tripStatistics.getStartTime()); + values.put(TracksColumns.STOPTIME, tripStatistics.getStopTime()); values.put(TracksColumns.NUMPOINTS, track.getNumberOfPoints()); - values.put(TracksColumns.TOTALDISTANCE, stats.getTotalDistance()); - values.put(TracksColumns.TOTALTIME, stats.getTotalTime()); - values.put(TracksColumns.MOVINGTIME, stats.getMovingTime()); - values.put(TracksColumns.MINLAT, stats.getBottom()); - values.put(TracksColumns.MAXLAT, stats.getTop()); - values.put(TracksColumns.MINLON, stats.getLeft()); - values.put(TracksColumns.MAXLON, stats.getRight()); - values.put(TracksColumns.AVGSPEED, stats.getAverageSpeed()); - values.put(TracksColumns.AVGMOVINGSPEED, stats.getAverageMovingSpeed()); - values.put(TracksColumns.MAXSPEED, stats.getMaxSpeed()); - values.put(TracksColumns.MINELEVATION, stats.getMinElevation()); - values.put(TracksColumns.MAXELEVATION, stats.getMaxElevation()); - values.put(TracksColumns.ELEVATIONGAIN, stats.getTotalElevationGain()); - values.put(TracksColumns.MINGRADE, stats.getMinGrade()); - values.put(TracksColumns.MAXGRADE, stats.getMaxGrade()); + values.put(TracksColumns.TOTALDISTANCE, tripStatistics.getTotalDistance()); + values.put(TracksColumns.TOTALTIME, tripStatistics.getTotalTime()); + values.put(TracksColumns.MOVINGTIME, tripStatistics.getMovingTime()); + values.put(TracksColumns.MINLAT, tripStatistics.getBottom()); + values.put(TracksColumns.MAXLAT, tripStatistics.getTop()); + values.put(TracksColumns.MINLON, tripStatistics.getLeft()); + values.put(TracksColumns.MAXLON, tripStatistics.getRight()); + values.put(TracksColumns.AVGSPEED, tripStatistics.getAverageSpeed()); + values.put(TracksColumns.AVGMOVINGSPEED, tripStatistics.getAverageMovingSpeed()); + values.put(TracksColumns.MAXSPEED, tripStatistics.getMaxSpeed()); + values.put(TracksColumns.MINELEVATION, tripStatistics.getMinElevation()); + values.put(TracksColumns.MAXELEVATION, tripStatistics.getMaxElevation()); + values.put(TracksColumns.ELEVATIONGAIN, tripStatistics.getTotalElevationGain()); + values.put(TracksColumns.MINGRADE, tripStatistics.getMinGrade()); + values.put(TracksColumns.MAXGRADE, tripStatistics.getMaxGrade()); values.put(TracksColumns.MAPID, track.getMapId()); values.put(TracksColumns.TABLEID, track.getTableId()); values.put(TracksColumns.ICON, track.getIcon()); return values; } - private static ContentValues createContentValues(Waypoint waypoint) { + /** + * Gets a waypoints cursor. + * + * @param projection the projection + * @param selection the selection + * @param selectionArgs the selection args + * @param sortOrder the sort order + * @param maxWaypoints the maximum number of waypoints + */ + private Cursor getWaypointsCursor(String[] projection, String selection, String[] selectionArgs, + String sortOrder, int maxWaypoints) { + if (sortOrder == null) { + sortOrder = WaypointsColumns._ID; + } + if (maxWaypoints > 0) { + sortOrder += " LIMIT " + maxWaypoints; + } + return contentResolver.query( + WaypointsColumns.CONTENT_URI, projection, selection, selectionArgs, sortOrder); + } + + @Override + public Cursor getWaypointsCursor( + String selection, String[] selectionArgs, String sortOrder, int maxWaypoints) { + return getWaypointsCursor(null, selection, selectionArgs, sortOrder, maxWaypoints); + } + + @Override + public Cursor getWaypointsCursor(long trackId, long minWaypointId, int maxWaypoints) { + if (trackId < 0) { + return null; + } + + String selection; + String[] selectionArgs; + if (minWaypointId >= 0) { + selection = WaypointsColumns.TRACKID + "=? AND " + WaypointsColumns._ID + ">=?"; + selectionArgs = new String[] { Long.toString(trackId), Long.toString(minWaypointId) }; + } else { + selection = WaypointsColumns.TRACKID + "=?"; + selectionArgs = new String[] { Long.toString(trackId) }; + } + return getWaypointsCursor(null, selection, selectionArgs, WaypointsColumns._ID, maxWaypoints); + } + + @Override + public Waypoint getFirstWaypoint(long trackId) { + if (trackId < 0) { + return null; + } + Cursor cursor = null; + try { + cursor = getWaypointsCursor(null, WaypointsColumns.TRACKID + "=?", new String[] { + Long.toString(trackId) }, WaypointsColumns._ID, 1); + if (cursor != null && cursor.moveToFirst()) { + return createWaypoint(cursor); + } + } finally { + if (cursor != null) { + cursor.close(); + } + } + return null; + } + + @Override + public long getFirstWaypointId(long trackId) { + if (trackId < 0) { + return -1L; + } + Cursor cursor = null; + try { + cursor = getWaypointsCursor(new String[] { WaypointsColumns._ID }, + WaypointsColumns.TRACKID + "=?", new String[] { Long.toString(trackId) }, + WaypointsColumns._ID, 1); + if (cursor != null && cursor.moveToFirst()) { + return cursor.getLong(cursor.getColumnIndexOrThrow(WaypointsColumns._ID)); + } + } finally { + if (cursor != null) { + cursor.close(); + } + } + return -1L; + } + + @Override + public long getLastWaypointId(long trackId) { + if (trackId < 0) { + return -1; + } + Cursor cursor = null; + try { + cursor = getWaypointsCursor(new String[] { WaypointsColumns._ID }, + WaypointsColumns.TRACKID + "=?", new String[] { Long.toString(trackId) }, + WaypointsColumns._ID + " DESC", 1); + if (cursor != null && cursor.moveToFirst()) { + return cursor.getLong(cursor.getColumnIndexOrThrow(WaypointsColumns._ID)); + } + } finally { + if (cursor != null) { + cursor.close(); + } + } + return -1L; + } + + @Override + public Waypoint getLastStatisticsWaypoint(long trackId) { + if (trackId < 0) { + return null; + } + Cursor cursor = null; + try { + String selection = WaypointsColumns.TRACKID + "=? AND " + WaypointsColumns.TYPE + "=" + + Waypoint.TYPE_STATISTICS; + String[] selectionArgs = new String[] { Long.toString(trackId) }; + cursor = getWaypointsCursor( + null, selection, selectionArgs, WaypointsColumns._ID + " DESC", 1); + if (cursor != null && cursor.moveToFirst()) { + return createWaypoint(cursor); + } + } finally { + if (cursor != null) { + cursor.close(); + } + } + return null; + } + + @Override + public int getNextMarkerNumber(long trackId, boolean statistics) { + if (trackId < 0) { + return -1; + } + Cursor cursor = null; + try { + String[] projection = { WaypointsColumns._ID }; + String selection = WaypointsColumns.TRACKID + "=? AND " + WaypointsColumns.TYPE + "=?"; + int type = statistics ? Waypoint.TYPE_STATISTICS : Waypoint.TYPE_WAYPOINT; + String[] selectionArgs = new String[] { Long.toString(trackId), Integer.toString(type) }; + cursor = getWaypointsCursor(projection, selection, selectionArgs, WaypointsColumns._ID, 0); + if (cursor != null) { + int count = cursor.getCount(); + /* + * For statistics markers, the first marker is for the track statistics, + * so return the count as the next user visible number. + */ + return statistics ? count : count + 1; + } + } finally { + if (cursor != null) { + cursor.close(); + } + } + return -1; + } + + @Override + public Waypoint getNextStatisticsWaypointAfter(Waypoint waypoint) { + Cursor cursor = null; + try { + String selection = WaypointsColumns._ID + ">? AND " + WaypointsColumns.TRACKID + "=? AND " + + WaypointsColumns.TYPE + "=" + Waypoint.TYPE_STATISTICS; + String[] selectionArgs = new String[] { + Long.toString(waypoint.getId()), Long.toString(waypoint.getTrackId()) }; + cursor = getWaypointsCursor(null, selection, selectionArgs, WaypointsColumns._ID, 1); + if (cursor != null && cursor.moveToFirst()) { + return createWaypoint(cursor); + } + } finally { + if (cursor != null) { + cursor.close(); + } + } + return null; + } + + @Override + public Waypoint getWaypoint(long waypointId) { + if (waypointId < 0) { + return null; + } + Cursor cursor = null; + try { + cursor = getWaypointsCursor(null, WaypointsColumns._ID + "=?", + new String[] { Long.toString(waypointId) }, WaypointsColumns._ID, 0); + if (cursor != null && cursor.moveToFirst()) { + return createWaypoint(cursor); + } + } finally { + if (cursor != null) { + cursor.close(); + } + } + return null; + } + + @Override + public Uri insertWaypoint(Waypoint waypoint) { + waypoint.setId(-1L); + return contentResolver.insert(WaypointsColumns.CONTENT_URI, createContentValues(waypoint)); + } + + @Override + public boolean updateWaypoint(Waypoint waypoint) { + int rows = contentResolver.update(WaypointsColumns.CONTENT_URI, createContentValues(waypoint), + WaypointsColumns._ID + "=?", new String[] { Long.toString(waypoint.getId()) }); + return rows == 1; + } + + @Override + public void deleteWaypoint(long waypointId, DescriptionGenerator descriptionGenerator) { + final Waypoint waypoint = getWaypoint(waypointId); + if (waypoint != null && waypoint.getType() == Waypoint.TYPE_STATISTICS) { + final Waypoint nextWaypoint = getNextStatisticsWaypointAfter(waypoint); + if (nextWaypoint == null) { + Log.d(TAG, "Unable to find the next statistics marker after deleting one."); + } else { + nextWaypoint.getTripStatistics().merge(waypoint.getTripStatistics()); + nextWaypoint.setDescription( + descriptionGenerator.generateWaypointDescription(nextWaypoint.getTripStatistics())); + if (!updateWaypoint(nextWaypoint)) { + Log.e(TAG, "Unable to update the next statistics marker after deleting one."); + } + } + } + contentResolver.delete(WaypointsColumns.CONTENT_URI, WaypointsColumns._ID + "=?", + new String[] { Long.toString(waypointId) }); + } + + @Override + public Waypoint createWaypoint(Cursor cursor) { + int idIndex = cursor.getColumnIndexOrThrow(WaypointsColumns._ID); + int nameIndex = cursor.getColumnIndexOrThrow(WaypointsColumns.NAME); + int descriptionIndex = cursor.getColumnIndexOrThrow(WaypointsColumns.DESCRIPTION); + int categoryIndex = cursor.getColumnIndexOrThrow(WaypointsColumns.CATEGORY); + int iconIndex = cursor.getColumnIndexOrThrow(WaypointsColumns.ICON); + int trackIdIndex = cursor.getColumnIndexOrThrow(WaypointsColumns.TRACKID); + int typeIndex = cursor.getColumnIndexOrThrow(WaypointsColumns.TYPE); + int lengthIndex = cursor.getColumnIndexOrThrow(WaypointsColumns.LENGTH); + int durationIndex = cursor.getColumnIndexOrThrow(WaypointsColumns.DURATION); + int startTimeIndex = cursor.getColumnIndexOrThrow(WaypointsColumns.STARTTIME); + int startIdIndex = cursor.getColumnIndexOrThrow(WaypointsColumns.STARTID); + int stopIdIndex = cursor.getColumnIndexOrThrow(WaypointsColumns.STOPID); + int longitudeIndex = cursor.getColumnIndexOrThrow(WaypointsColumns.LONGITUDE); + int latitudeIndex = cursor.getColumnIndexOrThrow(WaypointsColumns.LATITUDE); + int timeIndex = cursor.getColumnIndexOrThrow(WaypointsColumns.TIME); + int altitudeIndex = cursor.getColumnIndexOrThrow(WaypointsColumns.ALTITUDE); + int accuracyIndex = cursor.getColumnIndexOrThrow(WaypointsColumns.ACCURACY); + int speedIndex = cursor.getColumnIndexOrThrow(WaypointsColumns.SPEED); + int bearingIndex = cursor.getColumnIndexOrThrow(WaypointsColumns.BEARING); + int totalDistanceIndex = cursor.getColumnIndexOrThrow(WaypointsColumns.TOTALDISTANCE); + int totalTimeIndex = cursor.getColumnIndexOrThrow(WaypointsColumns.TOTALTIME); + int movingTimeIndex = cursor.getColumnIndexOrThrow(WaypointsColumns.MOVINGTIME); + int maxSpeedIndex = cursor.getColumnIndexOrThrow(WaypointsColumns.MAXSPEED); + int minElevationIndex = cursor.getColumnIndexOrThrow(WaypointsColumns.MINELEVATION); + int maxElevationIndex = cursor.getColumnIndexOrThrow(WaypointsColumns.MAXELEVATION); + int elevationGainIndex = cursor.getColumnIndexOrThrow(WaypointsColumns.ELEVATIONGAIN); + int minGradeIndex = cursor.getColumnIndexOrThrow(WaypointsColumns.MINGRADE); + int maxGradeIndex = cursor.getColumnIndexOrThrow(WaypointsColumns.MAXGRADE); + + Waypoint waypoint = new Waypoint(); + + if (!cursor.isNull(idIndex)) { + waypoint.setId(cursor.getLong(idIndex)); + } + if (!cursor.isNull(nameIndex)) { + waypoint.setName(cursor.getString(nameIndex)); + } + if (!cursor.isNull(descriptionIndex)) { + waypoint.setDescription(cursor.getString(descriptionIndex)); + } + if (!cursor.isNull(categoryIndex)) { + waypoint.setCategory(cursor.getString(categoryIndex)); + } + if (!cursor.isNull(iconIndex)) { + waypoint.setIcon(cursor.getString(iconIndex)); + } + if (!cursor.isNull(trackIdIndex)) { + waypoint.setTrackId(cursor.getLong(trackIdIndex)); + } + if (!cursor.isNull(typeIndex)) { + waypoint.setType(cursor.getInt(typeIndex)); + } + if (!cursor.isNull(lengthIndex)) { + waypoint.setLength(cursor.getFloat(lengthIndex)); + } + if (!cursor.isNull(durationIndex)) { + waypoint.setDuration(cursor.getLong(durationIndex)); + } + if (!cursor.isNull(startIdIndex)) { + waypoint.setStartId(cursor.getLong(startIdIndex)); + } + if (!cursor.isNull(stopIdIndex)) { + waypoint.setStopId(cursor.getLong(stopIdIndex)); + } + + Location location = new Location(""); + if (!cursor.isNull(longitudeIndex) && !cursor.isNull(latitudeIndex)) { + location.setLongitude(((double) cursor.getInt(longitudeIndex)) / 1E6); + location.setLatitude(((double) cursor.getInt(latitudeIndex)) / 1E6); + } + if (!cursor.isNull(timeIndex)) { + location.setTime(cursor.getLong(timeIndex)); + } + if (!cursor.isNull(altitudeIndex)) { + location.setAltitude(cursor.getFloat(altitudeIndex)); + } + if (!cursor.isNull(accuracyIndex)) { + location.setAccuracy(cursor.getFloat(accuracyIndex)); + } + if (!cursor.isNull(speedIndex)) { + location.setSpeed(cursor.getFloat(speedIndex)); + } + if (!cursor.isNull(bearingIndex)) { + location.setBearing(cursor.getFloat(bearingIndex)); + } + waypoint.setLocation(location); + + TripStatistics tripStatistics = new TripStatistics(); + boolean hasTripStatistics = false; + if (!cursor.isNull(startTimeIndex)) { + tripStatistics.setStartTime(cursor.getLong(startTimeIndex)); + hasTripStatistics = true; + } + if (!cursor.isNull(totalDistanceIndex)) { + tripStatistics.setTotalDistance(cursor.getFloat(totalDistanceIndex)); + hasTripStatistics = true; + } + if (!cursor.isNull(totalTimeIndex)) { + tripStatistics.setTotalTime(cursor.getLong(totalTimeIndex)); + hasTripStatistics = true; + } + if (!cursor.isNull(movingTimeIndex)) { + tripStatistics.setMovingTime(cursor.getLong(movingTimeIndex)); + hasTripStatistics = true; + } + if (!cursor.isNull(maxSpeedIndex)) { + tripStatistics.setMaxSpeed(cursor.getFloat(maxSpeedIndex)); + hasTripStatistics = true; + } + if (!cursor.isNull(minElevationIndex)) { + tripStatistics.setMinElevation(cursor.getFloat(minElevationIndex)); + hasTripStatistics = true; + } + if (!cursor.isNull(maxElevationIndex)) { + tripStatistics.setMaxElevation(cursor.getFloat(maxElevationIndex)); + hasTripStatistics = true; + } + if (!cursor.isNull(elevationGainIndex)) { + tripStatistics.setTotalElevationGain(cursor.getFloat(elevationGainIndex)); + hasTripStatistics = true; + } + if (!cursor.isNull(minGradeIndex)) { + tripStatistics.setMinGrade(cursor.getFloat(minGradeIndex)); + hasTripStatistics = true; + } + if (!cursor.isNull(maxGradeIndex)) { + tripStatistics.setMaxGrade(cursor.getFloat(maxGradeIndex)); + hasTripStatistics = true; + } + + if (hasTripStatistics) { + waypoint.setTripStatistics(tripStatistics); + } + return waypoint; + } + + ContentValues createContentValues(Waypoint waypoint) { ContentValues values = new ContentValues(); - // Values id < 0 indicate no id is available: + // Value < 0 indicates no id is available if (waypoint.getId() >= 0) { values.put(WaypointsColumns._ID, waypoint.getId()); } @@ -162,475 +742,45 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { } } - TripStatistics stats = waypoint.getTripStatistics(); - if (stats != null) { - values.put(WaypointsColumns.STARTTIME, stats.getStartTime()); - values.put(WaypointsColumns.TOTALDISTANCE, stats.getTotalDistance()); - values.put(WaypointsColumns.TOTALTIME, stats.getTotalTime()); - values.put(WaypointsColumns.MOVINGTIME, stats.getMovingTime()); - values.put(WaypointsColumns.AVGSPEED, stats.getAverageSpeed()); - values.put(WaypointsColumns.AVGMOVINGSPEED, stats.getAverageMovingSpeed()); - values.put(WaypointsColumns.MAXSPEED, stats.getMaxSpeed()); - values.put(WaypointsColumns.MINELEVATION, stats.getMinElevation()); - values.put(WaypointsColumns.MAXELEVATION, stats.getMaxElevation()); - values.put(WaypointsColumns.ELEVATIONGAIN, stats.getTotalElevationGain()); - values.put(WaypointsColumns.MINGRADE, stats.getMinGrade()); - values.put(WaypointsColumns.MAXGRADE, stats.getMaxGrade()); + TripStatistics tripStatistics = waypoint.getTripStatistics(); + if (tripStatistics != null) { + values.put(WaypointsColumns.STARTTIME, tripStatistics.getStartTime()); + values.put(WaypointsColumns.TOTALDISTANCE, tripStatistics.getTotalDistance()); + values.put(WaypointsColumns.TOTALTIME, tripStatistics.getTotalTime()); + values.put(WaypointsColumns.MOVINGTIME, tripStatistics.getMovingTime()); + values.put(WaypointsColumns.AVGSPEED, tripStatistics.getAverageSpeed()); + values.put(WaypointsColumns.AVGMOVINGSPEED, tripStatistics.getAverageMovingSpeed()); + values.put(WaypointsColumns.MAXSPEED, tripStatistics.getMaxSpeed()); + values.put(WaypointsColumns.MINELEVATION, tripStatistics.getMinElevation()); + values.put(WaypointsColumns.MAXELEVATION, tripStatistics.getMaxElevation()); + values.put(WaypointsColumns.ELEVATIONGAIN, tripStatistics.getTotalElevationGain()); + values.put(WaypointsColumns.MINGRADE, tripStatistics.getMinGrade()); + values.put(WaypointsColumns.MAXGRADE, tripStatistics.getMaxGrade()); } return values; } - @Override - public Location createLocation(Cursor cursor) { - Location location = new MyTracksLocation(""); - fillLocation(cursor, location); - return location; - } - /** - * A cache of track column indices. + * Gets a track points cursor. + * + * @param projection the projection + * @param selection the selection + * @param selectionArgs the selection arguments + * @param sortOrder the sort order */ - private static class CachedTrackColumnIndices { - public final int idxId; - public final int idxLongitude; - public final int idxLatitude; - public final int idxTime; - public final int idxAltitude; - public final int idxAccuracy; - public final int idxSpeed; - public final int idxBearing; - public final int idxSensor; - - public CachedTrackColumnIndices(Cursor cursor) { - idxId = cursor.getColumnIndex(TrackPointsColumns._ID); - idxLongitude = cursor.getColumnIndexOrThrow(TrackPointsColumns.LONGITUDE); - idxLatitude = cursor.getColumnIndexOrThrow(TrackPointsColumns.LATITUDE); - idxTime = cursor.getColumnIndexOrThrow(TrackPointsColumns.TIME); - idxAltitude = cursor.getColumnIndexOrThrow(TrackPointsColumns.ALTITUDE); - idxAccuracy = cursor.getColumnIndexOrThrow(TrackPointsColumns.ACCURACY); - idxSpeed = cursor.getColumnIndexOrThrow(TrackPointsColumns.SPEED); - idxBearing = cursor.getColumnIndexOrThrow(TrackPointsColumns.BEARING); - idxSensor = cursor.getColumnIndexOrThrow(TrackPointsColumns.SENSOR); - } + private Cursor getTrackPointsCursor( + String[] projection, String selection, String[] selectionArgs, String sortOrder) { + return contentResolver.query( + TrackPointsColumns.CONTENT_URI, projection, selection, selectionArgs, sortOrder); } - private void fillLocation( - Cursor cursor, CachedTrackColumnIndices columnIndices, Location location) { - location.reset(); - - if (!cursor.isNull(columnIndices.idxLongitude)) { - location.setLongitude(1. * cursor.getInt(columnIndices.idxLongitude) / 1E6); - } - if (!cursor.isNull(columnIndices.idxLatitude)) { - location.setLatitude(1. * cursor.getInt(columnIndices.idxLatitude) / 1E6); - } - if (!cursor.isNull(columnIndices.idxTime)) { - location.setTime(cursor.getLong(columnIndices.idxTime)); - } - if (!cursor.isNull(columnIndices.idxAltitude)) { - location.setAltitude(cursor.getFloat(columnIndices.idxAltitude)); - } - if (!cursor.isNull(columnIndices.idxAccuracy)) { - location.setAccuracy(cursor.getFloat(columnIndices.idxAccuracy)); - } - if (!cursor.isNull(columnIndices.idxSpeed)) { - location.setSpeed(cursor.getFloat(columnIndices.idxSpeed)); - } - if (!cursor.isNull(columnIndices.idxBearing)) { - location.setBearing(cursor.getFloat(columnIndices.idxBearing)); - } - if (location instanceof MyTracksLocation && !cursor.isNull(columnIndices.idxSensor)) { - MyTracksLocation myTracksLocation = (MyTracksLocation) location; - try { - myTracksLocation.setSensorDataSet( - SensorDataSet.parseFrom(cursor.getBlob(columnIndices.idxSensor))); - } catch (InvalidProtocolBufferException e) { - Log.w(TAG, "Failed to parse sensor data.", e); - } - } - } - - @Override - public void fillLocation(Cursor cursor, Location location) { - CachedTrackColumnIndices columnIndicies = new CachedTrackColumnIndices(cursor); - fillLocation(cursor, columnIndicies, location); - } - - @Override - public Track createTrack(Cursor cursor) { - int idxId = cursor.getColumnIndexOrThrow(TracksColumns._ID); - int idxName = cursor.getColumnIndexOrThrow(TracksColumns.NAME); - int idxDescription = cursor.getColumnIndexOrThrow(TracksColumns.DESCRIPTION); - int idxCategory = cursor.getColumnIndexOrThrow(TracksColumns.CATEGORY); - int idxStartId = cursor.getColumnIndexOrThrow(TracksColumns.STARTID); - int idxStopId = cursor.getColumnIndexOrThrow(TracksColumns.STOPID); - int idxStartTime = cursor.getColumnIndexOrThrow(TracksColumns.STARTTIME); - int idxStopTime = cursor.getColumnIndexOrThrow(TracksColumns.STOPTIME); - int idxNumPoints = cursor.getColumnIndexOrThrow(TracksColumns.NUMPOINTS); - int idxTotalDistance = cursor.getColumnIndexOrThrow(TracksColumns.TOTALDISTANCE); - int idxTotalTime = cursor.getColumnIndexOrThrow(TracksColumns.TOTALTIME); - int idxMovingTime = cursor.getColumnIndexOrThrow(TracksColumns.MOVINGTIME); - int idxMinlat = cursor.getColumnIndexOrThrow(TracksColumns.MINLAT); - int idxMaxlat = cursor.getColumnIndexOrThrow(TracksColumns.MAXLAT); - int idxMinlon = cursor.getColumnIndexOrThrow(TracksColumns.MINLON); - int idxMaxlon = cursor.getColumnIndexOrThrow(TracksColumns.MAXLON); - int idxMaxSpeed = cursor.getColumnIndexOrThrow(TracksColumns.MAXSPEED); - int idxMinElevation = cursor.getColumnIndexOrThrow(TracksColumns.MINELEVATION); - int idxMaxElevation = cursor.getColumnIndexOrThrow(TracksColumns.MAXELEVATION); - int idxElevationGain = cursor.getColumnIndexOrThrow(TracksColumns.ELEVATIONGAIN); - int idxMinGrade = cursor.getColumnIndexOrThrow(TracksColumns.MINGRADE); - int idxMaxGrade = cursor.getColumnIndexOrThrow(TracksColumns.MAXGRADE); - int idxMapId = cursor.getColumnIndexOrThrow(TracksColumns.MAPID); - int idxTableId = cursor.getColumnIndexOrThrow(TracksColumns.TABLEID); - int idxIcon = cursor.getColumnIndexOrThrow(TracksColumns.ICON); - - Track track = new Track(); - TripStatistics tripStatistics = track.getTripStatistics(); - if (!cursor.isNull(idxId)) { - track.setId(cursor.getLong(idxId)); - } - if (!cursor.isNull(idxName)) { - track.setName(cursor.getString(idxName)); - } - if (!cursor.isNull(idxDescription)) { - track.setDescription(cursor.getString(idxDescription)); - } - if (!cursor.isNull(idxCategory)) { - track.setCategory(cursor.getString(idxCategory)); - } - if (!cursor.isNull(idxStartId)) { - track.setStartId(cursor.getInt(idxStartId)); - } - if (!cursor.isNull(idxStopId)) { - track.setStopId(cursor.getInt(idxStopId)); - } - if (!cursor.isNull(idxStartTime)) { - tripStatistics.setStartTime(cursor.getLong(idxStartTime)); - } - if (!cursor.isNull(idxStopTime)) { - tripStatistics.setStopTime(cursor.getLong(idxStopTime)); - } - if (!cursor.isNull(idxNumPoints)) { - track.setNumberOfPoints(cursor.getInt(idxNumPoints)); - } - if (!cursor.isNull(idxTotalDistance)) { - tripStatistics.setTotalDistance(cursor.getFloat(idxTotalDistance)); - } - if (!cursor.isNull(idxTotalTime)) { - tripStatistics.setTotalTime(cursor.getLong(idxTotalTime)); - } - if (!cursor.isNull(idxMovingTime)) { - tripStatistics.setMovingTime(cursor.getLong(idxMovingTime)); - } - if (!cursor.isNull(idxMinlat) - && !cursor.isNull(idxMaxlat) - && !cursor.isNull(idxMinlon) - && !cursor.isNull(idxMaxlon)) { - int bottom = cursor.getInt(idxMinlat); - int top = cursor.getInt(idxMaxlat); - int left = cursor.getInt(idxMinlon); - int right = cursor.getInt(idxMaxlon); - tripStatistics.setBounds(left, top, right, bottom); - } - if (!cursor.isNull(idxMaxSpeed)) { - tripStatistics.setMaxSpeed(cursor.getFloat(idxMaxSpeed)); - } - if (!cursor.isNull(idxMinElevation)) { - tripStatistics.setMinElevation(cursor.getFloat(idxMinElevation)); - } - if (!cursor.isNull(idxMaxElevation)) { - tripStatistics.setMaxElevation(cursor.getFloat(idxMaxElevation)); - } - if (!cursor.isNull(idxElevationGain)) { - tripStatistics.setTotalElevationGain(cursor.getFloat(idxElevationGain)); - } - if (!cursor.isNull(idxMinGrade)) { - tripStatistics.setMinGrade(cursor.getFloat(idxMinGrade)); - } - if (!cursor.isNull(idxMaxGrade)) { - tripStatistics.setMaxGrade(cursor.getFloat(idxMaxGrade)); - } - if (!cursor.isNull(idxMapId)) { - track.setMapId(cursor.getString(idxMapId)); - } - if (!cursor.isNull(idxTableId)) { - track.setTableId(cursor.getString(idxTableId)); - } - if (!cursor.isNull(idxIcon)) { - track.setIcon(cursor.getString(idxIcon)); - } - return track; - } - - @Override - public Waypoint createWaypoint(Cursor cursor) { - int idxId = cursor.getColumnIndexOrThrow(WaypointsColumns._ID); - int idxName = cursor.getColumnIndexOrThrow(WaypointsColumns.NAME); - int idxDescription = cursor.getColumnIndexOrThrow(WaypointsColumns.DESCRIPTION); - int idxCategory = cursor.getColumnIndexOrThrow(WaypointsColumns.CATEGORY); - int idxIcon = cursor.getColumnIndexOrThrow(WaypointsColumns.ICON); - int idxTrackId = cursor.getColumnIndexOrThrow(WaypointsColumns.TRACKID); - int idxType = cursor.getColumnIndexOrThrow(WaypointsColumns.TYPE); - int idxLength = cursor.getColumnIndexOrThrow(WaypointsColumns.LENGTH); - int idxDuration = cursor.getColumnIndexOrThrow(WaypointsColumns.DURATION); - int idxStartTime = cursor.getColumnIndexOrThrow(WaypointsColumns.STARTTIME); - int idxStartId = cursor.getColumnIndexOrThrow(WaypointsColumns.STARTID); - int idxStopId = cursor.getColumnIndexOrThrow(WaypointsColumns.STOPID); - - int idxLongitude = cursor.getColumnIndexOrThrow(WaypointsColumns.LONGITUDE); - int idxLatitude = cursor.getColumnIndexOrThrow(WaypointsColumns.LATITUDE); - int idxTime = cursor.getColumnIndexOrThrow(WaypointsColumns.TIME); - int idxAltitude = cursor.getColumnIndexOrThrow(WaypointsColumns.ALTITUDE); - int idxAccuracy = cursor.getColumnIndexOrThrow(WaypointsColumns.ACCURACY); - int idxSpeed = cursor.getColumnIndexOrThrow(WaypointsColumns.SPEED); - int idxBearing = cursor.getColumnIndexOrThrow(WaypointsColumns.BEARING); - - int idxTotalDistance = cursor.getColumnIndexOrThrow(WaypointsColumns.TOTALDISTANCE); - int idxTotalTime = cursor.getColumnIndexOrThrow(WaypointsColumns.TOTALTIME); - int idxMovingTime = cursor.getColumnIndexOrThrow(WaypointsColumns.MOVINGTIME); - int idxMaxSpeed = cursor.getColumnIndexOrThrow(WaypointsColumns.MAXSPEED); - int idxMinElevation = cursor.getColumnIndexOrThrow(WaypointsColumns.MINELEVATION); - int idxMaxElevation = cursor.getColumnIndexOrThrow(WaypointsColumns.MAXELEVATION); - int idxElevationGain = cursor.getColumnIndexOrThrow(WaypointsColumns.ELEVATIONGAIN); - int idxMinGrade = cursor.getColumnIndexOrThrow(WaypointsColumns.MINGRADE); - int idxMaxGrade = cursor.getColumnIndexOrThrow(WaypointsColumns.MAXGRADE); - - Waypoint waypoint = new Waypoint(); - - if (!cursor.isNull(idxId)) { - waypoint.setId(cursor.getLong(idxId)); - } - if (!cursor.isNull(idxName)) { - waypoint.setName(cursor.getString(idxName)); - } - if (!cursor.isNull(idxDescription)) { - waypoint.setDescription(cursor.getString(idxDescription)); - } - if (!cursor.isNull(idxCategory)) { - waypoint.setCategory(cursor.getString(idxCategory)); - } - if (!cursor.isNull(idxIcon)) { - waypoint.setIcon(cursor.getString(idxIcon)); - } - if (!cursor.isNull(idxTrackId)) { - waypoint.setTrackId(cursor.getLong(idxTrackId)); - } - if (!cursor.isNull(idxType)) { - waypoint.setType(cursor.getInt(idxType)); - } - if (!cursor.isNull(idxLength)) { - waypoint.setLength(cursor.getDouble(idxLength)); - } - if (!cursor.isNull(idxDuration)) { - waypoint.setDuration(cursor.getLong(idxDuration)); - } - if (!cursor.isNull(idxStartId)) { - waypoint.setStartId(cursor.getLong(idxStartId)); - } - if (!cursor.isNull(idxStopId)) { - waypoint.setStopId(cursor.getLong(idxStopId)); - } - - Location location = new Location(""); - if (!cursor.isNull(idxLongitude) && !cursor.isNull(idxLatitude)) { - location.setLongitude(1. * cursor.getInt(idxLongitude) / 1E6); - location.setLatitude(1. * cursor.getInt(idxLatitude) / 1E6); - - } - if (!cursor.isNull(idxTime)) { - location.setTime(cursor.getLong(idxTime)); - } - if (!cursor.isNull(idxAltitude)) { - location.setAltitude(cursor.getFloat(idxAltitude)); - } - if (!cursor.isNull(idxAccuracy)) { - location.setAccuracy(cursor.getFloat(idxAccuracy)); - } - if (!cursor.isNull(idxSpeed)) { - location.setSpeed(cursor.getFloat(idxSpeed)); - } - if (!cursor.isNull(idxBearing)) { - location.setBearing(cursor.getFloat(idxBearing)); - } - waypoint.setLocation(location); - - TripStatistics tripStatistics = new TripStatistics(); - boolean hasTripStatistics = false; - if (!cursor.isNull(idxStartTime)) { - tripStatistics.setStartTime(cursor.getLong(idxStartTime)); - hasTripStatistics = true; - } - if (!cursor.isNull(idxTotalDistance)) { - tripStatistics.setTotalDistance(cursor.getFloat(idxTotalDistance)); - hasTripStatistics = true; - } - if (!cursor.isNull(idxTotalTime)) { - tripStatistics.setTotalTime(cursor.getLong(idxTotalTime)); - hasTripStatistics = true; - } - if (!cursor.isNull(idxMovingTime)) { - tripStatistics.setMovingTime(cursor.getLong(idxMovingTime)); - hasTripStatistics = true; - } - if (!cursor.isNull(idxMaxSpeed)) { - tripStatistics.setMaxSpeed(cursor.getFloat(idxMaxSpeed)); - hasTripStatistics = true; - } - if (!cursor.isNull(idxMinElevation)) { - tripStatistics.setMinElevation(cursor.getFloat(idxMinElevation)); - hasTripStatistics = true; - } - if (!cursor.isNull(idxMaxElevation)) { - tripStatistics.setMaxElevation(cursor.getFloat(idxMaxElevation)); - hasTripStatistics = true; - } - if (!cursor.isNull(idxElevationGain)) { - tripStatistics.setTotalElevationGain(cursor.getFloat(idxElevationGain)); - hasTripStatistics = true; - } - if (!cursor.isNull(idxMinGrade)) { - tripStatistics.setMinGrade(cursor.getFloat(idxMinGrade)); - hasTripStatistics = true; - } - if (!cursor.isNull(idxMaxGrade)) { - tripStatistics.setMaxGrade(cursor.getFloat(idxMaxGrade)); - hasTripStatistics = true; - } - if (hasTripStatistics) { - waypoint.setTripStatistics(tripStatistics); - } - return waypoint; - } - - @Override - public void deleteAllTracks() { - contentResolver.delete(TracksColumns.CONTENT_URI, null, null); - contentResolver.delete(TrackPointsColumns.CONTENT_URI, - null, null); - contentResolver.delete(WaypointsColumns.CONTENT_URI, null, null); - } - - @Override - public void deleteTrack(long trackId) { - Track track = getTrack(trackId); - if (track != null) { - contentResolver.delete(TrackPointsColumns.CONTENT_URI, - "_id>=" + track.getStartId() + " AND _id<=" + track.getStopId(), - null); - } - contentResolver.delete(WaypointsColumns.CONTENT_URI, - WaypointsColumns.TRACKID + "=" + trackId, null); - contentResolver.delete( - TracksColumns.CONTENT_URI, "_id=" + trackId, null); - } - - @Override - public void deleteWaypoint(long waypointId, - DescriptionGenerator descriptionGenerator) { - final Waypoint deletedWaypoint = getWaypoint(waypointId); - if (deletedWaypoint != null - && deletedWaypoint.getType() == Waypoint.TYPE_STATISTICS) { - final Waypoint nextWaypoint = - getNextStatisticsWaypointAfter(deletedWaypoint); - if (nextWaypoint != null) { - Log.d(TAG, "Correcting marker " + nextWaypoint.getId() - + " after deleted marker " + deletedWaypoint.getId()); - nextWaypoint.getTripStatistics().merge(deletedWaypoint.getTripStatistics()); - nextWaypoint.setDescription( - descriptionGenerator.generateWaypointDescription(nextWaypoint)); - if (!updateWaypoint(nextWaypoint)) { - Log.w(TAG, "Update of marker was unsuccessful."); - } - } else { - Log.d(TAG, "No statistics marker after the deleted one was found."); - } - } - contentResolver.delete( - WaypointsColumns.CONTENT_URI, "_id=" + waypointId, null); - } - - @Override - public Waypoint getNextStatisticsWaypointAfter(Waypoint waypoint) { - final String selection = WaypointsColumns._ID + ">" + waypoint.getId() - + " AND " + WaypointsColumns.TRACKID + "=" + waypoint.getTrackId() - + " AND " + WaypointsColumns.TYPE + "=" + Waypoint.TYPE_STATISTICS; - final String sortOrder = WaypointsColumns._ID + " LIMIT 1"; + private Location findLocationBy(String selection, String[] selectionArgs) { Cursor cursor = null; try { - cursor = contentResolver.query( - WaypointsColumns.CONTENT_URI, - null /*projection*/, - selection, - null /*selectionArgs*/, - sortOrder); - if (cursor != null && cursor.moveToFirst()) { - return createWaypoint(cursor); - } - } catch (RuntimeException e) { - Log.w(TAG, "Caught unexpected exception.", e); - } finally { - if (cursor != null) { - cursor.close(); - } - } - return null; - } - - @Override - public boolean updateWaypoint(Waypoint waypoint) { - try { - final int rows = contentResolver.update( - WaypointsColumns.CONTENT_URI, - createContentValues(waypoint), - "_id=" + waypoint.getId(), - null /*selectionArgs*/); - return rows == 1; - } catch (RuntimeException e) { - Log.e(TAG, "Caught unexpected exception.", e); - } - return false; - } - - /** - * Finds a locations from the provider by the given selection. - * - * @param select a selection argument that identifies a unique location - * @return the fist location matching, or null if not found - */ - private Location findLocationBy(String select) { - Cursor cursor = null; - try { - cursor = contentResolver.query( - TrackPointsColumns.CONTENT_URI, null, select, null, null); + cursor = getTrackPointsCursor(null, selection, selectionArgs, TrackPointsColumns._ID); if (cursor != null && cursor.moveToNext()) { return createLocation(cursor); } - } catch (RuntimeException e) { - Log.w(TAG, "Caught an unexpeceted exception.", e); - } finally { - if (cursor != null) { - cursor.close(); - } - } - return null; - } - - /** - * Finds a track from the provider by the given selection. - * - * @param select a selection argument that identifies a unique track - * @return the first track matching, or null if not found - */ - private Track findTrackBy(String select) { - Cursor cursor = null; - try { - cursor = contentResolver.query( - TracksColumns.CONTENT_URI, null, select, null, null); - if (cursor != null && cursor.moveToNext()) { - return createTrack(cursor); - } - } catch (RuntimeException e) { - Log.w(TAG, "Caught unexpected exception.", e); } finally { if (cursor != null) { cursor.close(); @@ -641,425 +791,96 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { @Override public Location getFirstLocation() { - return findLocationBy("_id=(select min(_id) from trackpoints)"); + String selection = TrackPointsColumns._ID + "=(select min(" + TrackPointsColumns._ID + ") from " + + TrackPointsColumns.TABLE_NAME + ")"; + return findLocationBy(selection, null); } - + @Override public Location getLastLocation() { - return findLocationBy("_id=(select max(_id) from trackpoints)"); + String selection = TrackPointsColumns._ID + "=(select max(" + TrackPointsColumns._ID + ") from " + + TrackPointsColumns.TABLE_NAME + ")"; + return findLocationBy(selection, null); } @Override - public Waypoint getFirstWaypoint(long trackId) { - if (trackId < 0) { + public Location getLocation(long trackPointId) { + if (trackPointId < 0) { return null; } - - Cursor cursor = contentResolver.query( - WaypointsColumns.CONTENT_URI, - null /*projection*/, - "trackid=" + trackId, - null /*selectionArgs*/, - "_id LIMIT 1"); - if (cursor != null) { - try { - if (cursor.moveToFirst()) { - return createWaypoint(cursor); - } - } catch (RuntimeException e) { - Log.w(TAG, "Caught an unexpected exception.", e); - } finally { - cursor.close(); - } - } - return null; + return findLocationBy( + TrackPointsColumns._ID + "=?", new String[] { Long.toString(trackPointId) }); } @Override - public Waypoint getWaypoint(long waypointId) { - if (waypointId < 0) { - return null; - } - - Cursor cursor = contentResolver.query( - WaypointsColumns.CONTENT_URI, - null /*projection*/, - "_id=" + waypointId, - null /*selectionArgs*/, - null /*sortOrder*/); - if (cursor != null) { - try { - if (cursor.moveToFirst()) { - return createWaypoint(cursor); - } - } catch (RuntimeException e) { - Log.w(TAG, "Caught an unexpected exception.", e); - } finally { - cursor.close(); - } - } - return null; - } - - @Override - public long getLastLocationId(long trackId) { - if (trackId < 0) { - return -1; - } - - final String[] projection = {"_id"}; - Cursor cursor = contentResolver.query( - TrackPointsColumns.CONTENT_URI, - projection, - "_id=(select max(_id) from trackpoints WHERE trackid=" + trackId + ")", - null /*selectionArgs*/, - null /*sortOrder*/); - if (cursor != null) { - try { - if (cursor.moveToFirst()) { - return cursor.getLong( - cursor.getColumnIndexOrThrow(TrackPointsColumns._ID)); - } - } catch (RuntimeException e) { - Log.w(TAG, "Caught an unexpected exception.", e); - } finally { - cursor.close(); - } - } - return -1; - } - - @Override - public long getFirstWaypointId(long trackId) { - if (trackId < 0) { - return -1; - } - - final String[] projection = {"_id"}; - Cursor cursor = contentResolver.query( - WaypointsColumns.CONTENT_URI, - projection, - "trackid=" + trackId, - null /*selectionArgs*/, - "_id LIMIT 1" /*sortOrder*/); - if (cursor != null) { - try { - if (cursor.moveToFirst()) { - return cursor.getLong( - cursor.getColumnIndexOrThrow(WaypointsColumns._ID)); - } - } catch (RuntimeException e) { - Log.w(TAG, "Caught an unexpected exception.", e); - } finally { - cursor.close(); - } - } - return -1; - } - - @Override - public long getLastWaypointId(long trackId) { - if (trackId < 0) { - return -1; - } - - final String[] projection = {"_id"}; - Cursor cursor = contentResolver.query( - WaypointsColumns.CONTENT_URI, - projection, - WaypointsColumns.TRACKID + "=" + trackId, - null /*selectionArgs*/, - "_id DESC LIMIT 1" /*sortOrder*/); - if (cursor != null) { - try { - if (cursor.moveToFirst()) { - return cursor.getLong( - cursor.getColumnIndexOrThrow(WaypointsColumns._ID)); - } - } catch (RuntimeException e) { - Log.w(TAG, "Caught an unexpected exception.", e); - } finally { - cursor.close(); - } - } - return -1; - } - - @Override - public int getNextMarkerNumber(long trackId, boolean statistics) { - if (trackId == -1L) { - return -1; - } - String[] projection = { "_id" }; - String selection = WaypointsColumns.TRACKID + "=? AND " + WaypointsColumns.TYPE + "=?"; - String[] selectionArgs = new String[] { String.valueOf(trackId), - String.valueOf(statistics ? Waypoint.TYPE_STATISTICS : Waypoint.TYPE_WAYPOINT) }; - Cursor cursor = null; - try { - cursor = contentResolver.query( - WaypointsColumns.CONTENT_URI, projection, selection, selectionArgs, null); - if (cursor != null) { - int count = cursor.getCount(); - /* - * For statistics markers, the first marker is for the track statistics, - * thus just return the count as the next user visible number. - */ - return statistics ? count : count + 1; - } - } catch (RuntimeException e) { - Log.w(TAG, "Caught unexpected exception.", e); - } finally { - if (cursor != null) { - cursor.close(); - } - } - return -1; - } - - @Override - public Track getLastTrack() { - Cursor cursor = null; - try { - cursor = contentResolver.query( - TracksColumns.CONTENT_URI, null, "_id=(select max(_id) from tracks)", - null, null); - if (cursor != null && cursor.moveToNext()) { - return createTrack(cursor); - } - } catch (RuntimeException e) { - Log.w(TAG, "Caught an unexpected exception.", e); - } finally { - if (cursor != null) { - cursor.close(); - } - } - return null; - } - - @Override - public long getLastTrackId() { - String[] proj = { TracksColumns._ID }; - Cursor cursor = contentResolver.query( - TracksColumns.CONTENT_URI, proj, "_id=(select max(_id) from tracks)", - null, null); - if (cursor != null) { - try { - if (cursor.moveToFirst()) { - return cursor.getLong( - cursor.getColumnIndexOrThrow(TracksColumns._ID)); - } - } finally { - cursor.close(); - } - } - return -1; - } - - @Override - public Location getLocation(long id) { - if (id < 0) { - return null; - } - - String selection = TrackPointsColumns._ID + "=" + id; - return findLocationBy(selection); - } - - @Override - public Cursor getLocationsCursor(long trackId, long minTrackPointId, - int maxLocations, boolean descending) { + public Cursor getLocationsCursor( + long trackId, long startTrackPointId, int maxLocations, boolean descending) { if (trackId < 0) { return null; } String selection; String[] selectionArgs; - if (minTrackPointId >= 0) { + if (startTrackPointId >= 0) { String comparison = descending ? "<=" : ">="; selection = TrackPointsColumns.TRACKID + "=? AND " + TrackPointsColumns._ID + comparison + "?"; - selectionArgs = new String[] { String.valueOf(trackId), String.valueOf(minTrackPointId) }; + selectionArgs = new String[] { Long.toString(trackId), Long.toString(startTrackPointId) }; } else { selection = TrackPointsColumns.TRACKID + "=?"; - selectionArgs = new String[] { String.valueOf(trackId) }; - } - - String sortOrder = "_id " + (descending ? "DESC" : "ASC"); - if (maxLocations > 0) { - sortOrder += " LIMIT " + maxLocations; - } - - return contentResolver.query(TrackPointsColumns.CONTENT_URI, null, selection, selectionArgs, sortOrder); - } - - @Override - public Cursor getWaypointsCursor(long trackId, long minWaypointId, - int maxWaypoints) { - if (trackId < 0) { - return null; - } - - String selection; - String[] selectionArgs; - if (minWaypointId > 0) { - selection = WaypointsColumns.TRACKID + "=? AND " + WaypointsColumns._ID + ">=?"; - selectionArgs = new String[] { - Long.toString(trackId), - Long.toString(minWaypointId) - }; - } else { - selection = WaypointsColumns.TRACKID + "=?"; selectionArgs = new String[] { Long.toString(trackId) }; } - return getWaypointsCursor(selection, selectionArgs, null, maxWaypoints); - } - - @Override - public Cursor getWaypointsCursor(String selection, String[] selectionArgs, String order, int maxWaypoints) { - if (order == null) { - order = "_id ASC"; + String sortOrder = TrackPointsColumns._ID; + if (descending) { + sortOrder += " DESC"; } - if (maxWaypoints > 0) { - order += " LIMIT " + maxWaypoints; + if (maxLocations > 0) { + sortOrder += " LIMIT " + maxLocations; } - - return contentResolver.query( - WaypointsColumns.CONTENT_URI, null, selection, selectionArgs, order); - } - - @Override - public Track getTrack(long id) { - if (id < 0) { - return null; - } - - String select = TracksColumns._ID + "=" + id; - return findTrackBy(select); - } - - @Override - public List getAllTracks() { - Cursor cursor = getTracksCursor(null, null, TracksColumns._ID); - ArrayList tracks = new ArrayList(); - if (cursor != null) { - tracks.ensureCapacity(cursor.getCount()); - - if (cursor.moveToFirst()) { - do { - tracks.add(createTrack(cursor)); - } while(cursor.moveToNext()); - } - - cursor.close(); - } - - return tracks; - } - - @Override - public Cursor getTracksCursor(String selection, String[] selectionArgs, String order) { - return contentResolver.query( - TracksColumns.CONTENT_URI, null, selection, selectionArgs, order); - } - - @Override - public Uri insertTrack(Track track) { - Log.d(TAG, "MyTracksProviderUtilsImpl.insertTrack"); - return contentResolver.insert(TracksColumns.CONTENT_URI, - createContentValues(track)); - } - - @Override - public Uri insertTrackPoint(Location location, long trackId) { - Log.d(TAG, "MyTracksProviderUtilsImpl.insertTrackPoint"); - return contentResolver.insert(TrackPointsColumns.CONTENT_URI, - createContentValues(location, trackId)); - } - - @Override - public int bulkInsertTrackPoints(Location[] locations, int length, long trackId) { - if (length == -1) { length = locations.length; } - - ContentValues[] values = new ContentValues[length]; - for (int i = 0; i < length; i++) { - values[i] = createContentValues(locations[i], trackId); - } - - return contentResolver.bulkInsert(TrackPointsColumns.CONTENT_URI, values); - } - - @Override - public Uri insertWaypoint(Waypoint waypoint) { - Log.d(TAG, "MyTracksProviderUtilsImpl.insertWaypoint"); - waypoint.setId(-1); - return contentResolver.insert(WaypointsColumns.CONTENT_URI, - createContentValues(waypoint)); - } - - @Override - public boolean trackExists(long id) { - if (id < 0) { - return false; - } - - Cursor cursor = null; - try { - final String[] projection = { TracksColumns._ID }; - cursor = contentResolver.query( - TracksColumns.CONTENT_URI, - projection, - TracksColumns._ID + "=" + id/*selection*/, - null/*selectionArgs*/, - null/*sortOrder*/); - if (cursor != null && cursor.moveToNext()) { - return true; - } - } finally { - if (cursor != null) { - cursor.close(); - } - } - return false; - } - - @Override - public void updateTrack(Track track) { - Log.d(TAG, "MyTracksProviderUtilsImpl.updateTrack"); - contentResolver.update(TracksColumns.CONTENT_URI, - createContentValues(track), "_id=" + track.getId(), null); + return getTrackPointsCursor(null, selection, selectionArgs, sortOrder); } @Override public LocationIterator getLocationIterator(final long trackId, final long startTrackPointId, final boolean descending, final LocationFactory locationFactory) { if (locationFactory == null) { - throw new IllegalArgumentException("Expecting non-null locationFactory"); + throw new IllegalArgumentException("locationFactory is null"); } return new LocationIterator() { - private long lastTrackPointId = startTrackPointId; + private long lastTrackPointId = -1L; private Cursor cursor = getCursor(startTrackPointId); - private final CachedTrackColumnIndices columnIndices = cursor != null ? - new CachedTrackColumnIndices(cursor) : null; + private final CachedTrackPointsIndexes + indexes = cursor != null ? new CachedTrackPointsIndexes(cursor) + : null; + /** + * Gets the track point cursor. + * + * @param trackPointId the starting track point id + */ private Cursor getCursor(long trackPointId) { return getLocationsCursor(trackId, trackPointId, defaultCursorBatchSize, descending); } + /** + * Advances the cursor to the next batch. Returns true if successful. + */ private boolean advanceCursorToNextBatch() { - long pointId = lastTrackPointId + (descending ? -1 : 1); - Log.d(TAG, "Advancing cursor point ID: " + pointId); + long trackPointId = lastTrackPointId == -1L ? -1L : lastTrackPointId + (descending ? -1 + : 1); + Log.d(TAG, "Advancing track point id: " + trackPointId); cursor.close(); - cursor = getCursor(pointId); + cursor = getCursor(trackPointId); return cursor != null; } - @Override + @Override public long getLocationId() { return lastTrackPointId; } - @Override + @Override public boolean hasNext() { if (cursor == null) { return false; @@ -1068,30 +889,31 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { return false; } if (cursor.isLast()) { - // If the current batch size was less that max, we can safely return, otherwise - // we need to advance to the next batch. - return cursor.getCount() == defaultCursorBatchSize && - advanceCursorToNextBatch() && !cursor.isAfterLast(); + if (cursor.getCount() != defaultCursorBatchSize) { + return false; + } + return advanceCursorToNextBatch() && !cursor.isAfterLast(); } - return true; } - @Override + @Override public Location next() { - if (cursor == null || - !(cursor.moveToNext() || advanceCursorToNextBatch() || cursor.moveToNext())) { + if (cursor == null) { throw new NoSuchElementException(); } - - lastTrackPointId = cursor.getLong(columnIndices.idxId); + if (!cursor.moveToNext()) { + if (!advanceCursorToNextBatch() || !cursor.moveToNext()) { + throw new NoSuchElementException(); + } + } + lastTrackPointId = cursor.getLong(indexes.idIndex); Location location = locationFactory.createLocation(); - fillLocation(cursor, columnIndices, location); - + fillLocation(cursor, indexes, location); return location; } - @Override + @Override public void close() { if (cursor != null) { cursor.close(); @@ -1099,14 +921,193 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { } } - @Override + @Override public void remove() { throw new UnsupportedOperationException(); } }; } - // @VisibleForTesting + @Override + public long getLastTrackLocationId(long trackId) { + if (trackId < 0) { + return -1L; + } + Cursor cursor = null; + try { + String selection = TrackPointsColumns._ID + "=(select max(" + TrackPointsColumns._ID + + ") from " + TrackPointsColumns.TABLE_NAME + " WHERE " + TrackPointsColumns.TRACKID + + "=?)"; + String[] selectionArgs = new String[] { Long.toString(trackId) }; + cursor = getTrackPointsCursor(new String[] { TrackPointsColumns._ID }, selection, + selectionArgs, TrackPointsColumns._ID); + if (cursor != null && cursor.moveToFirst()) { + return cursor.getLong(cursor.getColumnIndexOrThrow(TrackPointsColumns._ID)); + } + } finally { + if (cursor != null) { + cursor.close(); + } + } + return -1L; + } + + @Override + public Location getLastTrackLocation(long trackId) { + if (trackId < 0) { + return null; + } + String selection = TrackPointsColumns._ID + "=(select max(" + TrackPointsColumns._ID + + ") from " + TrackPointsColumns.TABLE_NAME + " WHERE " + TrackPointsColumns.TRACKID + + "=?)"; + String[] selectionArgs = new String[] { Long.toString(trackId) }; + return findLocationBy(selection, selectionArgs); + } + + @Override + public Uri insertTrackPoint(Location location, long trackId) { + return contentResolver.insert( + TrackPointsColumns.CONTENT_URI, createContentValues(location, trackId)); + } + + @Override + public int bulkInsertTrackPoints(Location[] locations, int length, long trackId) { + if (length == -1) { + length = locations.length; + } + ContentValues[] values = new ContentValues[length]; + for (int i = 0; i < length; i++) { + values[i] = createContentValues(locations[i], trackId); + } + return contentResolver.bulkInsert(TrackPointsColumns.CONTENT_URI, values); + } + + /** + * Fills a location from a cursor. + * + * @param cursor the cursor pointing to a location. + * @param indexes the cached track points indexes + * @param location the location + */ + private void fillLocation(Cursor cursor, CachedTrackPointsIndexes indexes, Location location) { + location.reset(); + + if (!cursor.isNull(indexes.longitudeIndex)) { + location.setLongitude(((double) cursor.getInt(indexes.longitudeIndex)) / 1E6); + } + if (!cursor.isNull(indexes.latitudeIndex)) { + location.setLatitude(((double) cursor.getInt(indexes.latitudeIndex)) / 1E6); + } + if (!cursor.isNull(indexes.timeIndex)) { + location.setTime(cursor.getLong(indexes.timeIndex)); + } + if (!cursor.isNull(indexes.altitudeIndex)) { + location.setAltitude(cursor.getFloat(indexes.altitudeIndex)); + } + if (!cursor.isNull(indexes.accuracyIndex)) { + location.setAccuracy(cursor.getFloat(indexes.accuracyIndex)); + } + if (!cursor.isNull(indexes.speedIndex)) { + location.setSpeed(cursor.getFloat(indexes.speedIndex)); + } + if (!cursor.isNull(indexes.bearingIndex)) { + location.setBearing(cursor.getFloat(indexes.bearingIndex)); + } + if (location instanceof MyTracksLocation && !cursor.isNull(indexes.sensorIndex)) { + MyTracksLocation myTracksLocation = (MyTracksLocation) location; + try { + myTracksLocation.setSensorDataSet( + SensorDataSet.parseFrom(cursor.getBlob(indexes.sensorIndex))); + } catch (InvalidProtocolBufferException e) { + Log.w(TAG, "Failed to parse sensor data.", e); + } + } + } + + @Override + public void fillLocation(Cursor cursor, Location location) { + fillLocation(cursor, new CachedTrackPointsIndexes(cursor), location); + } + + @Override + public Location createLocation(Cursor cursor) { + Location location = new MyTracksLocation(""); + fillLocation(cursor, location); + return location; + } + + /** + * Creates the {@link ContentValues} for a {@link Location}. + * + * @param location the location + * @param trackId the track id + */ + private ContentValues createContentValues(Location location, long trackId) { + ContentValues values = new ContentValues(); + values.put(TrackPointsColumns.TRACKID, trackId); + values.put(TrackPointsColumns.LONGITUDE, (int) (location.getLongitude() * 1E6)); + values.put(TrackPointsColumns.LATITUDE, (int) (location.getLatitude() * 1E6)); + + // Hack for Samsung phones that don't properly populate the time field + long time = location.getTime(); + if (time == 0) { + time = System.currentTimeMillis(); + } + values.put(TrackPointsColumns.TIME, time); + if (location.hasAltitude()) { + values.put(TrackPointsColumns.ALTITUDE, location.getAltitude()); + } + if (location.hasAccuracy()) { + values.put(TrackPointsColumns.ACCURACY, location.getAccuracy()); + } + if (location.hasSpeed()) { + values.put(TrackPointsColumns.SPEED, location.getSpeed()); + } + if (location.hasBearing()) { + values.put(TrackPointsColumns.BEARING, location.getBearing()); + } + + if (location instanceof MyTracksLocation) { + MyTracksLocation myTracksLocation = (MyTracksLocation) location; + if (myTracksLocation.getSensorDataSet() != null) { + values.put(TrackPointsColumns.SENSOR, myTracksLocation.getSensorDataSet().toByteArray()); + } + } + return values; + } + + /** + * A cache of track points indexes. + */ + private static class CachedTrackPointsIndexes { + public final int idIndex; + public final int longitudeIndex; + public final int latitudeIndex; + public final int timeIndex; + public final int altitudeIndex; + public final int accuracyIndex; + public final int speedIndex; + public final int bearingIndex; + public final int sensorIndex; + + public CachedTrackPointsIndexes(Cursor cursor) { + idIndex = cursor.getColumnIndex(TrackPointsColumns._ID); + longitudeIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.LONGITUDE); + latitudeIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.LATITUDE); + timeIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.TIME); + altitudeIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.ALTITUDE); + accuracyIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.ACCURACY); + speedIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.SPEED); + bearingIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.BEARING); + sensorIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.SENSOR); + } + } + + /** + * Sets the default cursor batch size. For testing purpose. + * + * @param defaultCursorBatchSize the default cursor batch size + */ void setDefaultCursorBatchSize(int defaultCursorBatchSize) { this.defaultCursorBatchSize = defaultCursorBatchSize; } diff --git a/MyTracksLib/src/com/google/android/apps/mytracks/content/Waypoint.java b/MyTracksLib/src/com/google/android/apps/mytracks/content/Waypoint.java index 605a6cda8..166905a6a 100644 --- a/MyTracksLib/src/com/google/android/apps/mytracks/content/Waypoint.java +++ b/MyTracksLib/src/com/google/android/apps/mytracks/content/Waypoint.java @@ -33,22 +33,39 @@ public final class Waypoint implements Parcelable { public static final int TYPE_WAYPOINT = 0; public static final int TYPE_STATISTICS = 1; - private long id = -1; + private long id = -1L; private String name = ""; private String description = ""; private String category = ""; private String icon = ""; - private long trackId = -1; + private long trackId = -1L; private int type = 0; private double length = 0.0; private long duration = 0; - private long startId = -1; - private long stopId = -1; + private long startId = -1L; + private long stopId = -1L; private Location location = null; private TripStatistics tripStatistics = null; public Waypoint() {} + public Waypoint(String name, String description, String category, String icon, long trackId, + int type, double length, long duration, long startId, long stopId, Location location, + TripStatistics tripStatistics) { + this.name = name; + this.description = description; + this.category = category; + this.icon = icon; + this.trackId = trackId; + this.type = type; + this.length = length; + this.duration = duration; + this.startId = startId; + this.stopId = stopId; + this.location = location; + this.tripStatistics = tripStatistics; + } + private Waypoint(Parcel source) { id = source.readLong(); name = source.readString(); diff --git a/MyTracksLib/src/com/google/android/apps/mytracks/stats/ExtremityMonitor.java b/MyTracksLib/src/com/google/android/apps/mytracks/stats/ExtremityMonitor.java index bb65ee76d..8a4618588 100644 --- a/MyTracksLib/src/com/google/android/apps/mytracks/stats/ExtremityMonitor.java +++ b/MyTracksLib/src/com/google/android/apps/mytracks/stats/ExtremityMonitor.java @@ -18,19 +18,15 @@ package com.google.android.apps.mytracks.stats; /** * A helper class that tracks a minimum and a maximum of a variable. - * + * * @author Sandor Dornbush */ public class ExtremityMonitor { - /** - * The smallest value seen so far. - */ + // The smallest value seen so far. private double min; - /** - * The largest value seen so far. - */ + // The largest value seen so far. private double max; public ExtremityMonitor() { @@ -38,9 +34,31 @@ public class ExtremityMonitor { } /** - * Updates the min and the max with the new value. - * - * @param value the new value for the monitor + * Resets this object to it's initial state where the min and max are unknown. + */ + public void reset() { + min = Double.POSITIVE_INFINITY; + max = Double.NEGATIVE_INFINITY; + } + + /** + * Gets the minimum value seen. + */ + public double getMin() { + return min; + } + + /** + * Gets the maximum value seen. + */ + public double getMax() { + return max; + } + + /** + * Updates the min and the max with a new value. + * + * @param value the new value * @return true if an extremity was found */ public boolean update(double value) { @@ -56,34 +74,11 @@ public class ExtremityMonitor { return changed; } - /** - * Gets the minimum value seen. - * - * @return The minimum value passed into the update() function - */ - public double getMin() { - return min; - } - - /** - * Gets the maximum value seen. - * - * @return The maximum value passed into the update() function - */ - public double getMax() { - return max; - } - - /** - * Resets this object to it's initial state where the min and max are unknown. - */ - public void reset() { - min = Double.POSITIVE_INFINITY; - max = Double.NEGATIVE_INFINITY; - } - /** * Sets the minimum and maximum values. + * + * @param min the minimum value + * @param max the maximum value */ public void set(double min, double max) { this.min = min; @@ -92,6 +87,8 @@ public class ExtremityMonitor { /** * Sets the minimum value. + * + * @param min the minimum value */ public void setMin(double min) { this.min = min; @@ -99,16 +96,20 @@ public class ExtremityMonitor { /** * Sets the maximum value. + * + * @param max the maximum value */ public void setMax(double max) { this.max = max; } + /** + * Returns true if has data. + */ public boolean hasData() { - return min != Double.POSITIVE_INFINITY - && max != Double.NEGATIVE_INFINITY; + return min != Double.POSITIVE_INFINITY && max != Double.NEGATIVE_INFINITY; } - + @Override public String toString() { return "Min: " + min + " Max: " + max; diff --git a/MyTracksLib/src/com/google/android/apps/mytracks/stats/TripStatistics.java b/MyTracksLib/src/com/google/android/apps/mytracks/stats/TripStatistics.java index 4d82d2814..892c0d590 100644 --- a/MyTracksLib/src/com/google/android/apps/mytracks/stats/TripStatistics.java +++ b/MyTracksLib/src/com/google/android/apps/mytracks/stats/TripStatistics.java @@ -13,182 +13,226 @@ * License for the specific language governing permissions and limitations under * the License. */ + package com.google.android.apps.mytracks.stats; import android.os.Parcel; import android.os.Parcelable; /** - * Statistical data about a trip. - * The data in this class should be filled out by TripStatisticsBuilder. - * + * Statistical data about a trip. The data in this class should be filled out by + * TripStatisticsBuilder. + *

* TODO: hashCode and equals - * + * * @author Rodrigo Damazio */ public class TripStatistics implements Parcelable { - /** - * The start time for the trip. This is system time which might not match gps - * time. - */ + // The trip start time. This is the system time, might not match the GPs time. private long startTime = -1L; - /** - * The stop time for the trip. This is the system time which might not match - * gps time. - */ + // The trip stop time. This is the system time, might not match the GPS time. private long stopTime = -1L; - /** - * The total time that we believe the user was traveling in milliseconds. - */ - private long movingTime; - - /** - * The total time of the trip in milliseconds. - * This is only updated when new points are received, so it may be stale. - */ - private long totalTime; - - /** - * The total distance in meters that the user traveled on this trip. - */ + // The total trip distance (meters). private double totalDistance; - /** - * The total elevation gained on this trip in meters. - */ - private double totalElevationGain; + // The total time (ms). Updated when new points are received, may be stale. + private long totalTime; - /** - * The maximum speed in meters/second reported that we believe to be a valid - * speed. - */ - private double maxSpeed; + // The total moving time (ms). Based on when we believe the user is traveling. + private long movingTime; - /** - * The min and max latitude values seen in this trip. - */ + // The min and max latitude seen in this trip. private final ExtremityMonitor latitudeExtremities = new ExtremityMonitor(); - /** - * The min and max longitude values seen in this trip. - */ + // The min and max longitude seen in this trip. private final ExtremityMonitor longitudeExtremities = new ExtremityMonitor(); - /** - * The min and max elevation seen on this trip in meters. - */ + // The maximum speed (meters/second) that we believe is valid. + private double maxSpeed; + + // The min and max elevation (meters) seen on this trip. private final ExtremityMonitor elevationExtremities = new ExtremityMonitor(); - /** - * The minimum and maximum grade calculations on this trip. - */ + // The total elevation gained (meters). + private double totalElevationGain; + + // The min and max grade seen on this trip. private final ExtremityMonitor gradeExtremities = new ExtremityMonitor(); /** * Default constructor. */ - public TripStatistics() { - } + public TripStatistics() {} /** * Copy constructor. - * + * * @param other another statistics data object to copy from */ public TripStatistics(TripStatistics other) { - this.maxSpeed = other.maxSpeed; - this.movingTime = other.movingTime; this.startTime = other.startTime; this.stopTime = other.stopTime; this.totalDistance = other.totalDistance; - this.totalElevationGain = other.totalElevationGain; this.totalTime = other.totalTime; - - this.latitudeExtremities.set(other.latitudeExtremities.getMin(), - other.latitudeExtremities.getMax()); - this.longitudeExtremities.set(other.longitudeExtremities.getMin(), - other.longitudeExtremities.getMax()); - this.elevationExtremities.set(other.elevationExtremities.getMin(), - other.elevationExtremities.getMax()); - this.gradeExtremities.set(other.gradeExtremities.getMin(), - other.gradeExtremities.getMax()); + this.movingTime = other.movingTime; + this.latitudeExtremities.set( + other.latitudeExtremities.getMin(), other.latitudeExtremities.getMax()); + this.longitudeExtremities.set( + other.longitudeExtremities.getMin(), other.longitudeExtremities.getMax()); + this.maxSpeed = other.maxSpeed; + this.elevationExtremities.set( + other.elevationExtremities.getMin(), other.elevationExtremities.getMax()); + this.totalElevationGain = other.totalElevationGain; + this.gradeExtremities.set(other.gradeExtremities.getMin(), other.gradeExtremities.getMax()); } /** - * Combines these statistics with those from another object. - * This assumes that the time periods covered by each do not intersect. - * - * @param other the other waypoint + * Combines these statistics with those from another object. This assumes that + * the time periods covered by each do not intersect. + * + * @param other another statistics data object */ public void merge(TripStatistics other) { startTime = Math.min(startTime, other.startTime); stopTime = Math.max(stopTime, other.stopTime); + totalDistance += other.totalDistance; totalTime += other.totalTime; movingTime += other.movingTime; - totalDistance += other.totalDistance; - totalElevationGain += other.totalElevationGain; - maxSpeed = Math.max(maxSpeed, other.maxSpeed); - - latitudeExtremities.update(other.latitudeExtremities.getMax()); latitudeExtremities.update(other.latitudeExtremities.getMin()); - longitudeExtremities.update(other.longitudeExtremities.getMax()); + latitudeExtremities.update(other.latitudeExtremities.getMax()); longitudeExtremities.update(other.longitudeExtremities.getMin()); - elevationExtremities.update(other.elevationExtremities.getMax()); + longitudeExtremities.update(other.longitudeExtremities.getMax()); + maxSpeed = Math.max(maxSpeed, other.maxSpeed); elevationExtremities.update(other.elevationExtremities.getMin()); - gradeExtremities.update(other.gradeExtremities.getMax()); + elevationExtremities.update(other.elevationExtremities.getMax()); + totalElevationGain += other.totalElevationGain; gradeExtremities.update(other.gradeExtremities.getMin()); + gradeExtremities.update(other.gradeExtremities.getMax()); } /** - * Gets the time that this track started. - * - * @return The number of milliseconds since epoch to the time when this track - * started + * Gets the trip start time. The number of milliseconds since epoch. */ public long getStartTime() { return startTime; } /** - * Gets the time that this track stopped. - * - * @return The number of milliseconds since epoch to the time when this track - * stopped + * Gets the trip stop time. The number of milliseconds since epoch. */ public long getStopTime() { return stopTime; } /** - * Gets the total time that this track has been active. - * This statistic is only updated when a new point is added to the statistics, - * so it may be off. If you need to calculate the proper total time, use - * {@link #getStartTime} with the current time. - * - * @return The total number of milliseconds the track was active - */ - public long getTotalTime() { - return totalTime; - } - - /** - * Gets the total distance the user traveled. - * - * @return The total distance traveled in meters + * Gets the total distance the user traveled in meters. */ public double getTotalDistance() { return totalDistance; } /** - * Gets the the average speed the user traveled. - * This calculation only takes into account the displacement until the last - * point that was accounted for in statistics. - * - * @return The average speed in m/s + * Gets the total time in milliseconds that this track has been active. This + * statistic is only updated when a new point is added to the statistics, so + * it may be off. If you need to calculate the proper total time, use + * {@link #getStartTime} with the current time. + */ + public long getTotalTime() { + return totalTime; + } + + /** + * Gets the moving time in milliseconds. + */ + public long getMovingTime() { + return movingTime; + } + + /** + * Gets the topmost position (highest latitude) of the track, in signed + * degrees. + */ + public double getTopDegrees() { + return latitudeExtremities.getMax(); + } + + /** + * Gets the topmost position (highest latitude) of the track, in signed + * millions of degrees. + */ + public int getTop() { + return (int) (latitudeExtremities.getMax() * 1E6); + } + + /** + * Gets the bottommost position (lowest latitude) of the track, in signed + * degrees. + */ + public double getBottomDegrees() { + return latitudeExtremities.getMin(); + } + + /** + * Gets the bottommost position (lowest latitude) of the track, in signed + * millions of degrees. + */ + public int getBottom() { + return (int) (latitudeExtremities.getMin() * 1E6); + } + + /** + * Gets the leftmost position (lowest longitude) of the track, in signed + * degrees. + */ + public double getLeftDegrees() { + return longitudeExtremities.getMin(); + } + + /** + * Gets the leftmost position (lowest longitude) of the track, in signed + * millions of degrees. + */ + public int getLeft() { + return (int) (longitudeExtremities.getMin() * 1E6); + } + + /** + * Gets the rightmost position (highest longitude) of the track, in signed + * degrees. + */ + public double getRightDegrees() { + return longitudeExtremities.getMax(); + } + + /** + * Gets the rightmost position (highest longitude) of the track, in signed + * millions of degrees. + */ + public int getRight() { + return (int) (longitudeExtremities.getMax() * 1E6); + } + + /** + * Gets the mean latitude position of the track, in signed degrees. + */ + public double getMeanLatitude() { + return (getBottomDegrees() + getTopDegrees()) / 2.0; + } + + /** + * Gets the mean longitude position of the track, in signed degrees. + */ + public double getMeanLongitude() { + return (getLeftDegrees() + getRightDegrees()) / 2.0; + } + + /** + * Gets the average speed in meters/second. This calculation only takes into + * account the displacement until the last point that was accounted for in + * statistics. */ public double getAverageSpeed() { if (totalTime == 0L) { @@ -198,10 +242,7 @@ public class TripStatistics implements Parcelable { } /** - * Gets the the average speed the user traveled when they were actively - * moving. - * - * @return The average moving speed in m/s + * Gets the average moving speed in meters/second. */ public double getAverageMovingSpeed() { if (movingTime == 0L) { @@ -211,261 +252,80 @@ public class TripStatistics implements Parcelable { } /** - * Gets the the maximum speed for this track. - * - * @return The maximum speed in m/s + * Gets the maximum speed in meters/second. */ public double getMaxSpeed() { return maxSpeed; } /** - * Gets the moving time. - * - * @return The total number of milliseconds the user was moving - */ - public long getMovingTime() { - return movingTime; - } - - /** - * Gets the total elevation gain for this trip. This is calculated as the sum - * of all positive differences in the smoothed elevation. - * - * @return The elevation gain in meters for this trip - */ - public double getTotalElevationGain() { - return totalElevationGain; - } - - /** - * Returns the leftmost position (lowest longitude) of the track, in signed degrees. - */ - public double getLeftDegrees() { - return longitudeExtremities.getMin(); - } - - /** - * Returns the leftmost position (lowest longitude) of the track, in signed millions of degrees. - */ - public int getLeft() { - return (int) (longitudeExtremities.getMin() * 1E6); - } - - /** - * Returns the rightmost position (highest longitude) of the track, in signed degrees. - */ - public double getRightDegrees() { - return longitudeExtremities.getMax(); - } - - /** - * Returns the rightmost position (highest longitude) of the track, in signed millions of degrees. - */ - public int getRight() { - return (int) (longitudeExtremities.getMax() * 1E6); - } - - /** - * Returns the bottommost position (lowest latitude) of the track, in signed degrees. - */ - public double getBottomDegrees() { - return latitudeExtremities.getMin(); - } - - /** - * Returns the bottommost position (lowest latitude) of the track, in signed millions of degrees. - */ - public int getBottom() { - return (int) (latitudeExtremities.getMin() * 1E6); - } - - /** - * Returns the topmost position (highest latitude) of the track, in signed degrees. - */ - public double getTopDegrees() { - return latitudeExtremities.getMax(); - } - - /** - * Returns the topmost position (highest latitude) of the track, in signed millions of degrees. - */ - public int getTop() { - return (int) (latitudeExtremities.getMax() * 1E6); - } - - /** - * Returns the mean position (center latitude) of the track, in signed degrees. - */ - public double getMeanLatitude() { - return (getBottomDegrees() + getTopDegrees()) / 2.0; - } - - /** - * Returns the mean position (center longitude) of the track, in signed degrees. - */ - public double getMeanLongitude() { - return (getLeftDegrees() + getRightDegrees()) / 2.0; - } - - /** - * Gets the minimum elevation seen on this trip. This is calculated from the - * smoothed elevation so this can actually be more than the current elevation. - * - * @return The smallest elevation reading for this trip in meters + * Gets the minimum elevation. This is calculated from the smoothed elevation + * so this can actually be more than the current elevation. */ public double getMinElevation() { return elevationExtremities.getMin(); } /** - * Gets the maximum elevation seen on this trip. This is calculated from the - * smoothed elevation so this can actually be less than the current elevation. - * - * @return The largest elevation reading for this trip in meters + * Gets the maximum elevation. This is calculated from the smoothed elevation + * so this can actually be less than the current elevation. */ public double getMaxElevation() { return elevationExtremities.getMax(); } + /** + * Gets the total elevation gain in meters. This is calculated as the sum of + * all positive differences in the smoothed elevation. + */ + public double getTotalElevationGain() { + return totalElevationGain; + } + + /** + * Gets the minimum grade for this trip. + */ + public double getMinGrade() { + return gradeExtremities.getMin(); + } + /** * Gets the maximum grade for this trip. - * - * @return The maximum grade for this trip as a fraction */ public double getMaxGrade() { return gradeExtremities.getMax(); } /** - * Gets the minimum grade for this trip. - * - * @return The minimum grade for this trip as a fraction - */ - public double getMinGrade() { - return gradeExtremities.getMin(); - } - - // Setters - to be used when restoring state or loading from the DB - - /** - * Sets the start time for this trip. - * - * @param startTime the start time, in milliseconds since the epoch + * Sets the trip start time. + * + * @param startTime the trip start time in milliseconds since the epoch */ public void setStartTime(long startTime) { this.startTime = startTime; } /** - * Sets the stop time for this trip. - * - * @param stopTime the stop time, in milliseconds since the epoch + * Sets the trip stop time. + * + * @param stopTime the stop time in milliseconds since the epoch */ public void setStopTime(long stopTime) { this.stopTime = stopTime; } - /** - * Sets the total moving time. - * - * @param movingTime the moving time in milliseconds - */ - public void setMovingTime(long movingTime) { - this.movingTime = movingTime; - } - - /** - * Sets the total trip time. - * - * @param totalTime the total trip time in milliseconds - */ - public void setTotalTime(long totalTime) { - this.totalTime = totalTime; - } - /** * Sets the total trip distance. - * + * * @param totalDistance the trip distance in meters */ public void setTotalDistance(double totalDistance) { this.totalDistance = totalDistance; } - /** - * Sets the total elevation variation during the trip. - * - * @param totalElevationGain the elevation variation in meters - */ - public void setTotalElevationGain(double totalElevationGain) { - this.totalElevationGain = totalElevationGain; - } - - /** - * Sets the maximum speed reached during the trip. - * - * @param maxSpeed the maximum speed in meters per second - */ - public void setMaxSpeed(double maxSpeed) { - this.maxSpeed = maxSpeed; - } - - /** - * Sets the minimum elevation reached during the trip. - * - * @param elevation the minimum elevation in meters - */ - public void setMinElevation(double elevation) { - elevationExtremities.setMin(elevation); - } - - /** - * Sets the maximum elevation reached during the trip. - * - * @param elevation the maximum elevation in meters - */ - public void setMaxElevation(double elevation) { - elevationExtremities.setMax(elevation); - } - - /** - * Sets the minimum grade obtained during the trip. - * - * @param grade the grade as a fraction (-1.0 would mean vertical downwards) - */ - public void setMinGrade(double grade) { - gradeExtremities.setMin(grade); - } - - /** - * Sets the maximum grade obtained during the trip). - * - * @param grade the grade as a fraction (1.0 would mean vertical upwards) - */ - public void setMaxGrade(double grade) { - gradeExtremities.setMax(grade); - } - - /** - * Sets the bounding box for this trip. - * The unit for all parameters is signed decimal degrees (degrees * 1E6). - * - * @param leftE6 the westmost longitude reached - * @param topE6 the northmost latitude reached - * @param rightE6 the eastmost longitude reached - * @param bottomE6 the southmost latitude reached - */ - public void setBounds(int leftE6, int topE6, int rightE6, int bottomE6) { - latitudeExtremities.set(bottomE6 / 1E6, topE6 / 1E6); - longitudeExtremities.set(leftE6 / 1E6, rightE6 / 1E6); - } - - // Data manipulation methods - /** * Adds to the current total distance. - * + * * @param distance the distance to add in meters */ void addTotalDistance(double distance) { @@ -473,17 +333,26 @@ public class TripStatistics implements Parcelable { } /** - * Adds to the total elevation variation. - * - * @param gain the elevation variation in meters + * Sets the trip total time. + * + * @param totalTime the trip total time in milliseconds */ - void addTotalElevationGain(double gain) { - totalElevationGain += gain; + public void setTotalTime(long totalTime) { + this.totalTime = totalTime; } /** - * Adds to the total moving time of the trip. - * + * Sets the trip total moving time. + * + * @param movingTime the trip total moving time in milliseconds + */ + public void setMovingTime(long movingTime) { + this.movingTime = movingTime; + } + + /** + * Adds to the trip total moving time. + * * @param time the time in milliseconds */ void addMovingTime(long time) { @@ -491,8 +360,22 @@ public class TripStatistics implements Parcelable { } /** - * Accounts for a new latitude value for the bounding box. - * + * Sets the bounding box for this trip. The unit for all parameters is signed + * millions of degree (degrees * 1E6). + * + * @param leftE6 the leftmost longitude reached + * @param topE6 the topmost latitude reached + * @param rightE6 the rightmost longitude reached + * @param bottomE6 the bottommost latitude reached + */ + public void setBounds(int leftE6, int topE6, int rightE6, int bottomE6) { + latitudeExtremities.set(bottomE6 / 1E6, topE6 / 1E6); + longitudeExtremities.set(leftE6 / 1E6, rightE6 / 1E6); + } + + /** + * Updates a new latitude value. + * * @param latitude the latitude value in signed decimal degrees */ void updateLatitudeExtremities(double latitude) { @@ -500,8 +383,8 @@ public class TripStatistics implements Parcelable { } /** - * Accounts for a new longitude value for the bounding box. - * + * Updates a new longitude value. + * * @param longitude the longitude value in signed decimal degrees */ void updateLongitudeExtremities(double longitude) { @@ -509,8 +392,35 @@ public class TripStatistics implements Parcelable { } /** - * Accounts for a new elevation value for the bounding box. - * + * Sets the maximum speed. + * + * @param maxSpeed the maximum speed in meters/second + */ + public void setMaxSpeed(double maxSpeed) { + this.maxSpeed = maxSpeed; + } + + /** + * Sets the minimum elevation. + * + * @param elevation the minimum elevation in meters + */ + public void setMinElevation(double elevation) { + elevationExtremities.setMin(elevation); + } + + /** + * Sets the maximum elevation. + * + * @param elevation the maximum elevation in meters + */ + public void setMaxElevation(double elevation) { + elevationExtremities.setMax(elevation); + } + + /** + * Updates a new elevation. + * * @param elevation the elevation value in meters */ void updateElevationExtremities(double elevation) { @@ -518,49 +428,77 @@ public class TripStatistics implements Parcelable { } /** - * Accounts for a new grade value. - * + * Sets the total elevation gain. + * + * @param totalElevationGain the elevation gain in meters + */ + public void setTotalElevationGain(double totalElevationGain) { + this.totalElevationGain = totalElevationGain; + } + + /** + * Adds to the total elevation gain. + * + * @param gain the elevation gain in meters + */ + void addTotalElevationGain(double gain) { + totalElevationGain += gain; + } + + /** + * Sets the minimum grade. + * + * @param grade the grade as a fraction (-1.0 would mean vertical downwards) + */ + public void setMinGrade(double grade) { + gradeExtremities.setMin(grade); + } + + /** + * Sets the maximum grade. + * + * @param grade the grade as a fraction (1.0 would mean vertical upwards) + */ + public void setMaxGrade(double grade) { + gradeExtremities.setMax(grade); + } + + /** + * Updates a new grade value. + * * @param grade the grade value as a fraction */ void updateGradeExtremities(double grade) { gradeExtremities.update(grade); } - // String conversion - @Override public String toString() { - return "TripStatistics { Start Time: " + getStartTime() - + "; Total Time: " + getTotalTime() - + "; Moving Time: " + getMovingTime() - + "; Total Distance: " + getTotalDistance() - + "; Elevation Gain: " + getTotalElevationGain() - + "; Min Elevation: " + getMinElevation() - + "; Max Elevation: " + getMaxElevation() - + "; Average Speed: " + getAverageMovingSpeed() - + "; Min Grade: " + getMinGrade() - + "; Max Grade: " + getMaxGrade() - + "}"; + return "TripStatistics { Start Time: " + getStartTime() + "; Stop Time: " + getStopTime() + + "; Total Time: " + getTotalTime() + "; Total Distance: " + getTotalDistance() + + "; Total Time: " + getTotalTime() + "; Moving Time: " + getMovingTime() + + "; Min Latitude: " + getBottomDegrees() + "; Max Latitude: " + getTopDegrees() + + "; Min Longitude: " + getLeftDegrees() + "; Max Longitude: " + getRightDegrees() + + "; Max Elevation: " + getMaxElevation() + "; Max Speed: " + getMaxSpeed() + + "; Min Elevation: " + getMinElevation() + "; Max Elevation: " + getMaxElevation() + + "; Elevation Gain: " + getTotalElevationGain() + "; Min Grade: " + getMinGrade() + + "; Max Grade: " + getMaxGrade() + "}"; } - // Parcelable interface and creator - /** * Creator of statistics data from parcels. */ - public static class Creator - implements Parcelable.Creator { + public static class Creator implements Parcelable.Creator { @Override public TripStatistics createFromParcel(Parcel source) { TripStatistics data = new TripStatistics(); data.startTime = source.readLong(); - data.movingTime = source.readLong(); - data.totalTime = source.readLong(); + data.stopTime = source.readLong(); data.totalDistance = source.readDouble(); - data.totalElevationGain = source.readDouble(); - data.maxSpeed = source.readDouble(); + data.totalTime = source.readLong(); + data.movingTime = source.readLong(); double minLat = source.readDouble(); double maxLat = source.readDouble(); @@ -570,9 +508,12 @@ public class TripStatistics implements Parcelable { double maxLong = source.readDouble(); data.longitudeExtremities.set(minLong, maxLong); + data.maxSpeed = source.readDouble(); + double minElev = source.readDouble(); double maxElev = source.readDouble(); data.elevationExtremities.set(minElev, maxElev); + data.totalElevationGain = source.readDouble(); double minGrade = source.readDouble(); double maxGrade = source.readDouble(); @@ -600,18 +541,18 @@ public class TripStatistics implements Parcelable { @Override public void writeToParcel(Parcel dest, int flags) { dest.writeLong(startTime); - dest.writeLong(movingTime); - dest.writeLong(totalTime); + dest.writeLong(stopTime); dest.writeDouble(totalDistance); - dest.writeDouble(totalElevationGain); - dest.writeDouble(maxSpeed); - + dest.writeLong(totalTime); + dest.writeLong(movingTime); dest.writeDouble(latitudeExtremities.getMin()); dest.writeDouble(latitudeExtremities.getMax()); dest.writeDouble(longitudeExtremities.getMin()); dest.writeDouble(longitudeExtremities.getMax()); + dest.writeDouble(maxSpeed); dest.writeDouble(elevationExtremities.getMin()); dest.writeDouble(elevationExtremities.getMax()); + dest.writeDouble(totalElevationGain); dest.writeDouble(gradeExtremities.getMin()); dest.writeDouble(gradeExtremities.getMax()); } diff --git a/MyTracksTest/.settings/org.eclipse.jdt.core.prefs b/MyTracksTest/.settings/org.eclipse.jdt.core.prefs index d386c5d41..4c42458af 100644 --- a/MyTracksTest/.settings/org.eclipse.jdt.core.prefs +++ b/MyTracksTest/.settings/org.eclipse.jdt.core.prefs @@ -1,4 +1,4 @@ -#Fri Oct 21 20:06:28 BRST 2011 +#Tue Jul 17 10:23:26 PDT 2012 eclipse.preferences.version=1 org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 @@ -77,29 +77,66 @@ org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning org.eclipse.jdt.core.compiler.processAnnotations=enabled org.eclipse.jdt.core.compiler.source=1.6 org.eclipse.jdt.core.formatter.align_type_members_on_columns=false +org.eclipse.jdt.core.formatter.alignment_for_annotations_on_field=1585 +org.eclipse.jdt.core.formatter.alignment_for_annotations_on_field.count_dependent=1585|-1|1585 +org.eclipse.jdt.core.formatter.alignment_for_annotations_on_local_variable=1585 +org.eclipse.jdt.core.formatter.alignment_for_annotations_on_local_variable.count_dependent=1585|-1|1585 +org.eclipse.jdt.core.formatter.alignment_for_annotations_on_method=1585 +org.eclipse.jdt.core.formatter.alignment_for_annotations_on_method.count_dependent=1585|-1|1585 +org.eclipse.jdt.core.formatter.alignment_for_annotations_on_package=1585 +org.eclipse.jdt.core.formatter.alignment_for_annotations_on_package.count_dependent=1585|-1|1585 +org.eclipse.jdt.core.formatter.alignment_for_annotations_on_parameter=1040 +org.eclipse.jdt.core.formatter.alignment_for_annotations_on_parameter.count_dependent=1040|-1|1040 +org.eclipse.jdt.core.formatter.alignment_for_annotations_on_type=1585 +org.eclipse.jdt.core.formatter.alignment_for_annotations_on_type.count_dependent=1585|-1|1585 org.eclipse.jdt.core.formatter.alignment_for_arguments_in_allocation_expression=16 +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_allocation_expression.count_dependent=16|-1|16 org.eclipse.jdt.core.formatter.alignment_for_arguments_in_annotation=0 +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_annotation.count_dependent=16|-1|16 org.eclipse.jdt.core.formatter.alignment_for_arguments_in_enum_constant=48 +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_enum_constant.count_dependent=16|-1|16 org.eclipse.jdt.core.formatter.alignment_for_arguments_in_explicit_constructor_call=16 +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_explicit_constructor_call.count_dependent=16|-1|16 org.eclipse.jdt.core.formatter.alignment_for_arguments_in_method_invocation=16 +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_method_invocation.count_dependent=16|-1|16 org.eclipse.jdt.core.formatter.alignment_for_arguments_in_qualified_allocation_expression=16 +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_qualified_allocation_expression.count_dependent=16|-1|16 org.eclipse.jdt.core.formatter.alignment_for_assignment=0 org.eclipse.jdt.core.formatter.alignment_for_binary_expression=16 +org.eclipse.jdt.core.formatter.alignment_for_binary_expression.count_dependent=16|-1|16 +org.eclipse.jdt.core.formatter.alignment_for_cascading_method_invocation_with_arguments=16 +org.eclipse.jdt.core.formatter.alignment_for_cascading_method_invocation_with_arguments.count_dependent=16|-1|16 org.eclipse.jdt.core.formatter.alignment_for_compact_if=16 org.eclipse.jdt.core.formatter.alignment_for_conditional_expression=80 org.eclipse.jdt.core.formatter.alignment_for_enum_constants=48 +org.eclipse.jdt.core.formatter.alignment_for_enum_constants.count_dependent=0|-1|0 org.eclipse.jdt.core.formatter.alignment_for_expressions_in_array_initializer=16 +org.eclipse.jdt.core.formatter.alignment_for_expressions_in_array_initializer.count_dependent=16|-1|16 +org.eclipse.jdt.core.formatter.alignment_for_field_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_for_statement=16 +org.eclipse.jdt.core.formatter.alignment_for_generic_type_arguments=16 +org.eclipse.jdt.core.formatter.alignment_for_generic_type_arguments.count_dependent=16|-1|16 +org.eclipse.jdt.core.formatter.alignment_for_local_variable_declaration=16 org.eclipse.jdt.core.formatter.alignment_for_method_declaration=0 org.eclipse.jdt.core.formatter.alignment_for_multiple_fields=16 +org.eclipse.jdt.core.formatter.alignment_for_multiple_fields.count_dependent=16|-1|16 +org.eclipse.jdt.core.formatter.alignment_for_new_anonymous_class=0 org.eclipse.jdt.core.formatter.alignment_for_parameters_in_constructor_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_parameters_in_constructor_declaration.count_dependent=16|-1|16 org.eclipse.jdt.core.formatter.alignment_for_parameters_in_method_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_parameters_in_method_declaration.count_dependent=16|-1|16 org.eclipse.jdt.core.formatter.alignment_for_resources_in_try=80 org.eclipse.jdt.core.formatter.alignment_for_selector_in_method_invocation=16 +org.eclipse.jdt.core.formatter.alignment_for_selector_in_method_invocation.count_dependent=16|-1|16 org.eclipse.jdt.core.formatter.alignment_for_superclass_in_type_declaration=16 org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_enum_declaration=48 +org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_enum_declaration.count_dependent=16|-1|16 org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_type_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_type_declaration.count_dependent=16|-1|16 org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_constructor_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_constructor_declaration.count_dependent=16|-1|16 org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_method_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_method_declaration.count_dependent=16|-1|16 org.eclipse.jdt.core.formatter.alignment_for_union_type_in_multicatch=16 org.eclipse.jdt.core.formatter.blank_lines_after_imports=1 org.eclipse.jdt.core.formatter.blank_lines_after_package=1 @@ -139,12 +176,14 @@ org.eclipse.jdt.core.formatter.comment.line_length=80 org.eclipse.jdt.core.formatter.comment.new_lines_at_block_boundaries=true org.eclipse.jdt.core.formatter.comment.new_lines_at_javadoc_boundaries=true org.eclipse.jdt.core.formatter.comment.preserve_white_space_between_code_and_line_comments=false +org.eclipse.jdt.core.formatter.comment_new_line_at_start_of_html_paragraph=true org.eclipse.jdt.core.formatter.compact_else_if=true org.eclipse.jdt.core.formatter.continuation_indentation=2 org.eclipse.jdt.core.formatter.continuation_indentation_for_array_initializer=2 org.eclipse.jdt.core.formatter.disabling_tag=@formatter\:off org.eclipse.jdt.core.formatter.enabling_tag=@formatter\:on -org.eclipse.jdt.core.formatter.format_guardian_clause_on_one_line=true +org.eclipse.jdt.core.formatter.force_if_else_statement_brace=false +org.eclipse.jdt.core.formatter.format_guardian_clause_on_one_line=false org.eclipse.jdt.core.formatter.format_line_comment_starting_on_first_column=true org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_annotation_declaration_header=true org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_constant_header=true @@ -211,6 +250,7 @@ org.eclipse.jdt.core.formatter.insert_space_after_comma_in_parameterized_type_re org.eclipse.jdt.core.formatter.insert_space_after_comma_in_superinterfaces=insert org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_arguments=insert org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_parameters=insert +org.eclipse.jdt.core.formatter.insert_space_after_comment_prefix=insert org.eclipse.jdt.core.formatter.insert_space_after_ellipsis=insert org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_parameterized_type_reference=do not insert org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_arguments=do not insert @@ -341,7 +381,7 @@ org.eclipse.jdt.core.formatter.join_lines_in_comments=true org.eclipse.jdt.core.formatter.join_wrapped_lines=true org.eclipse.jdt.core.formatter.keep_else_statement_on_same_line=false org.eclipse.jdt.core.formatter.keep_empty_array_initializer_on_one_line=false -org.eclipse.jdt.core.formatter.keep_imple_if_on_one_line=true +org.eclipse.jdt.core.formatter.keep_imple_if_on_one_line=false org.eclipse.jdt.core.formatter.keep_then_statement_on_same_line=false org.eclipse.jdt.core.formatter.lineSplit=100 org.eclipse.jdt.core.formatter.never_indent_block_comments_on_first_column=false @@ -349,10 +389,22 @@ org.eclipse.jdt.core.formatter.never_indent_line_comments_on_first_column=false org.eclipse.jdt.core.formatter.number_of_blank_lines_at_beginning_of_method_body=0 org.eclipse.jdt.core.formatter.number_of_empty_lines_to_preserve=1 org.eclipse.jdt.core.formatter.put_empty_statement_on_new_line=true +org.eclipse.jdt.core.formatter.sort_local_variable_annotations=false +org.eclipse.jdt.core.formatter.sort_member_annotations=false +org.eclipse.jdt.core.formatter.sort_package_annotations=false +org.eclipse.jdt.core.formatter.sort_parameter_annotations=false +org.eclipse.jdt.core.formatter.sort_type_annotations=false org.eclipse.jdt.core.formatter.tabulation.char=space org.eclipse.jdt.core.formatter.tabulation.size=2 org.eclipse.jdt.core.formatter.use_on_off_tags=false org.eclipse.jdt.core.formatter.use_tabs_only_for_leading_indentations=false org.eclipse.jdt.core.formatter.wrap_before_binary_operator=true org.eclipse.jdt.core.formatter.wrap_before_or_operator_multicatch=true +org.eclipse.jdt.core.formatter.wrap_comment_inline_tags=false +org.eclipse.jdt.core.formatter.wrap_non_simple_local_variable_annotation=true +org.eclipse.jdt.core.formatter.wrap_non_simple_member_annotation=true +org.eclipse.jdt.core.formatter.wrap_non_simple_package_annotation=true +org.eclipse.jdt.core.formatter.wrap_non_simple_parameter_annotation=false +org.eclipse.jdt.core.formatter.wrap_non_simple_type_annotation=true org.eclipse.jdt.core.formatter.wrap_outer_expressions_when_nested=true +org.eclipse.jdt.core.formatter.wrap_prefer_two_fragments=false diff --git a/MyTracksTest/.settings/org.eclipse.jdt.ui.prefs b/MyTracksTest/.settings/org.eclipse.jdt.ui.prefs index 1564b76dc..e852be307 100644 --- a/MyTracksTest/.settings/org.eclipse.jdt.ui.prefs +++ b/MyTracksTest/.settings/org.eclipse.jdt.ui.prefs @@ -1,8 +1,8 @@ -#Thu Jul 21 03:06:24 BRT 2011 +#Tue Jul 17 10:22:39 PDT 2012 eclipse.preferences.version=1 editor_save_participant_org.eclipse.jdt.ui.postsavelistener.cleanup=true -formatter_profile=_MyTracks -formatter_settings_version=12 +formatter_profile=_My Tracks +formatter_settings_version=13 org.eclipse.jdt.ui.ignorelowercasenames=true org.eclipse.jdt.ui.importorder=com;android;java;javax;org; org.eclipse.jdt.ui.ondemandthreshold=99 diff --git a/MyTracksTest/AndroidManifest.xml b/MyTracksTest/AndroidManifest.xml index 13ba18d63..cfaef984a 100644 --- a/MyTracksTest/AndroidManifest.xml +++ b/MyTracksTest/AndroidManifest.xml @@ -22,13 +22,16 @@ limitations under the License. + + 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/DescriptionGeneratorImplTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/content/DescriptionGeneratorImplTest.java index 1582d5a87..805a80dba 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/content/DescriptionGeneratorImplTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/content/DescriptionGeneratorImplTest.java @@ -112,7 +112,8 @@ public class DescriptionGeneratorImplTest extends AndroidTestCase { + "Max grade: 42 %\n" + "Min grade: 11 %\n" + "Recorded: " + StringUtils.formatDateTime(getContext(), START_TIME) + "\n"; - assertEquals(expected, descriptionGenerator.generateWaypointDescription(waypoint)); + assertEquals( + expected, descriptionGenerator.generateWaypointDescription(waypoint.getTripStatistics())); } /** diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/content/MyTracksProviderTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/content/MyTracksProviderTest.java new file mode 100644 index 000000000..3622175e4 --- /dev/null +++ b/MyTracksTest/src/com/google/android/apps/mytracks/content/MyTracksProviderTest.java @@ -0,0 +1,196 @@ +/* + * Copyright 2012 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 com.google.android.apps.mytracks.content.MyTracksProvider.DatabaseHelper; + +import android.database.sqlite.SQLiteDatabase; +import android.net.Uri; +import android.test.AndroidTestCase; + +/** + * A unit test for {@link MyTracksProvider}. + * + * @author Youtao Liu + */ +public class MyTracksProviderTest extends AndroidTestCase { + + private SQLiteDatabase db; + private MyTracksProvider myTracksProvider; + private String DATABASE_NAME = "mytrackstest.db"; + + @Override + protected void setUp() throws Exception { + getContext().deleteDatabase(DATABASE_NAME); + db = (new DatabaseHelper(getContext(), DATABASE_NAME)).getWritableDatabase(); + + myTracksProvider = new MyTracksProvider(); + super.setUp(); + } + + /** + * Tests the method {@link MyTracksProvider.DatabaseHelper#onCreate()}. + */ + public void testDatabaseHelper_OnCreate() { + assertTrue(checkTable(TrackPointsColumns.TABLE_NAME)); + assertTrue(checkTable(TracksColumns.TABLE_NAME)); + assertTrue(checkTable(WaypointsColumns.TABLE_NAME)); + } + + /** + * Tests the method + * {@link MyTracksProvider.DatabaseHelper#onUpgrade(SQLiteDatabase, int, int)} + * when version is less than 17. + */ + public void testDatabaseHelper_onUpgrade_Version16() { + DatabaseHelper databaseHelper = new DatabaseHelper(getContext()); + dropTable(TrackPointsColumns.TABLE_NAME); + dropTable(TracksColumns.TABLE_NAME); + dropTable(WaypointsColumns.TABLE_NAME); + databaseHelper.onUpgrade(db, 16, 20); + assertTrue(checkTable(TrackPointsColumns.TABLE_NAME)); + assertTrue(checkTable(TracksColumns.TABLE_NAME)); + assertTrue(checkTable(WaypointsColumns.TABLE_NAME)); + } + + /** + * Tests the method + * {@link MyTracksProvider.DatabaseHelper#onUpgrade(SQLiteDatabase, int, int)} + * when version is 17. + */ + public void testDatabaseHelper_onUpgrade_Version17() { + DatabaseHelper databaseHelper = new DatabaseHelper(getContext()); + + // Make two table is only contains one normal integer column. + dropTable(TrackPointsColumns.TABLE_NAME); + dropTable(TracksColumns.TABLE_NAME); + createEmptyTable(TrackPointsColumns.TABLE_NAME); + createEmptyTable(TracksColumns.TABLE_NAME); + databaseHelper.onUpgrade(db, 17, 20); + assertTrue(isColumnExisted(TrackPointsColumns.TABLE_NAME, TrackPointsColumns.SENSOR)); + assertTrue(isColumnExisted(TracksColumns.TABLE_NAME, TracksColumns.TABLEID)); + assertTrue(isColumnExisted(TracksColumns.TABLE_NAME, TracksColumns.ICON)); + } + + /** + * Tests the method + * {@link MyTracksProvider.DatabaseHelper#onUpgrade(SQLiteDatabase, int, int)} + * when version is 18. + */ + public void testDatabaseHelper_onUpgrade_Version18() { + DatabaseHelper databaseHelper = new DatabaseHelper(getContext()); + + // Make two table is only contains one normal integer column. + dropTable(TrackPointsColumns.TABLE_NAME); + dropTable(TracksColumns.TABLE_NAME); + createEmptyTable(TrackPointsColumns.TABLE_NAME); + createEmptyTable(TracksColumns.TABLE_NAME); + databaseHelper.onUpgrade(db, 18, 20); + assertFalse(isColumnExisted(TrackPointsColumns.TABLE_NAME, TrackPointsColumns.SENSOR)); + assertTrue(isColumnExisted(TracksColumns.TABLE_NAME, TracksColumns.TABLEID)); + assertTrue(isColumnExisted(TracksColumns.TABLE_NAME, TracksColumns.ICON)); + } + + /** + * Tests the method + * {@link MyTracksProvider.DatabaseHelper#onUpgrade(SQLiteDatabase, int, int)} + * when version is 19. + */ + public void testDatabaseHelper_onUpgrade_Version19() { + DatabaseHelper databaseHelper = new DatabaseHelper(getContext()); + + // Make two table is only contains one normal integer column. + dropTable(TrackPointsColumns.TABLE_NAME); + dropTable(TracksColumns.TABLE_NAME); + createEmptyTable(TrackPointsColumns.TABLE_NAME); + createEmptyTable(TracksColumns.TABLE_NAME); + databaseHelper.onUpgrade(db, 19, 20); + assertFalse(isColumnExisted(TrackPointsColumns.TABLE_NAME, TrackPointsColumns.SENSOR)); + assertFalse(isColumnExisted(TracksColumns.TABLE_NAME, TracksColumns.TABLEID)); + assertTrue(isColumnExisted(TracksColumns.TABLE_NAME, TracksColumns.ICON)); + } + + /** + * Tests the method {@link MyTracksProvider#onCreate()}. + */ + public void testOnCreate() { + assertTrue(myTracksProvider.onCreate(getContext())); + } + + /** + * Tests the method {@link MyTracksProvider#getType(Uri)}. + */ + public void testGetType() { + assertEquals(TrackPointsColumns.CONTENT_TYPE, + myTracksProvider.getType(TrackPointsColumns.CONTENT_URI)); + assertEquals(TracksColumns.CONTENT_TYPE, myTracksProvider.getType(TracksColumns.CONTENT_URI)); + assertEquals(WaypointsColumns.CONTENT_TYPE, + myTracksProvider.getType(WaypointsColumns.CONTENT_URI)); + } + + /** + * Creates an table only contains one column. + * + * @param table the name of table + */ + private void createEmptyTable(String table) { + db.execSQL("CREATE TABLE " + table + " (test INTEGER)"); + } + + /** + * Drops a table in database. + * + * @param table + */ + private void dropTable(String table) { + db.execSQL("Drop TABLE " + table); + } + + /** + * Checks whether a table is existed. + * + * @param table the name of table + * @return true means the table has existed + */ + private boolean checkTable(String table) { + try { + db.rawQuery("select count(*) from " + table, null); + return true; + } catch (Exception e) { + return false; + } + } + + /** + * Checks whether a column in a table is existed by whether can order by the + * column. + * + * @param table the name of table + * @param column the name of column + * @return true means the column has existed + */ + private boolean isColumnExisted(String table, String column) { + try { + db.execSQL("SElECT count(*) from " + table + " order by " + column); + } catch (Exception e) { + if (e.getMessage().indexOf("no such column") > -1) { + return false; + } + } + return true; + } + +} \ No newline at end of file diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/content/MyTracksProviderUtilsImplTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/content/MyTracksProviderUtilsImplTest.java index 9107221a9..c3e9b4ba6 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/content/MyTracksProviderUtilsImplTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/content/MyTracksProviderUtilsImplTest.java @@ -18,8 +18,14 @@ package com.google.android.apps.mytracks.content; import com.google.android.apps.mytracks.content.MyTracksProviderUtils.LocationFactory; import com.google.android.apps.mytracks.content.MyTracksProviderUtils.LocationIterator; import com.google.android.apps.mytracks.services.TrackRecordingServiceTest.MockContext; +import com.google.android.apps.mytracks.stats.TripStatistics; +import com.google.android.testing.mocking.AndroidMock; +import com.google.android.testing.mocking.UsesMocks; +import android.content.ContentResolver; +import android.content.ContentValues; import android.content.Context; +import android.database.Cursor; import android.location.Location; import android.test.AndroidTestCase; import android.test.RenamingDelegatingContext; @@ -27,16 +33,27 @@ import android.test.mock.MockContentResolver; import java.util.ArrayList; import java.util.List; +import java.util.Vector; import java.util.concurrent.atomic.AtomicInteger; /** * A unit test for {@link MyTracksProviderUtilsImpl}. * * @author Bartlomiej Niechwiej + * @author Youtao Liu */ public class MyTracksProviderUtilsImplTest extends AndroidTestCase { private Context context; private MyTracksProviderUtils providerUtils; + + private static final String NAME_PREFIX = "test name"; + private static final String TRACK_CATEGORY = "test category"; + 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 { @@ -52,6 +69,7 @@ public class MyTracksProviderUtilsImplTest extends AndroidTestCase { setContext(context); providerUtils = MyTracksProviderUtils.Factory.get(context); + providerUtils.deleteAllTracks(); } public void testLocationIterator_noPoints() { @@ -168,4 +186,888 @@ public class MyTracksProviderUtilsImplTest extends AndroidTestCase { return lastPointId; } + + /** + * Tests the method {@link MyTracksProviderUtilsImpl#getAllTracks()} + */ + public void testGetAllTracks() { + int initialTrackNumber = providerUtils.getAllTracks().size(); + long trackId = System.currentTimeMillis(); + providerUtils.insertTrack(getTrack(trackId, 0)); + List allTracks = providerUtils.getAllTracks(); + assertEquals(initialTrackNumber + 1, allTracks.size()); + assertEquals(trackId, allTracks.get(allTracks.size() - 1).getId()); + } + + /** + * Tests the method {@link MyTracksProviderUtilsImpl#getTrack(long)} + */ + public void testGetTrack() { + long trackId = System.currentTimeMillis(); + providerUtils.insertTrack(getTrack(trackId, 0)); + assertNotNull(providerUtils.getTrack(trackId)); + } + + /** + * Tests the method {@link MyTracksProviderUtilsImpl#getLastTrack()} + */ + public void testGetLastTrack() { + long trackId = System.currentTimeMillis(); + providerUtils.insertTrack(getTrack(trackId, 0)); + assertEquals(trackId, providerUtils.getLastTrack().getId()); + } + + /** + * Tests the method {@link MyTracksProviderUtilsImpl#getLastTrackId()} + */ + public void testGetLastTrackId() { + long trackId = System.currentTimeMillis(); + providerUtils.insertTrack(getTrack(trackId, 0)); + assertEquals(trackId, providerUtils.getLastTrackId()); + } + + /** + * Tests the method {@link MyTracksProviderUtilsImpl#trackExists(long)} + */ + public void testTrackExists() { + long trackId = System.currentTimeMillis(); + providerUtils.insertTrack(getTrack(trackId, 0)); + assertTrue(providerUtils.trackExists(trackId)); + } + + /** + * Tests the method {@link MyTracksProviderUtilsImpl#updateTrack(Track)} + */ + public void testUpdateTrack() { + long trackId = System.currentTimeMillis(); + Track track = getTrack(trackId, 0); + String nameOld = "name1"; + String nameNew = "name2"; + track.setName(nameOld); + providerUtils.insertTrack(track); + assertEquals(nameOld, providerUtils.getTrack(trackId).getName()); + track.setName(nameNew); + providerUtils.updateTrack(track); + assertEquals(nameNew, providerUtils.getTrack(trackId).getName()); + } + + /** + * Tests the method {@link MyTracksProviderUtilsImpl#deleteAllTracks()} + */ + public void testDeleteAllTracks() { + // Insert track, points and waypoint at first. + long trackId = System.currentTimeMillis(); + Track track = getTrack(trackId, 10); + insertTrackWithLocations(track); + Waypoint waypoint = new Waypoint(); + providerUtils.insertWaypoint(waypoint); + ContentResolver contentResolver = context.getContentResolver(); + Cursor tracksCursor = contentResolver.query(TracksColumns.CONTENT_URI, null, null, null, + TracksColumns._ID); + assertEquals(1, tracksCursor.getCount()); + Cursor tracksPointsCursor = contentResolver.query(TrackPointsColumns.CONTENT_URI, null, null, + null, TrackPointsColumns._ID); + assertEquals(10, tracksPointsCursor.getCount()); + Cursor waypointCursor = contentResolver.query(WaypointsColumns.CONTENT_URI, null, null, + null, WaypointsColumns._ID); + assertEquals(1, waypointCursor.getCount()); + // Delete all. + providerUtils.deleteAllTracks(); + // Check whether all have been deleted. + tracksCursor = contentResolver.query(TracksColumns.CONTENT_URI, null, null, null, + TracksColumns._ID); + assertEquals(0, tracksCursor.getCount()); + tracksPointsCursor = contentResolver.query(TrackPointsColumns.CONTENT_URI, null, null, + null, TrackPointsColumns._ID); + assertEquals(0, tracksPointsCursor.getCount()); + waypointCursor = contentResolver.query(WaypointsColumns.CONTENT_URI, null, null, + null, WaypointsColumns._ID); + assertEquals(0, waypointCursor.getCount()); + } + + /** + * Tests the method {@link MyTracksProviderUtilsImpl#deleteTrack(long)}. + */ + public void testDeleteTrack() { + // Insert three tracks, points of two tracks and way point of one track. + long trackId = System.currentTimeMillis(); + Track track = getTrack(trackId, 10); + + providerUtils.insertTrack(track); + insertTrackWithLocations(getTrack(trackId + 1, 10)); + insertTrackWithLocations(getTrack(trackId + 2, 10)); + + Waypoint waypoint = new Waypoint(); + waypoint.setTrackId(trackId); + providerUtils.insertWaypoint(waypoint); + + ContentResolver contentResolver = context.getContentResolver(); + Cursor tracksCursor = contentResolver.query(TracksColumns.CONTENT_URI, null, null, null, + TracksColumns._ID); + assertEquals(3, tracksCursor.getCount()); + Cursor tracksPointsCursor = contentResolver.query(TrackPointsColumns.CONTENT_URI, null, null, + null, TrackPointsColumns._ID); + assertEquals(20, tracksPointsCursor.getCount()); + Cursor waypointCursor = contentResolver.query(WaypointsColumns.CONTENT_URI, null, null, + null, WaypointsColumns._ID); + assertEquals(1, waypointCursor.getCount()); + // Delete one track. + providerUtils.deleteTrack(trackId); + // Check whether all data of a track has been deleted. + tracksCursor = contentResolver.query(TracksColumns.CONTENT_URI, null, null, null, + TracksColumns._ID); + assertEquals(2, tracksCursor.getCount()); + tracksPointsCursor = contentResolver.query(TrackPointsColumns.CONTENT_URI, null, null, + null, TrackPointsColumns._ID); + assertEquals(20, tracksPointsCursor.getCount()); + waypointCursor = contentResolver.query(WaypointsColumns.CONTENT_URI, null, null, + null, WaypointsColumns._ID); + assertEquals(0, waypointCursor.getCount()); + } + + /** + * Tests the method {@link MyTracksProviderUtilsImpl#createTrack(Cursor)}. + */ + @UsesMocks(Cursor.class) + public void testCreateTrack() { + Cursor cursorMock = AndroidMock.createNiceMock(Cursor.class); + int startColumnIndex = 1; + int columnIndex = startColumnIndex; + AndroidMock.expect(cursorMock.getColumnIndexOrThrow(TracksColumns._ID)) + .andReturn(columnIndex++); + AndroidMock.expect(cursorMock.getColumnIndexOrThrow(TracksColumns.NAME)).andReturn( + columnIndex++); + columnIndex = startColumnIndex; + // Id + AndroidMock.expect(cursorMock.isNull(columnIndex++)).andReturn(false); + // Name + AndroidMock.expect(cursorMock.isNull(columnIndex++)).andReturn(false); + long trackId = System.currentTimeMillis(); + columnIndex = startColumnIndex; + // Id + AndroidMock.expect(cursorMock.getLong(columnIndex++)).andReturn(trackId); + // Name + String name = NAME_PREFIX + Long.toString(trackId); + AndroidMock.expect(cursorMock.getString(columnIndex++)).andReturn(name); + AndroidMock.replay(cursorMock); + Track track = providerUtils.createTrack(cursorMock); + assertEquals(trackId, track.getId()); + assertEquals(name, track.getName()); + AndroidMock.verify(cursorMock); + } + + /** + * Tests the method {@link MyTracksProviderUtilsImpl#createContentValues(Track)}. + */ + @UsesMocks(TripStatistics.class) + public void testCreateContentValues_track() { + // ID + long trackId = System.currentTimeMillis(); + // Name + String name = NAME_PREFIX + Long.toString(trackId); + Track track = getTrack(trackId, 10); + track.setName(name); + track.setCategory(TRACK_CATEGORY); + TripStatistics tripStatistics = AndroidMock.createNiceMock(TripStatistics.class); + // Bottom + int bottom = 22; + // AverageSpeed + double averageSpeed = 1.11; + AndroidMock.expect(tripStatistics.getBottom()).andReturn(bottom); + AndroidMock.expect(tripStatistics.getAverageSpeed()).andReturn(averageSpeed); + track.setTripStatistics(tripStatistics); + AndroidMock.replay(tripStatistics); + + ContentValues contentValues = providerUtils.createContentValues(track); + assertEquals(trackId, contentValues.get(TracksColumns._ID)); + assertEquals(name, contentValues.get(TracksColumns.NAME)); + assertEquals(bottom, contentValues.get(TracksColumns.MINLAT)); + assertEquals(averageSpeed, contentValues.get(TracksColumns.AVGSPEED)); + AndroidMock.verify(tripStatistics); + } + + /** + * Tests the method {@link MyTracksProviderUtilsImpl#getFirstWaypoint(long)}. + */ + public void testGetFirstWaypoint() { + long trackId = System.currentTimeMillis(); + Track track = getTrack(trackId, 10); + providerUtils.insertTrack(track); + + Waypoint waypoint1 = new Waypoint(); + waypoint1.setDescription("Desc1"); + waypoint1.setTrackId(trackId); + Waypoint waypoint2 = new Waypoint(); + waypoint2.setDescription("Desc2"); + waypoint2.setTrackId(trackId); + providerUtils.insertWaypoint(waypoint1); + providerUtils.insertWaypoint(waypoint2); + + assertNull(providerUtils.getFirstWaypoint(-1)); + Waypoint wayPoint = providerUtils.getFirstWaypoint(trackId); + assertEquals("Desc1", wayPoint.getDescription()); + } + + /** + * Tests the method {@link MyTracksProviderUtilsImpl#getFirstWaypointId(long)}. + */ + public void testGetFirstWaypointId() { + long trackId = System.currentTimeMillis(); + Track track = getTrack(trackId, 10); + providerUtils.insertTrack(track); + + Waypoint waypoint1 = new Waypoint(); + waypoint1.setTrackId(trackId); + Waypoint waypoint2 = new Waypoint(); + waypoint2.setTrackId(trackId); + providerUtils.insertWaypoint(waypoint1); + providerUtils.insertWaypoint(waypoint2); + + assertEquals(-1L, providerUtils.getFirstWaypointId(-1)); + assertEquals(1L, providerUtils.getFirstWaypointId(trackId)); + } + + /** + * Tests the method {@link MyTracksProviderUtilsImpl#getLastWaypointId(long)}. + */ + public void testGetLastWaypointId() { + long trackId = System.currentTimeMillis(); + Track track = getTrack(trackId, 10); + providerUtils.insertTrack(track); + + Waypoint waypoint1 = new Waypoint(); + waypoint1.setTrackId(trackId); + Waypoint waypoint2 = new Waypoint(); + waypoint2.setTrackId(trackId); + Waypoint waypoint3 = new Waypoint(); + waypoint3.setTrackId(trackId); + providerUtils.insertWaypoint(waypoint1); + providerUtils.insertWaypoint(waypoint2); + providerUtils.insertWaypoint(waypoint3); + + assertEquals(-1L, providerUtils.getLastWaypointId(-1)); + assertEquals(3L, providerUtils.getLastWaypointId(trackId)); + } + + /** + * Tests the method + * {@link MyTracksProviderUtils#getLastStatisticsWaypoint(long)}. + */ + public void testGetLastStatisticsWaypoint() { + long trackId = System.currentTimeMillis(); + Track track = getTrack(trackId, 10); + providerUtils.insertTrack(track); + + Waypoint waypoint1 = new Waypoint(); + waypoint1.setTrackId(trackId); + waypoint1.setType(Waypoint.TYPE_STATISTICS); + waypoint1.setDescription("Desc1"); + Waypoint waypoint2 = new Waypoint(); + waypoint2.setTrackId(trackId); + waypoint2.setType(Waypoint.TYPE_STATISTICS); + waypoint2.setDescription("Desc2"); + Waypoint waypoint3 = new Waypoint(); + waypoint3.setTrackId(trackId); + waypoint3.setType(Waypoint.TYPE_WAYPOINT); + waypoint3.setDescription("Desc3"); + providerUtils.insertWaypoint(waypoint1); + providerUtils.insertWaypoint(waypoint2); + providerUtils.insertWaypoint(waypoint3); + + assertEquals("Desc2", providerUtils.getLastStatisticsWaypoint(trackId).getDescription()); + } + + /** + * Tests the method {@link MyTracksProviderUtilsImpl#getNextMarkerNumber(long, boolean)}. + */ + public void testGetNextMarkerNumber() { + long trackId = System.currentTimeMillis(); + Track track = getTrack(trackId, 10); + providerUtils.insertTrack(track); + + Waypoint waypoint1 = new Waypoint(); + waypoint1.setType(Waypoint.TYPE_STATISTICS); + waypoint1.setTrackId(trackId); + Waypoint waypoint2 = new Waypoint(); + waypoint2.setType(Waypoint.TYPE_WAYPOINT); + waypoint2.setTrackId(trackId); + Waypoint waypoint3 = new Waypoint(); + waypoint3.setType(Waypoint.TYPE_STATISTICS); + waypoint3.setTrackId(trackId); + Waypoint waypoint4 = new Waypoint(); + waypoint4.setType(Waypoint.TYPE_WAYPOINT); + waypoint4.setTrackId(trackId); + providerUtils.insertWaypoint(waypoint1); + providerUtils.insertWaypoint(waypoint2); + providerUtils.insertWaypoint(waypoint3); + providerUtils.insertWaypoint(waypoint4); + + assertEquals(2, providerUtils.getNextMarkerNumber(trackId, true)); + assertEquals(3, providerUtils.getNextMarkerNumber(trackId, false)); + } + + /** + * Tests the method {@link MyTracksProviderUtilsImpl#getNextStatisticsWaypointAfter(Waypoint)}. + */ + public void testGetNextStatisticsWaypointAfter() { + long trackId = System.currentTimeMillis(); + Track track = getTrack(trackId, 10); + providerUtils.insertTrack(track); + + Waypoint waypoint1 = new Waypoint(); + waypoint1.setType(Waypoint.TYPE_STATISTICS); + waypoint1.setTrackId(trackId); + Waypoint waypoint2 = new Waypoint(); + waypoint2.setType(Waypoint.TYPE_WAYPOINT); + waypoint2.setTrackId(trackId); + Waypoint waypoint3 = new Waypoint(); + waypoint3.setType(Waypoint.TYPE_STATISTICS); + waypoint3.setTrackId(trackId); + waypoint3.setDescription("Desc3"); + Waypoint waypoint4 = new Waypoint(); + waypoint4.setType(Waypoint.TYPE_STATISTICS); + waypoint4.setTrackId(trackId); + waypoint4.setDescription("Desc4"); + providerUtils.insertWaypoint(waypoint1); + providerUtils.insertWaypoint(waypoint2); + providerUtils.insertWaypoint(waypoint3); + providerUtils.insertWaypoint(waypoint4); + + assertEquals("Desc3", providerUtils.getNextStatisticsWaypointAfter(providerUtils.getFirstWaypoint(trackId)).getDescription()); + } + + /** + * Tests the method {@link MyTracksProviderUtilsImpl#insertWaypoint(Waypoint)} and + * {@link MyTracksProviderUtilsImpl#getWaypoint(long)}. + */ + public void testInsertAndGetWaypoint() { + long trackId = System.currentTimeMillis(); + Track track = getTrack(trackId, 10); + providerUtils.insertTrack(track); + + Waypoint waypoint = new Waypoint(); + waypoint.setDescription(TEST_DESC); + waypoint.setTrackId(trackId); + providerUtils.insertWaypoint(waypoint); + + assertEquals(TEST_DESC, providerUtils.getWaypoint(1).getDescription()); + } + + /** + * Tests the method {@link MyTracksProviderUtilsImpl#updateWaypoint(Waypoint)}. + */ + public void testUpdateWaypoint() { + long trackId = System.currentTimeMillis(); + Track track = getTrack(trackId, 10); + providerUtils.insertTrack(track); + // Insert at first. + Waypoint waypoint = new Waypoint(); + waypoint.setDescription(TEST_DESC); + waypoint.setTrackId(trackId); + providerUtils.insertWaypoint(waypoint); + // Update + waypoint = providerUtils.getWaypoint(1); + waypoint.setDescription(TEST_DESC_NEW); + providerUtils.updateWaypoint(waypoint); + + assertEquals(TEST_DESC_NEW, providerUtils.getWaypoint(1).getDescription()); + } + + /** + * Tests the method + * {@link MyTracksProviderUtilsImpl#deleteWaypoint(long, DescriptionGenerator)} + * when there is only one waypoint in the track. + */ + public void testDeleteWaypoint_onlyOneWayPoint() { + long trackId = System.currentTimeMillis(); + Track track = getTrack(trackId, 10); + providerUtils.insertTrack(track); + + // Insert at first. + Waypoint waypoint1 = new Waypoint(); + waypoint1.setDescription(TEST_DESC); + waypoint1.setTrackId(trackId); + waypoint1.setType(Waypoint.TYPE_STATISTICS); + providerUtils.insertWaypoint(waypoint1); + + // Delete + DescriptionGenerator descriptionGenerator = new DescriptionGenerator() { + + @Override + public String generateWaypointDescription(TripStatistics tripStatistics) { + return MyTracksProviderUtilsImplTest.MOCK_DESC; + } + + @SuppressWarnings("hiding") + @Override + public String generateTrackDescription(Track track, Vector distances, + Vector elevations, boolean html) { + return null; + } + }; + providerUtils.deleteWaypoint(1, descriptionGenerator); + + assertNull(providerUtils.getWaypoint(1)); + } + + /** + * Tests the method + * {@link MyTracksProviderUtilsImpl#deleteWaypoint(long, DescriptionGenerator)} + * when there is more than one waypoint in the track. + */ + public void testDeleteWaypoint_hasNextWayPoint() { + long trackId = System.currentTimeMillis(); + Track track = getTrack(trackId, 10); + providerUtils.insertTrack(track); + + TripStatistics statistics = new TripStatistics(); + statistics.setStartTime(1000L); + statistics.setStopTime(2500L); + statistics.setTotalTime(1500L); + statistics.setMovingTime(700L); + statistics.setTotalDistance(750.0); + statistics.setTotalElevationGain(50.0); + statistics.setMaxSpeed(60.0); + statistics.setMaxElevation(1250.0); + statistics.setMinElevation(1200.0); + statistics.setMaxGrade(15.0); + statistics.setMinGrade(-25.0); + statistics.setBounds(-10000, 20000, 30000, -40000); + // Insert at first. + Waypoint waypoint1 = new Waypoint(); + waypoint1.setDescription(Long.toString(trackId)); + waypoint1.setTrackId(trackId); + waypoint1.setType(Waypoint.TYPE_STATISTICS); + waypoint1.setTripStatistics(statistics); + providerUtils.insertWaypoint(waypoint1); + Waypoint waypoint2 = new Waypoint(); + waypoint2.setDescription(Long.toString(trackId)); + waypoint2.setTrackId(trackId); + waypoint2.setType(Waypoint.TYPE_STATISTICS); + waypoint2.setTripStatistics(statistics); + providerUtils.insertWaypoint(waypoint2); + + // Delete + DescriptionGenerator descriptionGenerator = new DescriptionGenerator() { + @Override + public String generateWaypointDescription(TripStatistics tripStatistics) { + return MyTracksProviderUtilsImplTest.MOCK_DESC; + } + + @SuppressWarnings("hiding") + @Override + public String generateTrackDescription(Track track, Vector distances, + Vector elevations, boolean html) { + return null; + } + }; + providerUtils.deleteWaypoint(1, descriptionGenerator); + + assertNull(providerUtils.getWaypoint(1)); + assertEquals(MyTracksProviderUtilsImplTest.MOCK_DESC, providerUtils.getWaypoint(2) + .getDescription()); + } + + /** + * Tests the method {@link MyTracksProviderUtilsImpl#createWaypoint(Cursor)}. + */ + @UsesMocks(Cursor.class) + public void testCreateWaypoint() { + Cursor cursorMock = AndroidMock.createNiceMock(Cursor.class); + int startColumnIndex = 1; + int columnIndex = startColumnIndex; + AndroidMock.expect(cursorMock.getColumnIndexOrThrow(WaypointsColumns._ID)) + .andReturn(columnIndex++); + AndroidMock.expect(cursorMock.getColumnIndexOrThrow(WaypointsColumns.NAME)).andReturn( + columnIndex++); + AndroidMock.expect(cursorMock.getColumnIndexOrThrow(WaypointsColumns.TRACKID)).andReturn( + columnIndex++); + columnIndex = startColumnIndex; + // Id + AndroidMock.expect(cursorMock.isNull(columnIndex++)).andReturn(false); + // Name + AndroidMock.expect(cursorMock.isNull(columnIndex++)).andReturn(false); + // trackIdIndex + AndroidMock.expect(cursorMock.isNull(columnIndex++)).andReturn(false); + long id = System.currentTimeMillis(); + columnIndex = startColumnIndex; + // Id + AndroidMock.expect(cursorMock.getLong(columnIndex++)).andReturn(id); + // Name + String name = NAME_PREFIX + Long.toString(id); + AndroidMock.expect(cursorMock.getString(columnIndex++)).andReturn(name); + // trackIdIndex + long trackId = 11L; + AndroidMock.expect(cursorMock.getLong(columnIndex++)).andReturn(trackId); + AndroidMock.replay(cursorMock); + Waypoint waypoint = providerUtils.createWaypoint(cursorMock); + assertEquals(id, waypoint.getId()); + assertEquals(name, waypoint.getName()); + assertEquals(trackId, waypoint.getTrackId()); + AndroidMock.verify(cursorMock); + } + + /** + * Tests the method {@link MyTracksProviderUtilsImpl#createContentValues(Waypoint)}. + */ + public void testCreateContentValues_waypoint() { + long trackId = System.currentTimeMillis(); + Track track = getTrack(trackId, 10); + providerUtils.insertTrack(track); + // Bottom + long startTime = 1000L; + // AverageSpeed + double minGrade = -20.11; + TripStatistics statistics = new TripStatistics(); + statistics.setStartTime(startTime); + statistics.setStopTime(2500L); + statistics.setTotalTime(1500L); + statistics.setMovingTime(700L); + statistics.setTotalDistance(750.0); + statistics.setTotalElevationGain(50.0); + statistics.setMaxSpeed(60.0); + statistics.setMaxElevation(1250.0); + statistics.setMinElevation(1200.0); + statistics.setMaxGrade(15.0); + statistics.setMinGrade(minGrade); + statistics.setBounds(-10000, 20000, 30000, -40000); + // Insert at first. + Waypoint waypoint = new Waypoint(); + waypoint.setDescription(TEST_DESC); + waypoint.setType(Waypoint.TYPE_STATISTICS); + waypoint.setTripStatistics(statistics); + + Location loc = new Location("test"); + loc.setLatitude(22); + loc.setLongitude(22); + loc.setAccuracy((float) 1 / 100.0f); + loc.setAltitude(2.5); + waypoint.setLocation(loc); + providerUtils.insertWaypoint(waypoint); + + MyTracksProviderUtilsImpl myTracksProviderUtilsImpl = new MyTracksProviderUtilsImpl( + new MockContentResolver()); + + long waypointId = System.currentTimeMillis(); + waypoint.setId(waypointId); + ContentValues contentValues = myTracksProviderUtilsImpl.createContentValues(waypoint); + assertEquals(waypointId, contentValues.get(WaypointsColumns._ID)); + assertEquals(22 * 1000000, contentValues.get(WaypointsColumns.LONGITUDE)); + assertEquals(TEST_DESC, contentValues.get(WaypointsColumns.DESCRIPTION)); + assertEquals(startTime, contentValues.get(WaypointsColumns.STARTTIME)); + assertEquals(minGrade, contentValues.get(WaypointsColumns.MINGRADE)); + } + + /** + * Tests the method {@link MyTracksProviderUtilsImpl#getFirstLocation()}. + */ + public void testGetFirstLocation() { + // Insert track, points at first. + long trackId = System.currentTimeMillis(); + Track track = getTrack(trackId, 10); + insertTrackWithLocations(track); + + Location firstLocation = providerUtils.getFirstLocation(); + checkLocation(0, firstLocation); + } + + /** + * Tests the method {@link MyTracksProviderUtilsImpl#getLastLocation()}. + */ + public void testGetLastLocation() { + // Insert track, points at first. + long trackId = System.currentTimeMillis(); + Track track = getTrack(trackId, 10); + insertTrackWithLocations(track); + + Location lastLocation = providerUtils.getLastLocation(); + checkLocation(9, lastLocation); + } + + /** + * Tests the method {@link MyTracksProviderUtilsImpl#getLocation(long)}. + */ + public void testGetLocation() { + // Insert track, points at first. + long trackId = System.currentTimeMillis(); + Track track = getTrack(trackId, 10); + insertTrackWithLocations(track); + + Location location1 = providerUtils.getLocation(1L); + checkLocation(0, location1); + Location location5 = providerUtils.getLocation(5L); + checkLocation(4, location5); + Location location10 = providerUtils.getLocation(10L); + checkLocation(9, location10); + } + + /** + * Tests the method + * {@link MyTracksProviderUtilsImpl#getLocationsCursor(long, long, int, boolean)} + * in descending. + */ + public void testGetLocationCursor_desc() { + // Insert track, points at first. + long trackId = System.currentTimeMillis(); + Track track = getTrack(trackId, 10); + insertTrackWithLocations(track); + + Cursor cursor = providerUtils.getLocationsCursor(trackId, 2L, 5, true); + assertEquals(2, cursor.getCount()); + } + + /** + * Tests the method + * {@link MyTracksProviderUtilsImpl#getLocationsCursor(long, long, int, boolean)} + * in ascending. + */ + public void testGetLocationCursor_asc() { + // Insert track, points at first. + long trackId = System.currentTimeMillis(); + Track track = getTrack(trackId, 10); + insertTrackWithLocations(track); + + Cursor cursor = providerUtils.getLocationsCursor(trackId, 2L, 5, false); + assertEquals(5, cursor.getCount()); + } + + /** + * Tests the method + * {@link MyTracksProviderUtilsImpl#getLocationIterator(long, long, boolean, LocationFactory)} + * in descending. + */ + public void testGeLocationIterator_desc() { + // Insert track, points at first. + long trackId = System.currentTimeMillis(); + Track track = getTrack(trackId, 10); + insertTrackWithLocations(track); + + long startTrackPointId = 2L; + + LocationIterator locationIterator = providerUtils.getLocationIterator(trackId, + startTrackPointId, true, MyTracksProviderUtils.DEFAULT_LOCATION_FACTORY); + 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()); + } + + /** + * Tests the method + * {@link MyTracksProviderUtilsImpl#getLocationIterator(long, long, boolean, LocationFactory)} + * in ascending. + */ + public void testGeLocationIterator_asc() { + // Insert track, point at first. + long trackId = System.currentTimeMillis(); + Track track = getTrack(trackId, 10); + insertTrackWithLocations(track); + + long startTrackPointId = 2L; + + LocationIterator locationIterator = providerUtils.getLocationIterator(trackId, + startTrackPointId, false, MyTracksProviderUtils.DEFAULT_LOCATION_FACTORY); + + 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()); + } + + /** + * Tests the method {@link MyTracksProviderUtilsImpl#getLastTrackLocationId(long)}. + */ + public void testGetLastTrackLocationId() { + // Insert track, point at first. + long trackId = System.currentTimeMillis(); + Track track = getTrack(trackId, 10); + insertTrackWithLocations(track); + + assertEquals(10, providerUtils.getLastTrackLocationId(trackId)); + } + + /** + * Tests the method {@link MyTracksProviderUtilsImpl#getLastTrackLocation(long)}. + */ + public void testGetLastTrackLocation() { + // Insert track, point at first. + long trackId = System.currentTimeMillis(); + Track track = getTrack(trackId, 10); + insertTrackWithLocations(track); + checkLocation(9, providerUtils.getLastTrackLocation(trackId)); + } + + /** + * Tests the method + * {@link MyTracksProviderUtilsImpl#insertTrackPoint(Location, long)}. This + * test also covers the method + * {@link MyTracksProviderUtilsImpl#createContentValues(Location, long)}. + */ + @SuppressWarnings("javadoc") + public void testInsertTrackPoint() { + // Insert track, point at first. + long trackId = System.currentTimeMillis(); + Track track = getTrack(trackId, 10); + insertTrackWithLocations(track); + + providerUtils.insertTrackPoint(createLocation(22), trackId); + assertEquals(11, providerUtils.getLocationsCursor(trackId, 0, 1000, false).getCount()); + } + + /** + * Tests the method + * {@link MyTracksProviderUtilsImpl#bulkInsertTrackPoints(Location[], int, long)} + * . This test also covers the method + * {@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(); + Track track = getTrack(trackId, 10); + insertTrackWithLocations(track); + + providerUtils.bulkInsertTrackPoints(track.getLocations().toArray(new Location[0]), -1, trackId); + assertEquals(20, providerUtils.getLocationsCursor(trackId, 0, 1000, false).getCount()); + providerUtils.bulkInsertTrackPoints(track.getLocations().toArray(new Location[0]), 8, trackId); + assertEquals(28, providerUtils.getLocationsCursor(trackId, 0, 1000, false).getCount()); + } + + /** + * Tests the method {@link MyTracksProviderUtilsImpl#createLocation(Cursor)}. + * This test also covers the method + * {@link MyTracksProviderUtilsImpl#fillLocation(Cursor, Location)} and + * {@link MyTracksProviderUtilsImpl#fillLocation(Cursor, MyTracksProviderUtilsImpl.CachedTrackPointsIndexes, Location)} + * and the inner class + * {@link MyTracksProviderUtilsImpl.CachedTrackPointsIndexes}. + */ + @SuppressWarnings("javadoc") + @UsesMocks(Cursor.class) + public void testCreateLocation() { + Cursor cursorMock = AndroidMock.createNiceMock(Cursor.class); + + // Set index. + int index = 1; + // Id + AndroidMock.expect(cursorMock.getColumnIndex(TrackPointsColumns._ID)).andReturn(index++); + // Longitude + AndroidMock.expect(cursorMock.getColumnIndexOrThrow(TrackPointsColumns.LONGITUDE)).andReturn( + index++); + // Latitude + AndroidMock.expect(cursorMock.getColumnIndexOrThrow(TrackPointsColumns.LATITUDE)).andReturn( + index++); + // Time + AndroidMock.expect(cursorMock.getColumnIndexOrThrow(TrackPointsColumns.TIME)) + .andReturn(index++); + // Speed + AndroidMock.expect(cursorMock.getColumnIndexOrThrow(TrackPointsColumns.SPEED)).andReturn( + index++); + // Sensor + AndroidMock.expect(cursorMock.getColumnIndexOrThrow(TrackPointsColumns.SENSOR)).andReturn( + index++); + + // Set return value of isNull(). + index = 2; + // Longitude + AndroidMock.expect(cursorMock.isNull(index++)).andReturn(false); + // Latitude + AndroidMock.expect(cursorMock.isNull(index++)).andReturn(false); + // Time + AndroidMock.expect(cursorMock.isNull(index++)).andReturn(false); + // Speed + AndroidMock.expect(cursorMock.isNull(index++)).andReturn(false); + // Sensor + AndroidMock.expect(cursorMock.isNull(index++)).andReturn(false); + + // Set return value of isNull(). + index = 2; + // Longitude + int longitude = 11; + AndroidMock.expect(cursorMock.getInt(index++)).andReturn(longitude * 1000000); + // Latitude. + int latitude = 22; + AndroidMock.expect(cursorMock.getInt(index++)).andReturn(latitude * 1000000); + // Time + long time = System.currentTimeMillis(); + AndroidMock.expect(cursorMock.getLong(index++)).andReturn(time); + // Speed + float speed = 2.2f; + AndroidMock.expect(cursorMock.getFloat(index++)).andReturn(speed); + // Sensor + byte[] sensor = "Sensor state".getBytes(); + AndroidMock.expect(cursorMock.getBlob(index++)).andReturn(sensor); + + AndroidMock.replay(cursorMock); + Location location = providerUtils.createLocation(cursorMock); + assertEquals((double) longitude, location.getLongitude()); + assertEquals((double) latitude, location.getLatitude()); + assertEquals(time, location.getTime()); + assertEquals(speed, location.getSpeed()); + AndroidMock.verify(cursorMock); + } + + /** + * Simulates a track which is used for testing. + * + * @param id the id of the track + * @param numPoints the location number in the track + * @return the simulated track + */ + private Track getTrack(long id,int numPoints) { + Track track = new Track(); + track.setId(id); + track.setName("Test: " + id); + track.setNumberOfPoints(numPoints); + for(int i = 0; i < numPoints; i++) { + track.addLocation(createLocation(i)); + } + return track; + } + + /** + * Creates a location. + * @param i the index to set the value of location. + * @return created location + */ + private Location createLocation(int i) { + + Location loc = new Location("test"); + 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 * ALTITUDE_INTERVAL); + return loc; + } + + /** + * Checks the value of a location. + * + * @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(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 * ALTITUDE_INTERVAL, location.getAltitude()); + } + + /** + * Inserts a track with locations into the database. + * + * @param track track to be inserted + */ + private void insertTrackWithLocations(Track track) { + providerUtils.insertTrack(track); + providerUtils.bulkInsertTrackPoints(track.getLocations().toArray(new Location[0]), track + .getLocations().size(), track.getId()); + } } 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 da068091d..421b1f6ab 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/content/TrackDataHubTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/content/TrackDataHubTest.java @@ -15,23 +15,23 @@ */ package com.google.android.apps.mytracks.content; -import static com.google.android.testing.mocking.AndroidMock.anyInt; 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 static com.google.android.testing.mocking.AndroidMock.same; import com.google.android.apps.mytracks.Constants; +import com.google.android.apps.mytracks.TrackStubUtils; +import com.google.android.apps.mytracks.content.DataSourceManager.CurrentLocationListener; +import com.google.android.apps.mytracks.content.DataSourceManager.HeadingListener; 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.TrackDataHub.ListenerDataType; -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; import com.google.android.testing.mocking.AndroidMock; +import com.google.android.testing.mocking.UsesMocks; import android.content.Context; import android.content.SharedPreferences; @@ -39,7 +39,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; @@ -59,7 +58,7 @@ import org.easymock.IAnswer; /** * Tests for {@link TrackDataHub}. - * + * * @author Rodrigo Damazio */ public class TrackDataHubTest extends AndroidTestCase { @@ -67,134 +66,617 @@ 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 TrackDataListeners listeners; - private DataSourcesWrapper dataSources; - 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 }) @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); - dataSources = AndroidMock.createNiceMock("dataSources", DataSourcesWrapper.class); - - listeners = new TrackDataListeners(); - hub = new TrackDataHub(context, listeners, sharedPreferences, providerUtils, TARGET_POINTS) { - @Override - protected DataSourcesWrapper newDataSources() { - return dataSources; + myTracksProviderUtils = AndroidMock.createMock(MyTracksProviderUtils.class); + dataSource = AndroidMock.createMock(DataSource.class, context); + trackDataManager = new TrackDataManager(); + 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); } @Override protected void tearDown() throws Exception { - AndroidMock.reset(dataSources); + AndroidMock.reset(dataSource); // Expect everything to be unregistered. - if (preferenceListenerCapture.hasCaptured()) { - dataSources.unregisterOnSharedPreferenceChangeListener(preferenceListenerCapture.getValue()); - } - dataSources.removeLocationUpdates(isA(LocationListener.class)); - dataSources.unregisterSensorListener(isA(SensorEventListener.class)); - dataSources.unregisterContentObserver(isA(ContentObserver.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); - AndroidMock.replay(dataSources); - - 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(); - dataSources.registerContentObserver( - eq(TracksColumns.CONTENT_URI), eq(false), 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(ListenerDataType.TRACK_UPDATES)); - hub.start(); - hub.registerTrackDataListener(listener2, EnumSet.of(ListenerDataType.TRACK_UPDATES)); - + 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 - dataSources.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.getLastTrackLocationId(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.getLastTrackLocationId(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.getLastTrackLocationId(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.getLastTrackLocationId(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.getLastTrackLocationId(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.getLastTrackLocationId(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.getLastTrackLocationId(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.getLastTrackLocationId(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.getLastTrackLocationId(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.getLastTrackLocationId(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.getLastTrackLocationId(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.getLastTrackLocationId(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; @@ -205,79 +687,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 @@ -287,7 +776,7 @@ public class TrackDataHubTest extends AndroidTestCase { @Override public long getLocationId() { - return startId + currentIdx; + return startId + currentIndex; } @Override @@ -295,549 +784,179 @@ public class TrackDataHubTest extends AndroidTestCase { // Do nothing } } - - public void testWaypointListen() { + + /** + * Tests the method {@link TrackDataHub#start()}. This method would also cover + * some logic of {@link TrackDataHub#loadDataForAll()} and + * {@link TrackDataHub#notifySelectedTrackChanged(Set)}. + */ + public void testRegisterSelectedTrackListener() { + dataSource.registerOnSharedPreferenceChangeListener(capture(preferenceChangeListenerCapture)); + Track track = TrackStubUtils.createTrack(1); + expect(myTracksProviderUtils.getTrack(capture(new Capture()))).andReturn(track); + // Make the track id is unique. + PreferencesUtils.setLong(context, R.string.recording_track_id_key, System.currentTimeMillis()); + trackDataListener1.onSelectedTrackChanged(track, false); + replay(); + trackDataHub.start(); + trackDataHub.registerTrackDataListener(trackDataListener1, EnumSet.of(TrackDataType.SELECTED_TRACK)); + verifyAndReset(); + } + + /** + * Tests the method {@link TrackDataHub#start()}. This method would also cover + * some logic of PreferencesUtils and + * {@link TrackDataHub#notifyTracksTableUpdate(Set)}. + */ + public void testRegisterTracksTableListener() { + dataSource.registerOnSharedPreferenceChangeListener(capture(preferenceChangeListenerCapture)); 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(); - dataSources.registerContentObserver( - eq(WaypointsColumns.CONTENT_URI), eq(false), 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(); - + dataSource.registerContentObserver(eq(TracksColumns.CONTENT_URI), capture(observerCapture)); + Track track = TrackStubUtils.createTrack(1); + expect(myTracksProviderUtils.getTrack(capture(new Capture()))).andReturn(track); + // Make the track id is unique. + PreferencesUtils.setLong(context, R.string.recording_track_id_key, System.currentTimeMillis()); + trackDataListener1.onTrackUpdated(track); replay(); - - hub.registerTrackDataListener(listener1, EnumSet.of(ListenerDataType.WAYPOINT_UPDATES)); - hub.start(); - hub.registerTrackDataListener(listener2, EnumSet.of(ListenerDataType.WAYPOINT_UPDATES)); - - 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 - dataSources.unregisterContentObserver(observer); - - replay(); - - hub.unregisterTrackDataListener(listener2); - observer.onChange(false); - + trackDataHub.start(); + trackDataHub.registerTrackDataListener(trackDataListener1, EnumSet.of(TrackDataType.TRACKS_TABLE)); verifyAndReset(); } - - public void testPointsListen() { + + /** + * Tests the method {@link TrackDataHub#start()}. This method would also cover + * some logic of {@link TrackDataHub#loadDataForAll()} and + * {@link TrackDataHub#notifyWaypointsTableUpdate(Set)}. + */ + public void testRegisterWaypointsTableListener() { + dataSource.registerOnSharedPreferenceChangeListener(capture(preferenceChangeListenerCapture)); Capture observerCapture = new Capture(); - - expectStart(); - dataSources.registerContentObserver( - eq(TrackPointsColumns.CONTENT_URI), eq(false), 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(); - + dataSource.registerContentObserver(eq(WaypointsColumns.CONTENT_URI), capture(observerCapture)); + expect(myTracksProviderUtils.getWaypointsCursor(capture(new Capture()), capture(new Capture()), + capture(new Capture()))).andReturn(null); + trackDataListener1.clearWaypoints(); + trackDataListener1.onNewWaypointsDone(); replay(); - - hub.start(); - hub.registerTrackDataListener(listener1, EnumSet.of(ListenerDataType.POINT_UPDATES)); - + trackDataHub.start(); + trackDataHub.registerTrackDataListener(trackDataListener1, EnumSet.of(TrackDataType.WAYPOINTS_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(); - + } + + /** + * Tests the method {@link TrackDataHub#start()}. This method would also cover + * some logic of {@link TrackDataHub#loadDataForAll()} and + * {@link TrackDataHub#notifyLocationStateChanged(Set)} when no last seen + * location. + */ + public void testRegisterLocationListener_noLastSeenLocation() { + dataSource.registerOnSharedPreferenceChangeListener(capture(preferenceChangeListenerCapture)); + Capture currentLocationListener = new Capture(); + dataSource.registerLocationListener(capture(currentLocationListener)); + trackDataListener1.onLocationStateChanged(capture(new Capture())); replay(); - - hub.start(); - hub.registerTrackDataListener(listener2, EnumSet.of(ListenerDataType.POINT_UPDATES)); - + trackDataHub.setLastSeenLocation(null); + trackDataHub.start(); + trackDataHub.registerTrackDataListener(trackDataListener1, EnumSet.of(TrackDataType.LOCATION)); 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(); - + } + + /** + * Tests the method {@link TrackDataHub#start()}. This method would also cover + * some logic of {@link TrackDataHub#loadDataForAll()} and + * {@link TrackDataHub#notifyLocationChanged(Location, boolean, Set)} when has + * last seen location. + */ + public void testRegisterLocationListener_hasLastSeenLocation() { + dataSource.registerOnSharedPreferenceChangeListener(capture(preferenceChangeListenerCapture)); + Capture currentLocationListener = new Capture(); + dataSource.registerLocationListener(capture(currentLocationListener)); + trackDataListener1.onLocationStateChanged(capture(new Capture())); + trackDataListener1.onLocationChanged(capture(new Capture())); replay(); - - observer.onChange(false); - + trackDataHub.setLastSeenLocation(new Location("gps")); + trackDataHub.start(); + trackDataHub.registerTrackDataListener(trackDataListener1, EnumSet.of(TrackDataType.LOCATION)); 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(); - + } + + /** + * Tests the method {@link TrackDataHub#start()}. This method would also cover + * some logic of {@link TrackDataHub#loadDataForAllListeners()} and + * {@link TrackDataHub#notifyHeadingChanged(float)}. + */ + public void testRegisterHeadingListener() { + dataSource.registerOnSharedPreferenceChangeListener(capture(preferenceChangeListenerCapture)); + Capture compassListener = new Capture(); + dataSource.registerHeadingListener(capture(compassListener)); + trackDataListener1.onHeadingChanged(capture(new Capture())); replay(); - - hub.unregisterTrackDataListener(listener1); - hub.loadTrack(TRACK_ID + 1); - + trackDataHub.start(); + trackDataHub.registerTrackDataListener(trackDataListener1, EnumSet.of(TrackDataType.HEADING)); verifyAndReset(); } - public void testPointsListen_beforeStart() { - + /** + * Tests the method {@link TrackDataHub#notifyPreferenceChanged(String)} when + * the key is R.string.min_required_accuracy_key. + */ + public void testNotifyPreferenceChanged_minRequiredAccuracy() { + int value = 1; + PreferencesUtils.setInt(context, R.string.min_required_accuracy_key, value); + trackDataHub.notifyPreferenceChanged(PreferencesUtils + .getKey(context, R.string.min_required_accuracy_key)); + assertEquals(value, trackDataHub.getMinRequiredAccuracy()); + PreferencesUtils.setInt(context, R.string.min_required_accuracy_key, value + 1); + trackDataHub.notifyPreferenceChanged(PreferencesUtils + .getKey(context, R.string.min_required_accuracy_key)); + assertEquals(value + 1, trackDataHub.getMinRequiredAccuracy()); } - - public void testPointsListen_reRegister() { - Capture observerCapture = new Capture(); - - expectStart(); - dataSources.registerContentObserver( - eq(TrackPointsColumns.CONTENT_URI), eq(false), 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(ListenerDataType.POINT_UPDATES)); - - verifyAndReset(); - - // Unregister - ContentObserver observer = observerCapture.getValue(); - dataSources.unregisterContentObserver(observer); - - replay(); - - hub.unregisterTrackDataListener(listener1); - - verifyAndReset(); - - // Register again, except only points since unregistered. - dataSources.registerContentObserver( - eq(TrackPointsColumns.CONTENT_URI), eq(false), 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(ListenerDataType.POINT_UPDATES)); - - verifyAndReset(); + + /** + * Tests the method {@link TrackDataHub#notifyPreferenceChanged(String)} when + * the key is R.string.metric_units_key. + */ + public void testNotifyPreferenceChanged_metricUnitsNoNotify() { + boolean value = false; + PreferencesUtils.setBoolean(context, R.string.metric_units_key, value); + trackDataHub.notifyPreferenceChanged(PreferencesUtils + .getKey(context, R.string.metric_units_key)); + assertEquals(value, trackDataHub.isMetricUnits()); + PreferencesUtils.setBoolean(context, R.string.metric_units_key, !value); + trackDataHub.notifyPreferenceChanged(PreferencesUtils + .getKey(context, R.string.metric_units_key)); + assertEquals(!value, trackDataHub.isMetricUnits()); } - - public void testPointsListen_reRegisterTrackChanged() { - Capture observerCapture = new Capture(); - - expectStart(); - dataSources.registerContentObserver( - eq(TrackPointsColumns.CONTENT_URI), eq(false), 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(ListenerDataType.POINT_UPDATES)); - - verifyAndReset(); - - // Unregister - ContentObserver observer = observerCapture.getValue(); - dataSources.unregisterContentObserver(observer); - - replay(); - - hub.unregisterTrackDataListener(listener1); - - verifyAndReset(); - - // Register again after track changed, expect all points. - dataSources.registerContentObserver( - eq(TrackPointsColumns.CONTENT_URI), eq(false), 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(ListenerDataType.POINT_UPDATES)); - - verifyAndReset(); + + + /** + * Tests the method {@link TrackDataHub#notifyPreferenceChanged(String)} when + * the key is R.string.metric_units_key. + */ + public void testNotifyPreferenceChanged_reportSpeedNoNotify() { + boolean value = false; + PreferencesUtils.setBoolean(context, R.string.report_speed_key, value); + trackDataHub.notifyPreferenceChanged(PreferencesUtils + .getKey(context, R.string.report_speed_key)); + assertEquals(value, trackDataHub.isReportSpeed()); + PreferencesUtils.setBoolean(context, R.string.report_speed_key, !value); + trackDataHub.notifyPreferenceChanged(PreferencesUtils + .getKey(context, R.string.report_speed_key)); + assertEquals(!value, trackDataHub.isReportSpeed()); } - - public void testPointsListen_largeTrackSampling() { - Capture observerCapture = new Capture(); - - expectStart(); - dataSources.registerContentObserver( - eq(TrackPointsColumns.CONTENT_URI), eq(false), 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(ListenerDataType.POINT_UPDATES)); - hub.registerTrackDataListener(listener2, - EnumSet.of(ListenerDataType.POINT_UPDATES, ListenerDataType.SAMPLED_OUT_POINT_UPDATES)); - hub.start(); - - verifyAndReset(); - } - - public void testPointsListen_resampling() { - Capture observerCapture = new Capture(); - - expectStart(); - dataSources.registerContentObserver( - eq(TrackPointsColumns.CONTENT_URI), eq(false), 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(ListenerDataType.POINT_UPDATES)); - - 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); - - Sensor compass = newSensor(); - expect(dataSources.getSensor(Sensor.TYPE_ORIENTATION)).andReturn(compass); - Capture listenerCapture = new Capture(); - dataSources.registerSensorListener(capture(listenerCapture), same(compass), anyInt()); - - Capture locationListenerCapture = new Capture(); - dataSources.requestLocationUpdates(capture(locationListenerCapture)); - - SensorEvent event = newSensorEvent(); - event.sensor = compass; - - // 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(ListenerDataType.COMPASS_UPDATES, ListenerDataType.LOCATION_UPDATES)); - 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); - - Capture listenerCapture = - new Capture(); - dataSources.registerOnSharedPreferenceChangeListener(capture(listenerCapture)); - - 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(ListenerDataType.DISPLAY_PREFERENCES)); - hub.start(); - hub.registerTrackDataListener(listener2, EnumSet.of(ListenerDataType.DISPLAY_PREFERENCES)); - - 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 = listenerCapture.getValue(); - listener.onSharedPreferenceChanged( - sharedPreferences, PreferencesUtils.getKey(context, R.string.report_speed_key)); - - AndroidMock.verify(dataSources, providerUtils, listener1, listener2); - AndroidMock.reset(dataSources, 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() { - dataSources.registerOnSharedPreferenceChangeListener(capture(preferenceListenerCapture)); - } - - private void replay() { - AndroidMock.replay(dataSources, providerUtils, listener1, listener2); - } - - private void verifyAndReset() { - AndroidMock.verify(listener1, listener2, dataSources, providerUtils); - AndroidMock.reset(listener1, listener2, dataSources, providerUtils); + + /** + * Tests the method {@link TrackDataHub#notifyPreferenceChanged(String)} when + * the key is R.string.selected_track_id_key and no listener is registered. + */ + public void testNotifyPreferenceChanged_trackIdNoListener() { + long value = 1; + PreferencesUtils.setLong(context, R.string.selected_track_id_key, value); + trackDataHub.notifyPreferenceChanged(PreferencesUtils + .getKey(context, R.string.selected_track_id_key)); + assertEquals(value, trackDataHub.getSelectedTrackId()); + PreferencesUtils.setLong(context, R.string.selected_track_id_key, value + 1); + trackDataHub.notifyPreferenceChanged(PreferencesUtils + .getKey(context, R.string.selected_track_id_key)); + assertEquals(value + 1, trackDataHub.getSelectedTrackId()); } } 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 9bd1b1b89..c33bb6931 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/endtoendtest/CreateAndSendTrackTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/endtoendtest/CreateAndSendTrackTest.java @@ -23,7 +23,6 @@ import android.app.Instrumentation; import android.test.ActivityInstrumentationTestCase2; import android.view.KeyEvent; import android.widget.CheckBox; -import android.widget.EditText; import java.util.ArrayList; @@ -104,6 +103,8 @@ public class CreateAndSendTrackTest extends ActivityInstrumentationTestCase2 editTexts = EndToEndTestUtils.SOLO.getCurrentEditTexts(); - - EndToEndTestUtils.SOLO.enterText(editTexts.get(0), newTrackName); - EndToEndTestUtils.SOLO.enterText(editTexts.get(1), newType); - // In landscape, there are only two visible edit texts. - if (editTexts.size() > 2) { - EndToEndTestUtils.SOLO.enterText(editTexts.get(2), newDesc); - } else { - EndToEndTestUtils.SOLO.scrollDown(); - editTexts = EndToEndTestUtils.SOLO.getCurrentEditTexts(); - EndToEndTestUtils.SOLO.enterText(editTexts.get(editTexts.size() - 1), newDesc); - } + + EndToEndTestUtils.enterTextAvoidSoftKeyBoard(0, newTrackName); + EndToEndTestUtils.enterTextAvoidSoftKeyBoard(1, newType); + EndToEndTestUtils.enterTextAvoidSoftKeyBoard(2, newDesc); EndToEndTestUtils.SOLO.clickOnButton(activityMyTracks.getString(R.string.generic_save)); instrumentation.waitForIdleSync(); // Go back to track list. @@ -169,6 +177,23 @@ public class CreateAndSendTrackTest extends ActivityInstrumentationTestCase2 0); + } else { + assertTrue(EndToEndTestUtils.SOLO.getCurrentListViews().get(0).getCount() == 0); + } + EndToEndTestUtils.SOLO.goBack(); + + EndToEndTestUtils.stopRecording(true); } /** @@ -243,15 +299,13 @@ public class CreateAndSendTrackTest extends ActivityInstrumentationTestCase2 RELATIVE_STARTTIME_POSTFIX_MULTILINGUAL = new HashMap(); - private static final HashMap VIEW_MODE_ENGLISH_MULTILINGUAL = new HashMap(); + private static final HashMap VIEW_MODE_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 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"); + RELATIVE_STARTTIME_POSTFIX_MULTILINGUAL.put("fr", "minute"); + RELATIVE_STARTTIME_POSTFIX_MULTILINGUAL.put("ar", "دقيقة"); + RELATIVE_STARTTIME_POSTFIX_MULTILINGUAL.put("zh", "分钟前"); + + VIEW_MODE_MULTILINGUAL.put("es", "mode"); + VIEW_MODE_MULTILINGUAL.put("de", "modus"); + VIEW_MODE_MULTILINGUAL.put("fr", "Mode"); + VIEW_MODE_MULTILINGUAL.put("ar", "وضع"); + VIEW_MODE_MULTILINGUAL.put("zh", "模式"); + + KM_MULTILINGUAL.put("es", "km"); + KM_MULTILINGUAL.put("de", "km"); + KM_MULTILINGUAL.put("fr", "km"); + KM_MULTILINGUAL.put("ar", "كم"); + KM_MULTILINGUAL.put("zh", "公里"); + + 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; static Instrumentation instrumentation; @@ -82,32 +127,35 @@ public class EndToEndTestUtils { // Android OS. static boolean hasActionBar = false; static boolean isEmulator = true; + static boolean hasGpsSingal = true; static boolean isCheckedFirstLaunch = false; + public static final String LOG_TAG = "MyTracksTest"; private EndToEndTestUtils() {} + /** + * Checks the language, then sets the fields with right string. + */ private static void checkLanguage() { - RELATIVE_STARTTIME_POSTFIX_MULTILINGUAL.put("es", "mins ago"); - RELATIVE_STARTTIME_POSTFIX_MULTILINGUAL.put("de", "Minuten"); - RELATIVE_STARTTIME_POSTFIX_MULTILINGUAL.put("fr", "minute"); - RELATIVE_STARTTIME_POSTFIX_MULTILINGUAL.put("ar", "دقيقة"); - RELATIVE_STARTTIME_POSTFIX_MULTILINGUAL.put("zh", "分钟前"); - - VIEW_MODE_ENGLISH_MULTILINGUAL.put("es", "mode"); - VIEW_MODE_ENGLISH_MULTILINGUAL.put("de", "modus"); - VIEW_MODE_ENGLISH_MULTILINGUAL.put("fr", "Mode"); - VIEW_MODE_ENGLISH_MULTILINGUAL.put("ar", "وضع"); - VIEW_MODE_ENGLISH_MULTILINGUAL.put("zh", "模式"); + Locale locale = null; + Configuration config=null; + config = activityMytracks.getBaseContext().getResources().getConfiguration(); + locale = new Locale("en"); + Locale.setDefault(locale); + config.locale = locale; - // Check the language, test in Chinese or English. - 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_ENGLISH_MULTILINGUAL.get(deviceLanguage); + VIEW_MODE = VIEW_MODE_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_ENGLISH_MULTILINGUAL.get("es"); + VIEW_MODE = VIEW_MODE_MULTILINGUAL.get("es"); + KM = KM_MULTILINGUAL.get("es"); + MILE = MILE_MULTILINGUAL.get("es"); } } @@ -128,7 +176,7 @@ public class EndToEndTestUtils { PrintStream out = null; Socket socket = null; try { - socket = new Socket(ANDROID_LOCAL_IP, ANDROID_LOCAL_PORT); + socket = new Socket(ANDROID_LOCAL_IP, emulatorPort); out = new PrintStream(socket.getOutputStream()); float longitude = START_LONGITUDE; float latitude = START_LATITUDE; @@ -138,6 +186,8 @@ public class EndToEndTestUtils { longitude += DELTA_LONGITUDE; latitude += DELTA_LADITUDE; } + // Wait the GPS signal can be obtained by MyTracks. + Thread.sleep(SHORT_WAIT_TIME); } catch (UnknownHostException e) { System.exit(-1); } catch (IOException e) { @@ -166,27 +216,39 @@ public class EndToEndTestUtils { */ static void setupForAllTest(Instrumentation instrumentation, TrackListActivity activityMyTracks) { setIsEmulator(); + EndToEndTestUtils.instrumentation = instrumentation; EndToEndTestUtils.activityMytracks = activityMyTracks; - EndToEndTestUtils.SOLO = new Solo(EndToEndTestUtils.instrumentation, + SOLO = new Solo(EndToEndTestUtils.instrumentation, EndToEndTestUtils.activityMytracks); + + // For emulator, we would fix GPS signal. + if(!isEmulator) { + GoToMyLocationTest.findAndClickMyLocation(activityMyTracks); + hasGpsSingal = !SOLO.waitForText(NO_GPS_MESSAGE_PREFIX, 1, + EndToEndTestUtils.SHORT_WAIT_TIME); + SOLO.goBack(); + } + // Check if open MyTracks first time after install. If so, there would be a // welcome view with accept buttons. And makes sure only check once. - if (!EndToEndTestUtils.isCheckedFirstLaunch) { - if ((EndToEndTestUtils.getButtonOnScreen(EndToEndTestUtils.activityMytracks + if (!isCheckedFirstLaunch) { + if ((getButtonOnScreen(EndToEndTestUtils.activityMytracks .getString(R.string.eula_accept), false, false) != null)) { - EndToEndTestUtils.verifyFirstLaunch(); - } else if (EndToEndTestUtils.SOLO.waitForText( + verifyFirstLaunch(); + } else if (SOLO.waitForText( // After reset setting, welcome page will show again. - activityMytracks.getString(R.string.welcome_title), 0, 500)) { + activityMytracks.getString(R.string.welcome_title), 0, SHORT_WAIT_TIME)) { resetPreferredUnits(); } + hasActionBar = setHasActionBar(); checkLanguage(); - EndToEndTestUtils.isCheckedFirstLaunch = true; - EndToEndTestUtils.setHasActionBar(); - } else if (EndToEndTestUtils.SOLO.waitForText( + isCheckedFirstLaunch = true; + deleteAllTracks(); + resetAllSettings(activityMyTracks, false); + } else if (SOLO.waitForText( // After reset setting, welcome page will show again. - activityMytracks.getString(R.string.welcome_title), 0, 500)) { + activityMytracks.getString(R.string.welcome_title), 0, SHORT_WAIT_TIME)) { resetPreferredUnits(); } @@ -241,7 +303,7 @@ public class EndToEndTestUtils { */ static void createSimpleTrack(int numberOfGpsData) { startRecording(); - EndToEndTestUtils.sendGps(numberOfGpsData); + sendGps(numberOfGpsData); instrumentation.waitForIdleSync(); stopRecording(true); } @@ -276,12 +338,14 @@ public class EndToEndTestUtils { * activity */ static void createTrackIfEmpty(int gpsNumber, boolean showTrackList) { - if (EndToEndTestUtils.isTrackListEmpty(!showTrackList)) { + if (isTrackListEmpty(!showTrackList)) { // Create a simple track. - EndToEndTestUtils.createSimpleTrack(gpsNumber); + createSimpleTrack(gpsNumber); + instrumentation.waitForIdleSync(); if (showTrackList) { - EndToEndTestUtils.SOLO.goBack(); + SOLO.goBack(); } + instrumentation.waitForIdleSync(); } } @@ -290,7 +354,7 @@ public class EndToEndTestUtils { */ static void startRecording() { if (hasActionBar) { - Button startButton = getButtonOnScreen(activityMytracks.getString(R.string.menu_record_track), false, false); + View startButton = getButtonOnScreen(activityMytracks.getString(R.string.menu_record_track), false, false); // In case a track is recording. if (startButton == null) { stopRecording(true); @@ -298,7 +362,7 @@ public class EndToEndTestUtils { } SOLO.clickOnView(startButton); } else { - showMoreMenuItem(); + showMenuItem(); if (!SOLO.searchText(activityMytracks.getString(R.string.menu_record_track))) { // Check if in TrackDetailActivity. if (SOLO.searchText(activityMytracks.getString(R.string.menu_play))) { @@ -306,12 +370,13 @@ public class EndToEndTestUtils { } else { // In case a track is recording. stopRecording(true); - showMoreMenuItem(); + showMenuItem(); } } instrumentation.waitForIdleSync(); SOLO.clickOnText(activityMytracks.getString(R.string.menu_record_track)); } + instrumentation.waitForIdleSync(); } /** @@ -324,7 +389,7 @@ public class EndToEndTestUtils { return getButtonOnScreen(activityMytracks .getString(R.string.menu_record_track), false, false) == null; } - showMoreMenuItem(); + showMenuItem(); if (SOLO.searchText(activityMytracks.getString(R.string.menu_record_track)) || SOLO.searchText(activityMytracks.getString(R.string.menu_play))) { SOLO.goBack(); @@ -343,22 +408,21 @@ public class EndToEndTestUtils { if (hasActionBar) { getButtonOnScreen(activityMytracks.getString(R.string.menu_stop_recording), false, true); } else { - showMoreMenuItem(); + showMenuItem(); instrumentation.waitForIdleSync(); SOLO.clickOnText(activityMytracks.getString(R.string.menu_stop_recording)); } if (isSave) { - EndToEndTestUtils.SOLO.waitForText(activityMytracks.getString(R.string.generic_save), 1, 5000); + SOLO.waitForText(activityMytracks.getString(R.string.generic_save), 1, 5000); // Make every track name is unique to make sure every check can be // trusted. - EndToEndTestUtils.trackName = EndToEndTestUtils.TRACK_NAME_PREFIX + trackName = TRACK_NAME_PREFIX + System.currentTimeMillis(); - SOLO.enterText(0, trackName); - if(!EndToEndTestUtils.isEmulator) { - // Close soft keyboard. - EndToEndTestUtils.SOLO.goBack(); - } - SOLO.clickLongOnText(activityMytracks.getString(R.string.generic_save)); + SOLO.sendKey(KeyEvent.KEYCODE_DEL); + enterTextAvoidSoftKeyBoard(0, trackName); + enterTextAvoidSoftKeyBoard(1, DEFAULTACTIVITY); + SOLO.clickOnText(activityMytracks.getString(R.string.generic_save)); + instrumentation.waitForIdleSync(); } } @@ -376,7 +440,16 @@ public class EndToEndTestUtils { } } } - + + /** + * Deletes all tracks. This method should be call when the TracksListActivity + * is shown. + */ + static void deleteAllTracks() { + findMenuItem(activityMytracks.getString(R.string.menu_delete_all), true); + getButtonOnScreen(activityMytracks.getString(R.string.generic_ok), true, true); + } + /** * Gets a kind of exported files. * @@ -417,22 +490,49 @@ public class EndToEndTestUtils { * @param isClick whether click the button if find it * @return the button to search, and null means can not find the button */ - static Button getButtonOnScreen(String buttonName, boolean isWait,boolean isClick) { + static View getButtonOnScreen(String buttonName, boolean isWait,boolean isClick) { + View button = null; + instrumentation.waitForIdleSync(); if (isWait) { SOLO.waitForText(buttonName); } - ArrayList