merge heads with Google Location settings change from My Tracks 2.0.1

This commit is contained in:
Jimmy Shih
2012-08-14 12:32:56 -07:00
100 changed files with 8715 additions and 7279 deletions
+55 -3
View File
@@ -1,4 +1,4 @@
#Fri Oct 21 20:06:28 BRST 2011 #Tue Jul 17 10:23:26 PDT 2012
eclipse.preferences.version=1 eclipse.preferences.version=1
org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 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.problem.varargsArgumentNeedCast=warning
org.eclipse.jdt.core.compiler.source=1.6 org.eclipse.jdt.core.compiler.source=1.6
org.eclipse.jdt.core.formatter.align_type_members_on_columns=false 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=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=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=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=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=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=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_assignment=0
org.eclipse.jdt.core.formatter.alignment_for_binary_expression=16 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_compact_if=16
org.eclipse.jdt.core.formatter.alignment_for_conditional_expression=80 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=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=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_method_declaration=0
org.eclipse.jdt.core.formatter.alignment_for_multiple_fields=16 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=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=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_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=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_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=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=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=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=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.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_imports=1
org.eclipse.jdt.core.formatter.blank_lines_after_package=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_block_boundaries=true
org.eclipse.jdt.core.formatter.comment.new_lines_at_javadoc_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.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.compact_else_if=true
org.eclipse.jdt.core.formatter.continuation_indentation=2 org.eclipse.jdt.core.formatter.continuation_indentation=2
org.eclipse.jdt.core.formatter.continuation_indentation_for_array_initializer=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.disabling_tag=@formatter\:off
org.eclipse.jdt.core.formatter.enabling_tag=@formatter\:on 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.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_annotation_declaration_header=true
org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_constant_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_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_arguments=insert
org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_parameters=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_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_parameterized_type_reference=do not insert
org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_arguments=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.join_wrapped_lines=true
org.eclipse.jdt.core.formatter.keep_else_statement_on_same_line=false 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_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.keep_then_statement_on_same_line=false
org.eclipse.jdt.core.formatter.lineSplit=100 org.eclipse.jdt.core.formatter.lineSplit=100
org.eclipse.jdt.core.formatter.never_indent_block_comments_on_first_column=false 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_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.number_of_empty_lines_to_preserve=1
org.eclipse.jdt.core.formatter.put_empty_statement_on_new_line=true 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.char=space
org.eclipse.jdt.core.formatter.tabulation.size=2 org.eclipse.jdt.core.formatter.tabulation.size=2
org.eclipse.jdt.core.formatter.use_on_off_tags=false 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.use_tabs_only_for_leading_indentations=false
org.eclipse.jdt.core.formatter.wrap_before_binary_operator=true 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_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_outer_expressions_when_nested=true
org.eclipse.jdt.core.formatter.wrap_prefer_two_fragments=false
+3 -3
View File
@@ -1,8 +1,8 @@
#Thu Jul 21 03:04:13 BRT 2011 #Tue Jul 17 10:22:06 PDT 2012
eclipse.preferences.version=1 eclipse.preferences.version=1
editor_save_participant_org.eclipse.jdt.ui.postsavelistener.cleanup=true editor_save_participant_org.eclipse.jdt.ui.postsavelistener.cleanup=true
formatter_profile=_MyTracks formatter_profile=_My Tracks
formatter_settings_version=12 formatter_settings_version=13
org.eclipse.jdt.ui.ignorelowercasenames=true org.eclipse.jdt.ui.ignorelowercasenames=true
org.eclipse.jdt.ui.importorder=com;android;java;javax;org; org.eclipse.jdt.ui.importorder=com;android;java;javax;org;
org.eclipse.jdt.ui.ondemandthreshold=99 org.eclipse.jdt.ui.ondemandthreshold=99
@@ -81,9 +81,6 @@ public class ChartView extends View {
private static final int SPACER = 4; private static final int SPACER = 4;
private static final int Y_AXIS_OFFSET = 16; 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 ChartValueSeries[] series = new ChartValueSeries[NUM_SERIES];
private final ArrayList<double[]> chartData = new ArrayList<double[]>(); private final ArrayList<double[]> chartData = new ArrayList<double[]>();
private final ArrayList<Waypoint> waypoints = new ArrayList<Waypoint>(); private final ArrayList<Waypoint> waypoints = new ArrayList<Waypoint>();
@@ -559,8 +556,8 @@ public class ChartView extends View {
float x = getX(getWaypointXValue(waypoint)); float x = getX(getWaypointXValue(waypoint));
canvas.drawLine( canvas.drawLine(
x, topBorder + spacer + markerHeight / 2, x, topBorder + effectiveHeight, markerPaint); x, topBorder + spacer + markerHeight / 2, x, topBorder + effectiveHeight, markerPaint);
canvas.translate( canvas.translate(x - (float) (markerWidth * Constants.WAYPOINT_X_OFFSET_PERCENTAGE),
x - (float) (markerWidth * MARKER_PIN_X_POSITION_PERCENTAGE), topBorder + spacer); topBorder + spacer);
if (waypoints.get(i).getType() == Waypoint.TYPE_STATISTICS) { if (waypoints.get(i).getType() == Waypoint.TYPE_STATISTICS) {
statisticsMarker.draw(canvas); statisticsMarker.draw(canvas);
} else { } else {
@@ -126,6 +126,10 @@ public abstract class Constants {
public static final String SETTINGS_NAME = "SettingsActivity"; 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. * This is an abstract utility class.
*/ */
@@ -13,6 +13,7 @@
* License for the specific language governing permissions and limitations under * License for the specific language governing permissions and limitations under
* the License. * the License.
*/ */
package com.google.android.apps.mytracks; package com.google.android.apps.mytracks;
import static com.google.android.apps.mytracks.Constants.TAG; 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.content.Waypoint;
import com.google.android.apps.mytracks.maps.TrackPathPainter; import com.google.android.apps.mytracks.maps.TrackPathPainter;
import com.google.android.apps.mytracks.maps.TrackPathPainterFactory; 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.IntentUtils;
import com.google.android.apps.mytracks.util.LocationUtils; import com.google.android.apps.mytracks.util.LocationUtils;
import com.google.android.apps.mytracks.util.PreferencesUtils; import com.google.android.apps.mytracks.util.PreferencesUtils;
@@ -30,6 +31,7 @@ import com.google.android.maps.MapView;
import com.google.android.maps.Overlay; import com.google.android.maps.Overlay;
import com.google.android.maps.Projection; import com.google.android.maps.Projection;
import com.google.android.maps.mytracks.R; import com.google.android.maps.mytracks.R;
import com.google.common.annotations.VisibleForTesting;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
@@ -50,46 +52,46 @@ import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue; import java.util.concurrent.BlockingQueue;
/** /**
* A map overlay that displays a "MyLocation" arrow, an error circle, the * A map overlay that displays my location arrow, error circle, and track info.
* currently recording track and optionally a selected track.
* *
* @author Leif Hendrik Wilden * @author Leif Hendrik Wilden
*/ */
public class MapOverlay extends Overlay implements OnSharedPreferenceChangeListener { 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 Context context;
private final List<Waypoint> waypoints; private final List<Waypoint> waypoints;
private final List<CachedLocation> points; private final List<CachedLocation> points;
private final BlockingQueue<CachedLocation> pendingPoints; private final BlockingQueue<CachedLocation> 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 boolean trackDrawingEnabled;
private int lastHeading = 0;
private Location myLocation;
private boolean showEndMarker = true; private boolean showEndMarker = true;
// TODO: Remove it completely after completing performance tests. private int headingIndex = 0;
private boolean alwaysVisible = true; private Location myLocation;
private GeoPoint lastReferencePoint; private GeoPoint lastReferencePoint;
private Rect lastViewRect; private Rect lastViewRect;
private boolean lastPathExists;
private TrackPathPainter trackPathPainter;
/** /**
* Represents a pre-processed {@code Location} to speed up drawing. * A pre-processed {@link Location} to speed up drawing.
* This class is more like a data object and doesn't provide accessors. *
* @author Jimmy Shih
*/ */
public static class CachedLocation { public static class CachedLocation {
public final boolean valid;
public final GeoPoint geoPoint; private final boolean valid;
public final int speed; private final GeoPoint geoPoint;
private final int speed;
/** /**
* Constructor for an invalid cached location. * 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.geoPoint = valid ? LocationUtils.getGeoPoint(location) : null;
this.speed = (int) Math.floor(location.getSpeed() * UnitConversions.MS_TO_KMH); 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) { public MapOverlay(Context context) {
this.context = context; this.context = context;
this.waypoints = new ArrayList<Waypoint>(); this.waypoints = new ArrayList<Waypoint>();
this.points = new ArrayList<CachedLocation>(1024); this.points = new ArrayList<CachedLocation>(1024);
this.pendingPoints = new ArrayBlockingQueue<CachedLocation>( this.pendingPoints = new ArrayBlockingQueue<CachedLocation>(
Constants.MAX_DISPLAYED_TRACK_POINTS, true); 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(); final Resources resources = context.getResources();
arrows = new Drawable[] { arrows = new Drawable[] { resources.getDrawable(R.drawable.arrow_0),
resources.getDrawable(R.drawable.arrow_0), resources.getDrawable(R.drawable.arrow_20), resources.getDrawable(R.drawable.arrow_40),
resources.getDrawable(R.drawable.arrow_20), resources.getDrawable(R.drawable.arrow_60), resources.getDrawable(R.drawable.arrow_80),
resources.getDrawable(R.drawable.arrow_40), resources.getDrawable(R.drawable.arrow_100), resources.getDrawable(R.drawable.arrow_120),
resources.getDrawable(R.drawable.arrow_60), resources.getDrawable(R.drawable.arrow_140), resources.getDrawable(R.drawable.arrow_160),
resources.getDrawable(R.drawable.arrow_80), resources.getDrawable(R.drawable.arrow_180), resources.getDrawable(R.drawable.arrow_200),
resources.getDrawable(R.drawable.arrow_100), resources.getDrawable(R.drawable.arrow_220), resources.getDrawable(R.drawable.arrow_240),
resources.getDrawable(R.drawable.arrow_120), resources.getDrawable(R.drawable.arrow_260), resources.getDrawable(R.drawable.arrow_280),
resources.getDrawable(R.drawable.arrow_140), resources.getDrawable(R.drawable.arrow_300), resources.getDrawable(R.drawable.arrow_320),
resources.getDrawable(R.drawable.arrow_160), resources.getDrawable(R.drawable.arrow_340) };
resources.getDrawable(R.drawable.arrow_180), arrowWidth = arrows[headingIndex].getIntrinsicWidth();
resources.getDrawable(R.drawable.arrow_200), arrowHeight = arrows[headingIndex].getIntrinsicHeight();
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();
for (Drawable arrow : arrows) { for (Drawable arrow : arrows) {
arrow.setBounds(0, 0, arrowWidth, arrowHeight); arrow.setBounds(0, 0, arrowWidth, arrowHeight);
} }
@@ -152,16 +166,16 @@ public class MapOverlay extends Overlay implements OnSharedPreferenceChangeListe
markerHeight = statsMarker.getIntrinsicHeight(); markerHeight = statsMarker.getIntrinsicHeight();
statsMarker.setBounds(0, 0, markerWidth, markerHeight); 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 = resources.getDrawable(R.drawable.green_dot);
startMarker.setBounds(0, 0, markerWidth, markerHeight); startMarker.setBounds(0, 0, markerWidth, markerHeight);
endMarker = resources.getDrawable(R.drawable.red_dot); endMarker = resources.getDrawable(R.drawable.red_dot);
endMarker.setBounds(0, 0, markerWidth, markerHeight); endMarker.setBounds(0, 0, markerWidth, markerHeight);
waypointMarker = resources.getDrawable(R.drawable.blue_pushpin); errorCirclePaint = TrackPathUtils.getPaint(context, R.color.blue);
waypointMarker.setBounds(0, 0, markerWidth, markerHeight);
errorCirclePaint = TrackPathUtilities.getPaint(R.color.blue, context);
errorCirclePaint.setAlpha(127); errorCirclePaint.setAlpha(127);
trackPathPainter = TrackPathPainterFactory.getTrackPathPainter(context); trackPathPainter = TrackPathPainterFactory.getTrackPathPainter(context);
@@ -172,16 +186,16 @@ public class MapOverlay extends Overlay implements OnSharedPreferenceChangeListe
/** /**
* Add a location to the map overlay. * Add a location to the map overlay.
* * <p>
* NOTE: This method doesn't take ownership of the given location, so it is * NOTE: This method doesn't take ownership of the given location, so it is
* safe to reuse the same location while calling this method. * 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) { public void addLocation(Location location) {
// Queue up in the pending queue until it's merged with {@code #points}. // Queue up in the pendingPoints until it's merged with points.
if (!pendingPoints.offer(new CachedLocation(l))) { if (!pendingPoints.offer(new CachedLocation(location))) {
Log.e(TAG, "Unable to add pending points"); 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. * Adds a segment split to the map overlay.
*/ */
public void addSegmentSplit() { public void addSegmentSplit() {
// Queue up in the pendingPoints until it's merged with points.
if (!pendingPoints.offer(new CachedLocation())) { 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. * Clears the locations.
if (wpt != null && wpt.getLocation() != null) { */
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) { synchronized (waypoints) {
waypoints.add(wpt); waypoints.add(waypoint);
} }
} }
} }
public int getNumLocations() { /**
synchronized (points) { * Clears the waypoints.
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();
}
}
public void clearWaypoints() { public void clearWaypoints() {
synchronized (waypoints) { synchronized (waypoints) {
waypoints.clear(); waypoints.clear();
} }
} }
/**
* Sets whether to draw the track or not.
*
* @param trackDrawingEnabled true to draw track
*/
public void setTrackDrawingEnabled(boolean trackDrawingEnabled) { public void setTrackDrawingEnabled(boolean trackDrawingEnabled) {
this.trackDrawingEnabled = 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) { public void setShowEndMarker(boolean showEndMarker) {
this.showEndMarker = 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 @Override
public void draw(Canvas canvas, MapView mapView, boolean shadow) { public void draw(Canvas canvas, MapView mapView, boolean shadow) {
if (shadow) { if (shadow) {
return; return;
} }
// It's safe to keep projection within a single draw operation. // It's safe to keep projection within a single draw operation
final Projection projection = getMapProjection(mapView); Projection projection = getMapProjection(mapView);
if (projection == null) { if (projection == null) {
Log.w(TAG, "No projection, unable to draw"); Log.w(TAG, "No projection, unable to draw.");
return; return;
} }
// Get the current viewing window.
if (trackDrawingEnabled) { if (trackDrawingEnabled) {
// Get the current viewing Rect
Rect viewRect = getMapViewRect(mapView); Rect viewRect = getMapViewRect(mapView);
// Draw the selected track: // Draw the selected track
drawTrack(canvas, projection, viewRect); drawTrack(canvas, projection, viewRect);
// Draw the "Start" and "End" markers: // Draw the start and end markers
drawMarkers(canvas, projection); drawMarkers(canvas, projection);
// Draw the waypoints: // Draw the waypoints
drawWaypoints(canvas, projection); drawWaypoints(canvas, projection);
} }
@@ -271,164 +329,19 @@ public class MapOverlay extends Overlay implements OnSharedPreferenceChangeListe
drawMyLocation(canvas, projection); 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 @Override
public boolean onTap(GeoPoint p, MapView mapView) { public boolean onTap(GeoPoint geoPoint, MapView mapView) {
if (p.equals(mapView.getMapCenter())) { 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 * There is (unfortunately) no good way to determine whether the tap was
// location is equal to the map center,then it was a track ball press with * caused by an actual tap on the screen or the track ball. If the
// very high likelihood. * location is equal to the map center,then it was a track ball press with
* very high likelihood.
*/
return false; return false;
} }
final Location tapLocation = LocationUtils.getLocation(geoPoint);
final Location tapLocation = LocationUtils.getLocation(p); double minDistance = Double.MAX_VALUE;
double dmin = Double.MAX_VALUE;
Waypoint waypoint = null; Waypoint waypoint = null;
synchronized (waypoints) { synchronized (waypoints) {
for (int i = 0; i < waypoints.size(); i++) { for (int i = 0; i < waypoints.size(); i++) {
@@ -436,35 +349,231 @@ public class MapOverlay extends Overlay implements OnSharedPreferenceChangeListe
if (waypointLocation == null) { if (waypointLocation == null) {
continue; continue;
} }
final double d = waypointLocation.distanceTo(tapLocation); final double distance = waypointLocation.distanceTo(tapLocation);
if (d < dmin) { if (distance < minDistance) {
dmin = d; minDistance = distance;
waypoint = waypoints.get(i); 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) Intent intent = IntentUtils.newIntent(context, MarkerDetailActivity.class)
.putExtra(MarkerDetailActivity.EXTRA_MARKER_ID, waypoint.getId()); .putExtra(MarkerDetailActivity.EXTRA_MARKER_ID, waypoint.getId());
context.startActivity(intent); context.startActivity(intent);
return true; return true;
} }
return super.onTap(p, mapView); return super.onTap(geoPoint, mapView);
}
/**
* @return the points
*/
public List<CachedLocation> getPoints() {
return points;
} }
@Override @Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { 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)) { if (PreferencesUtils.getKey(context, R.string.track_color_mode_key).equals(key)) {
trackPathPainter = TrackPathPainterFactory.getTrackPathPainter(context); trackPathPainter = TrackPathPainterFactory.getTrackPathPainter(context);
} }
} }
/**
* Gets the points.
*/
@VisibleForTesting
public List<CachedLocation> 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;
}
} }
@@ -165,7 +165,7 @@ public class MarkerEditActivity extends AbstractMyTracksActivity {
waypointName.getText().toString(), waypointName.getText().toString(),
waypointMarkerType.getText().toString(), waypointMarkerType.getText().toString(),
waypointDescription.getText().toString(), waypointDescription.getText().toString(),
getString(R.string.marker_waypoint_icon_url)); null);
TrackRecordingServiceConnectionUtils.addMarker( TrackRecordingServiceConnectionUtils.addMarker(
this, trackRecordingServiceConnection, waypointCreationRequest); this, trackRecordingServiceConnection, waypointCreationRequest);
} }
@@ -15,7 +15,6 @@
*/ */
package com.google.android.apps.mytracks; 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.services.RemoveTempFilesService;
import com.google.android.apps.mytracks.util.AnalyticsUtils; import com.google.android.apps.mytracks.util.AnalyticsUtils;
import com.google.android.apps.mytracks.util.ApiAdapterFactory; import com.google.android.apps.mytracks.util.ApiAdapterFactory;
@@ -31,8 +30,6 @@ import android.content.Intent;
*/ */
public class MyTracksApplication extends Application { public class MyTracksApplication extends Application {
private TrackDataHub trackDataHub;
@Override @Override
public void onCreate() { public void onCreate() {
super.onCreate(); super.onCreate();
@@ -43,16 +40,4 @@ public class MyTracksApplication extends Application {
Intent intent = new Intent(this, RemoveTempFilesService.class); Intent intent = new Intent(this, RemoveTempFilesService.class);
startService(intent); 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;
}
} }
@@ -323,9 +323,10 @@ public class SearchListActivity extends AbstractMyTracksActivity implements Dele
String textQuery = intent.getStringExtra(SearchManager.QUERY); String textQuery = intent.getStringExtra(SearchManager.QUERY);
setTitle(textQuery); setTitle(textQuery);
Location currentLocation = myTracksLocationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER); Location currentLocation = myTracksLocationManager.getLastKnownLocation(
final SearchQuery query = new SearchQuery(textQuery, currentLocation, -1L, System LocationManager.GPS_PROVIDER);
.currentTimeMillis()); final SearchQuery query = new SearchQuery(
textQuery, currentLocation, -1L, System.currentTimeMillis());
new Thread() { new Thread() {
@Override @Override
public void run() { public void run() {
@@ -137,8 +137,7 @@ public class TrackDetailActivity extends AbstractMyTracksActivity implements Del
getSharedPreferences(Constants.SETTINGS_NAME, Context.MODE_PRIVATE) getSharedPreferences(Constants.SETTINGS_NAME, Context.MODE_PRIVATE)
.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener); .registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
trackRecordingServiceConnection = new TrackRecordingServiceConnection(this, null); trackRecordingServiceConnection = new TrackRecordingServiceConnection(this, null);
trackDataHub = ((MyTracksApplication) getApplication()).getTrackDataHub(); trackDataHub = TrackDataHub.newInstance(this);
trackDataHub.loadTrack(trackId);
mapViewContainer = getLayoutInflater().inflate(R.layout.map, null); mapViewContainer = getLayoutInflater().inflate(R.layout.map, null);
ApiAdapterFactory.getApiAdapter().disableHardwareAccelerated(mapViewContainer); ApiAdapterFactory.getApiAdapter().disableHardwareAccelerated(mapViewContainer);
@@ -167,7 +166,6 @@ public class TrackDetailActivity extends AbstractMyTracksActivity implements Del
public void onNewIntent(Intent intent) { public void onNewIntent(Intent intent) {
setIntent(intent); setIntent(intent);
handleIntent(intent); handleIntent(intent);
trackDataHub.loadTrack(trackId);
showMarker(); showMarker();
} }
@@ -180,6 +178,7 @@ public class TrackDetailActivity extends AbstractMyTracksActivity implements Del
@Override @Override
protected void onResume() { protected void onResume() {
super.onResume(); super.onResume();
trackDataHub.loadTrack(trackId);
TrackRecordingServiceConnectionUtils.resume(this, trackRecordingServiceConnection); TrackRecordingServiceConnectionUtils.resume(this, trackRecordingServiceConnection);
setTitle(trackId == PreferencesUtils.getLong(this, R.string.recording_track_id_key)); 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() { public View getMapViewContainer() {
return mapViewContainer; return mapViewContainer;
} }
/**
* Gets the {@link TrackDataHub}.
*/
public TrackDataHub getTrackDataHub() {
return trackDataHub;
}
/** /**
* Handles the data in the intent. * Handles the data in the intent.
*/ */
@@ -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);
}
}
@@ -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);
}
@@ -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; package com.google.android.apps.mytracks.content;
import static com.google.android.apps.mytracks.Constants.TAG; 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;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener; import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
@@ -10,7 +26,6 @@ import android.database.ContentObserver;
import android.hardware.Sensor; import android.hardware.Sensor;
import android.hardware.SensorEvent; import android.hardware.SensorEvent;
import android.hardware.SensorEventListener; import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.location.Location; import android.location.Location;
import android.location.LocationListener; import android.location.LocationListener;
import android.location.LocationManager; import android.location.LocationManager;
@@ -23,87 +38,79 @@ import java.util.EnumSet;
import java.util.Set; 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 * @author Rodrigo Damazio
*/ */
class DataSourceManager { public class DataSourceManager {
/** Single interface for receiving system events that were registered for. */ /**
interface DataSourceListener { * Observer when the tracks table is updated.
void notifyTrackUpdated(); *
void notifyWaypointUpdated(); * @author Jimmy Shih
void notifyPointsUpdated(); */
void notifyPreferenceChanged(String key); private class TracksTableObserver extends ContentObserver {
void notifyLocationProviderEnabled(boolean enabled);
void notifyLocationProviderAvailable(boolean available);
void notifyLocationChanged(Location loc);
void notifyHeadingChanged(float heading);
}
private final DataSourceListener listener; public TracksTableObserver() {
super(handler);
/** Observer for when the tracks table is updated. */
private class TrackObserver extends ContentObserver {
public TrackObserver() {
super(contentHandler);
} }
@Override @Override
public void onChange(boolean selfChange) { public void onChange(boolean selfChange) {
listener.notifyTrackUpdated(); dataSourceListener.notifyTracksTableUpdated();
} }
} }
/** Observer for when the waypoints table is updated. */ /**
private class WaypointObserver extends ContentObserver { * Observer when the waypoints table is updated.
public WaypointObserver() { *
super(contentHandler); * @author Jimmy Shih
*/
private class WaypointsTableObserver extends ContentObserver {
public WaypointsTableObserver() {
super(handler);
} }
@Override @Override
public void onChange(boolean selfChange) { public void onChange(boolean selfChange) {
listener.notifyWaypointUpdated(); dataSourceListener.notifyWaypointsTableUpdated();
} }
} }
/** Observer for when the points table is updated. */ /**
private class PointObserver extends ContentObserver { * Observer when the track points table is updated.
public PointObserver() { *
super(contentHandler); * @author Jimmy Shih
*/
private class TrackPointsTableObserver extends ContentObserver {
public TrackPointsTableObserver() {
super(handler);
} }
@Override @Override
public void onChange(boolean selfChange) { public void onChange(boolean selfChange) {
listener.notifyPointsUpdated(); dataSourceListener.notifyTrackPointsTableUpdated();
} }
} }
/** Listener for when preferences change. */ /**
private class HubSharedPreferenceListener implements OnSharedPreferenceChangeListener { * Listener for location changes.
@Override *
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { * @author Jimmy Shih
listener.notifyPreferenceChanged(key); */
} @VisibleForTesting
} class CurrentLocationListener implements LocationListener {
/** Listener for the current location (independent from track data). */
private class CurrentLocationListener implements
LocationListener {
@Override @Override
public void onStatusChanged(String provider, int status, Bundle extras) { public void onLocationChanged(Location location) {
if (!dataSources.isAllowed() || !LocationManager.GPS_PROVIDER.equals(provider)) { if (!dataSource.isAllowed()) {
return; return;
} }
listener.notifyLocationProviderAvailable(status == LocationProvider.AVAILABLE); dataSourceListener.notifyLocationChanged(location);
}
@Override
public void onProviderEnabled(String provider) {
if (!dataSources.isAllowed() || !LocationManager.GPS_PROVIDER.equals(provider)) {
return;
}
listener.notifyLocationProviderEnabled(true);
} }
@Override @Override
@@ -111,161 +118,206 @@ class DataSourceManager {
if (!LocationManager.GPS_PROVIDER.equals(provider)) { if (!LocationManager.GPS_PROVIDER.equals(provider)) {
return; return;
} }
listener.notifyLocationProviderEnabled(false); dataSourceListener.notifyLocationProviderEnabled(false);
} }
@Override @Override
public void onLocationChanged(Location location) { public void onProviderEnabled(String provider) {
if (!dataSources.isAllowed()) { if (!dataSource.isAllowed() || !LocationManager.GPS_PROVIDER.equals(provider)) {
return; 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 * Listener for heading changes.
SensorEventListener { *
@Override * @author Jimmy Shih
public void onSensorChanged(SensorEvent event) { */
listener.notifyHeadingChanged(event.values[0]); @VisibleForTesting
} class HeadingListener implements SensorEventListener {
@Override @Override
public void onAccuracyChanged(Sensor sensor, int accuracy) { public void onAccuracyChanged(Sensor sensor, int accuracy) {
// Do nothing // 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) @Override
private final Set<ListenerDataType> registeredListeners = public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
EnumSet.noneOf(ListenerDataType.class); dataSourceListener.notifyPreferenceChanged(key);
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();
} }
/** Updates the internal (sensor, position, etc) listeners. */ private final DataSource dataSource;
void updateAllListeners(EnumSet<ListenerDataType> externallyNeededListeners) { private final DataSourceListener dataSourceListener;
EnumSet<ListenerDataType> neededListeners = EnumSet.copyOf(externallyNeededListeners);
// Special case - map sampled-out points type to points type since they // Registered listeners
// correspond to the same internal listener. private final Set<TrackDataType> registeredListeners = EnumSet.noneOf(TrackDataType.class);
if (neededListeners.contains(ListenerDataType.SAMPLED_OUT_POINT_UPDATES)) {
neededListeners.remove(ListenerDataType.SAMPLED_OUT_POINT_UPDATES); private final Handler handler;
neededListeners.add(ListenerDataType.POINT_UPDATES); 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<TrackDataType> listeners) {
EnumSet<TrackDataType> 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 // Unnecessary = registered - needed
Set<ListenerDataType> unnecessaryListeners = EnumSet.copyOf(registeredListeners); Set<TrackDataType> unnecessaryListeners = EnumSet.copyOf(registeredListeners);
unnecessaryListeners.removeAll(neededListeners); unnecessaryListeners.removeAll(neededListeners);
// Missing = needed - registered // Missing = needed - registered
Set<ListenerDataType> missingListeners = EnumSet.copyOf(neededListeners); Set<TrackDataType> missingListeners = EnumSet.copyOf(neededListeners);
missingListeners.removeAll(registeredListeners); missingListeners.removeAll(registeredListeners);
// Remove all unnecessary listeners. // Remove unnecessary listeners
for (ListenerDataType type : unnecessaryListeners) { for (TrackDataType trackDataType : unnecessaryListeners) {
unregisterListener(type); unregisterListener(trackDataType);
} }
// Add all missing listeners. // Add missing listeners
for (ListenerDataType type : missingListeners) { for (TrackDataType trackDataType : missingListeners) {
registerListener(type); registerListener(trackDataType);
} }
// Now all needed types are registered. // Update registered listeners
registeredListeners.clear(); registeredListeners.clear();
registeredListeners.addAll(neededListeners); registeredListeners.addAll(neededListeners);
} }
private void registerListener(ListenerDataType type) { /**
switch (type) { * Registers a listener with data source.
case COMPASS_UPDATES: { *
// Listen to compass * @param trackDataType the listener data type
Sensor compass = dataSources.getSensor(Sensor.TYPE_ORIENTATION); */
if (compass != null) { private void registerListener(TrackDataType trackDataType) {
Log.d(TAG, "TrackDataHub: Now registering sensor listener."); switch (trackDataType) {
dataSources.registerSensorListener(compassListener, compass, SensorManager.SENSOR_DELAY_UI); case SELECTED_TRACK:
} // Do nothing
break; break;
} case TRACKS_TABLE:
case LOCATION_UPDATES: dataSource.registerContentObserver(TracksColumns.CONTENT_URI, tracksTableObserver);
dataSources.requestLocationUpdates(locationListener);
break; break;
case POINT_UPDATES: case WAYPOINTS_TABLE:
dataSources.registerContentObserver( dataSource.registerContentObserver(WaypointsColumns.CONTENT_URI, waypointsTableObserver);
TrackPointsColumns.CONTENT_URI, false, pointObserver);
break; break;
case TRACK_UPDATES: case SAMPLED_IN_TRACK_POINTS_TABLE:
dataSources.registerContentObserver(TracksColumns.CONTENT_URI, false, trackObserver); dataSource.registerContentObserver(
TrackPointsColumns.CONTENT_URI, trackPointsTableObserver);
break; break;
case WAYPOINT_UPDATES: case SAMPLED_OUT_TRACK_POINTS_TABLE:
dataSources.registerContentObserver( // Do nothing. SAMPLED_OUT_POINT_UPDATES is mapped to POINT_UPDATES.
WaypointsColumns.CONTENT_URI, false, waypointObserver);
break; break;
case DISPLAY_PREFERENCES: case LOCATION:
dataSources.registerOnSharedPreferenceChangeListener(preferenceListener); dataSource.registerLocationListener(currentLocationListener);
break;
case HEADING:
dataSource.registerHeadingListener(headingListener);
break;
case PREFERENCE:
dataSource.registerOnSharedPreferenceChangeListener(preferenceListener);
break;
default:
break; break;
case SAMPLED_OUT_POINT_UPDATES:
throw new IllegalArgumentException("Should have been mapped to point updates");
} }
} }
private void unregisterListener(ListenerDataType type) { /**
switch (type) { * Unregisters a listener with data source.
case COMPASS_UPDATES: *
dataSources.unregisterSensorListener(compassListener); * @param trackDataType listener data type
*/
private void unregisterListener(TrackDataType trackDataType) {
switch (trackDataType) {
case SELECTED_TRACK:
// Do nothing
break; break;
case LOCATION_UPDATES: case TRACKS_TABLE:
dataSources.removeLocationUpdates(locationListener); dataSource.unregisterContentObserver(tracksTableObserver);
break; break;
case POINT_UPDATES: case WAYPOINTS_TABLE:
dataSources.unregisterContentObserver(pointObserver); dataSource.unregisterContentObserver(waypointsTableObserver);
break; break;
case TRACK_UPDATES: case SAMPLED_IN_TRACK_POINTS_TABLE:
dataSources.unregisterContentObserver(trackObserver); dataSource.unregisterContentObserver(trackPointsTableObserver);
break; break;
case WAYPOINT_UPDATES: case SAMPLED_OUT_TRACK_POINTS_TABLE:
dataSources.unregisterContentObserver(waypointObserver); // Do nothing. SAMPLED_OUT_POINT_UPDATES is mapped to POINT_UPDATES.
break; break;
case DISPLAY_PREFERENCES: case LOCATION:
dataSources.unregisterOnSharedPreferenceChangeListener(preferenceListener); dataSource.unregisterLocationListener(currentLocationListener);
break;
case HEADING:
dataSource.unregisterHeadingListener(headingListener);
break;
case PREFERENCE:
dataSource.unregisterOnSharedPreferenceChangeListener(preferenceListener);
break;
default:
break; 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() { * Unregisters all listeners with data source.
dataSources.removeLocationUpdates(locationListener); */
dataSources.unregisterSensorListener(compassListener); public void unregisterAllListeners() {
dataSources.unregisterContentObserver(trackObserver); for (TrackDataType trackDataType : TrackDataType.values()) {
dataSources.unregisterContentObserver(waypointObserver); unregisterListener(trackDataType);
dataSources.unregisterContentObserver(pointObserver); }
dataSources.unregisterOnSharedPreferenceChangeListener(preferenceListener);
} }
} }
@@ -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();
}
@@ -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);
}
}
@@ -76,8 +76,8 @@ public class DescriptionGeneratorImpl implements DescriptionGenerator {
} }
@Override @Override
public String generateWaypointDescription(Waypoint waypoint) { public String generateWaypointDescription(TripStatistics tripStatistics) {
return generateTripStatisticsDescription(waypoint.getTripStatistics(), false); return generateTripStatisticsDescription(tripStatistics, false);
} }
/** /**
@@ -16,9 +16,9 @@
package com.google.android.apps.mytracks.content; 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.apps.mytracks.util.PreferencesUtils;
import com.google.android.maps.mytracks.R; import com.google.android.maps.mytracks.R;
import com.google.common.annotations.VisibleForTesting;
import android.content.ContentProvider; import android.content.ContentProvider;
import android.content.ContentUris; import android.content.ContentUris;
@@ -38,29 +38,31 @@ import android.text.TextUtils;
import android.util.Log; 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 * @author Leif Hendrik Wilden
*/ */
public class MyTracksProvider extends ContentProvider { 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(); 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) { 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 @Override
@@ -74,28 +76,27 @@ public class MyTracksProvider extends ContentProvider {
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
Log.w(TAG, "Upgrading database from version " + oldVersion + " to " + newVersion); Log.w(TAG, "Upgrading database from version " + oldVersion + " to " + newVersion);
if (oldVersion < 17) { if (oldVersion < 17) {
// Delete the old data Log.w(TAG, "Deleting all old data.");
Log.w(TAG, "Delete all old data");
db.execSQL("DROP TABLE IF EXISTS " + TrackPointsColumns.TABLE_NAME); db.execSQL("DROP TABLE IF EXISTS " + TrackPointsColumns.TABLE_NAME);
db.execSQL("DROP TABLE IF EXISTS " + TracksColumns.TABLE_NAME); db.execSQL("DROP TABLE IF EXISTS " + TracksColumns.TABLE_NAME);
db.execSQL("DROP TABLE IF EXISTS " + WaypointsColumns.TABLE_NAME); db.execSQL("DROP TABLE IF EXISTS " + WaypointsColumns.TABLE_NAME);
onCreate(db); onCreate(db);
} else { } else {
// Incremental updates go here. For each DB version, add a corresponding if clause. // Incremental upgrades. One if statement per DB version.
// Track points sensor column // Add track points SENSOR column
if (oldVersion <= 17) { if (oldVersion <= 17) {
Log.w(TAG, "Upgrade DB: Adding sensor column."); Log.w(TAG, "Upgrade DB: Adding sensor column.");
db.execSQL("ALTER TABLE " + TrackPointsColumns.TABLE_NAME + " ADD " db.execSQL("ALTER TABLE " + TrackPointsColumns.TABLE_NAME + " ADD "
+ TrackPointsColumns.SENSOR + " BLOB"); + TrackPointsColumns.SENSOR + " BLOB");
} }
// Tracks table id column // Add tracks TABLEID column
if (oldVersion <= 18) { if (oldVersion <= 18) {
Log.w(TAG, "Upgrade DB: Adding tableid column."); Log.w(TAG, "Upgrade DB: Adding tableid column.");
db.execSQL("ALTER TABLE " + TracksColumns.TABLE_NAME + " ADD " + TracksColumns.TABLEID db.execSQL("ALTER TABLE " + TracksColumns.TABLE_NAME + " ADD " + TracksColumns.TABLEID
+ " STRING"); + " STRING");
} }
// Tracks table icon column // Add tracks ICON column
if (oldVersion <= 19) { if (oldVersion <= 19) {
Log.w(TAG, "Upgrade DB: Adding icon column."); Log.w(TAG, "Upgrade DB: Adding icon column.");
db.execSQL( 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; private SQLiteDatabase db;
public MyTracksProvider() { public MyTracksProvider() {
urlMatcher = new UriMatcher(UriMatcher.NO_MATCH); uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
urlMatcher.addURI(MyTracksProviderUtils.AUTHORITY, uriMatcher.addURI(MyTracksProviderUtils.AUTHORITY, TrackPointsColumns.TABLE_NAME,
"trackpoints", TRACKPOINTS); UrlType.TRACKPOINTS.ordinal());
urlMatcher.addURI(MyTracksProviderUtils.AUTHORITY, uriMatcher.addURI(MyTracksProviderUtils.AUTHORITY, TrackPointsColumns.TABLE_NAME + "/#",
"trackpoints/#", TRACKPOINTS_ID); UrlType.TRACKPOINTS_ID.ordinal());
urlMatcher.addURI(MyTracksProviderUtils.AUTHORITY, "tracks", TRACKS); uriMatcher.addURI(
urlMatcher.addURI(MyTracksProviderUtils.AUTHORITY, "tracks/#", TRACKS_ID); MyTracksProviderUtils.AUTHORITY, TracksColumns.TABLE_NAME, UrlType.TRACKS.ordinal());
urlMatcher.addURI(MyTracksProviderUtils.AUTHORITY, "waypoints", WAYPOINTS); uriMatcher.addURI(MyTracksProviderUtils.AUTHORITY, TracksColumns.TABLE_NAME + "/#",
urlMatcher.addURI(MyTracksProviderUtils.AUTHORITY, UrlType.TRACKS_ID.ordinal());
"waypoints/#", WAYPOINTS_ID); uriMatcher.addURI(
} MyTracksProviderUtils.AUTHORITY, WaypointsColumns.TABLE_NAME, UrlType.WAYPOINTS.ordinal());
uriMatcher.addURI(MyTracksProviderUtils.AUTHORITY, WaypointsColumns.TABLE_NAME + "/#",
private boolean canAccess() { UrlType.WAYPOINTS_ID.ordinal());
if (Binder.getCallingPid() == Process.myPid()) {
return true;
} else {
return PreferencesUtils.getBoolean(
getContext(), R.string.allow_access_key, PreferencesUtils.ALLOW_ACCESS_DEFAULT);
}
} }
@Override @Override
public boolean onCreate() { 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()) { if (!canAccess()) {
return false; return false;
} }
DatabaseHelper dbHelper = new DatabaseHelper(getContext()); DatabaseHelper databaseHelper = new DatabaseHelper(context);
try { try {
db = dbHelper.getWritableDatabase(); db = databaseHelper.getWritableDatabase();
} catch (SQLiteException e) { } 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; return db != null;
} }
@@ -152,7 +166,7 @@ public class MyTracksProvider extends ContentProvider {
} }
String table; String table;
boolean shouldVacuum = false; boolean shouldVacuum = false;
switch (urlMatcher.match(url)) { switch (getUrlType(url)) {
case TRACKPOINTS: case TRACKPOINTS:
table = TrackPointsColumns.TABLE_NAME; table = TrackPointsColumns.TABLE_NAME;
break; break;
@@ -167,16 +181,22 @@ public class MyTracksProvider extends ContentProvider {
throw new IllegalArgumentException("Unknown URL " + url); throw new IllegalArgumentException("Unknown URL " + url);
} }
Log.w(MyTracksProvider.TAG, "provider delete in " + table + "!"); Log.w(MyTracksProvider.TAG, "Deleting table " + table);
int count = db.delete(table, where, selectionArgs); int count;
try {
db.beginTransaction();
count = db.delete(table, where, selectionArgs);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
getContext().getContentResolver().notifyChange(url, null, true); getContext().getContentResolver().notifyChange(url, null, true);
if (shouldVacuum) { if (shouldVacuum) {
// If a potentially large amount of data was deleted, we want to reclaim its space. // If a potentially large amount of data was deleted, reclaim its space.
Log.i(TAG, "Vacuuming the database"); Log.i(TAG, "Vacuuming the database.");
db.execSQL("VACUUM"); db.execSQL("VACUUM");
} }
return count; return count;
} }
@@ -185,7 +205,7 @@ public class MyTracksProvider extends ContentProvider {
if (!canAccess()) { if (!canAccess()) {
return null; return null;
} }
switch (urlMatcher.match(url)) { switch (getUrlType(url)) {
case TRACKPOINTS: case TRACKPOINTS:
return TrackPointsColumns.CONTENT_TYPE; return TrackPointsColumns.CONTENT_TYPE;
case TRACKPOINTS_ID: case TRACKPOINTS_ID:
@@ -208,197 +228,240 @@ public class MyTracksProvider extends ContentProvider {
if (!canAccess()) { if (!canAccess()) {
return null; return null;
} }
Log.d(MyTracksProvider.TAG, "MyTracksProvider.insert"); if (initialValues == null) {
ContentValues values; initialValues = new ContentValues();
if (initialValues != null) {
values = initialValues;
} else {
values = new ContentValues();
} }
Uri result = null;
int urlMatchType = urlMatcher.match(url); try {
return insertType(url, urlMatchType, values); db.beginTransaction();
} result = insertContentValues(url, getUrlType(url), initialValues);
db.setTransactionSuccessful();
private Uri insertType(Uri url, int urlMatchType, ContentValues values) { } finally {
switch (urlMatchType) { db.endTransaction();
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);
} }
return result;
} }
@Override @Override
public int bulkInsert(Uri url, ContentValues[] valuesBulk) { public int bulkInsert(Uri url, ContentValues[] valuesBulk) {
if (!canAccess()) { if (!canAccess()) {
return 0; return 0;
} }
Log.d(MyTracksProvider.TAG, "MyTracksProvider.bulkInsert");
int numInserted = 0; int numInserted = 0;
try { try {
// Use a transaction in order to make the insertions run as a single batch // Use a transaction in order to make the insertions run as a single batch
db.beginTransaction(); db.beginTransaction();
int urlMatch = urlMatcher.match(url); UrlType urlType = getUrlType(url);
for (numInserted = 0; numInserted < valuesBulk.length; numInserted++) { for (numInserted = 0; numInserted < valuesBulk.length; numInserted++) {
ContentValues values = valuesBulk[numInserted]; ContentValues contentValues = valuesBulk[numInserted];
if (values == null) { values = new ContentValues(); } if (contentValues == null) {
contentValues = new ContentValues();
insertType(url, urlMatch, values); }
insertContentValues(url, urlType, contentValues);
} }
db.setTransactionSuccessful(); db.setTransactionSuccessful();
} finally { } finally {
db.endTransaction(); db.endTransaction();
} }
return numInserted; 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 @Override
public Cursor query( public Cursor query(
Uri url, String[] projection, String selection, String[] selectionArgs, Uri url, String[] projection, String selection, String[] selectionArgs, String sort) {
String sort) {
if (!canAccess()) { if (!canAccess()) {
return null; return null;
} }
SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
int match = urlMatcher.match(url);
String sortOrder = null; String sortOrder = null;
if (match == TRACKPOINTS) { switch (getUrlType(url)) {
qb.setTables(TrackPointsColumns.TABLE_NAME); case TRACKPOINTS:
if (sort != null) { queryBuilder.setTables(TrackPointsColumns.TABLE_NAME);
sortOrder = sort; sortOrder = sort != null ? sort : TrackPointsColumns.DEFAULT_SORT_ORDER;
} else { break;
sortOrder = TrackPointsColumns.DEFAULT_SORT_ORDER; case TRACKPOINTS_ID:
} queryBuilder.setTables(TrackPointsColumns.TABLE_NAME);
} else if (match == TRACKPOINTS_ID) { queryBuilder.appendWhere("_id=" + url.getPathSegments().get(1));
qb.setTables(TrackPointsColumns.TABLE_NAME); break;
qb.appendWhere("_id=" + url.getPathSegments().get(1)); case TRACKS:
} else if (match == TRACKS) { queryBuilder.setTables(TracksColumns.TABLE_NAME);
qb.setTables(TracksColumns.TABLE_NAME); sortOrder = sort != null ? sort : TracksColumns.DEFAULT_SORT_ORDER;
if (sort != null) { break;
sortOrder = sort; case TRACKS_ID:
} else { queryBuilder.setTables(TracksColumns.TABLE_NAME);
sortOrder = TracksColumns.DEFAULT_SORT_ORDER; queryBuilder.appendWhere("_id=" + url.getPathSegments().get(1));
} break;
} else if (match == TRACKS_ID) { case WAYPOINTS:
qb.setTables(TracksColumns.TABLE_NAME); queryBuilder.setTables(WaypointsColumns.TABLE_NAME);
qb.appendWhere("_id=" + url.getPathSegments().get(1)); sortOrder = sort != null ? sort : WaypointsColumns.DEFAULT_SORT_ORDER;
} else if (match == WAYPOINTS) { break;
qb.setTables(WaypointsColumns.TABLE_NAME); case WAYPOINTS_ID:
if (sort != null) { queryBuilder.setTables(WaypointsColumns.TABLE_NAME);
sortOrder = sort; queryBuilder.appendWhere("_id=" + url.getPathSegments().get(1));
} else { break;
sortOrder = WaypointsColumns.DEFAULT_SORT_ORDER; default:
} throw new IllegalArgumentException("Unknown url " + url);
} else if (match == WAYPOINTS_ID) {
qb.setTables(WaypointsColumns.TABLE_NAME);
qb.appendWhere("_id=" + url.getPathSegments().get(1));
} else {
throw new IllegalArgumentException("Unknown URL " + url);
} }
Log.i(Constants.TAG, "Build query: " Cursor cursor = queryBuilder.query(
+ qb.buildQuery(projection, selection, selectionArgs, null, null, sortOrder, null)); db, projection, selection, selectionArgs, null, null, sortOrder);
cursor.setNotificationUri(getContext().getContentResolver(), url);
Cursor c = qb.query(db, projection, selection, selectionArgs, null, null, return cursor;
sortOrder);
c.setNotificationUri(getContext().getContentResolver(), url);
return c;
} }
@Override @Override
public int update(Uri url, ContentValues values, String where, public int update(Uri url, ContentValues values, String where, String[] selectionArgs) {
String[] selectionArgs) {
if (!canAccess()) { if (!canAccess()) {
return 0; 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 count;
int match = urlMatcher.match(url); try {
if (match == TRACKPOINTS) { db.beginTransaction();
count = db.update(TrackPointsColumns.TABLE_NAME, values, where, selectionArgs); count = db.update(table, values, whereClause, selectionArgs);
} else if (match == TRACKPOINTS_ID) { db.setTransactionSuccessful();
String segment = url.getPathSegments().get(1); } finally {
count = db.update(TrackPointsColumns.TABLE_NAME, values, "_id=" + segment db.endTransaction();
+ (!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);
} }
getContext().getContentResolver().notifyChange(url, null, true); getContext().getContentResolver().notifyChange(url, null, true);
return count; 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);
}
} }
File diff suppressed because it is too large Load Diff
@@ -13,138 +13,131 @@
* License for the specific language governing permissions and limitations under * License for the specific language governing permissions and limitations under
* the License. * the License.
*/ */
package com.google.android.apps.mytracks.content;
import com.google.android.apps.mytracks.content.Track; package com.google.android.apps.mytracks.content;
import com.google.android.apps.mytracks.content.Waypoint;
import android.location.Location; import android.location.Location;
/** /**
* Listener for track data, for both initial and incremental loading. * Listener for track data changes.
* *
* @author Rodrigo Damazio * @author Rodrigo Damazio
*/ */
public interface TrackDataListener { public interface TrackDataListener {
/** States for the GPS location provider. */ /**
public enum ProviderState { * Location state.
DISABLED, *
NO_FIX, * @author Jimmy Shih
BAD_FIX, */
GOOD_FIX; 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. * Called when the location changes. This is meant for location display only,
* This is meant for immediate location display only - track point data is * track point data is reported with other methods like
* delivered by other methods below, such as {@link #onNewTrackPoint}. * {@link #onSampledInTrackPoint(Location)} and
* {@link #onSampledOutTrackPoint(Location)}.
* *
* @param loc the last known location * @param location the location
*/ */
void onCurrentLocationChanged(Location loc); public void onLocationChanged(Location location);
/** /**
* Called when the current heading changes. * Called when the heading changes.
* *
* @param heading the current heading, already accounting magnetic declination * @param heading the heading
*/ */
void onCurrentHeadingChanged(double heading); public void onHeadingChanged(double heading);
/** /**
* Called when the currently-selected track changes. * Called when the selected track changes. This will be followed by calls to
* This will be followed by calls to data methods such as * data methods such as {@link #onTrackUpdated(Track)},
* {@link #onTrackUpdated}, {@link #clearTrackPoints}, * {@link #clearTrackPoints()}, {@link #onSampledInTrackPoint(Location)},
* {@link #onNewTrackPoint(Location)}, etc., even if no track is currently * etc., even if no track is currently selected (in which case you'll only get
* selected (in which case you'll only get calls to clear the current data). * calls to clear the current data).
* *
* @param track the selected track, or null if no track is selected * @param track the selected track or null if no track is selected
* @param isRecording whether we're currently recording the selected track * @param isRecording true if the selected track is recording
*/ */
void onSelectedTrackChanged(Track track, boolean isRecording); public void onSelectedTrackChanged(Track track, boolean isRecording);
/** /**
* Called when the track and/or its statistics have been updated. * Called when the track or its statistics has been updated.
* *
* @param track the updated version of the track * @param track the track
*/ */
void onTrackUpdated(Track track); public void onTrackUpdated(Track track);
/** /**
* Called to clear any previously-sent track points. * Called to clear 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.
*/ */
void clearTrackPoints(); public void clearTrackPoints();
/** /**
* Called when a new interesting track point is read. * Called when a sampled in 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 * @param location the location
*/ */
void onNewTrackPoint(Location loc); public void onSampledInTrackPoint(Location location);
/** /**
* Called when a uninteresting track point is read. * Called when a sampled out track point is read.
* Uninteresting points are all points that get sampled out of the track.
* *
* @param loc the new track point * @param location the location
*/ */
void onSampledOutTrackPoint(Location loc); public void onSampledOutTrackPoint(Location location);
/** /**
* Called when an invalid point (representing a segment split) is read. * Called when an invalid track point representing a segment split is read.
*/ */
void onSegmentSplit(); public void onSegmentSplit();
/** /**
* Called when we're done (for the time being) sending new points. * Called when finish sending new track points. This gets called after every
* This gets called after every batch of calls to {@link #onNewTrackPoint}, * batch of calls to {@link #onSampledInTrackPoint(Location)},
* {@link #onSampledOutTrackPoint} and {@link #onSegmentSplit}. * {@link #onSampledOutTrackPoint(Location)} and {@link #onSegmentSplit()}.
*/ */
void onNewTrackPointsDone(); public void onNewTrackPointsDone();
/** /**
* Called to clear any previously-sent waypoints. * Called to clear previously sent waypoints.
* This can be called at any time that we decide the data needs to be
* reloaded.
*/ */
void clearWaypoints(); public void clearWaypoints();
/** /**
* Called when a new waypoint is read. * 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. * Called when finish sending new waypoints. This gets called after every
* This gets called after every batch of calls to {@link #clearWaypoints} and * batch of calls to {@link #clearWaypoints()} and
* {@link #onNewWaypoint}. * {@link #onNewWaypoint(Waypoint)}.
*/ */
void onNewWaypointsDone(); public void onNewWaypointsDone();
/** /**
* Called when the display units are changed by the user. * Called when the metric units preference value is change.
* *
* @param metric true if the units are metric, false if imperial * @param metricUnits true to use metric units, false to use imperial units
* @return true to reload all the data, false otherwise * @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. * Called when the report speed preference value is changed.
* *
* @param reportSpeed true to report speed, false for pace * @param reportSpeed true to report speed, false to report pace
* @return true to reload all the data, false otherwise * @return true to reload all the data, false otherwise.
*/ */
boolean onReportSpeedChanged(boolean reportSpeed); public boolean onReportSpeedChanged(boolean reportSpeed);
} }
@@ -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<ListenerDataType> 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<ListenerDataType> 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<TrackDataListener, ListenerRegistration> registeredListeners =
new HashMap<TrackDataListener, ListenerRegistration>();
/** Map of data type to external listeners interested in it. */
private final Map<ListenerDataType, Set<TrackDataListener>> listenerSetsPerType =
new EnumMap<ListenerDataType, Set<TrackDataListener>>(ListenerDataType.class);
public TrackDataListeners() {
// Create sets for all data types at startup.
for (ListenerDataType type : ListenerDataType.values()) {
listenerSetsPerType.put(type, new LinkedHashSet<TrackDataListener>());
}
}
/**
* 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<ListenerDataType> 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<TrackDataListener> 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<TrackDataListener> getListenersFor(ListenerDataType type) {
return listenerSetsPerType.get(type);
}
public EnumSet<ListenerDataType> getAllRegisteredTypes() {
EnumSet<ListenerDataType> 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();
}
}
@@ -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<TrackDataListener, EnumSet<TrackDataType>>
listenerToTypesMap = new HashMap<TrackDataListener, EnumSet<TrackDataType>>();
// Map of track data type to listeners
private final Map<TrackDataType, Set<TrackDataListener>>
typeToListenersMap = new EnumMap<TrackDataType, Set<TrackDataListener>>(TrackDataType.class);
public TrackDataManager() {
for (TrackDataType trackDataType : TrackDataType.values()) {
typeToListenersMap.put(trackDataType, new LinkedHashSet<TrackDataListener>());
}
}
/**
* Registers a listener.
*
* @param listener the listener
* @param trackDataTypes the track data types the listener is interested
*/
public void registerListener(
TrackDataListener listener, EnumSet<TrackDataType> 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<TrackDataType> 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<TrackDataType> getTrackDataTypes(TrackDataListener listener) {
return listenerToTypesMap.get(listener);
}
/**
* Gets the listeners for a {@link TrackDataType}.
*
* @param type the type
*/
public Set<TrackDataListener> getListeners(TrackDataType type) {
return typeToListenersMap.get(type);
}
/**
* Gets all the registered {@link TrackDataType}.
*/
public EnumSet<TrackDataType> getRegisteredTrackDataTypes() {
EnumSet<TrackDataType> types = EnumSet.noneOf(TrackDataType.class);
for (EnumSet<TrackDataType> value : listenerToTypesMap.values()) {
types.addAll(value);
}
// Always include preference
types.add(TrackDataType.PREFERENCE);
return types;
}
}
@@ -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
}
@@ -18,14 +18,14 @@ package com.google.android.apps.mytracks.fragments;
import com.google.android.apps.mytracks.ChartView; import com.google.android.apps.mytracks.ChartView;
import com.google.android.apps.mytracks.Constants; 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.MyTracksLocation;
import com.google.android.apps.mytracks.content.Sensor; import com.google.android.apps.mytracks.content.Sensor;
import com.google.android.apps.mytracks.content.Sensor.SensorDataSet; import com.google.android.apps.mytracks.content.Sensor.SensorDataSet;
import com.google.android.apps.mytracks.content.Track; import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.content.TrackDataHub; 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.TrackDataListener;
import com.google.android.apps.mytracks.content.TrackDataType;
import com.google.android.apps.mytracks.content.Waypoint; import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.stats.DoubleBuffer; import com.google.android.apps.mytracks.stats.DoubleBuffer;
import com.google.android.apps.mytracks.stats.TripStatisticsBuilder; import com.google.android.apps.mytracks.stats.TripStatisticsBuilder;
@@ -96,7 +96,7 @@ public class ChartFragment extends Fragment implements TrackDataListener {
zoomControls.setIsZoomInEnabled(chartView.canZoomIn()); zoomControls.setIsZoomInEnabled(chartView.canZoomIn());
zoomControls.setIsZoomOutEnabled(chartView.canZoomOut()); zoomControls.setIsZoomOutEnabled(chartView.canZoomOut());
chartView.setShowPointer(isRecording()); chartView.setShowPointer(isSelectedTrackRecording());
chartView.invalidate(); chartView.invalidate();
} }
}; };
@@ -164,17 +164,17 @@ public class ChartFragment extends Fragment implements TrackDataListener {
} }
@Override @Override
public void onProviderStateChange(ProviderState state) { public void onLocationStateChanged(LocationState state) {
// We don't care. // We don't care.
} }
@Override @Override
public void onCurrentLocationChanged(Location loc) { public void onLocationChanged(Location loc) {
// We don't care. // We don't care.
} }
@Override @Override
public void onCurrentHeadingChanged(double heading) { public void onHeadingChanged(double heading) {
// We don't care. // We don't care.
} }
@@ -213,7 +213,7 @@ public class ChartFragment extends Fragment implements TrackDataListener {
} }
@Override @Override
public void onNewTrackPoint(Location location) { public void onSampledInTrackPoint(Location location) {
if (LocationUtils.isValidLocation(location)) { if (LocationUtils.isValidLocation(location)) {
double[] data = new double[ChartView.NUM_SERIES + 1]; double[] data = new double[ChartView.NUM_SERIES + 1];
fillDataPoint(location, data); fillDataPoint(location, data);
@@ -259,7 +259,7 @@ public class ChartFragment extends Fragment implements TrackDataListener {
} }
@Override @Override
public boolean onUnitsChanged(boolean metric) { public boolean onMetricUnitsChanged(boolean metric) {
if (metricUnits == metric) { if (metricUnits == metric) {
return false; return false;
} }
@@ -358,14 +358,14 @@ public class ChartFragment extends Fragment implements TrackDataListener {
* accessed by multiple threads. * accessed by multiple threads.
*/ */
private synchronized void resumeTrackDataHub() { private synchronized void resumeTrackDataHub() {
trackDataHub = ((MyTracksApplication) getActivity().getApplication()).getTrackDataHub(); trackDataHub = ((TrackDetailActivity) getActivity()).getTrackDataHub();
trackDataHub.registerTrackDataListener(this, EnumSet.of( trackDataHub.registerTrackDataListener(this, EnumSet.of(
ListenerDataType.SELECTED_TRACK_CHANGED, TrackDataType.SELECTED_TRACK,
ListenerDataType.TRACK_UPDATES, TrackDataType.TRACKS_TABLE,
ListenerDataType.WAYPOINT_UPDATES, TrackDataType.WAYPOINTS_TABLE,
ListenerDataType.POINT_UPDATES, TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE,
ListenerDataType.SAMPLED_OUT_POINT_UPDATES, TrackDataType.SAMPLED_OUT_TRACK_POINTS_TABLE,
ListenerDataType.DISPLAY_PREFERENCES)); TrackDataType.PREFERENCE));
} }
/** /**
@@ -378,11 +378,11 @@ public class ChartFragment extends Fragment implements TrackDataListener {
} }
/** /**
* Returns true if recording. Needs to be synchronized because trackDataHub * Returns true if the selected track is recording. Needs to be synchronized
* can be accessed by multiple threads. * because trackDataHub can be accessed by multiple threads.
*/ */
private synchronized boolean isRecording() { private synchronized boolean isSelectedTrackRecording() {
return trackDataHub != null && trackDataHub.isRecordingSelected(); return trackDataHub != null && trackDataHub.isSelectedTrackRecording();
} }
/** /**
@@ -17,14 +17,13 @@
package com.google.android.apps.mytracks.fragments; package com.google.android.apps.mytracks.fragments;
import com.google.android.apps.mytracks.MapOverlay; 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.TrackDetailActivity;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils; import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils.Factory; import com.google.android.apps.mytracks.content.MyTracksProviderUtils.Factory;
import com.google.android.apps.mytracks.content.Track; import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.content.TrackDataHub; 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.TrackDataListener;
import com.google.android.apps.mytracks.content.TrackDataType;
import com.google.android.apps.mytracks.content.Waypoint; import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.stats.TripStatistics; import com.google.android.apps.mytracks.stats.TripStatistics;
import com.google.android.apps.mytracks.util.ApiAdapterFactory; import com.google.android.apps.mytracks.util.ApiAdapterFactory;
@@ -275,7 +274,7 @@ public class MapFragment extends Fragment
} }
@Override @Override
public void onProviderStateChange(ProviderState state) { public void onLocationStateChanged(LocationState state) {
final String message; final String message;
final boolean isGpsDisabled; final boolean isGpsDisabled;
switch (state) { switch (state) {
@@ -322,13 +321,13 @@ public class MapFragment extends Fragment
} }
@Override @Override
public void onCurrentLocationChanged(Location location) { public void onLocationChanged(Location location) {
currentLocation = location; currentLocation = location;
updateCurrentLocation(); updateCurrentLocation();
} }
@Override @Override
public void onCurrentHeadingChanged(double heading) { public void onHeadingChanged(double heading) {
if (mapOverlay.setHeading((float) heading)) { if (mapOverlay.setHeading((float) heading)) {
mapView.postInvalidate(); mapView.postInvalidate();
} }
@@ -369,7 +368,7 @@ public class MapFragment extends Fragment
} }
@Override @Override
public void onNewTrackPoint(Location location) { public void onSampledInTrackPoint(Location location) {
if (LocationUtils.isValidLocation(location)) { if (LocationUtils.isValidLocation(location)) {
mapOverlay.addLocation(location); mapOverlay.addLocation(location);
} }
@@ -409,7 +408,7 @@ public class MapFragment extends Fragment
} }
@Override @Override
public boolean onUnitsChanged(boolean metric) { public boolean onMetricUnitsChanged(boolean metric) {
// We don't care. // We don't care.
return false; return false;
} }
@@ -425,13 +424,13 @@ public class MapFragment extends Fragment
* accessed by multiple threads. * accessed by multiple threads.
*/ */
private synchronized void resumeTrackDataHub() { private synchronized void resumeTrackDataHub() {
trackDataHub = ((MyTracksApplication) getActivity().getApplication()).getTrackDataHub(); trackDataHub = ((TrackDetailActivity) getActivity()).getTrackDataHub();
trackDataHub.registerTrackDataListener(this, EnumSet.of( trackDataHub.registerTrackDataListener(this, EnumSet.of(
ListenerDataType.SELECTED_TRACK_CHANGED, TrackDataType.SELECTED_TRACK,
ListenerDataType.WAYPOINT_UPDATES, TrackDataType.WAYPOINTS_TABLE,
ListenerDataType.POINT_UPDATES, TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE,
ListenerDataType.LOCATION_UPDATES, TrackDataType.LOCATION,
ListenerDataType.COMPASS_UPDATES)); TrackDataType.HEADING));
} }
/** /**
@@ -16,11 +16,11 @@
package com.google.android.apps.mytracks.fragments; 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.Track;
import com.google.android.apps.mytracks.content.TrackDataHub; 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.TrackDataListener;
import com.google.android.apps.mytracks.content.TrackDataType;
import com.google.android.apps.mytracks.content.Waypoint; import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.stats.TripStatistics; import com.google.android.apps.mytracks.stats.TripStatistics;
import com.google.android.apps.mytracks.util.PreferencesUtils; 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. // A runnable to update the total time field.
private final Runnable updateTotalTime = new Runnable() { private final Runnable updateTotalTime = new Runnable() {
public void run() { public void run() {
if (isRecording()) { if (isSelectedTrackRecording()) {
StatsUtils.setTotalTimeValue(getActivity(), System.currentTimeMillis() - startTime); StatsUtils.setTotalTimeValue(getActivity(), System.currentTimeMillis() - startTime);
} }
} }
@@ -117,8 +117,8 @@ public class StatsFragment extends Fragment implements TrackDataListener {
} }
@Override @Override
public void onProviderStateChange(ProviderState state) { public void onLocationStateChanged(LocationState state) {
if (isResumed() && (state == ProviderState.DISABLED || state == ProviderState.NO_FIX)) { if (isResumed() && (state == LocationState.DISABLED || state == LocationState.NO_FIX)) {
getActivity().runOnUiThread(new Runnable() { getActivity().runOnUiThread(new Runnable() {
@Override @Override
public void run() { public void run() {
@@ -130,8 +130,8 @@ public class StatsFragment extends Fragment implements TrackDataListener {
} }
@Override @Override
public void onCurrentLocationChanged(final Location location) { public void onLocationChanged(final Location location) {
if (isResumed() && isRecording()) { if (isResumed() && isSelectedTrackRecording()) {
getActivity().runOnUiThread(new Runnable() { getActivity().runOnUiThread(new Runnable() {
@Override @Override
public void run() { public void run() {
@@ -143,7 +143,7 @@ public class StatsFragment extends Fragment implements TrackDataListener {
} }
@Override @Override
public void onCurrentHeadingChanged(double heading) { public void onHeadingChanged(double heading) {
// We don't care. // We don't care.
} }
@@ -175,7 +175,7 @@ public class StatsFragment extends Fragment implements TrackDataListener {
lastTripStatistics = track.getTripStatistics(); lastTripStatistics = track.getTripStatistics();
startTime = track.getTripStatistics().getStartTime(); startTime = track.getTripStatistics().getStartTime();
if (!isRecording()) { if (!isSelectedTrackRecording()) {
lastLocation = null; lastLocation = null;
} }
updateUi(); updateUi();
@@ -190,7 +190,7 @@ public class StatsFragment extends Fragment implements TrackDataListener {
} }
@Override @Override
public void onNewTrackPoint(Location loc) { public void onSampledInTrackPoint(Location loc) {
// We don't care. // We don't care.
} }
@@ -225,7 +225,7 @@ public class StatsFragment extends Fragment implements TrackDataListener {
} }
@Override @Override
public boolean onUnitsChanged(final boolean metric) { public boolean onMetricUnitsChanged(final boolean metric) {
if (isResumed()) { if (isResumed()) {
getActivity().runOnUiThread(new Runnable() { getActivity().runOnUiThread(new Runnable() {
@Override @Override
@@ -255,12 +255,12 @@ public class StatsFragment extends Fragment implements TrackDataListener {
* be accessed by multiple threads. * be accessed by multiple threads.
*/ */
private synchronized void resumeTrackDataHub() { private synchronized void resumeTrackDataHub() {
trackDataHub = ((MyTracksApplication) getActivity().getApplication()).getTrackDataHub(); trackDataHub = ((TrackDetailActivity) getActivity()).getTrackDataHub();
trackDataHub.registerTrackDataListener(this, EnumSet.of( trackDataHub.registerTrackDataListener(this, EnumSet.of(
ListenerDataType.SELECTED_TRACK_CHANGED, TrackDataType.SELECTED_TRACK,
ListenerDataType.TRACK_UPDATES, TrackDataType.TRACKS_TABLE,
ListenerDataType.LOCATION_UPDATES, TrackDataType.LOCATION,
ListenerDataType.DISPLAY_PREFERENCES)); TrackDataType.PREFERENCE));
} }
/** /**
@@ -273,11 +273,11 @@ public class StatsFragment extends Fragment implements TrackDataListener {
} }
/** /**
* Returns true if recording. Needs to be synchronized because trackDataHub * Returns true if the selected track is recording. Needs to be synchronized
* can be accessed by multiple threads. * because trackDataHub can be accessed by multiple threads.
*/ */
private synchronized boolean isRecording() { private synchronized boolean isSelectedTrackRecording() {
return trackDataHub != null && trackDataHub.isRecordingSelected(); return trackDataHub != null && trackDataHub.isSelectedTrackRecording();
} }
/** /**
@@ -56,26 +56,31 @@ import java.util.Locale;
* @author Matthew Simmons * @author Matthew Simmons
*/ */
public class SendDocsUtils { 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?" "https://docs.google.com/feeds/documents/private/full?"
+ "category=mine,spreadsheet&title=%s&title-exact=true"; + "category=mine,spreadsheet&title=%s&title-exact=true";
private static final String CREATE_SPREADSHEET_URI = private static final String CREATE_SPREADSHEET_URI =
"https://docs.google.com/feeds/documents/private/full"; "https://docs.google.com/feeds/documents/private/full";
private static final String GET_WORKSHEETS_URI = private static final String GET_WORKSHEETS_URI =
"https://spreadsheets.google.com/feeds/worksheets/%s/private/full"; "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"; "https://spreadsheets.google.com/feeds/list/%s/%s/private/full";
@VisibleForTesting
private static final String SPREADSHEET_ID_PREFIX = public static final String SPREADSHEET_ID_PREFIX =
"https://docs.google.com/feeds/documents/private/full/spreadsheet%3A"; "https://docs.google.com/feeds/documents/private/full/spreadsheet%3A";
@VisibleForTesting
private static final String CONTENT_TYPE = "Content-Type"; public static final String CONTENT_TYPE = "Content-Type";
private static final String ATOM_FEED_MIME_TYPE = "application/atom+xml"; @VisibleForTesting
public static final String ATOM_FEED_MIME_TYPE = "application/atom+xml";
private static final String OPENDOCUMENT_SPREADSHEET_MIME_TYPE = private static final String OPENDOCUMENT_SPREADSHEET_MIME_TYPE =
"application/x-vnd.oasis.opendocument.spreadsheet"; "application/x-vnd.oasis.opendocument.spreadsheet";
private static final String AUTHORIZATION = "Authorization"; @VisibleForTesting
private static final String AUTHORIZATION_PREFIX = "GoogleLogin auth="; public static final String AUTHORIZATION = "Authorization";
@VisibleForTesting
public static final String AUTHORIZATION_PREFIX = "GoogleLogin auth=";
private static final String SLUG = "Slug"; private static final String SLUG = "Slug";
@@ -306,7 +306,7 @@ public class GpxImporter extends DefaultHandler {
tripStatisticsBuilder = new TripStatisticsBuilder(0); tripStatisticsBuilder = new TripStatisticsBuilder(0);
tripStatisticsBuilder.pauseAt(0); tripStatisticsBuilder.pauseAt(0);
} }
track.setTripStatistics(tripStatisticsBuilder.getStatistics()); track.setTripStatistics(tripStatisticsBuilder.getTripStatistics());
track.setNumberOfPoints(numberOfLocations); track.setNumberOfPoints(numberOfLocations);
myTracksProviderUtils.updateTrack(track); myTracksProviderUtils.updateTrack(track);
tracksIds.add(track.getId()); tracksIds.add(track.getId());
@@ -527,7 +527,7 @@ public class GpxImporter extends DefaultHandler {
*/ */
private long getLastPointId() { private long getLastPointId() {
flushPoints(); flushPoints();
return myTracksProviderUtils.getLastLocationId(track.getId()); return myTracksProviderUtils.getLastTrackLocationId(track.getId());
} }
/** /**
@@ -169,6 +169,7 @@ public class TcxTrackWriter implements TrackFormatWriter {
public void writeEndTrack(Location lastPoint) { public void writeEndTrack(Location lastPoint) {
if (printWriter != null) { if (printWriter != null) {
printWriter.println("</Lap>"); printWriter.println("</Lap>");
printWriter.println("<Notes>" + StringUtils.formatCData(track.getDescription()) + "</Notes>");
printWriter.println("<Creator xsi:type=\"Device_t\">"); printWriter.println("<Creator xsi:type=\"Device_t\">");
printWriter.println("<Name>" printWriter.println("<Name>"
+ StringUtils.formatCData(context.getString(R.string.send_google_by_my_tracks, "", "")) + StringUtils.formatCData(context.getString(R.string.send_google_by_my_tracks, "", ""))
@@ -27,6 +27,7 @@ import com.google.api.client.http.HttpResponse;
import com.google.api.client.http.HttpTransport; import com.google.api.client.http.HttpTransport;
import com.google.api.client.http.InputStreamContent; import com.google.api.client.http.InputStreamContent;
import com.google.api.client.util.Strings; import com.google.api.client.util.Strings;
import com.google.common.annotations.VisibleForTesting;
import android.accounts.Account; import android.accounts.Account;
import android.accounts.AccountManager; import android.accounts.AccountManager;
@@ -52,13 +53,17 @@ import java.util.Vector;
*/ */
public class SendFusionTablesAsyncTask extends AbstractSendAsyncTask { 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 SQL_KEY = "sql=";
private static final String CONTENT_TYPE = "application/x-www-form-urlencoded"; @VisibleForTesting
private static final String FUSION_TABLES_BASE_URL = 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"; "https://www.google.com/fusiontables/api/query";
private static final int MAX_POINTS_PER_UPLOAD = 2048; 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_CREATE_TABLE = 0;
private static final int PROGRESS_UNLIST_TABLE = 5; private static final int PROGRESS_UNLIST_TABLE = 5;
@@ -248,7 +253,7 @@ public class SendFusionTablesAsyncTask extends AbstractSendAsyncTask {
elevationBuffer.setNext(metricUnits ? location.getAltitude() elevationBuffer.setNext(metricUnits ? location.getAltitude()
: location.getAltitude() * UnitConversions.M_TO_FT); : location.getAltitude() * UnitConversions.M_TO_FT);
if (i % elevationSamplingFrequency == 0) { if (i % elevationSamplingFrequency == 0) {
distances.add(tripStatisticsBuilder.getStatistics().getTotalDistance()); distances.add(tripStatisticsBuilder.getTripStatistics().getTotalDistance());
elevations.add(elevationBuffer.getAverage()); elevations.add(elevationBuffer.getAverage());
} }
lastLocation = location; lastLocation = location;
@@ -274,7 +279,7 @@ public class SendFusionTablesAsyncTask extends AbstractSendAsyncTask {
// Create an end marker // Create an end marker
if (lastLocation != null) { if (lastLocation != null) {
distances.add(tripStatisticsBuilder.getStatistics().getTotalDistance()); distances.add(tripStatisticsBuilder.getTripStatistics().getTotalDistance());
elevations.add(elevationBuffer.getAverage()); elevations.add(elevationBuffer.getAverage());
DescriptionGenerator descriptionGenerator = new DescriptionGeneratorImpl(context); DescriptionGenerator descriptionGenerator = new DescriptionGeneratorImpl(context);
track.setDescription( track.setDescription(
@@ -297,7 +297,7 @@ public class SendMapsAsyncTask extends AbstractSendAsyncTask {
elevationBuffer.setNext(metricUnits ? location.getAltitude() elevationBuffer.setNext(metricUnits ? location.getAltitude()
: location.getAltitude() * UnitConversions.M_TO_FT); : location.getAltitude() * UnitConversions.M_TO_FT);
if (i % elevationSamplingFrequency == 0) { if (i % elevationSamplingFrequency == 0) {
distances.add(tripStatisticsBuilder.getStatistics().getTotalDistance()); distances.add(tripStatisticsBuilder.getTripStatistics().getTotalDistance());
elevations.add(elevationBuffer.getAverage()); elevations.add(elevationBuffer.getAverage());
} }
lastLocation = location; lastLocation = location;
@@ -323,7 +323,7 @@ public class SendMapsAsyncTask extends AbstractSendAsyncTask {
// Create an end marker // Create an end marker
if (lastLocation != null) { if (lastLocation != null) {
distances.add(tripStatisticsBuilder.getStatistics().getTotalDistance()); distances.add(tripStatisticsBuilder.getTripStatistics().getTotalDistance());
elevations.add(elevationBuffer.getAverage()); elevations.add(elevationBuffer.getAverage());
track.setDescription(getTrackDescription(track, distances, elevations)); track.setDescription(getTrackDescription(track, distances, elevations));
@@ -13,6 +13,7 @@
* License for the specific language governing permissions and limitations under * License for the specific language governing permissions and limitations under
* the License. * the License.
*/ */
package com.google.android.apps.mytracks.maps; package com.google.android.apps.mytracks.maps;
import static com.google.android.apps.mytracks.Constants.TAG; import static com.google.android.apps.mytracks.Constants.TAG;
@@ -36,60 +37,43 @@ import android.util.Log;
* *
* @author Vangelis S. * @author Vangelis S.
*/ */
public class DynamicSpeedTrackPathDescriptor implements TrackPathDescriptor, public class DynamicSpeedTrackPathDescriptor
OnSharedPreferenceChangeListener { implements TrackPathDescriptor, OnSharedPreferenceChangeListener {
private final Context context;
private int speedMargin;
private int slowSpeed; private int slowSpeed;
private int normalSpeed; private int normalSpeed;
private int speedMargin;
private double averageMovingSpeed; private double averageMovingSpeed;
private final Context context;
@VisibleForTesting @VisibleForTesting
static final int CRITICAL_DIFFERENCE_PERCENTAGE = 20; static final int CRITICAL_DIFFERENCE_PERCENTAGE = 20;
public DynamicSpeedTrackPathDescriptor(Context context) { public DynamicSpeedTrackPathDescriptor(Context context) {
this.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, speedMargin = PreferencesUtils.getInt(context, R.string.track_color_mode_percentage_key,
PreferencesUtils.TRACK_COLOR_MODE_PERCENTAGE_DEFAULT); PreferencesUtils.TRACK_COLOR_MODE_PERCENTAGE_DEFAULT);
context.getSharedPreferences(Constants.SETTINGS_NAME, Context.MODE_PRIVATE)
.registerOnSharedPreferenceChangeListener(this);
} }
/** @Override
* Get the slow speed calculated based on the % below the average speed.
*
* @return The speed limit considered as slow.
*/
public int getSlowSpeed() { public int getSlowSpeed() {
slowSpeed = (int) (averageMovingSpeed - (averageMovingSpeed * speedMargin / 100)); slowSpeed = (int) (averageMovingSpeed - (averageMovingSpeed * speedMargin / 100.0));
return slowSpeed; return slowSpeed;
} }
/** @Override
* Gets the medium speed calculated based on the % above the average speed.
*
* @return The speed limit considered as normal.
*/
public int getNormalSpeed() { public int getNormalSpeed() {
normalSpeed = (int) (averageMovingSpeed + (averageMovingSpeed * speedMargin / 100)); normalSpeed = (int) (averageMovingSpeed + (averageMovingSpeed * speedMargin / 100.0));
return normalSpeed; return normalSpeed;
} }
@Override @Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { public boolean updateState() {
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() {
long selectedTrackId = PreferencesUtils.getLong(context, R.string.selected_track_id_key); long selectedTrackId = PreferencesUtils.getLong(context, R.string.selected_track_id_key);
if (selectedTrackId == PreferencesUtils.SELECTED_TRACK_ID_DEFAULT) { if (selectedTrackId == PreferencesUtils.SELECTED_TRACK_ID_DEFAULT) {
// Could not find track. Log.d(TAG, "No selected track id.");
return false; return false;
} }
Track track = MyTracksProviderUtils.Factory.get(context).getTrack(selectedTrackId); Track track = MyTracksProviderUtils.Factory.get(context).getTrack(selectedTrackId);
@@ -97,40 +81,48 @@ public class DynamicSpeedTrackPathDescriptor implements TrackPathDescriptor,
Log.d(TAG, "No track for " + selectedTrackId); Log.d(TAG, "No track for " + selectedTrackId);
return false; return false;
} }
TripStatistics stats = track.getTripStatistics(); TripStatistics tripStatistics = track.getTripStatistics();
double newAverageMovingSpeed = (int) Math.floor( double newAverageMovingSpeed = (int) Math.floor(
stats.getAverageMovingSpeed() * UnitConversions.MS_TO_KMH); tripStatistics.getAverageMovingSpeed() * UnitConversions.MS_TO_KMH);
return isDifferenceSignificant(averageMovingSpeed, newAverageMovingSpeed); 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);
}
} }
/** /**
* Checks whether the old speed and the new speed differ significantly or not. * Returns true if the average moving speed and the new average moving speed
* are significantly different.
*
* @param oldAverageMovingSpeed
* @param newAverageMovingSpeed
*/ */
public boolean isDifferenceSignificant(double oldAverageMovingSpeed, double newAverageMovingSpeed) { @VisibleForTesting
boolean isDifferenceSignificant(double oldAverageMovingSpeed, double newAverageMovingSpeed) {
if (oldAverageMovingSpeed == 0) { if (oldAverageMovingSpeed == 0) {
if (newAverageMovingSpeed == 0) { return newAverageMovingSpeed != 0;
return false;
} else {
averageMovingSpeed = newAverageMovingSpeed;
return true;
}
} }
// Here, both oldAverageMovingSpeed and newAverageMovingSpeed are not zero. // Here, both oldAverageMovingSpeed and newAverageMovingSpeed are not zero.
double maxValue = Math.max(oldAverageMovingSpeed, newAverageMovingSpeed); double maxValue = Math.max(oldAverageMovingSpeed, newAverageMovingSpeed);
double differencePercentage = Math.abs(oldAverageMovingSpeed - newAverageMovingSpeed) double differencePercentage = Math.abs(oldAverageMovingSpeed - newAverageMovingSpeed) / maxValue
/ maxValue * 100; * 100.0;
if (differencePercentage >= CRITICAL_DIFFERENCE_PERCENTAGE) { return differencePercentage >= CRITICAL_DIFFERENCE_PERCENTAGE;
averageMovingSpeed = newAverageMovingSpeed;
return true;
}
return false;
} }
/** /**
* Gets the value of variable speedMargin to check the result of test. * Gets the speed margin.
* @return the value of speedMargin.
*/ */
@VisibleForTesting @VisibleForTesting
int getSpeedMargin() { int getSpeedMargin() {
@@ -138,21 +130,20 @@ public class DynamicSpeedTrackPathDescriptor implements TrackPathDescriptor,
} }
/** /**
* Sets the value of newAverageMovingSpeed to test the method isDifferenceSignificant. * Gets the average moving speed.
* @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
*/ */
@VisibleForTesting @VisibleForTesting
double getAverageMovingSpeed() { double getAverageMovingSpeed() {
return averageMovingSpeed; return averageMovingSpeed;
} }
/**
* Sets the average moving speed.
*
* @param value the value
*/
@VisibleForTesting
void setAverageMovingSpeed(double value) {
averageMovingSpeed = value;
}
} }
@@ -25,7 +25,6 @@ import com.google.common.annotations.VisibleForTesting;
import android.content.Context; import android.content.Context;
import android.graphics.Canvas; import android.graphics.Canvas;
import android.graphics.Paint; import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Point; import android.graphics.Point;
import android.graphics.Rect; import android.graphics.Rect;
@@ -33,117 +32,105 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
/** /**
* A path painter that varies the path colors based on fixed speeds or average speed margin * A path painter that varies the path colors based on fixed speeds or average
* depending of the TrackPathDescriptor passed to its constructor. * speed margin depending of the TrackPathDescriptor passed to its constructor.
* *
* @author Vangelis S. * @author Vangelis S.
*/ */
public class DynamicSpeedTrackPathPainter implements TrackPathPainter { public class DynamicSpeedTrackPathPainter implements TrackPathPainter {
private final Paint selectedTrackPaintSlow;
private final Paint selectedTrackPaintMedium;
private final Paint selectedTrackPaintFast;
private final List<ColoredPath> coloredPaths;
private final TrackPathDescriptor trackPathDescriptor; private final TrackPathDescriptor trackPathDescriptor;
private int slowSpeed; private final Paint slowPaint;
private int normalSpeed; private final Paint mediumPaint;
private final Paint fastPaint;
private final List<ColoredPath> coloredPaths;
public DynamicSpeedTrackPathPainter (Context context, TrackPathDescriptor trackPathDescriptor) { public DynamicSpeedTrackPathPainter(Context context, TrackPathDescriptor trackPathDescriptor) {
this.trackPathDescriptor = trackPathDescriptor; this.trackPathDescriptor = trackPathDescriptor;
slowPaint = TrackPathUtils.getPaint(context, R.color.slow_path);
selectedTrackPaintSlow = TrackPathUtilities.getPaint(R.color.slow_path, context); mediumPaint = TrackPathUtils.getPaint(context, R.color.normal_path);
selectedTrackPaintMedium = TrackPathUtilities.getPaint(R.color.normal_path, context); fastPaint = TrackPathUtils.getPaint(context, R.color.fast_path);
selectedTrackPaintFast = TrackPathUtilities.getPaint(R.color.fast_path, context); coloredPaths = new ArrayList<ColoredPath>();
this.coloredPaths = new ArrayList<ColoredPath>();
} }
@Override @Override
public void drawTrack(Canvas canvas) { public boolean hasPath() {
for(int i = 0; i < coloredPaths.size(); ++i) { return !coloredPaths.isEmpty();
ColoredPath coloredPath = coloredPaths.get(i); }
canvas.drawPath(coloredPath.getPath(), coloredPath.getPathPaint());
@Override
public boolean updateState() {
return trackPathDescriptor.updateState();
}
@Override
public void updatePath(
Projection projection, Rect viewRect, int startIndex, List<CachedLocation> 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);
} }
} boolean newSegment = !hasLastPoint;
// Assume if last point exists, it is visible
boolean lastPointVisible = hasLastPoint;
int slowSpeed = trackPathDescriptor.getSlowSpeed();
int normalSpeed = trackPathDescriptor.getNormalSpeed();
@Override for (int i = startIndex; i < points.size(); ++i) {
public void updatePath(Projection projection, Rect viewRect, int startLocationIdx, CachedLocation cachedLocation = points.get(i);
Boolean alwaysVisible, List<CachedLocation> 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(); // If not valid, start a new segment
if (!cachedLocation.isValid()) {
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) {
newSegment = true; newSegment = true;
continue; continue;
} }
final GeoPoint geoPoint = loc.geoPoint; GeoPoint geoPoint = cachedLocation.getGeoPoint();
// Check if this breaks the existing segment. // Check if this breaks the existing segment.
boolean visible = alwaysVisible || viewRect.contains( boolean pointVisible = viewRect.contains(geoPoint.getLongitudeE6(), geoPoint.getLatitudeE6());
geoPoint.getLongitudeE6(), geoPoint.getLatitudeE6()); if (!pointVisible && !lastPointVisible) {
if (!visible && !lastVisible) { // This point and the last point are both outside visible area.
// This is a point outside view not connected to a visible one.
newSegment = true; newSegment = true;
} }
lastVisible = visible; lastPointVisible = pointVisible;
// Either move to beginning of a new segment or continue the old one. // Either update point or draw a line from the last point
if (newSegment) { if (newSegment) {
projection.toPixels(geoPoint, pt); projection.toPixels(geoPoint, point);
newSegment = false; newSegment = false;
} else { } else {
ColoredPath coloredPath; ColoredPath coloredPath;
if(loc.speed <= slowSpeed) { if (cachedLocation.getSpeed() <= slowSpeed) {
coloredPath = new ColoredPath(selectedTrackPaintSlow); coloredPath = new ColoredPath(slowPaint);
} } else if (cachedLocation.getSpeed() <= normalSpeed) {
else if(loc.speed <= normalSpeed) { coloredPath = new ColoredPath(mediumPaint);
coloredPath = new ColoredPath(selectedTrackPaintMedium);
} else { } else {
coloredPath = new ColoredPath(selectedTrackPaintFast); coloredPath = new ColoredPath(fastPaint);
} }
coloredPath.getPath().moveTo(pt.x, pt.y); coloredPath.getPath().moveTo(point.x, point.y);
projection.toPixels(geoPoint, pt); projection.toPixels(geoPoint, point);
coloredPath.getPath().lineTo(pt.x, pt.y); coloredPath.getPath().lineTo(point.x, point.y);
coloredPaths.add(coloredPath); coloredPaths.add(coloredPath);
} }
} }
} }
@Override @Override
public void clear() public void clearPath() {
{
coloredPaths.clear(); coloredPaths.clear();
} }
@Override @Override
public boolean needsRedraw() { public void drawPath(Canvas canvas) {
return trackPathDescriptor.needsRedraw(); for (int i = 0; i < coloredPaths.size(); i++) {
} ColoredPath coloredPath = coloredPaths.get(i);
canvas.drawPath(coloredPath.getPath(), coloredPath.getPathPaint());
@Override
public Path getLastPath() {
Path path = new Path();
for(int i = 0; i < coloredPaths.size(); ++i) {
path.addPath(coloredPaths.get(i).getPath());
} }
return path;
} }
/** /**
* Returns coloredPaths. * Gets the colored paths.
*
* @return coloredPaths
*/ */
@VisibleForTesting @VisibleForTesting
List<ColoredPath> getColoredPaths() { List<ColoredPath> getColoredPaths() {
@@ -31,40 +31,36 @@ import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
*/ */
public class FixedSpeedTrackPathDescriptor public class FixedSpeedTrackPathDescriptor
implements TrackPathDescriptor, OnSharedPreferenceChangeListener { implements TrackPathDescriptor, OnSharedPreferenceChangeListener {
private final Context context;
private int slowSpeed; private int slowSpeed;
private int normalSpeed; private int normalSpeed;
private final Context context;
public FixedSpeedTrackPathDescriptor(Context context) { public FixedSpeedTrackPathDescriptor(Context context) {
this.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, slowSpeed = PreferencesUtils.getInt(context, R.string.track_color_mode_slow_key,
PreferencesUtils.TRACK_COLOR_MODE_SLOW_DEFAULT); PreferencesUtils.TRACK_COLOR_MODE_SLOW_DEFAULT);
normalSpeed = PreferencesUtils.getInt(context, R.string.track_color_mode_medium_key, normalSpeed = PreferencesUtils.getInt(context, R.string.track_color_mode_medium_key,
PreferencesUtils.TRACK_COLOR_MODE_MEDIUM_DEFAULT); PreferencesUtils.TRACK_COLOR_MODE_MEDIUM_DEFAULT);
context.getSharedPreferences(Constants.SETTINGS_NAME, Context.MODE_PRIVATE)
.registerOnSharedPreferenceChangeListener(this);
} }
/** @Override
* Gets the slow speed for reference.
*
* @return The speed limit considered as slow.
*/
public int getSlowSpeed() { public int getSlowSpeed() {
return slowSpeed; return slowSpeed;
} }
/** @Override
* Gets the normal speed for reference.
*
* @return The speed limit considered as normal.
*/
public int getNormalSpeed() { public int getNormalSpeed() {
return normalSpeed; return normalSpeed;
} }
@Override
public boolean updateState() {
return false;
}
@Override @Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
if (PreferencesUtils.getKey(context, R.string.track_color_mode_slow_key).equals(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); PreferencesUtils.TRACK_COLOR_MODE_MEDIUM_DEFAULT);
} }
} }
@Override
public boolean needsRedraw() {
return false;
}
} }
@@ -40,88 +40,103 @@ public class SingleColorTrackPathPainter implements TrackPathPainter {
private Path path; private Path path;
public SingleColorTrackPathPainter(Context context) { public SingleColorTrackPathPainter(Context context) {
selectedTrackPaint = TrackPathUtilities.getPaint(R.color.red, context); selectedTrackPaint = TrackPathUtils.getPaint(context, R.color.red);
} }
@Override @Override
public void drawTrack(Canvas canvas) { public boolean hasPath() {
canvas.drawPath(path, selectedTrackPaint); return path != null;
} }
@Override @Override
public void updatePath(Projection projection, Rect viewRect, int startLocationIdx, public boolean updateState() {
Boolean alwaysVisible, List<CachedLocation> points) { return false;
path = newPath(); }
updatePath(projection, viewRect, startLocationIdx, alwaysVisible, points, path);
@Override
public void updatePath(
Projection projection, Rect viewRect, int startIndex, List<CachedLocation> 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. * Updates the path.
* *
* @param projection The Canvas to draw upon. * @param projection the projection
* @param viewRect The Path to be drawn. * @param viewRect the view rectangle
* @param startLocationIdx The start point from where update the path. * @param startIndex the start index
* @param alwaysVisible Flag for always visible. * @param points the points
* @param points The list of points used to update the path. * @param pathToUpdate the path to update
* @param pathToUpdate The path to be created.
*/ */
@VisibleForTesting @VisibleForTesting
void updatePath(Projection projection, Rect viewRect, int startLocationIdx, void updatePath(Projection projection, Rect viewRect, int startIndex, List<CachedLocation> points,
Boolean alwaysVisible, List<CachedLocation> points, Path pathToUpdate) { Path pathToUpdate) {
pathToUpdate.incReserve(points.size()); pathToUpdate.incReserve(points.size() - startIndex);
// 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);
// Check if valid, if not then indicate a new segment. boolean hasLastPoint = startIndex != 0 && points.get(startIndex - 1).isValid();
if (!loc.valid) { 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; newSegment = true;
continue; continue;
} }
final GeoPoint geoPoint = loc.geoPoint; GeoPoint geoPoint = cachedLocation.getGeoPoint();
// Check if this breaks the existing segment. // Check if this breaks the existing segment.
boolean visible = alwaysVisible boolean pointVisible = viewRect.contains(geoPoint.getLongitudeE6(), geoPoint.getLatitudeE6());
|| viewRect.contains(geoPoint.getLongitudeE6(), geoPoint.getLatitudeE6()); if (!pointVisible && !lastPointVisible) {
if (!visible && !lastVisible) { // This point and the last point are both outside visible area.
// This is a point outside view not connected to a visible one.
newSegment = true; newSegment = true;
} }
lastVisible = visible; lastPointVisible = pointVisible;
// Either move to beginning of a new segment or continue the old one. // Either update point or draw a line from the last point
projection.toPixels(geoPoint, pt); projection.toPixels(geoPoint, point);
if (newSegment) { if (newSegment) {
pathToUpdate.moveTo(pt.x, pt.y); pathToUpdate.moveTo(point.x, point.y);
newSegment = false; newSegment = false;
} else { } else {
pathToUpdate.lineTo(pt.x, pt.y); pathToUpdate.lineTo(point.x, point.y);
} }
} }
} }
@Override /**
public void clear() { * Creates a new path.
path = null; */
@VisibleForTesting
protected Path newPath() {
return new Path();
} }
@Override /**
public boolean needsRedraw() { * Gets the path.
return false; */
} @VisibleForTesting
public Path getPath() {
@Override
public Path getLastPath() {
return path; return path;
} }
// Visible for testing
public Path newPath() {
return new Path();
}
} }
@@ -13,6 +13,7 @@
* License for the specific language governing permissions and limitations under * License for the specific language governing permissions and limitations under
* the License. * the License.
*/ */
package com.google.android.apps.mytracks.maps; package com.google.android.apps.mytracks.maps;
/** /**
@@ -21,18 +22,19 @@ package com.google.android.apps.mytracks.maps;
* @author Vangelis S. * @author Vangelis S.
*/ */
public interface TrackPathDescriptor { public interface TrackPathDescriptor {
/**
* @return The maximum speed which is considered slow.
*/
int getSlowSpeed();
/** /**
* @return The maximum speed which is considered normal. * Gets the maximum speed which is considered slow.
*/ */
int getNormalSpeed(); public int getSlowSpeed();
/** /**
* @return True if the path needs to be updated. * Gets the maximum speed which is considered normal.
*/ */
boolean needsRedraw(); public int getNormalSpeed();
/**
* Updates state. Returns true if the state is updated.
*/
public boolean updateState();
} }
@@ -13,13 +13,13 @@
* License for the specific language governing permissions and limitations under * License for the specific language governing permissions and limitations under
* the License. * the License.
*/ */
package com.google.android.apps.mytracks.maps; package com.google.android.apps.mytracks.maps;
import com.google.android.apps.mytracks.MapOverlay.CachedLocation; import com.google.android.apps.mytracks.MapOverlay.CachedLocation;
import com.google.android.maps.Projection; import com.google.android.maps.Projection;
import android.graphics.Canvas; import android.graphics.Canvas;
import android.graphics.Path;
import android.graphics.Rect; import android.graphics.Rect;
import java.util.List; import java.util.List;
@@ -32,37 +32,35 @@ import java.util.List;
public interface TrackPathPainter { public interface TrackPathPainter {
/** /**
* Clears the related data. * Returns true if has path.
*/ */
void clear(); public boolean hasPath();
/** /**
* Draws the path to the canvas. * Updates state. Returns true if the state is updated.
* @param canvas The Canvas to draw upon
*/ */
void drawTrack(Canvas canvas); public boolean updateState();
/** /**
* Updates the path. * Updates the path. Creates a new path if necessary
* @param projection The Canvas to draw upon. *
* @param viewRect The Path to be drawn. * @param projection the projection
* @param startLocationIdx The start point from where update the path. * @param viewRect the view rectangle
* @param alwaysVisible Flag for alwaysvisible. * @param startIndex the start index
* @param points The list of points used to update the path. * @param points the points
*/ */
void updatePath(Projection projection, Rect viewRect, int startLocationIdx, public void updatePath(
Boolean alwaysVisible, List<CachedLocation> points); Projection projection, Rect viewRect, int startIndex, List<CachedLocation> 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);
} }
@@ -13,6 +13,7 @@
* License for the specific language governing permissions and limitations under * License for the specific language governing permissions and limitations under
* the License. * the License.
*/ */
package com.google.android.apps.mytracks.maps; package com.google.android.apps.mytracks.maps;
import static com.google.android.apps.mytracks.Constants.TAG; import static com.google.android.apps.mytracks.Constants.TAG;
@@ -24,32 +25,31 @@ import android.content.Context;
import android.util.Log; import android.util.Log;
/** /**
* A factory for TrackPathPainters. * A factory for {@link TrackPathPainter}.
* *
* @author Vangelis S. * @author Vangelis S.
*/ */
public class TrackPathPainterFactory { public class TrackPathPainterFactory {
private TrackPathPainterFactory() { private TrackPathPainterFactory() {}
}
/** /**
* Get a new TrackPathPainter. * Get a new {@link TrackPathPainter}.
* @param context Context to fetch system preferences. *
* @return The TrackPathPainter that corresponds to the track color mode setting. * @param context the context
*/ */
public static TrackPathPainter getTrackPathPainter(Context context) { public static TrackPathPainter getTrackPathPainter(Context context) {
String trackColorMode = PreferencesUtils.getString(context, R.string.track_color_mode_key, String trackColorMode = PreferencesUtils.getString(context, R.string.track_color_mode_key,
context.getString(R.string.settings_map_track_color_mode_single_value)); context.getString(R.string.settings_map_track_color_mode_single_value));
Log.i(TAG, "Creating track path painter of type: " + trackColorMode); Log.i(TAG, "Creating track path painter of type: " + trackColorMode);
if (context.getString(R.string.settings_map_track_color_mode_fixed_value) if (context.getString(R.string.settings_map_track_color_mode_dynamic_value)
.equals(trackColorMode)) {
return new DynamicSpeedTrackPathPainter(context, new FixedSpeedTrackPathDescriptor(context));
} else if (context.getString(R.string.settings_map_track_color_mode_dynamic_value)
.equals(trackColorMode)) { .equals(trackColorMode)) {
return new DynamicSpeedTrackPathPainter(context, new DynamicSpeedTrackPathDescriptor( return new DynamicSpeedTrackPathPainter(context, new DynamicSpeedTrackPathDescriptor(
context)); context));
} else if (context.getString(R.string.settings_map_track_color_mode_fixed_value)
.equals(trackColorMode)) {
return new DynamicSpeedTrackPathPainter(context, new FixedSpeedTrackPathDescriptor(context));
} else { } else {
return new SingleColorTrackPathPainter(context); return new SingleColorTrackPathPainter(context);
} }
@@ -13,21 +13,30 @@
* License for the specific language governing permissions and limitations under * License for the specific language governing permissions and limitations under
* the License. * the License.
*/ */
package com.google.android.apps.mytracks.maps; package com.google.android.apps.mytracks.maps;
import android.content.Context; import android.content.Context;
import android.graphics.Paint; import android.graphics.Paint;
/** /**
* Various utility functions for TrackPath painting. * Various utility functions for track path painting.
* *
* @author Vangelis S. * @author Vangelis S.
*/ */
public class TrackPathUtilities { public class TrackPathUtils {
public static Paint getPaint(int id, Context context) { 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 paint = new Paint();
paint.setColor(context.getResources().getColor(id)); paint.setColor(context.getResources().getColor(colorId));
paint.setStrokeWidth(3); paint.setStrokeWidth(3);
paint.setStyle(Paint.Style.STROKE); paint.setStyle(Paint.Style.STROKE);
paint.setAntiAlias(true); paint.setAntiAlias(true);
@@ -24,36 +24,29 @@ package com.google.android.apps.mytracks.services;
*/ */
public class AbsoluteLocationListenerPolicy implements LocationListenerPolicy { public class AbsoluteLocationListenerPolicy implements LocationListenerPolicy {
/**
* The interval to request for gps signal.
*/
private final long interval; 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; this.interval = interval;
} }
/** @Override
* @return The interval given in the constructor
*/
public long getDesiredPollingInterval() { public long getDesiredPollingInterval() {
return interval; return interval;
} }
/** @Override
* Discards the idle time.
*/
public void updateIdleTime(long idleTime) {
}
/**
* Returns the minimum distance between updates.
* Get all updates to properly measure moving time.
*/
public int getMinDistance() { public int getMinDistance() {
return 0; return 0;
} }
@Override
public void updateIdleTime(long idleTime) {
// Ignore
}
} }
@@ -16,67 +16,58 @@
package com.google.android.apps.mytracks.services; package com.google.android.apps.mytracks.services;
/** /**
* A LocationListenerPolicy that will change based on how long the user has been * A {@link LocationListenerPolicy} that will change based on how long the user
* stationary. * 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,
* This policy will dictate a policy based on a min, max and idle time. * and is half of the idle time.
* The policy will dictate an interval bounded by min and max whic is half of
* the idle time.
* *
* @author Sandor Dornbush * @author Sandor Dornbush
*/ */
public class AdaptiveLocationListenerPolicy implements LocationListenerPolicy { public class AdaptiveLocationListenerPolicy implements LocationListenerPolicy {
/**
* Smallest interval this policy will dictate, in milliseconds.
*/
private final long minInterval; private final long minInterval;
/**
* Largest interval this policy will dictate, in milliseconds.
*/
private final long maxInterval; private final long maxInterval;
private final int minDistance; private final int minDistance;
/** // The time the user has been idle at the current location, in milliseconds.
* The time the user has been at the current location, in milliseconds.
*/
private long idleTime; private long idleTime;
/** /**
* Creates a policy that will be bounded by the given min and max. * Creates a policy that will be bounded by the given minInterval and
* maxInterval.
* *
* @param min Smallest interval this policy will dictate, in milliseconds * @param minInterval the smallest interval this policy will dictate, in
* @param max Largest interval this policy will dictate, in milliseconds * 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) { public AdaptiveLocationListenerPolicy(long minInterval, long maxInterval, int minDistance) {
this.minInterval = min; this.minInterval = minInterval;
this.maxInterval = max; this.maxInterval = maxInterval;
this.minDistance = minDistance; 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() { public long getDesiredPollingInterval() {
long desiredInterval = idleTime / 2; 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; desiredInterval = (desiredInterval / 1000) * 1000;
return Math.max(Math.min(maxInterval, desiredInterval), return Math.max(Math.min(maxInterval, desiredInterval), minInterval);
minInterval);
} }
public void updateIdleTime(long newIdleTime) { @Override
this.idleTime = newIdleTime;
}
/**
* Returns the minimum distance between updates.
*/
public int getMinDistance() { public int getMinDistance() {
return minDistance; return minDistance;
} }
@Override
public void updateIdleTime(long newIdleTime) {
idleTime = newIdleTime;
}
} }
@@ -17,19 +17,18 @@
package com.google.android.apps.mytracks.services; package com.google.android.apps.mytracks.services;
/** /**
* This is an interface for classes that will manage the location listener policy. * This is an interface for classes that will manage the location listener
* Different policy options are: * policy. Different policy options are: {@link AbsoluteLocationListenerPolicy}
* Absolute * and {@link AdaptiveLocationListenerPolicy}.
* Addaptive
* *
* @author Sandor Dornbush * @author Sandor Dornbush
*/ */
public interface LocationListenerPolicy { public interface LocationListenerPolicy {
/** /**
* Returns the polling time this policy would like at this time. * Returns the polling interval this policy would like at this moment.
* *
* @return The polling that this policy dictates * @return the polling interval
*/ */
public long getDesiredPollingInterval(); public long getDesiredPollingInterval();
@@ -39,10 +38,10 @@ public interface LocationListenerPolicy {
public int getMinDistance(); public int getMinDistance();
/** /**
* Notifies the amount of time the user has been idle at their current * Notifies the amount of time the user has been idle at his current location.
* location.
* *
* @param idleTime The time that the user has been idle at this spot * @param idleTime the time that the user has been idle at his current
* location
*/ */
public void updateIdleTime(long idleTime); public void updateIdleTime(long idleTime);
} }
@@ -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;
}
}
File diff suppressed because it is too large Load Diff
@@ -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".
* <p>
* 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;
}
}
}
@@ -20,18 +20,14 @@ import com.google.android.apps.mytracks.util.ApiAdapterFactory;
import android.content.Context; import android.content.Context;
/** /**
* Factory which wraps construction and setup of text-to-speech announcements in * A {@link PeriodicTaskFactory} for text-to-speech announcement periodic task.
* an API-level-safe way.
* *
* @author Rodrigo Damazio * @author Rodrigo Damazio
*/ */
public class StatusAnnouncerFactory implements PeriodicTaskFactory { public class AnnouncementPeriodicTaskFactory implements PeriodicTaskFactory {
public StatusAnnouncerFactory() {
}
@Override @Override
public PeriodicTask create(Context context) { public PeriodicTask create(Context context) {
return ApiAdapterFactory.getApiAdapter().getStatusAnnouncerTask(context); return ApiAdapterFactory.getApiAdapter().getAnnouncementPeriodicTask(context);
} }
} }
@@ -16,8 +16,6 @@
package com.google.android.apps.mytracks.services.tasks; package com.google.android.apps.mytracks.services.tasks;
import static com.google.android.apps.mytracks.Constants.TAG;
import android.annotation.TargetApi; import android.annotation.TargetApi;
import android.content.Context; import android.content.Context;
import android.media.AudioManager; import android.media.AudioManager;
@@ -27,57 +25,57 @@ import android.util.Log;
import java.util.HashMap; import java.util.HashMap;
/** /**
* This class will periodically announce the user's trip statistics. This class * This class will periodically announce the user's trip statistics. This class
* will request and release audio focus. <br> * will request and release audio focus.
* For API Level 8 or higher.
* *
* @author Sandor Dornbush * @author Sandor Dornbush
*/ */
@TargetApi(8) @TargetApi(8)
public class Api8StatusAnnouncerTask extends StatusAnnouncerTask { public class Api8AnnouncementPeriodicTask extends AnnouncementPeriodicTask {
private final static HashMap<String, String> SPEECH_PARAMS = new HashMap<String, String>();
private static final String TAG = Api8AnnouncementPeriodicTask.class.getSimpleName();
private static final HashMap<String, String> SPEECH_PARAMS = new HashMap<String, String>();
static { static {
SPEECH_PARAMS.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, "not_used"); SPEECH_PARAMS.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, "not_used");
} }
private final AudioManager audioManager; private final OnUtteranceCompletedListener
private final OnUtteranceCompletedListener utteranceListener = utteranceListener = new OnUtteranceCompletedListener() {
new OnUtteranceCompletedListener() { @Override
@Override
public void onUtteranceCompleted(String utteranceId) { public void onUtteranceCompleted(String utteranceId) {
int result = audioManager.abandonAudioFocus(null); int result = audioManager.abandonAudioFocus(null);
if (result == AudioManager.AUDIOFOCUS_REQUEST_FAILED) { 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) { private final AudioManager audioManager;
super(context);
public Api8AnnouncementPeriodicTask(Context context) {
super(context);
audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
} }
@Override @Override
protected void onTtsReady() { protected void onTtsReady() {
super.onTtsReady(); super.onTtsReady();
tts.setOnUtteranceCompletedListener(utteranceListener); tts.setOnUtteranceCompletedListener(utteranceListener);
} }
@Override @Override
protected synchronized void speakAnnouncement(String announcement) { protected synchronized void speakAnnouncement(String announcement) {
int result = audioManager.requestAudioFocus(null, int result = audioManager.requestAudioFocus(
TextToSpeech.Engine.DEFAULT_STREAM, null, TextToSpeech.Engine.DEFAULT_STREAM, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK);
AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK);
if (result == AudioManager.AUDIOFOCUS_REQUEST_FAILED) { 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); tts.speak(announcement, TextToSpeech.QUEUE_FLUSH, SPEECH_PARAMS);
} }
} }
@@ -31,8 +31,10 @@ public interface PeriodicTask {
/** /**
* This method will be called periodically. * 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. * Shuts down this task and clean up resources.
@@ -15,109 +15,156 @@
*/ */
package com.google.android.apps.mytracks.services.tasks; 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.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.PreferencesUtils;
import com.google.android.apps.mytracks.util.UnitConversions; import com.google.android.apps.mytracks.util.UnitConversions;
import android.util.Log; 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 * @author Sandor Dornbush
*/ */
public class PeriodicTaskExecutor { 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. * The task frequency. A positive value is a time frequency (minutes). A
* A value greater than zero is a frequency in time. * negative value is a distance frequency (km or mi). A zero value is to turn
* A value less than zero is considered a frequency in distance. * off periodic task.
*/ */
private int taskFrequency = PreferencesUtils.FREQUENCY_OFF; private int taskFrequency = PreferencesUtils.FREQUENCY_OFF;
/** private PeriodicTask periodicTask;
* The next distance when the task should execute.
*/
private double nextTaskDistance = 0;
/** // Time periodic task executor
* Time based executor. private TimerTaskExecutor timerTaskExecutor = null;
*/
private TimerTaskExecutor timerExecutor = null;
private boolean metricUnits; 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; public PeriodicTaskExecutor(
TrackRecordingService trackRecordingService, PeriodicTaskFactory periodicTaskFactory) {
private PeriodicTask task; this.trackRecordingService = trackRecordingService;
this.periodicTaskFactory = periodicTaskFactory;
public PeriodicTaskExecutor(TrackRecordingService service, PeriodicTaskFactory factory) {
this.service = service;
this.factory = factory;
} }
/** /**
* Restores the manager. * Restores the executor.
*/ */
public void restore() { public void restore() {
// TODO: Decouple service from this class once and forever. if (!trackRecordingService.isRecording()) {
if (!service.isRecording()) { Log.d(TAG, "Not recording.");
return; return;
} }
if (!isTimeFrequency()) { if (!isTimeFrequency()) {
if (timerExecutor != null) { if (timerTaskExecutor != null) {
timerExecutor.shutdown(); timerTaskExecutor.shutdown();
timerExecutor = null; timerTaskExecutor = null;
} }
} }
if (taskFrequency == PreferencesUtils.FREQUENCY_OFF) { if (taskFrequency == PreferencesUtils.FREQUENCY_OFF) {
Log.d(TAG, "Task frequency is off.");
return; return;
} }
// Try to make the task. periodicTask = periodicTaskFactory.create(trackRecordingService);
task = factory.create(service);
// Returning null is ok. // Returning null is ok
if (task == null) { if (periodicTask == null) {
Log.d(TAG, "Peridoic task is null.");
return; return;
} }
task.start(); periodicTask.start();
if (isTimeFrequency()) { if (isTimeFrequency()) {
if (timerExecutor == null) { if (timerTaskExecutor == null) {
timerExecutor = new TimerTaskExecutor(task, service); timerTaskExecutor = new TimerTaskExecutor(periodicTask, trackRecordingService);
} }
timerExecutor.scheduleTask(taskFrequency * 60000L); timerTaskExecutor.scheduleTask(taskFrequency * MINUTE_TO_MILLISECONDS);
} else { } else {
// For distance based splits. // For distance periodic task
calculateNextTaskDistance(); calculateNextTaskDistance();
} }
} }
/** /**
* Shuts down the manager. * Shuts down the executor.
*/ */
public void shutdown() { public void shutdown() {
if (task != null) { if (periodicTask != null) {
task.shutdown(); periodicTask.shutdown();
task = null; periodicTask = null;
} }
if (timerExecutor != null) { if (timerTaskExecutor != null) {
timerExecutor.shutdown(); timerTaskExecutor.shutdown();
timerExecutor = null; timerTaskExecutor = null;
} }
} }
/** /**
* Calculates the next distance when the task should execute. * Updates the executor.
*/ */
void calculateNextTaskDistance() { public void update() {
// TODO: Decouple service from this class once and forever. if (!isDistanceFrequency() || periodicTask == null) {
if (!service.isRecording() || task == 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; return;
} }
@@ -127,7 +174,8 @@ public class PeriodicTaskExecutor {
return; return;
} }
double distance = service.getTripStatistics().getTotalDistance() * UnitConversions.M_TO_KM; double distance = tripStatistics.getTotalDistance()
* UnitConversions.M_TO_KM;
if (!metricUnits) { if (!metricUnits) {
distance *= UnitConversions.KM_TO_MI; distance *= UnitConversions.KM_TO_MI;
} }
@@ -135,56 +183,19 @@ public class PeriodicTaskExecutor {
int index = (int) (distance / taskFrequency); int index = (int) (distance / taskFrequency);
index -= 1; index -= 1;
nextTaskDistance = taskFrequency * index; 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() { private boolean isTimeFrequency() {
return taskFrequency > 0; return taskFrequency > 0;
} }
/**
* True if distance frequency.
*/
private boolean isDistanceFrequency() { private boolean isDistanceFrequency() {
return taskFrequency < 0; return taskFrequency < 0;
} }
/**
* Sets the task frequency.
* &lt; 0 Use the absolute value as a distance in the current measurement km
* or mi
* 0 Turn off the task
* &gt; 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;
}
} }
@@ -13,21 +13,22 @@
* License for the specific language governing permissions and limitations under * License for the specific language governing permissions and limitations under
* the License. * the License.
*/ */
package com.google.android.apps.mytracks.services.tasks; package com.google.android.apps.mytracks.services.tasks;
import android.content.Context; 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 * @author Sandor Dornbush
*/ */
public interface PeriodicTaskFactory { public interface PeriodicTaskFactory {
/** /**
* Creates a periodic task which does voice announcements. * Creates a {@link PeriodicTask}.
* *
* @return the task, or null if task is not supported * @return the task, or null if the task is not supported
*/ */
PeriodicTask create(Context context); public PeriodicTask create(Context context);
} }
@@ -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.content.WaypointCreationRequest;
import com.google.android.apps.mytracks.services.TrackRecordingService; import com.google.android.apps.mytracks.services.TrackRecordingService;
import android.content.Context;
/** /**
* A simple task to insert statistics markers periodically. * A simple task to insert statistics markers periodically.
*
* @author Sandor Dornbush * @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 @Override
public void run(TrackRecordingService service) { public void shutdown() {}
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();
}
}
} }
@@ -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();
}
}
@@ -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".
* <p>
* 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();
}
}
@@ -16,16 +16,13 @@
package com.google.android.apps.mytracks.services.tasks; 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 android.util.Log;
import java.util.Date; import java.util.Date;
import java.util.Timer; import java.util.Timer;
import java.util.TimerTask; import java.util.TimerTask;
import com.google.android.apps.mytracks.services.TrackRecordingService;
/** /**
* This class will periodically perform a task. * This class will periodically perform a task.
* *
@@ -33,29 +30,32 @@ import com.google.android.apps.mytracks.services.TrackRecordingService;
*/ */
public class TimerTaskExecutor { public class TimerTaskExecutor {
private final PeriodicTask task; private final PeriodicTask periodicTask;
private final TrackRecordingService service; private final TrackRecordingService trackRecordingService;
/** /**
* A timer to schedule the announcements. * A timer to schedule the announcements. This is non-null if the task is in
* This is non-null if the task is in started (scheduled) state. * started (scheduled) state.
*/ */
private Timer timer; private Timer timer;
public TimerTaskExecutor(PeriodicTask task, public TimerTaskExecutor(PeriodicTask periodicTask, TrackRecordingService trackRecordingService) {
TrackRecordingService service) { this.periodicTask = periodicTask;
this.task = task; this.trackRecordingService = trackRecordingService;
this.service = service;
} }
/** /**
* Schedules the task at the given interval. * Schedules the periodic task at an interval.
* *
* @param interval The interval in milliseconds * @param interval the interval in milliseconds
*/ */
public void scheduleTask(long interval) { public void scheduleTask(long interval) {
// TODO: Decouple service from this class once and forever. if (!trackRecordingService.isRecording()) {
if (!service.isRecording()) { return;
}
TripStatistics tripStatistics = trackRecordingService.getTripStatistics();
if (tripStatistics == null) {
return; return;
} }
@@ -64,7 +64,7 @@ public class TimerTaskExecutor {
timer.purge(); timer.purge();
} else { } else {
// First start, or we were previously shut down. // First start, or we were previously shut down.
task.start(); periodicTask.start();
} }
timer = new Timer(); timer = new Timer();
@@ -73,27 +73,22 @@ public class TimerTaskExecutor {
} }
long now = System.currentTimeMillis(); long now = System.currentTimeMillis();
long next = service.getTripStatistics().getStartTime(); long next = tripStatistics.getStartTime();
if (next < now) { if (next < now) {
next = now + interval - ((now - next) % interval); next = now + interval - ((now - next) % interval);
} }
timer.scheduleAtFixedRate(new PeriodicTimerTask(), new Date(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);
} }
/** /**
* Cleans up this object. * Shuts down.
*/ */
public void shutdown() { public void shutdown() {
Log.i(TAG, task.getClass().getSimpleName() + " shutting down.");
if (timer != null) { if (timer != null) {
timer.cancel(); timer.cancel();
timer.purge(); timer.purge();
timer = null; timer = null;
task.shutdown(); periodicTask.shutdown();
} }
} }
@@ -101,9 +96,10 @@ public class TimerTaskExecutor {
* The timer task to announce the trip status. * The timer task to announce the trip status.
*/ */
private class PeriodicTimerTask extends TimerTask { private class PeriodicTimerTask extends TimerTask {
@Override @Override
public void run() { public void run() {
task.run(service); periodicTask.run(trackRecordingService);
} }
} }
} }
@@ -25,61 +25,30 @@ package com.google.android.apps.mytracks.stats;
*/ */
public class DoubleBuffer { public class DoubleBuffer {
/** // The location that the next write will occur at.
* The location that the next write will occur at.
*/
private int index; private int index;
/** // The sliding buffer of doubles.
* The sliding buffer of doubles.
*/
private final double[] buffer; private final double[] buffer;
/** // True if the buffer is full
* Have all of the slots in the buffer been filled?
*/
private boolean isFull; private boolean isFull;
/** /**
* Creates a buffer with size elements. * Creates a buffer with a certain size.
* *
* @param size the number of elements in the buffer * @param size the size
* @throws IllegalArgumentException if the size is not a positive value
*/ */
public DoubleBuffer(int size) { public DoubleBuffer(int size) {
if (size < 1) { 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]; buffer = new double[size];
reset(); reset();
} }
/** /**
* Adds a double to the buffer. If the buffer is full the oldest element is * Resets the buffer.
* 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.
*/ */
public void reset() { public void reset() {
index = 0; index = 0;
@@ -87,16 +56,20 @@ public class DoubleBuffer {
} }
/** /**
* Gets the average of values from the buffer. * Returns true if the buffer is full.
* */
* @return The average of the buffer public boolean isFull() {
return isFull;
}
/**
* Gets the average of the buffer.
*/ */
public double getAverage() { public double getAverage() {
int numberOfEntries = isFull ? buffer.length : index; int numberOfEntries = isFull ? buffer.length : index;
if (numberOfEntries == 0) { if (numberOfEntries == 0) {
return 0; return 0;
} }
double sum = 0; double sum = 0;
for (int i = 0; i < numberOfEntries; i++) { for (int i = 0; i < numberOfEntries; i++) {
sum += buffer[i]; sum += buffer[i];
@@ -105,15 +78,15 @@ public class DoubleBuffer {
} }
/** /**
* Gets the average and standard deviation of the buffer. * Gets the average and the variance of the buffer.
* *
* @return An array of two elements - the first is the average, and the second * @return an array of two elements - the first is the average, the second is
* is the variance * the variance
*/ */
public double[] getAverageAndVariance() { public double[] getAverageAndVariance() {
int numberOfEntries = isFull ? buffer.length : index; int numberOfEntries = isFull ? buffer.length : index;
if (numberOfEntries == 0) { if (numberOfEntries == 0) {
return new double[]{0, 0}; return new double[] { 0, 0 };
} }
double sum = 0; double sum = 0;
@@ -124,8 +97,25 @@ public class DoubleBuffer {
} }
double average = sum / numberOfEntries; double average = sum / numberOfEntries;
return new double[]{average, double variance = sumSquares / numberOfEntries - Math.pow(average, 2);
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 @Override
@@ -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.Constants;
import com.google.android.apps.mytracks.util.PreferencesUtils; import com.google.android.apps.mytracks.util.PreferencesUtils;
import com.google.common.annotations.VisibleForTesting;
import android.location.Location; import android.location.Location;
import android.util.Log; 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 Sandor Dornbush
* @author Rodrigo Damazio * @author Rodrigo Damazio
*/ */
public class TripStatisticsBuilder { public class TripStatisticsBuilder {
/**
* Statistical data about the trip, which can be displayed to the user.
*/
private final TripStatistics data;
/** // The trip statistics.
* The last location that the gps reported. private final TripStatistics tripStatistics;
*/
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 minimum recording distance.
private int minRecordingDistance = PreferencesUtils.MIN_RECORDING_DISTANCE_DEFAULT; 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) { public TripStatisticsBuilder(long startTime) {
data = new TripStatistics(); tripStatistics = new TripStatistics();
resumeAt(startTime); resumeAt(startTime);
} }
/** /**
* Creates a new trip, starting with existing statistics data. * Creates a new trip, starting with an existing {@link TripStatistics}.
* *
* @param statsData the statistics data to copy and start from * @param other the existing {@link TripStatistics}
*/ */
public TripStatisticsBuilder(TripStatistics statsData) { public TripStatisticsBuilder(TripStatistics other) {
data = new TripStatistics(statsData); tripStatistics = new TripStatistics(other);
if (data.getStartTime() > 0) { if (tripStatistics.getStartTime() > 0) {
resumeAt(data.getStartTime()); resumeAt(tripStatistics.getStartTime());
} }
} }
/** /**
* Adds a location to the current trip. This will update all of the internal * Sets the min recording distance.
* variables with this new location.
* *
* @param currentLocation the current gps location * @param minRecordingDistance the min recording distance
* @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
*/ */
public boolean addLocation(Location currentLocation, long systemTime) { public void setMinRecordingDistance(int minRecordingDistance) {
if (paused) { this.minRecordingDistance = minRecordingDistance;
Log.w(TAG,
"Tried to account for location while track is paused");
return false;
}
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. * Resumes the current track at a given time.
*/
private void updateBounds(Location location) {
data.updateLatitudeExtremities(location.getLatitude());
data.updateLongitudeExtremities(location.getLongitude());
}
/**
* Updates the elevation measurements.
* *
* @param elevation the current elevation * @param time the time
*/ */
// @VisibleForTesting public void resumeAt(long time) {
double updateElevation(double elevation) { if (!paused) {
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;
}
/**
* Updates the speed measurements.
*
* @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
*/
// @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) {
return; return;
} }
currentGrade = elevationDifference / smoothedDistance;
gradeBuffer.setNext(currentGrade); tripStatistics.setStartTime(time);
data.updateGradeExtremities(gradeBuffer.getAverage()); 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 * @param time the time to pause at
*/ */
public void pauseAt(long time) { public void pauseAt(long time) {
if (paused) { return; } if (paused) {
return;
data.setStopTime(time); }
data.setTotalTime(time - data.getStartTime()); tripStatistics.setStopTime(time);
lastLocation = null; // Make sure the counter restarts. // TODO: total time needs to take into account pauses
tripStatistics.setTotalTime(time - tripStatistics.getStartTime());
paused = true; paused = true;
} }
/** /**
* Resumes the current track at the given time. * Gets the trip statistics.
*
* @param time the time to resume at
*/ */
public void resumeAt(long time) { public TripStatistics getTripStatistics() {
if (!paused) { return; } // Take a snapshot - we don't want anyone messing with our internals
return new TripStatistics(tripStatistics);
// 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
+ "}";
} }
/** /**
* 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() { public long getIdleTime() {
if (lastLocation == null || lastMovingLocation == null) if (lastLocation == null || lastMovingLocation == null) {
return 0; return 0;
}
return lastLocation.getTime() - lastMovingLocation.getTime(); return lastLocation.getTime() - lastMovingLocation.getTime();
} }
/** /**
* Gets the current elevation smoothed over several readings. The elevation * Gets the smoothed elevation over several readings. The elevation readings
* data is very noisy so it is better to use the smoothed elevation than the * is noisy so the smoothed elevation is better than the raw elevation for
* raw elevation for many tasks. * many tasks.
*
* @return The elevation smoothed over several readings
*/ */
public double getSmoothedElevation() { public double getSmoothedElevation() {
return elevationBuffer.getAverage(); return elevationBuffer.getAverage();
} }
public TripStatistics getStatistics() { /**
// Take a snapshot - we don't want anyone messing with our internals * Adds a location. This will update all of the internal variables with this
return new TripStatistics(data); * 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);
} }
} }
@@ -19,8 +19,8 @@ import com.google.android.apps.mytracks.Constants;
import com.google.android.apps.mytracks.ContextualActionModeCallback; import com.google.android.apps.mytracks.ContextualActionModeCallback;
import com.google.android.apps.mytracks.io.backup.BackupPreferencesListener; 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.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.PeriodicTask;
import com.google.android.apps.mytracks.services.tasks.StatusAnnouncerTask;
import com.google.api.client.http.HttpTransport; import com.google.api.client.http.HttpTransport;
import com.google.api.client.http.apache.ApacheHttpTransport; import com.google.api.client.http.apache.ApacheHttpTransport;
@@ -50,8 +50,8 @@ import java.util.List;
public class Api7Adapter implements ApiAdapter { public class Api7Adapter implements ApiAdapter {
@Override @Override
public PeriodicTask getStatusAnnouncerTask(Context context) { public PeriodicTask getAnnouncementPeriodicTask(Context context) {
return new StatusAnnouncerTask(context); return new AnnouncementPeriodicTask(context);
} }
@Override @Override
@@ -15,9 +15,9 @@
*/ */
package com.google.android.apps.mytracks.util; 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.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 com.google.android.apps.mytracks.services.tasks.PeriodicTask;
import android.content.Context; import android.content.Context;
@@ -30,8 +30,8 @@ import android.content.Context;
public class Api8Adapter extends Api7Adapter { public class Api8Adapter extends Api7Adapter {
@Override @Override
public PeriodicTask getStatusAnnouncerTask(Context context) { public PeriodicTask getAnnouncementPeriodicTask(Context context) {
return new Api8StatusAnnouncerTask(context); return new Api8AnnouncementPeriodicTask(context);
} }
@Override @Override
@@ -42,13 +42,13 @@ import java.util.List;
public interface ApiAdapter { public interface ApiAdapter {
/** /**
* Gets a status announcer task. * Gets an announcement periodic task.
* <p> * <p>
* Due to changes in API level 8. * Due to changes in API level 8.
* *
* @param context the context * @param context the context
*/ */
public PeriodicTask getStatusAnnouncerTask(Context context); public PeriodicTask getAnnouncementPeriodicTask(Context context);
/** /**
* Gets a {@link BackupPreferencesListener}. * Gets a {@link BackupPreferencesListener}.
@@ -1,4 +1,4 @@
#Fri Oct 21 20:06:28 BRST 2011 #Tue Jul 17 10:23:25 PDT 2012
eclipse.preferences.version=1 eclipse.preferences.version=1
org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 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.problem.varargsArgumentNeedCast=warning
org.eclipse.jdt.core.compiler.source=1.6 org.eclipse.jdt.core.compiler.source=1.6
org.eclipse.jdt.core.formatter.align_type_members_on_columns=false 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=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=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=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=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=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=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_assignment=0
org.eclipse.jdt.core.formatter.alignment_for_binary_expression=16 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_compact_if=16
org.eclipse.jdt.core.formatter.alignment_for_conditional_expression=80 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=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=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_method_declaration=0
org.eclipse.jdt.core.formatter.alignment_for_multiple_fields=16 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=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=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_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=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_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=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=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=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=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.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_imports=1
org.eclipse.jdt.core.formatter.blank_lines_after_package=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_block_boundaries=true
org.eclipse.jdt.core.formatter.comment.new_lines_at_javadoc_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.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.compact_else_if=true
org.eclipse.jdt.core.formatter.continuation_indentation=2 org.eclipse.jdt.core.formatter.continuation_indentation=2
org.eclipse.jdt.core.formatter.continuation_indentation_for_array_initializer=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.disabling_tag=@formatter\:off
org.eclipse.jdt.core.formatter.enabling_tag=@formatter\:on 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.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_annotation_declaration_header=true
org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_constant_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_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_arguments=insert
org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_parameters=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_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_parameterized_type_reference=do not insert
org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_arguments=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.join_wrapped_lines=true
org.eclipse.jdt.core.formatter.keep_else_statement_on_same_line=false 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_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.keep_then_statement_on_same_line=false
org.eclipse.jdt.core.formatter.lineSplit=100 org.eclipse.jdt.core.formatter.lineSplit=100
org.eclipse.jdt.core.formatter.never_indent_block_comments_on_first_column=false 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_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.number_of_empty_lines_to_preserve=1
org.eclipse.jdt.core.formatter.put_empty_statement_on_new_line=true 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.char=space
org.eclipse.jdt.core.formatter.tabulation.size=2 org.eclipse.jdt.core.formatter.tabulation.size=2
org.eclipse.jdt.core.formatter.use_on_off_tags=false 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.use_tabs_only_for_leading_indentations=false
org.eclipse.jdt.core.formatter.wrap_before_binary_operator=true 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_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_outer_expressions_when_nested=true
org.eclipse.jdt.core.formatter.wrap_prefer_two_fragments=false
@@ -1,8 +1,8 @@
#Thu Jul 21 03:05:33 BRT 2011 #Tue Jul 17 10:22:31 PDT 2012
eclipse.preferences.version=1 eclipse.preferences.version=1
editor_save_participant_org.eclipse.jdt.ui.postsavelistener.cleanup=true editor_save_participant_org.eclipse.jdt.ui.postsavelistener.cleanup=true
formatter_profile=_MyTracks formatter_profile=_My Tracks
formatter_settings_version=12 formatter_settings_version=13
org.eclipse.jdt.ui.ignorelowercasenames=true org.eclipse.jdt.ui.ignorelowercasenames=true
org.eclipse.jdt.ui.importorder=com;android;java;javax;org; org.eclipse.jdt.ui.importorder=com;android;java;javax;org;
org.eclipse.jdt.ui.ondemandthreshold=99 org.eclipse.jdt.ui.ondemandthreshold=99
@@ -15,6 +15,8 @@
*/ */
package com.google.android.apps.mytracks.content; package com.google.android.apps.mytracks.content;
import com.google.android.apps.mytracks.stats.TripStatistics;
import java.util.Vector; import java.util.Vector;
/** /**
@@ -37,9 +39,9 @@ public interface DescriptionGenerator {
Track track, Vector<Double> distances, Vector<Double> elevations, boolean html); Track track, Vector<Double> distances, Vector<Double> 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);
} }
@@ -13,458 +13,427 @@
* License for the specific language governing permissions and limitations under * License for the specific language governing permissions and limitations under
* the License. * the License.
*/ */
package com.google.android.apps.mytracks.content; package com.google.android.apps.mytracks.content;
import android.content.ContentValues; import android.content.ContentValues;
import android.content.Context; import android.content.Context;
import android.database.Cursor; import android.database.Cursor;
import android.location.Location; import android.location.Location;
import android.location.LocationManager;
import android.net.Uri; import android.net.Uri;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; 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 * @author Rodrigo Damazio
*/ */
public interface MyTracksProviderUtils { 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"; public static final String AUTHORITY = "com.google.android.maps.mytracks";
/** /**
* Deletes all tracks (including track points) from the provider. * Gets a track cursor. The caller owns the returned cursor and is responsible
*/ * for closing it.
void deleteAllTracks();
/**
* Deletes a track with the given track id.
* *
* @param trackId the unique track id * @param selection the selection
* @param selectionArgs the selection arguments
* @param sortOrder the sort order
*/ */
void deleteTrack(long trackId); public Cursor getTracksCursor(String selection, String[] selectionArgs, String sortOrder);
/** /**
* Deletes a way point with the given way point id. * Gets all the tracks. If no track exists, an empty list is returned.
* This will also correct the next statistics way point after the deleted one * <p>
* to reflect the deletion. * Note that the returned tracks do not have any track points attached.
* The generator is needed to stitch together statistics waypoints. */
public List<Track> getAllTracks();
/**
* Gets a track by a track id. Returns null if not found.
* <p>
* Note that the returned track doesn't have any track points attached.
* *
* @param waypointId the unique way point id * @param trackId the track id.
* @param descriptionGenerator the class to generate descriptions
*/ */
void deleteWaypoint(long waypointId, public Track getTrack(long trackId);
DescriptionGenerator descriptionGenerator);
/** /**
* Finds the next statistics waypoint after the given waypoint. * Gets the last track. Returns null if doesn't exist.
*/
public Track getLastTrack();
/**
* Gets the last track id. Returns -1L if doesn't exist.
*/
public long getLastTrackId();
/**
* Returns true if a track exists.
* *
* @param waypoint a given waypoint * @param trackId the track id
* @return the next statistics waypoint, or null if none found.
*/ */
Waypoint getNextStatisticsWaypointAfter(Waypoint waypoint); public boolean trackExists(long trackId);
/** /**
* Updates the waypoint in the provider. * Inserts a track.
* <p>
* Note: This doesn't insert any track points.
* *
* @param waypoint * @param track the track
* @return true if successful * @return the content provider URI of the inserted track.
*/ */
boolean updateWaypoint(Waypoint waypoint); public Uri insertTrack(Track track);
/** /**
* Finds the first recorded location from the location provider. * Updates a track.
* <p>
* Note: This doesn't update any track points.
* *
* @return the first location, or null if no locations available * @param track the track
*/ */
Location getFirstLocation(); public void updateTrack(Track track);
/** /**
* Finds the last recorded location from the location provider. * Deletes all tracks (including waypoints and track points).
*/
public void deleteAllTracks();
/**
* Deletes a track.
* *
* @return the last location, or null if no locations available * @param trackId the track id
*/ */
Location getLastLocation(); public void deleteTrack(long trackId);
/** /**
* Finds the first recorded waypoint for a given track from the location * Creates a {@link Track} from a cursor.
* provider.
* This is a special waypoint that holds the stats for current segment.
* *
* @param trackId the id of the track the waypoint belongs to * @param cursor the cursor pointing to the track
* @return the first waypoint, or null if no waypoints available
*/ */
Waypoint getFirstWaypoint(long trackId); public Track createTrack(Cursor cursor);
/** /**
* Finds the given waypoint from the location provider. * Creates a {@link ContentValues} from a track.
* *
* @param waypointId * @param track the track
* @return the waypoint, or null if it does not exist
*/ */
Waypoint getWaypoint(long waypointId); public ContentValues createContentValues(Track track);
/** /**
* Finds the last recorded location id from the track points provider. * Gets a waypoint cursor. The caller owns the returned cursor and is
* responsible for closing it.
* *
* @param trackId find last location on this track * @param selection the selection
* @return the location id, or -1 if no locations available * @param selectionArgs the selection arguments
* @param sortOrder the sort order
* @param maxWaypoints the maximum number of waypoints to return
*/ */
long getLastLocationId(long trackId); public Cursor getWaypointsCursor(
String selection, String[] selectionArgs, String sortOrder, int maxWaypoints);
/** /**
* Finds the id of the 1st waypoint for a given track. * Gets a waypoint cursor for a track. The caller owns the returned cursor and
* The 1st waypoint is special as it contains the stats for the current * is responsible for closing it.
* segment.
* *
* @param trackId find last location on this track * @param trackId the track id
* @return the waypoint id, or -1 if no waypoints are available * @param minWaypointId the minimum waypoint id
* @param maxWaypoints the maximum number of waypoints to return
*/ */
long getFirstWaypointId(long trackId); public Cursor getWaypointsCursor(long trackId, long minWaypointId, int maxWaypoints);
/** /**
* Finds the id of the 1st waypoint for a given track. * Gets the first recorded waypoint for a track. The first waypoint is special
* The 1st waypoint is special as it contains the stats for the current * as it contains the stats for the current segment. Returns null if it
* segment. * doesn't exist.
* *
* @param trackId find last location on this track * @param trackId the track id
* @return the waypoint id, or -1 if no waypoints are available
*/ */
long getLastWaypointId(long trackId); public Waypoint getFirstWaypoint(long trackId);
/** /**
* Gets the next marker number. * 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);
/**
* Gets the next marker number. Returns -1 if not able to get the next marker
* number.
* *
* @param trackId the track id * @param trackId the track id
* @param statistics true for statistics marker, false for waypoint marker * @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 int getNextMarkerNumber(long trackId, boolean statistics);
/** /**
* Finds the last recorded track from the track provider. * Gets the next statistics waypoint after the given waypoint. Returns null if
* it doesn't exists.
* *
* @return the last track, or null if no tracks available * @param waypoint the given waypoint
*/ */
Track getLastTrack(); public Waypoint getNextStatisticsWaypointAfter(Waypoint waypoint);
/** /**
* Finds the last recorded track id from the tracks provider. * Gets a waypoint from a waypoint id. Returns null if not found.
* *
* @return the track id, or -1 if no tracks available * @param waypointId the waypoint id
*/ */
long getLastTrackId(); public Waypoint getWaypoint(long waypointId);
/** /**
* Finds a location by given unique id. * Inserts a waypoint.
* *
* @param id the desired id * @param waypoint the waypoint
* @return a Location object, or null if not found * @return the content provider URI of the inserted waypoint.
*/ */
Location getLocation(long id); public Uri insertWaypoint(Waypoint waypoint);
/** /**
* Creates a cursor over the locations in the track points provider which * Updates a waypoint. Returns true if successful.
* 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 waypoint the waypoint
* @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
*/ */
Cursor getLocationsCursor(long trackId, long minTrackPointId, public boolean updateWaypoint(Waypoint waypoint);
int maxLocations, boolean descending);
/** /**
* Creates a cursor over the waypoints of a track. * Deletes a waypoint. If deleting a statistics waypoint, this will also
* Caller owns the returned cursor and is responsible for closing it. * correct the next statistics waypoint after the deleted one to reflect the
* deletion. The generator is used to update the next statistics waypoint.
* *
* @param trackId the id of the track for which to get the points * @param waypointId the waypoint id
* @param minWaypointId the minimum id for the track points * @param descriptionGenerator the description generator
* @param maxWaypoints the maximum number of waypoints to return
* @return A cursor over the selected range of locations
*/ */
Cursor getWaypointsCursor(long trackId, long minWaypointId, public void deleteWaypoint(long waypointId, DescriptionGenerator descriptionGenerator);
int maxWaypoints);
/** /**
* Creates a cursor over waypoints with the given selection. * Creates a waypoint from a cursor.
* Caller owns the returned cursor and is responsible for closing it.
* *
* @param selection a given selection * @param cursor the cursor pointing to the waypoint
* @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
*/ */
Cursor getWaypointsCursor(String selection, String[] selectionArgs, String order, int maxWaypoints); public Waypoint createWaypoint(Cursor cursor);
/** /**
* Finds a track by given unique track id. * Gets the first recorded location. Returns null if it doesn't exist.
* 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
*/ */
Track getTrack(long id); public Location getFirstLocation();
/** /**
* Retrieves all tracks without track points. If no tracks exist, an empty * Gets the last recorded location. Returns null if it doesn't exist.
* list will be returned. Use {@link #getLocationIterator(long, long, boolean, LocationFactory)}
* to load the track points.
*
* @return a list of all the recorded tracks
*/ */
List<Track> getAllTracks(); public Location getLastLocation();
/** /**
* Creates a cursor over the tracks provider with a given selection. * Gets a location by track point id. Returns null if not found.
* Caller owns the returned cursor and is responsible for closing it.
* *
* @param selection a given selection * @param trackPointId the track point id
* @param selectionArgs parameters for the given selection
* @param order the order to return results in
* @return a cursor of the selected tracks
*/ */
Cursor getTracksCursor(String selection, String[] selectionArgs, String order); public Location getLocation(long trackPointId);
/** /**
* Inserts a track in the tracks provider. * Creates a location cursor. The caller owns the returned cursor and is
* Note: This does not insert any track points. * responsible for closing it.
* Use {@link #insertTrackPoint(Location, long)} to insert them.
* *
* @param track the track to insert * @param trackId the track id
* @return the content provider URI for the inserted track * @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 insertTrack(Track track); public Cursor getLocationsCursor(
long trackId, long startTrackPointId, int maxLocations, boolean descending);
/** /**
* Inserts a track point in the tracks provider. * 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 location the location to insert * @param trackId the track id
* @return the content provider URI for the inserted track point * @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
*/ */
Uri insertTrackPoint(Location location, long trackId); public LocationIterator getLocationIterator(
long trackId, long startTrackPointId, boolean descending, LocationFactory locationFactory);
/** /**
* Inserts multiple track points in a single operation. * Gets the last location id for a track. Returns -1L if it doesn't exist.
* *
* @param locations an array of locations to insert * @param trackId the track id
* @param length the number of locations (from the beginning of the array) */
* to actually insert, or -1 for all of them public long getLastTrackLocationId(long trackId);
* @param trackId the ID of the track to insert the points into
/**
* 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 * @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. * Fills a location from a cursor.
* *
* @param waypoint the waypoint to insert * @param cursor the cursor pointing to the location
* @return the content provider URI for the inserted track * @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. * Creates a location object from a cursor.
* *
* @param id the unique id * @param cursor the cursor pointing to the location
* @return true if track exists
*/ */
boolean trackExists(long id); public Location createLocation(Cursor cursor);
/** /**
* Updates a track in the content provider. * A lightweight wrapper around the original {@link Cursor} with a method to
* Note: This will not update any track points. * clean up.
*
* @param track a given track
*/ */
void updateTrack(Track track); public interface LocationIterator extends Iterator<Location> {
/**
* 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<Location> {
/**
* 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();
/** /**
* Should be called in case the underlying iterator hasn't reached the last record. * Gets the most recently retrieved track point id by {@link #next()}.
* Calling it if it has reached the last record is a no-op.
*/ */
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. * Creates a new {@link Location}. An implementation can create new
* It's up to the implementing class to decide whether to create a new instance or reuse * instances or reuse existing instances for optimization.
* existing to optimize for speed.
*
* @return a {@link Location} to be populated from the database.
*/ */
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() { public LocationFactory DEFAULT_LOCATION_FACTORY = new LocationFactory() {
@Override @Override
public Location createLocation() { 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, * A {@link LocationFactory} which uses two location instances (one for the
* and one for the previous), useful when we need to keep the last location. * current location and one for the previous), useful when needing to keep the
* last location.
*/ */
public class DoubleBufferedLocationFactory implements LocationFactory { public class DoubleBufferedLocationFactory implements LocationFactory {
private final Location locs[] = new MyTracksLocation[] {
new MyTracksLocation("gps"), private final Location locations[] = new MyTracksLocation[] {
new MyTracksLocation("gps") new MyTracksLocation(LocationManager.GPS_PROVIDER),
}; new MyTracksLocation(LocationManager.GPS_PROVIDER) };
private int lastLoc = 0; private int lastLocation = 0;
@Override @Override
public Location createLocation() { public Location createLocation() {
lastLoc = (lastLoc + 1) % locs.length; lastLocation = (lastLocation + 1) % locations.length;
return locs[lastLoc]; return locations[lastLocation];
} }
} }
/** /**
* Creates a new read-only iterator over all track points for the given track. It provides * A factory which can produce instances of {@link MyTracksProviderUtils}, and
* a lightweight way of iterating over long tracks without failing due to the underlying cursor * can be overridden for testing.
* 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:
* <code>
* ...
* LocationIterator it = providerUtils.getLocationIterator(
* 1, MyTracksProviderUtils.DEFAULT_LOCATION_FACTORY);
* try {
* for (Location loc : it) {
* ... // Do something useful with the location.
* }
* } finally {
* it.close();
* }
* ...
* </code>
*
* @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).
*/ */
public static class Factory { public static class Factory {
private static Factory instance = new Factory(); private static Factory instance = new Factory();
/** /**
* Creates and returns an instance of {@link MyTracksProviderUtils} which * Creates an instance of {@link MyTracksProviderUtils}.
* uses the given context to access its data. *
* @param context the context
*/ */
public static MyTracksProviderUtils get(Context context) { public static MyTracksProviderUtils get(Context context) {
return instance.newForContext(context); return instance.newForContext(context);
} }
/** /**
* Returns the global instance of this factory. * Returns the factory instance.
*/ */
public static Factory getInstance() { public static Factory getInstance() {
return instance; return instance;
} }
/** /**
* Overrides the global instance for this factory, to be used for testing. * Overrides the factory instance for testing. Don't forget to set it back
* If used, don't forget to set it back to the original value after the * to the original value after testing.
* test is run. *
* @param factory the factory
*/ */
public static void overrideInstance(Factory factory) { public static void overrideInstance(Factory factory) {
instance = 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) { protected MyTracksProviderUtils newForContext(Context context) {
return new MyTracksProviderUtilsImpl(context.getContentResolver()); return new MyTracksProviderUtilsImpl(context.getContentResolver());
@@ -33,22 +33,39 @@ public final class Waypoint implements Parcelable {
public static final int TYPE_WAYPOINT = 0; public static final int TYPE_WAYPOINT = 0;
public static final int TYPE_STATISTICS = 1; public static final int TYPE_STATISTICS = 1;
private long id = -1; private long id = -1L;
private String name = ""; private String name = "";
private String description = ""; private String description = "";
private String category = ""; private String category = "";
private String icon = ""; private String icon = "";
private long trackId = -1; private long trackId = -1L;
private int type = 0; private int type = 0;
private double length = 0.0; private double length = 0.0;
private long duration = 0; private long duration = 0;
private long startId = -1; private long startId = -1L;
private long stopId = -1; private long stopId = -1L;
private Location location = null; private Location location = null;
private TripStatistics tripStatistics = null; private TripStatistics tripStatistics = null;
public Waypoint() {} 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) { private Waypoint(Parcel source) {
id = source.readLong(); id = source.readLong();
name = source.readString(); name = source.readString();
@@ -23,14 +23,10 @@ package com.google.android.apps.mytracks.stats;
*/ */
public class ExtremityMonitor { public class ExtremityMonitor {
/** // The smallest value seen so far.
* The smallest value seen so far.
*/
private double min; private double min;
/** // The largest value seen so far.
* The largest value seen so far.
*/
private double max; private double max;
public ExtremityMonitor() { public ExtremityMonitor() {
@@ -38,9 +34,31 @@ public class ExtremityMonitor {
} }
/** /**
* Updates the min and the max with the new value. * 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 for the monitor * @param value the new value
* @return true if an extremity was found * @return true if an extremity was found
*/ */
public boolean update(double value) { public boolean update(double value) {
@@ -56,34 +74,11 @@ public class ExtremityMonitor {
return changed; 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. * Sets the minimum and maximum values.
*
* @param min the minimum value
* @param max the maximum value
*/ */
public void set(double min, double max) { public void set(double min, double max) {
this.min = min; this.min = min;
@@ -92,6 +87,8 @@ public class ExtremityMonitor {
/** /**
* Sets the minimum value. * Sets the minimum value.
*
* @param min the minimum value
*/ */
public void setMin(double min) { public void setMin(double min) {
this.min = min; this.min = min;
@@ -99,14 +96,18 @@ public class ExtremityMonitor {
/** /**
* Sets the maximum value. * Sets the maximum value.
*
* @param max the maximum value
*/ */
public void setMax(double max) { public void setMax(double max) {
this.max = max; this.max = max;
} }
/**
* Returns true if has data.
*/
public boolean hasData() { public boolean hasData() {
return min != Double.POSITIVE_INFINITY return min != Double.POSITIVE_INFINITY && max != Double.NEGATIVE_INFINITY;
&& max != Double.NEGATIVE_INFINITY;
} }
@Override @Override
@@ -13,85 +13,59 @@
* License for the specific language governing permissions and limitations under * License for the specific language governing permissions and limitations under
* the License. * the License.
*/ */
package com.google.android.apps.mytracks.stats; package com.google.android.apps.mytracks.stats;
import android.os.Parcel; import android.os.Parcel;
import android.os.Parcelable; import android.os.Parcelable;
/** /**
* Statistical data about a trip. * Statistical data about a trip. The data in this class should be filled out by
* The data in this class should be filled out by TripStatisticsBuilder. * TripStatisticsBuilder.
* * <p>
* TODO: hashCode and equals * TODO: hashCode and equals
* *
* @author Rodrigo Damazio * @author Rodrigo Damazio
*/ */
public class TripStatistics implements Parcelable { public class TripStatistics implements Parcelable {
/** // The trip start time. This is the system time, might not match the GPs time.
* The start time for the trip. This is system time which might not match gps
* time.
*/
private long startTime = -1L; private long startTime = -1L;
/** // The trip stop time. This is the system time, might not match the GPS time.
* The stop time for the trip. This is the system time which might not match
* gps time.
*/
private long stopTime = -1L; private long stopTime = -1L;
/** // The total trip distance (meters).
* 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.
*/
private double totalDistance; private double totalDistance;
/** // The total time (ms). Updated when new points are received, may be stale.
* The total elevation gained on this trip in meters. private long totalTime;
*/
private double totalElevationGain;
/** // The total moving time (ms). Based on when we believe the user is traveling.
* The maximum speed in meters/second reported that we believe to be a valid private long movingTime;
* speed.
*/
private double maxSpeed;
/** // The min and max latitude seen in this trip.
* The min and max latitude values seen in this trip.
*/
private final ExtremityMonitor latitudeExtremities = new ExtremityMonitor(); private final ExtremityMonitor latitudeExtremities = new ExtremityMonitor();
/** // The min and max longitude seen in this trip.
* The min and max longitude values seen in this trip.
*/
private final ExtremityMonitor longitudeExtremities = new ExtremityMonitor(); private final ExtremityMonitor longitudeExtremities = new ExtremityMonitor();
/** // The maximum speed (meters/second) that we believe is valid.
* The min and max elevation seen on this trip in meters. private double maxSpeed;
*/
// The min and max elevation (meters) seen on this trip.
private final ExtremityMonitor elevationExtremities = new ExtremityMonitor(); private final ExtremityMonitor elevationExtremities = new ExtremityMonitor();
/** // The total elevation gained (meters).
* The minimum and maximum grade calculations on this trip. private double totalElevationGain;
*/
// The min and max grade seen on this trip.
private final ExtremityMonitor gradeExtremities = new ExtremityMonitor(); private final ExtremityMonitor gradeExtremities = new ExtremityMonitor();
/** /**
* Default constructor. * Default constructor.
*/ */
public TripStatistics() { public TripStatistics() {}
}
/** /**
* Copy constructor. * Copy constructor.
@@ -99,96 +73,166 @@ public class TripStatistics implements Parcelable {
* @param other another statistics data object to copy from * @param other another statistics data object to copy from
*/ */
public TripStatistics(TripStatistics other) { public TripStatistics(TripStatistics other) {
this.maxSpeed = other.maxSpeed;
this.movingTime = other.movingTime;
this.startTime = other.startTime; this.startTime = other.startTime;
this.stopTime = other.stopTime; this.stopTime = other.stopTime;
this.totalDistance = other.totalDistance; this.totalDistance = other.totalDistance;
this.totalElevationGain = other.totalElevationGain;
this.totalTime = other.totalTime; this.totalTime = other.totalTime;
this.movingTime = other.movingTime;
this.latitudeExtremities.set(other.latitudeExtremities.getMin(), this.latitudeExtremities.set(
other.latitudeExtremities.getMax()); other.latitudeExtremities.getMin(), other.latitudeExtremities.getMax());
this.longitudeExtremities.set(other.longitudeExtremities.getMin(), this.longitudeExtremities.set(
other.longitudeExtremities.getMax()); other.longitudeExtremities.getMin(), other.longitudeExtremities.getMax());
this.elevationExtremities.set(other.elevationExtremities.getMin(), this.maxSpeed = other.maxSpeed;
other.elevationExtremities.getMax()); this.elevationExtremities.set(
this.gradeExtremities.set(other.gradeExtremities.getMin(), other.elevationExtremities.getMin(), other.elevationExtremities.getMax());
other.gradeExtremities.getMax()); this.totalElevationGain = other.totalElevationGain;
this.gradeExtremities.set(other.gradeExtremities.getMin(), other.gradeExtremities.getMax());
} }
/** /**
* Combines these statistics with those from another object. * Combines these statistics with those from another object. This assumes that
* This assumes that the time periods covered by each do not intersect. * the time periods covered by each do not intersect.
* *
* @param other the other waypoint * @param other another statistics data object
*/ */
public void merge(TripStatistics other) { public void merge(TripStatistics other) {
startTime = Math.min(startTime, other.startTime); startTime = Math.min(startTime, other.startTime);
stopTime = Math.max(stopTime, other.stopTime); stopTime = Math.max(stopTime, other.stopTime);
totalDistance += other.totalDistance;
totalTime += other.totalTime; totalTime += other.totalTime;
movingTime += other.movingTime; 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()); latitudeExtremities.update(other.latitudeExtremities.getMin());
longitudeExtremities.update(other.longitudeExtremities.getMax()); latitudeExtremities.update(other.latitudeExtremities.getMax());
longitudeExtremities.update(other.longitudeExtremities.getMin()); 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()); 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.getMin());
gradeExtremities.update(other.gradeExtremities.getMax());
} }
/** /**
* Gets the time that this track started. * Gets the trip start time. The number of milliseconds since epoch.
*
* @return The number of milliseconds since epoch to the time when this track
* started
*/ */
public long getStartTime() { public long getStartTime() {
return startTime; return startTime;
} }
/** /**
* Gets the time that this track stopped. * Gets the trip stop time. The number of milliseconds since epoch.
*
* @return The number of milliseconds since epoch to the time when this track
* stopped
*/ */
public long getStopTime() { public long getStopTime() {
return stopTime; return stopTime;
} }
/** /**
* Gets the total time that this track has been active. * Gets the total distance the user traveled in meters.
* 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
*/ */
public double getTotalDistance() { public double getTotalDistance() {
return totalDistance; return totalDistance;
} }
/** /**
* Gets the the average speed the user traveled. * Gets the total time in milliseconds that this track has been active. This
* This calculation only takes into account the displacement until the last * statistic is only updated when a new point is added to the statistics, so
* point that was accounted for in statistics. * it may be off. If you need to calculate the proper total time, use
* * {@link #getStartTime} with the current time.
* @return The average speed in m/s */
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() { public double getAverageSpeed() {
if (totalTime == 0L) { if (totalTime == 0L) {
@@ -198,10 +242,7 @@ public class TripStatistics implements Parcelable {
} }
/** /**
* Gets the the average speed the user traveled when they were actively * Gets the average moving speed in meters/second.
* moving.
*
* @return The average moving speed in m/s
*/ */
public double getAverageMovingSpeed() { public double getAverageMovingSpeed() {
if (movingTime == 0L) { if (movingTime == 0L) {
@@ -211,179 +252,68 @@ public class TripStatistics implements Parcelable {
} }
/** /**
* Gets the the maximum speed for this track. * Gets the maximum speed in meters/second.
*
* @return The maximum speed in m/s
*/ */
public double getMaxSpeed() { public double getMaxSpeed() {
return maxSpeed; return maxSpeed;
} }
/** /**
* Gets the moving time. * Gets the minimum elevation. This is calculated from the smoothed elevation
* * so this can actually be more than the current elevation.
* @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
*/ */
public double getMinElevation() { public double getMinElevation() {
return elevationExtremities.getMin(); return elevationExtremities.getMin();
} }
/** /**
* Gets the maximum elevation seen on this trip. This is calculated from the * Gets the maximum elevation. This is calculated from the smoothed elevation
* smoothed elevation so this can actually be less than the current elevation. * so this can actually be less than the current elevation.
*
* @return The largest elevation reading for this trip in meters
*/ */
public double getMaxElevation() { public double getMaxElevation() {
return elevationExtremities.getMax(); 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. * Gets the maximum grade for this trip.
*
* @return The maximum grade for this trip as a fraction
*/ */
public double getMaxGrade() { public double getMaxGrade() {
return gradeExtremities.getMax(); return gradeExtremities.getMax();
} }
/** /**
* Gets the minimum grade for this trip. * Sets the trip start time.
* *
* @return The minimum grade for this trip as a fraction * @param startTime the trip start time in milliseconds since the epoch
*/
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
*/ */
public void setStartTime(long startTime) { public void setStartTime(long startTime) {
this.startTime = startTime; this.startTime = startTime;
} }
/** /**
* Sets the stop time for this trip. * Sets the trip stop time.
* *
* @param stopTime the stop time, in milliseconds since the epoch * @param stopTime the stop time in milliseconds since the epoch
*/ */
public void setStopTime(long stopTime) { public void setStopTime(long stopTime) {
this.stopTime = 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. * Sets the total trip distance.
* *
@@ -393,76 +323,6 @@ public class TripStatistics implements Parcelable {
this.totalDistance = 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. * Adds to the current total distance.
* *
@@ -473,16 +333,25 @@ public class TripStatistics implements Parcelable {
} }
/** /**
* Adds to the total elevation variation. * Sets the trip total time.
* *
* @param gain the elevation variation in meters * @param totalTime the trip total time in milliseconds
*/ */
void addTotalElevationGain(double gain) { public void setTotalTime(long totalTime) {
totalElevationGain += gain; 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 * @param time the time in milliseconds
*/ */
@@ -491,7 +360,21 @@ 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 * @param latitude the latitude value in signed decimal degrees
*/ */
@@ -500,7 +383,7 @@ 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 * @param longitude the longitude value in signed decimal degrees
*/ */
@@ -509,7 +392,34 @@ 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 * @param elevation the elevation value in meters
*/ */
@@ -518,7 +428,43 @@ 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 * @param grade the grade value as a fraction
*/ */
@@ -526,41 +472,33 @@ public class TripStatistics implements Parcelable {
gradeExtremities.update(grade); gradeExtremities.update(grade);
} }
// String conversion
@Override @Override
public String toString() { public String toString() {
return "TripStatistics { Start Time: " + getStartTime() return "TripStatistics { Start Time: " + getStartTime() + "; Stop Time: " + getStopTime()
+ "; Total Time: " + getTotalTime() + "; Total Time: " + getTotalTime() + "; Total Distance: " + getTotalDistance()
+ "; Moving Time: " + getMovingTime() + "; Total Time: " + getTotalTime() + "; Moving Time: " + getMovingTime()
+ "; Total Distance: " + getTotalDistance() + "; Min Latitude: " + getBottomDegrees() + "; Max Latitude: " + getTopDegrees()
+ "; Elevation Gain: " + getTotalElevationGain() + "; Min Longitude: " + getLeftDegrees() + "; Max Longitude: " + getRightDegrees()
+ "; Min Elevation: " + getMinElevation() + "; Max Elevation: " + getMaxElevation() + "; Max Speed: " + getMaxSpeed()
+ "; Max Elevation: " + getMaxElevation() + "; Min Elevation: " + getMinElevation() + "; Max Elevation: " + getMaxElevation()
+ "; Average Speed: " + getAverageMovingSpeed() + "; Elevation Gain: " + getTotalElevationGain() + "; Min Grade: " + getMinGrade()
+ "; Min Grade: " + getMinGrade() + "; Max Grade: " + getMaxGrade() + "}";
+ "; Max Grade: " + getMaxGrade()
+ "}";
} }
// Parcelable interface and creator
/** /**
* Creator of statistics data from parcels. * Creator of statistics data from parcels.
*/ */
public static class Creator public static class Creator implements Parcelable.Creator<TripStatistics> {
implements Parcelable.Creator<TripStatistics> {
@Override @Override
public TripStatistics createFromParcel(Parcel source) { public TripStatistics createFromParcel(Parcel source) {
TripStatistics data = new TripStatistics(); TripStatistics data = new TripStatistics();
data.startTime = source.readLong(); data.startTime = source.readLong();
data.movingTime = source.readLong(); data.stopTime = source.readLong();
data.totalTime = source.readLong();
data.totalDistance = source.readDouble(); data.totalDistance = source.readDouble();
data.totalElevationGain = source.readDouble(); data.totalTime = source.readLong();
data.maxSpeed = source.readDouble(); data.movingTime = source.readLong();
double minLat = source.readDouble(); double minLat = source.readDouble();
double maxLat = source.readDouble(); double maxLat = source.readDouble();
@@ -570,9 +508,12 @@ public class TripStatistics implements Parcelable {
double maxLong = source.readDouble(); double maxLong = source.readDouble();
data.longitudeExtremities.set(minLong, maxLong); data.longitudeExtremities.set(minLong, maxLong);
data.maxSpeed = source.readDouble();
double minElev = source.readDouble(); double minElev = source.readDouble();
double maxElev = source.readDouble(); double maxElev = source.readDouble();
data.elevationExtremities.set(minElev, maxElev); data.elevationExtremities.set(minElev, maxElev);
data.totalElevationGain = source.readDouble();
double minGrade = source.readDouble(); double minGrade = source.readDouble();
double maxGrade = source.readDouble(); double maxGrade = source.readDouble();
@@ -600,18 +541,18 @@ public class TripStatistics implements Parcelable {
@Override @Override
public void writeToParcel(Parcel dest, int flags) { public void writeToParcel(Parcel dest, int flags) {
dest.writeLong(startTime); dest.writeLong(startTime);
dest.writeLong(movingTime); dest.writeLong(stopTime);
dest.writeLong(totalTime);
dest.writeDouble(totalDistance); dest.writeDouble(totalDistance);
dest.writeDouble(totalElevationGain); dest.writeLong(totalTime);
dest.writeDouble(maxSpeed); dest.writeLong(movingTime);
dest.writeDouble(latitudeExtremities.getMin()); dest.writeDouble(latitudeExtremities.getMin());
dest.writeDouble(latitudeExtremities.getMax()); dest.writeDouble(latitudeExtremities.getMax());
dest.writeDouble(longitudeExtremities.getMin()); dest.writeDouble(longitudeExtremities.getMin());
dest.writeDouble(longitudeExtremities.getMax()); dest.writeDouble(longitudeExtremities.getMax());
dest.writeDouble(maxSpeed);
dest.writeDouble(elevationExtremities.getMin()); dest.writeDouble(elevationExtremities.getMin());
dest.writeDouble(elevationExtremities.getMax()); dest.writeDouble(elevationExtremities.getMax());
dest.writeDouble(totalElevationGain);
dest.writeDouble(gradeExtremities.getMin()); dest.writeDouble(gradeExtremities.getMin());
dest.writeDouble(gradeExtremities.getMax()); dest.writeDouble(gradeExtremities.getMax());
} }
@@ -1,4 +1,4 @@
#Fri Oct 21 20:06:28 BRST 2011 #Tue Jul 17 10:23:26 PDT 2012
eclipse.preferences.version=1 eclipse.preferences.version=1
org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 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.processAnnotations=enabled
org.eclipse.jdt.core.compiler.source=1.6 org.eclipse.jdt.core.compiler.source=1.6
org.eclipse.jdt.core.formatter.align_type_members_on_columns=false 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=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=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=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=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=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=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_assignment=0
org.eclipse.jdt.core.formatter.alignment_for_binary_expression=16 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_compact_if=16
org.eclipse.jdt.core.formatter.alignment_for_conditional_expression=80 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=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=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_method_declaration=0
org.eclipse.jdt.core.formatter.alignment_for_multiple_fields=16 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=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=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_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=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_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=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=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=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=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.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_imports=1
org.eclipse.jdt.core.formatter.blank_lines_after_package=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_block_boundaries=true
org.eclipse.jdt.core.formatter.comment.new_lines_at_javadoc_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.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.compact_else_if=true
org.eclipse.jdt.core.formatter.continuation_indentation=2 org.eclipse.jdt.core.formatter.continuation_indentation=2
org.eclipse.jdt.core.formatter.continuation_indentation_for_array_initializer=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.disabling_tag=@formatter\:off
org.eclipse.jdt.core.formatter.enabling_tag=@formatter\:on 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.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_annotation_declaration_header=true
org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_constant_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_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_arguments=insert
org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_parameters=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_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_parameterized_type_reference=do not insert
org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_arguments=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.join_wrapped_lines=true
org.eclipse.jdt.core.formatter.keep_else_statement_on_same_line=false 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_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.keep_then_statement_on_same_line=false
org.eclipse.jdt.core.formatter.lineSplit=100 org.eclipse.jdt.core.formatter.lineSplit=100
org.eclipse.jdt.core.formatter.never_indent_block_comments_on_first_column=false 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_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.number_of_empty_lines_to_preserve=1
org.eclipse.jdt.core.formatter.put_empty_statement_on_new_line=true 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.char=space
org.eclipse.jdt.core.formatter.tabulation.size=2 org.eclipse.jdt.core.formatter.tabulation.size=2
org.eclipse.jdt.core.formatter.use_on_off_tags=false 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.use_tabs_only_for_leading_indentations=false
org.eclipse.jdt.core.formatter.wrap_before_binary_operator=true 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_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_outer_expressions_when_nested=true
org.eclipse.jdt.core.formatter.wrap_prefer_two_fragments=false
@@ -1,8 +1,8 @@
#Thu Jul 21 03:06:24 BRT 2011 #Tue Jul 17 10:22:39 PDT 2012
eclipse.preferences.version=1 eclipse.preferences.version=1
editor_save_participant_org.eclipse.jdt.ui.postsavelistener.cleanup=true editor_save_participant_org.eclipse.jdt.ui.postsavelistener.cleanup=true
formatter_profile=_MyTracks formatter_profile=_My Tracks
formatter_settings_version=12 formatter_settings_version=13
org.eclipse.jdt.ui.ignorelowercasenames=true org.eclipse.jdt.ui.ignorelowercasenames=true
org.eclipse.jdt.ui.importorder=com;android;java;javax;org; org.eclipse.jdt.ui.importorder=com;android;java;javax;org;
org.eclipse.jdt.ui.ondemandthreshold=99 org.eclipse.jdt.ui.ondemandthreshold=99
+4 -1
View File
@@ -23,12 +23,15 @@ limitations under the License.
android:name="android.test.InstrumentationTestRunner" android:name="android.test.InstrumentationTestRunner"
android:targetPackage="com.google.android.maps.mytracks" /> android:targetPackage="com.google.android.maps.mytracks" />
<instrumentation
android:name="com.google.android.apps.mytracks.endtoendtest.EndToEndTestRunner"
android:targetPackage="com.google.android.maps.mytracks" />
<uses-sdk <uses-sdk
android:minSdkVersion="7" android:minSdkVersion="7"
android:targetSdkVersion="15" /> android:targetSdkVersion="15" />
<application <application
android:icon="@android:drawable/sym_def_app_icon"
android:label="My Tracks Tests" > android:label="My Tracks Tests" >
<uses-library android:name="android.test.runner" /> <uses-library android:name="android.test.runner" />
</application> </application>
Binary file not shown.
Binary file not shown.
@@ -25,121 +25,137 @@ import android.location.Location;
import android.test.AndroidTestCase; import android.test.AndroidTestCase;
/** /**
* Tests for the MyTracks map overlay. * Tests {@link MapOverlay}.
* *
* @author Bartlomiej Niechwiej * @author Bartlomiej Niechwiej
* @author Vangelis S. * @author Vangelis S.
*/ */
public class MapOverlayTest extends AndroidTestCase { public class MapOverlayTest extends AndroidTestCase {
private Canvas canvas; private Canvas canvas;
private MockMyTracksOverlay myTracksOverlay; private MockMyTracksOverlay mockMyTracksOverlay;
private MapView mockView; private MapView mapView;
@Override @Override
protected void setUp() throws Exception { protected void setUp() throws Exception {
super.setUp(); super.setUp();
canvas = new Canvas(); canvas = new Canvas();
myTracksOverlay = new MockMyTracksOverlay(getContext()); mockMyTracksOverlay = new MockMyTracksOverlay(getContext());
// Enable drawing. // Enable drawing.
myTracksOverlay.setTrackDrawingEnabled(true); mockMyTracksOverlay.setTrackDrawingEnabled(true);
// Set a TrackPathPainter with a MockPath. // Set a TrackPathPainter with a MockPath.
myTracksOverlay.setTrackPathPainter(new SingleColorTrackPathPainter(getContext()) { mockMyTracksOverlay.setTrackPathPainter(new SingleColorTrackPathPainter(getContext()) {
@Override @Override
public Path newPath() { public Path newPath() {
return new MockPath(); return new MockPath();
} }
}); });
mockView = null; mapView = null;
} }
/**
* Tests {@link MapOverlay#addLocation(Location)}.
*/
public void testAddLocation() throws Exception { public void testAddLocation() throws Exception {
Location location = new Location("gps"); Location location = new Location("gps");
location.setLatitude(10); location.setLatitude(10);
location.setLongitude(20); location.setLongitude(20);
myTracksOverlay.addLocation(location); mockMyTracksOverlay.addLocation(location);
assertEquals(1, myTracksOverlay.getNumLocations()); assertEquals(1, mockMyTracksOverlay.getNumLocations());
assertEquals(0, myTracksOverlay.getNumWaypoints()); assertEquals(0, mockMyTracksOverlay.getNumWaypoints());
location.setLatitude(20); location.setLatitude(20);
location.setLongitude(30); location.setLongitude(30);
myTracksOverlay.addLocation(location); mockMyTracksOverlay.addLocation(location);
assertEquals(2, myTracksOverlay.getNumLocations()); assertEquals(2, mockMyTracksOverlay.getNumLocations());
assertEquals(0, myTracksOverlay.getNumWaypoints()); assertEquals(0, mockMyTracksOverlay.getNumWaypoints());
assertNull(myTracksOverlay.getTrackPathPainter().getLastPath()); assertFalse(mockMyTracksOverlay.getTrackPathPainter().hasPath());
// Draw and make sure that we don't lose any point. // Draw and make sure that we don't lose any point.
myTracksOverlay.draw(canvas, mockView, false); mockMyTracksOverlay.draw(canvas, mapView, false);
assertEquals(2, myTracksOverlay.getNumLocations()); assertEquals(2, mockMyTracksOverlay.getNumLocations());
assertEquals(0, myTracksOverlay.getNumWaypoints()); assertEquals(0, mockMyTracksOverlay.getNumWaypoints());
assertNotNull(myTracksOverlay.getTrackPathPainter().getLastPath()); assertTrue(mockMyTracksOverlay.getTrackPathPainter().hasPath());
assertTrue(myTracksOverlay.getTrackPathPainter().getLastPath() instanceof MockPath); SingleColorTrackPathPainter trackPathPainter = (SingleColorTrackPathPainter) mockMyTracksOverlay
MockPath path = (MockPath) myTracksOverlay.getTrackPathPainter().getLastPath(); .getTrackPathPainter();
assertEquals(2, path.totalPoints); MockPath path = (MockPath) trackPathPainter.getPath();
assertEquals(2, path.getTotalPoints());
myTracksOverlay.draw(canvas, mockView, true); mockMyTracksOverlay.draw(canvas, mapView, true);
assertEquals(2, myTracksOverlay.getNumLocations()); assertEquals(2, mockMyTracksOverlay.getNumLocations());
assertEquals(0, myTracksOverlay.getNumWaypoints()); assertEquals(0, mockMyTracksOverlay.getNumWaypoints());
assertNotNull(myTracksOverlay.getTrackPathPainter().getLastPath()); assertTrue(mockMyTracksOverlay.getTrackPathPainter().hasPath());
} }
/**
* Tests {@link MapOverlay#clearPoints()}.
*/
public void testClearPoints() throws Exception { public void testClearPoints() throws Exception {
Location location = new Location("gps"); Location location = new Location("gps");
location.setLatitude(10); location.setLatitude(10);
location.setLongitude(20); location.setLongitude(20);
myTracksOverlay.addLocation(location); mockMyTracksOverlay.addLocation(location);
assertEquals(1, myTracksOverlay.getNumLocations()); assertEquals(1, mockMyTracksOverlay.getNumLocations());
myTracksOverlay.clearPoints(); mockMyTracksOverlay.clearPoints();
assertEquals(0, myTracksOverlay.getNumLocations()); assertEquals(0, mockMyTracksOverlay.getNumLocations());
// Same after drawing on canvas. // Test after drawing on canvas
final int locations = 100; final int locations = 100;
for (int i = 0; i < locations; ++i) { for (int i = 0; i < locations; ++i) {
myTracksOverlay.addLocation(location); mockMyTracksOverlay.addLocation(location);
} }
assertEquals(locations, myTracksOverlay.getNumLocations()); assertEquals(locations, mockMyTracksOverlay.getNumLocations());
myTracksOverlay.draw(canvas, mockView, false); mockMyTracksOverlay.draw(canvas, mapView, false);
myTracksOverlay.draw(canvas, mockView, true); mockMyTracksOverlay.draw(canvas, mapView, true);
myTracksOverlay.clearPoints(); mockMyTracksOverlay.clearPoints();
assertEquals(0, myTracksOverlay.getNumLocations()); assertEquals(0, mockMyTracksOverlay.getNumLocations());
} }
/**
* Tests {@link MapOverlay#addWaypoint(Waypoint)}.
*/
public void testAddWaypoint() throws Exception { public void testAddWaypoint() throws Exception {
Location location = new Location("gps"); Location location = new Location("gps");
location.setLatitude(10); location.setLatitude(10);
location.setLongitude(20); location.setLongitude(20);
Waypoint waypoint = new Waypoint(); Waypoint waypoint = new Waypoint();
waypoint.setLocation(location); waypoint.setLocation(location);
myTracksOverlay.addWaypoint(waypoint); mockMyTracksOverlay.addWaypoint(waypoint);
assertEquals(1, myTracksOverlay.getNumWaypoints()); assertEquals(1, mockMyTracksOverlay.getNumWaypoints());
assertEquals(0, myTracksOverlay.getNumLocations()); assertEquals(0, mockMyTracksOverlay.getNumLocations());
assertNull(myTracksOverlay.getTrackPathPainter().getLastPath()); assertFalse(mockMyTracksOverlay.getTrackPathPainter().hasPath());
final int waypoints = 10; final int waypoints = 10;
for (int i = 0; i < waypoints; ++i) { for (int i = 0; i < waypoints; ++i) {
waypoint = new Waypoint(); waypoint = new Waypoint();
waypoint.setLocation(location); waypoint.setLocation(location);
myTracksOverlay.addWaypoint(waypoint); mockMyTracksOverlay.addWaypoint(waypoint);
} }
assertEquals(1 + waypoints, myTracksOverlay.getNumWaypoints()); assertEquals(1 + waypoints, mockMyTracksOverlay.getNumWaypoints());
assertEquals(0, myTracksOverlay.getNumLocations()); assertEquals(0, mockMyTracksOverlay.getNumLocations());
assertNull(myTracksOverlay.getTrackPathPainter().getLastPath()); assertFalse(mockMyTracksOverlay.getTrackPathPainter().hasPath());
} }
/**
* Tests {@link MapOverlay#clearWaypoints()}.
*/
public void testClearWaypoints() throws Exception { public void testClearWaypoints() throws Exception {
Location location = new Location("gps"); Location location = new Location("gps");
location.setLatitude(10); location.setLatitude(10);
location.setLongitude(20); location.setLongitude(20);
Waypoint waypoint = new Waypoint(); Waypoint waypoint = new Waypoint();
waypoint.setLocation(location); waypoint.setLocation(location);
myTracksOverlay.addWaypoint(waypoint); mockMyTracksOverlay.addWaypoint(waypoint);
assertEquals(1, myTracksOverlay.getNumWaypoints()); assertEquals(1, mockMyTracksOverlay.getNumWaypoints());
myTracksOverlay.clearWaypoints(); mockMyTracksOverlay.clearWaypoints();
assertEquals(0, myTracksOverlay.getNumWaypoints()); assertEquals(0, mockMyTracksOverlay.getNumWaypoints());
} }
/**
* Tests {@link MapOverlay#draw(Canvas, MapView, boolean)}.
*/
public void testDrawing() { public void testDrawing() {
Location location = new Location("gps"); Location location = new Location("gps");
location.setLatitude(10); location.setLatitude(10);
@@ -147,30 +163,30 @@ public class MapOverlayTest extends AndroidTestCase {
location.setLongitude(20 + i); location.setLongitude(20 + i);
Waypoint waypoint = new Waypoint(); Waypoint waypoint = new Waypoint();
waypoint.setLocation(location); waypoint.setLocation(location);
myTracksOverlay.addWaypoint(waypoint); mockMyTracksOverlay.addWaypoint(waypoint);
} }
for (int i = 0; i < 100; ++i) { for (int i = 0; i < 100; ++i) {
location = new Location("gps"); location = new Location("gps");
location.setLatitude(20 + i / 2); location.setLatitude(20 + i / 2);
location.setLongitude(150 - i); location.setLongitude(150 - i);
myTracksOverlay.addLocation(location); mockMyTracksOverlay.addLocation(location);
} }
// Shadow. // Shadow.
myTracksOverlay.draw(canvas, mockView, true); mockMyTracksOverlay.draw(canvas, mapView, true);
// We don't expect to do anything if // We don't expect to do anything if
assertNull(myTracksOverlay.getTrackPathPainter().getLastPath()); assertFalse(mockMyTracksOverlay.getTrackPathPainter().hasPath());
assertEquals(40, myTracksOverlay.getNumWaypoints()); assertEquals(40, mockMyTracksOverlay.getNumWaypoints());
assertEquals(100, myTracksOverlay.getNumLocations()); assertEquals(100, mockMyTracksOverlay.getNumLocations());
// No shadow. // No shadow.
myTracksOverlay.draw(canvas, mockView, false); mockMyTracksOverlay.draw(canvas, mapView, false);
assertNotNull(myTracksOverlay.getTrackPathPainter().getLastPath()); assertTrue(mockMyTracksOverlay.getTrackPathPainter().hasPath());
assertTrue(myTracksOverlay.getTrackPathPainter().getLastPath() instanceof MockPath); SingleColorTrackPathPainter trackPathPainter = (SingleColorTrackPathPainter) mockMyTracksOverlay
MockPath path = (MockPath) myTracksOverlay.getTrackPathPainter().getLastPath(); .getTrackPathPainter();
assertEquals(40, myTracksOverlay.getNumWaypoints()); MockPath path = (MockPath) trackPathPainter.getPath();
assertEquals(100, myTracksOverlay.getNumLocations()); assertEquals(40, mockMyTracksOverlay.getNumWaypoints());
assertEquals(100, path.totalPoints); assertEquals(100, mockMyTracksOverlay.getNumLocations());
// TODO: Check the points from the path (and the segments). assertEquals(100, path.getTotalPoints());
} }
} }
@@ -22,30 +22,28 @@ import android.content.Context;
import android.graphics.Rect; import android.graphics.Rect;
/** /**
* Elements for Tests for the MyTracks map overlay. * A mock {@link MapOverlay} for testing.
* *
* @author Bartlomiej Niechwiej * @author Bartlomiej Niechwiej
* @author Vangelis S. * @author Vangelis S.
* *
* A mock version of {@code MapOverlay} that does not use
* {@class MapView}.
*/ */
public class MockMyTracksOverlay extends MapOverlay { public class MockMyTracksOverlay extends MapOverlay {
private Projection mockProjection; private Projection projection;
public MockMyTracksOverlay(Context context) { public MockMyTracksOverlay(Context context) {
super(context); super(context);
mockProjection = new MockProjection(); projection = new MockProjection();
} }
@Override @Override
public Projection getMapProjection(MapView mapView) { public Projection getMapProjection(MapView mapView) {
return mockProjection; return projection;
} }
@Override @Override
public Rect getMapViewRect(MapView mapView) { public Rect getMapViewRect(MapView mapView) {
return new Rect(0, 0, 100, 100); return new Rect(0, 0, (int) (100 * 1E6), (int) (100 * 1E6));
} }
} }
@@ -26,20 +26,19 @@ import java.util.List;
import junit.framework.Assert; 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 Bartlomiej Niechwiej
* @author Vangelis S. * @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 { public class MockPath extends Path {
/** A list of disjoined path segments. */ // A list of disjointed path segments.
public final List<List<PointF>> segments = new LinkedList<List<PointF>>(); private final List<List<PointF>> segments = new LinkedList<List<PointF>>();
/** The total number of points in this path. */
public int totalPoints; // The total number of points in this path.
private int totalPoints;
private List<PointF> currentSegment; private List<PointF> currentSegment;
@Override @Override
@@ -53,8 +52,16 @@ public class MockPath extends Path {
@Override @Override
public void moveTo(float x, float y) { public void moveTo(float x, float y) {
super.moveTo(x, y); super.moveTo(x, y);
segments.add(currentSegment = currentSegment = new ArrayList<PointF>(Arrays.asList(new PointF(x, y)));
new ArrayList<PointF>(Arrays.asList(new PointF(x, y)))); segments.add(currentSegment);
totalPoints++; totalPoints++;
} }
/**
* Gets the total number of points in this path.
*/
public int getTotalPoints() {
return totalPoints;
}
} }
@@ -13,6 +13,7 @@
* License for the specific language governing permissions and limitations under * License for the specific language governing permissions and limitations under
* the License. * the License.
*/ */
package com.google.android.apps.mytracks; package com.google.android.apps.mytracks;
import com.google.android.maps.GeoPoint; import com.google.android.maps.GeoPoint;
@@ -21,18 +22,17 @@ import com.google.android.maps.Projection;
import android.graphics.Point; 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 Bartlomiej Niechwiej
* @author Vangelis S. * @author Vangelis S.
* *
* A mock {@code Projection} that acts as the identity matrix.
*/ */
public class MockProjection implements Projection { public class MockProjection implements Projection {
@Override @Override
public Point toPixels(GeoPoint in, Point out) { public Point toPixels(GeoPoint geoPoint, Point point) {
return out; return point;
} }
@Override @Override
@@ -112,7 +112,8 @@ public class DescriptionGeneratorImplTest extends AndroidTestCase {
+ "Max grade: 42 %\n" + "Max grade: 42 %\n"
+ "Min grade: 11 %\n" + "Min grade: 11 %\n"
+ "Recorded: " + StringUtils.formatDateTime(getContext(), START_TIME) + "\n"; + "Recorded: " + StringUtils.formatDateTime(getContext(), START_TIME) + "\n";
assertEquals(expected, descriptionGenerator.generateWaypointDescription(waypoint)); assertEquals(
expected, descriptionGenerator.generateWaypointDescription(waypoint.getTripStatistics()));
} }
/** /**
@@ -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;
}
}
@@ -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.LocationFactory;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils.LocationIterator; import com.google.android.apps.mytracks.content.MyTracksProviderUtils.LocationIterator;
import com.google.android.apps.mytracks.services.TrackRecordingServiceTest.MockContext; 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.content.Context;
import android.database.Cursor;
import android.location.Location; import android.location.Location;
import android.test.AndroidTestCase; import android.test.AndroidTestCase;
import android.test.RenamingDelegatingContext; import android.test.RenamingDelegatingContext;
@@ -27,17 +33,28 @@ import android.test.mock.MockContentResolver;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Vector;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
/** /**
* A unit test for {@link MyTracksProviderUtilsImpl}. * A unit test for {@link MyTracksProviderUtilsImpl}.
* *
* @author Bartlomiej Niechwiej * @author Bartlomiej Niechwiej
* @author Youtao Liu
*/ */
public class MyTracksProviderUtilsImplTest extends AndroidTestCase { public class MyTracksProviderUtilsImplTest extends AndroidTestCase {
private Context context; private Context context;
private MyTracksProviderUtils providerUtils; 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 @Override
protected void setUp() throws Exception { protected void setUp() throws Exception {
super.setUp(); super.setUp();
@@ -52,6 +69,7 @@ public class MyTracksProviderUtilsImplTest extends AndroidTestCase {
setContext(context); setContext(context);
providerUtils = MyTracksProviderUtils.Factory.get(context); providerUtils = MyTracksProviderUtils.Factory.get(context);
providerUtils.deleteAllTracks();
} }
public void testLocationIterator_noPoints() { public void testLocationIterator_noPoints() {
@@ -168,4 +186,888 @@ public class MyTracksProviderUtilsImplTest extends AndroidTestCase {
return lastPointId; 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<Track> 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<Double> distances,
Vector<Double> 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<Double> distances,
Vector<Double> 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());
}
} }
File diff suppressed because it is too large Load Diff
@@ -23,7 +23,6 @@ import android.app.Instrumentation;
import android.test.ActivityInstrumentationTestCase2; import android.test.ActivityInstrumentationTestCase2;
import android.view.KeyEvent; import android.view.KeyEvent;
import android.widget.CheckBox; import android.widget.CheckBox;
import android.widget.EditText;
import java.util.ArrayList; import java.util.ArrayList;
@@ -104,6 +103,8 @@ public class CreateAndSendTrackTest extends ActivityInstrumentationTestCase2<Tra
if (checkBoxs.size() < 3) { if (checkBoxs.size() < 3) {
EndToEndTestUtils.SOLO.scrollDown(); EndToEndTestUtils.SOLO.scrollDown();
checkBoxs = EndToEndTestUtils.SOLO.getCurrentCheckBoxes(); checkBoxs = EndToEndTestUtils.SOLO.getCurrentCheckBoxes();
// Choose all Google service.
for (int i = 0; i < checkBoxs.size(); i++) { for (int i = 0; i < checkBoxs.size(); i++) {
if (!checkBoxs.get(i).isChecked()) { if (!checkBoxs.get(i).isChecked()) {
EndToEndTestUtils.SOLO.clickOnCheckBox(i); EndToEndTestUtils.SOLO.clickOnCheckBox(i);
@@ -111,24 +112,39 @@ public class CreateAndSendTrackTest extends ActivityInstrumentationTestCase2<Tra
} }
} }
instrumentation.waitForIdleSync(); instrumentation.waitForIdleSync();
EndToEndTestUtils.SOLO.clickOnText(activityMyTracks.getString(R.string.send_google_send_now)); EndToEndTestUtils.getButtonOnScreen(activityMyTracks.getString(R.string.send_google_send_now),
true, true);
// If no account is binded with this device. // If no account is binded with this device.
if (EndToEndTestUtils.SOLO.waitForText(activityMyTracks if (EndToEndTestUtils.SOLO.waitForText(
.getString(R.string.send_google_no_account_title), 1, 10000)) { activityMyTracks.getString(R.string.send_google_no_account_title), 1,
EndToEndTestUtils.getButtonOnScreen(activityMyTracks.getString(R.string.generic_ok), true, true); EndToEndTestUtils.SHORT_WAIT_TIME)) {
} else { EndToEndTestUtils.getButtonOnScreen(activityMyTracks.getString(R.string.generic_ok), true,
true);
} else if (EndToEndTestUtils.SOLO.waitForText(
activityMyTracks.getString(R.string.send_google_no_account_permission), 1,
EndToEndTestUtils.SHORT_WAIT_TIME)) {} else {
assertTrue(EndToEndTestUtils.SOLO.waitForText(activityMyTracks assertTrue(EndToEndTestUtils.SOLO.waitForText(activityMyTracks
.getString(R.string.generic_progress_title))); .getString(R.string.generic_progress_title)));
// Waiting the send is finish. // Waiting the send is finish.
while (EndToEndTestUtils.SOLO.waitForText(activityMyTracks while (EndToEndTestUtils.SOLO.waitForText(
.getString(R.string.generic_progress_title), 1, 500)) { activityMyTracks.getString(R.string.generic_progress_title), 1,
} EndToEndTestUtils.SHORT_WAIT_TIME)) {}
// For we not sure the send will be successful, just check whether the result dialog is display. // Check whether the result dialog is display.
assertTrue(EndToEndTestUtils.SOLO.waitForText(activityMyTracks assertTrue(EndToEndTestUtils.SOLO.waitForText(activityMyTracks
.getString(R.string.share_track_share_url))); .getString(R.string.share_track_share_url)));
EndToEndTestUtils.getButtonOnScreen(activityMyTracks.getString(R.string.generic_ok), true, true); EndToEndTestUtils.getButtonOnScreen(activityMyTracks.getString(R.string.generic_ok), true,
true);
// Check whether all data is correct on Google Map, Documents, and
// Spreadsheet.
assertTrue(GoogleUtils.deleteMap(EndToEndTestUtils.trackName, activityMyTracks));
assertTrue(GoogleUtils.searchFusionTableByTitle(EndToEndTestUtils.TRACK_NAME_PREFIX,
activityMyTracks));
assertTrue(GoogleUtils
.deleteTrackInSpreadSheet(EndToEndTestUtils.trackName, activityMyTracks));
assertTrue(GoogleUtils.dropFusionTables(EndToEndTestUtils.trackName, activityMyTracks));
} }
} }
@@ -142,25 +158,17 @@ public class CreateAndSendTrackTest extends ActivityInstrumentationTestCase2<Tra
EndToEndTestUtils.findMenuItem(activityMyTracks.getString(R.string.menu_edit), true); EndToEndTestUtils.findMenuItem(activityMyTracks.getString(R.string.menu_edit), true);
String newTrackName = EndToEndTestUtils.TRACK_NAME_PREFIX + "_new" + System.currentTimeMillis(); String newTrackName = EndToEndTestUtils.TRACK_NAME_PREFIX + "_new" + System.currentTimeMillis();
String newType = "type" + newTrackName; String newType = EndToEndTestUtils.DEFAULTACTIVITY;
String newDesc = "desc" + newTrackName; String newDesc = "desc" + newTrackName;
instrumentation.waitForIdleSync(); instrumentation.waitForIdleSync();
EndToEndTestUtils.rotateAllActivities(); EndToEndTestUtils.rotateAllActivities();
EndToEndTestUtils.SOLO.waitForText(activityMyTracks.getString(R.string.generic_save)); EndToEndTestUtils.SOLO.waitForText(activityMyTracks.getString(R.string.generic_save));
sendKeys(KeyEvent.KEYCODE_DEL); sendKeys(KeyEvent.KEYCODE_DEL);
ArrayList<EditText> editTexts = EndToEndTestUtils.SOLO.getCurrentEditTexts();
EndToEndTestUtils.SOLO.enterText(editTexts.get(0), newTrackName); EndToEndTestUtils.enterTextAvoidSoftKeyBoard(0, newTrackName);
EndToEndTestUtils.SOLO.enterText(editTexts.get(1), newType); EndToEndTestUtils.enterTextAvoidSoftKeyBoard(1, newType);
// In landscape, there are only two visible edit texts. EndToEndTestUtils.enterTextAvoidSoftKeyBoard(2, newDesc);
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.SOLO.clickOnButton(activityMyTracks.getString(R.string.generic_save)); EndToEndTestUtils.SOLO.clickOnButton(activityMyTracks.getString(R.string.generic_save));
instrumentation.waitForIdleSync(); instrumentation.waitForIdleSync();
// Go back to track list. // Go back to track list.
@@ -170,6 +178,23 @@ public class CreateAndSendTrackTest extends ActivityInstrumentationTestCase2<Tra
assertTrue(EndToEndTestUtils.SOLO.searchText(newDesc)); assertTrue(EndToEndTestUtils.SOLO.searchText(newDesc));
} }
/**
* Checks the voice frequency and split frequency menus.
*/
public void testFrequencyMenu() {
EndToEndTestUtils.startRecording();
assertTrue(EndToEndTestUtils.findMenuItem(
activityMyTracks.getString(R.string.menu_voice_frequency), false));
assertTrue(EndToEndTestUtils.findMenuItem(
activityMyTracks.getString(R.string.menu_split_frequency), false));
EndToEndTestUtils.stopRecording(true);
assertFalse(EndToEndTestUtils.findMenuItem(
activityMyTracks.getString(R.string.menu_voice_frequency), false));
assertFalse(EndToEndTestUtils.findMenuItem(
activityMyTracks.getString(R.string.menu_split_frequency), false));
}
/** /**
* Creates one track with a two locations, a way point and a statistics point. * Creates one track with a two locations, a way point and a statistics point.
*/ */
@@ -183,6 +208,7 @@ public class CreateAndSendTrackTest extends ActivityInstrumentationTestCase2<Tra
assertTrue(EndToEndTestUtils.SOLO.searchText(activityMyTracks assertTrue(EndToEndTestUtils.SOLO.searchText(activityMyTracks
.getString(R.string.icon_recording))); .getString(R.string.icon_recording)));
} }
createWaypoint(); createWaypoint();
EndToEndTestUtils.sendGps(2); EndToEndTestUtils.sendGps(2);
// Back to tracks list. // Back to tracks list.
@@ -192,16 +218,15 @@ public class CreateAndSendTrackTest extends ActivityInstrumentationTestCase2<Tra
EndToEndTestUtils.stopRecording(false); EndToEndTestUtils.stopRecording(false);
EndToEndTestUtils.trackName = EndToEndTestUtils.TRACK_NAME_PREFIX + System.currentTimeMillis(); EndToEndTestUtils.trackName = EndToEndTestUtils.TRACK_NAME_PREFIX + System.currentTimeMillis();
EndToEndTestUtils.SOLO.enterText(0, EndToEndTestUtils.trackName); EndToEndTestUtils.SOLO.enterText(0, EndToEndTestUtils.trackName);
if(!EndToEndTestUtils.isEmulator) { EndToEndTestUtils.SOLO.enterText(1, EndToEndTestUtils.DEFAULTACTIVITY);
// Close soft keyboard.
EndToEndTestUtils.SOLO.goBack();
}
EndToEndTestUtils.SOLO.clickOnButton(activityMyTracks.getString(R.string.generic_save)); EndToEndTestUtils.SOLO.clickOnButton(activityMyTracks.getString(R.string.generic_save));
instrumentation.waitForIdleSync(); instrumentation.waitForIdleSync();
// Check the new track // Check the new track
assertTrue(EndToEndTestUtils.SOLO.waitForText(EndToEndTestUtils.trackName, 1, 5000, true, EndToEndTestUtils.SOLO.scrollUp();
false)); assertTrue(EndToEndTestUtils.SOLO.waitForText(EndToEndTestUtils.trackName, 1,
EndToEndTestUtils.NORMAL_WAIT_TIME, true, false));
} }
/** /**
@@ -211,9 +236,7 @@ public class CreateAndSendTrackTest extends ActivityInstrumentationTestCase2<Tra
*/ */
public void testTrackStartTime() { public void testTrackStartTime() {
// Delete all track first. // Delete all track first.
EndToEndTestUtils.findMenuItem(activityMyTracks.getString(R.string.menu_delete_all), true); EndToEndTestUtils.deleteAllTracks();
EndToEndTestUtils.getButtonOnScreen(activityMyTracks.getString(R.string.generic_ok), true, true);
// Reset all settings. // Reset all settings.
EndToEndTestUtils.resetAllSettings(activityMyTracks, false); EndToEndTestUtils.resetAllSettings(activityMyTracks, false);
@@ -223,17 +246,50 @@ public class CreateAndSendTrackTest extends ActivityInstrumentationTestCase2<Tra
instrumentation.waitForIdleSync(); instrumentation.waitForIdleSync();
EndToEndTestUtils.stopRecording(false); EndToEndTestUtils.stopRecording(false);
instrumentation.waitForIdleSync(); instrumentation.waitForIdleSync();
EndToEndTestUtils.SOLO.clickOnText(activityMyTracks.getString(R.string.generic_save)); EndToEndTestUtils.getButtonOnScreen(activityMyTracks.getString(R.string.generic_save), true,
true);
instrumentation.waitForIdleSync(); instrumentation.waitForIdleSync();
EndToEndTestUtils.SOLO.goBack(); EndToEndTestUtils.SOLO.goBack();
instrumentation.waitForIdleSync(); instrumentation.waitForIdleSync();
assertFalse(EndToEndTestUtils.SOLO.waitForText(EndToEndTestUtils.RELATIVE_STARTTIME_POSTFIX, 1, 5000)); assertFalse(EndToEndTestUtils.SOLO.searchText(EndToEndTestUtils.RELATIVE_STARTTIME_POSTFIX, 1, false, true));
// Test should show relative time for createSimpleTrack would save a track // Test should show relative time for createSimpleTrack would save a track
// name that is different with the start time. // name that is different with the start time.
EndToEndTestUtils.createSimpleTrack(2); EndToEndTestUtils.createSimpleTrack(2);
EndToEndTestUtils.SOLO.goBack(); EndToEndTestUtils.SOLO.goBack();
assertTrue(EndToEndTestUtils.SOLO.waitForText(EndToEndTestUtils.RELATIVE_STARTTIME_POSTFIX, 1, 5000)); assertTrue(EndToEndTestUtils.SOLO.waitForText(EndToEndTestUtils.RELATIVE_STARTTIME_POSTFIX, 1, EndToEndTestUtils.NORMAL_WAIT_TIME));
}
/**
* Tests whether the split marker is created as setting.
* @throws InterruptedException
*/
public void testSplitSetting() throws InterruptedException {
EndToEndTestUtils.startRecording();
EndToEndTestUtils.findMenuItem(activityMyTracks.getString(R.string.menu_split_frequency), true);
boolean isFoundKM = EndToEndTestUtils.SOLO.searchText(EndToEndTestUtils.KM);
if (isFoundKM) {
EndToEndTestUtils.SOLO.clickOnText(EndToEndTestUtils.KM, 0);
} else {
EndToEndTestUtils.SOLO.clickOnText(EndToEndTestUtils.MILE, 0);
}
EndToEndTestUtils
.getButtonOnScreen(activityMyTracks.getString(R.string.generic_ok), true, true);
// Send Gps to give a distance more than one kilometer or one mile.
EndToEndTestUtils.sendGps(20);
assertTrue(EndToEndTestUtils.findMenuItem(activityMyTracks.getString(R.string.menu_markers),
true));
instrumentation.waitForIdleSync();
if (EndToEndTestUtils.hasGpsSingal) {
assertTrue(EndToEndTestUtils.SOLO.getCurrentListViews().get(0).getCount() > 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<Tra
EndToEndTestUtils.findMenuItem(activityMyTracks.getString(R.string.menu_markers), true); EndToEndTestUtils.findMenuItem(activityMyTracks.getString(R.string.menu_markers), true);
EndToEndTestUtils.SOLO.waitForText(activityMyTracks.getString(R.string.marker_list_empty_message)); EndToEndTestUtils.SOLO.waitForText(activityMyTracks.getString(R.string.marker_list_empty_message));
EndToEndTestUtils.findMenuItem(activityMyTracks.getString(R.string.menu_insert_marker), true); EndToEndTestUtils.findMenuItem(activityMyTracks.getString(R.string.menu_insert_marker), true);
// Rotate when show insert page. EndToEndTestUtils.enterTextAvoidSoftKeyBoard(0, WAYPOINT_NAME);
EndToEndTestUtils.rotateAllActivities();
EndToEndTestUtils.SOLO.enterText(0, WAYPOINT_NAME);
if(!EndToEndTestUtils.isEmulator) {
// Close soft keyboard.
EndToEndTestUtils.SOLO.goBack();
}
EndToEndTestUtils.SOLO.clickOnButton(activityMyTracks.getString(R.string.generic_add)); EndToEndTestUtils.SOLO.clickOnButton(activityMyTracks.getString(R.string.generic_add));
assertTrue(EndToEndTestUtils.SOLO.searchText(WAYPOINT_NAME)); if (EndToEndTestUtils.hasGpsSingal) {
assertTrue(EndToEndTestUtils.SOLO.waitForText(WAYPOINT_NAME, 1, EndToEndTestUtils.LONG_WAIT_TIME, true));
} else {
assertFalse(EndToEndTestUtils.SOLO.searchText(WAYPOINT_NAME));
}
EndToEndTestUtils.SOLO.goBack(); EndToEndTestUtils.SOLO.goBack();
} }
@@ -62,8 +62,11 @@ public class DeleteTest extends ActivityInstrumentationTestCase2<TrackListActivi
EndToEndTestUtils.findMenuItem(activityMyTracks.getString(R.string.menu_delete_all), true); EndToEndTestUtils.findMenuItem(activityMyTracks.getString(R.string.menu_delete_all), true);
instrumentation.waitForIdleSync(); instrumentation.waitForIdleSync();
EndToEndTestUtils.rotateAllActivities(); EndToEndTestUtils.rotateAllActivities();
EndToEndTestUtils.getButtonOnScreen(activityMyTracks.getString(R.string.generic_ok), true, true); EndToEndTestUtils
.getButtonOnScreen(activityMyTracks.getString(R.string.generic_ok), true, true);
instrumentation.waitForIdleSync(); instrumentation.waitForIdleSync();
assertTrue(EndToEndTestUtils.SOLO.waitForText(activityMyTracks
.getString(R.string.track_list_empty_message)));
// There is no track now. // There is no track now.
trackListView = EndToEndTestUtils.SOLO.getCurrentListViews(); trackListView = EndToEndTestUtils.SOLO.getCurrentListViews();
assertEquals(0, trackListView.get(0).getCount()); assertEquals(0, trackListView.get(0).getCount());
@@ -74,7 +77,8 @@ public class DeleteTest extends ActivityInstrumentationTestCase2<TrackListActivi
EndToEndTestUtils.GPX.toUpperCase())); EndToEndTestUtils.GPX.toUpperCase()));
assertTrue(EndToEndTestUtils.SOLO.waitForText(activityMyTracks assertTrue(EndToEndTestUtils.SOLO.waitForText(activityMyTracks
.getString(R.string.sd_card_save_error_no_track))); .getString(R.string.sd_card_save_error_no_track)));
EndToEndTestUtils.getButtonOnScreen(activityMyTracks.getString(R.string.generic_ok), true, true); EndToEndTestUtils
.getButtonOnScreen(activityMyTracks.getString(R.string.generic_ok), true, true);
} }
/** /**
@@ -90,7 +94,7 @@ public class DeleteTest extends ActivityInstrumentationTestCase2<TrackListActivi
EndToEndTestUtils.SOLO.clickOnView(trackListView.get(0).getChildAt(0)); EndToEndTestUtils.SOLO.clickOnView(trackListView.get(0).getChildAt(0));
EndToEndTestUtils.findMenuItem(activityMyTracks.getString(R.string.menu_delete), true); EndToEndTestUtils.findMenuItem(activityMyTracks.getString(R.string.menu_delete), true);
EndToEndTestUtils EndToEndTestUtils
.getButtonOnScreen(activityMyTracks.getString(R.string.generic_ok), true, true); .getButtonOnScreen(activityMyTracks.getString(R.string.generic_ok), true, true);
instrumentation.waitForIdleSync(); instrumentation.waitForIdleSync();
trackListView = EndToEndTestUtils.SOLO.getCurrentListViews(); trackListView = EndToEndTestUtils.SOLO.getCurrentListViews();
int trackNumberNew = trackListView.get(0).getCount(); int trackNumberNew = trackListView.get(0).getCount();
@@ -0,0 +1,44 @@
/*
* 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.endtoendtest;
import android.os.Bundle;
import android.test.InstrumentationTestRunner;
import android.util.Log;
/**
* A test runner can be used to run end-to-end test.
*
* @author Youtao Liu
*/
public class EndToEndTestRunner extends InstrumentationTestRunner {
/**
* Gets the parameter port which is used to fix GPS signal to emulators.
*/
@Override
public void onCreate(Bundle arguments) {
try {
EndToEndTestUtils.emulatorPort = Integer.parseInt(arguments.getString("port"));
} catch (Exception e) {
Log.e(EndToEndTestUtils.LOG_TAG, "Unable to get port parameter, use default value." + EndToEndTestUtils.emulatorPort, e);
}
Log.d(EndToEndTestUtils.LOG_TAG, "Use port number when run test on emulator:" + EndToEndTestUtils.emulatorPort);
super.onCreate(arguments);
}
}
@@ -23,10 +23,14 @@ import com.jayway.android.robotium.solo.Solo;
import android.app.Activity; import android.app.Activity;
import android.app.Instrumentation; import android.app.Instrumentation;
import android.content.Context;
import android.content.res.Configuration; import android.content.res.Configuration;
import android.util.Log;
import android.view.KeyEvent; import android.view.KeyEvent;
import android.view.View; import android.view.View;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button; import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageButton; import android.widget.ImageButton;
import android.widget.TextView; import android.widget.TextView;
@@ -38,6 +42,7 @@ import java.net.Socket;
import java.net.UnknownHostException; import java.net.UnknownHostException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.Locale;
/** /**
* Provides utilities to smoke test. * Provides utilities to smoke test.
@@ -45,9 +50,10 @@ import java.util.HashMap;
* @author Youtao Liu * @author Youtao Liu
*/ */
public class EndToEndTestUtils { public class EndToEndTestUtils {
private static final String ANDROID_LOCAL_IP = "10.0.2.2"; private static final String ANDROID_LOCAL_IP = "10.0.2.2";
// usually 5554. // usually 5554.
private static final int ANDROID_LOCAL_PORT = 5554; public static int emulatorPort = 5558;
private static final int ORIENTATION_PORTRAIT = 1; private static final int ORIENTATION_PORTRAIT = 1;
private static final int ORIENTATION_LANDSCAPE = 0; private static final int ORIENTATION_LANDSCAPE = 0;
@@ -57,9 +63,11 @@ public class EndToEndTestUtils {
private static final float START_LATITUDE = -1.3f; private static final float START_LATITUDE = -1.3f;
private static final float DELTA_LONGITUDE = 0.0005f; private static final float DELTA_LONGITUDE = 0.0005f;
private static final float DELTA_LADITUDE = 0.0005f; private static final float DELTA_LADITUDE = 0.0005f;
private static final String NO_GPS_MESSAGE_PREFIX = "GPS is not available";
private static final String MOREOPTION_CLASSNAME = "com.android.internal.view.menu.ActionMenuPresenter$OverflowMenuButton"; private static final String MOREOPTION_CLASSNAME = "com.android.internal.view.menu.ActionMenuPresenter$OverflowMenuButton";
static final String DEFAULTACTIVITY = "TestActivity";
static final String TRACK_NAME_PREFIX = "testTrackName"; static final String TRACK_NAME_PREFIX = "testTrackName";
static final String GPX = "gpx"; static final String GPX = "gpx";
static final String KML = "kml"; static final String KML = "kml";
@@ -71,9 +79,46 @@ public class EndToEndTestUtils {
// Following is some check strings in English and Chinese // Following is some check strings in English and Chinese
private static final HashMap<String, String> RELATIVE_STARTTIME_POSTFIX_MULTILINGUAL = new HashMap<String, String>(); private static final HashMap<String, String> RELATIVE_STARTTIME_POSTFIX_MULTILINGUAL = new HashMap<String, String>();
private static final HashMap<String, String> VIEW_MODE_ENGLISH_MULTILINGUAL = new HashMap<String, String>(); private static final HashMap<String, String> VIEW_MODE_MULTILINGUAL = new HashMap<String, String>();
private static final HashMap<String, String> KM_MULTILINGUAL = new HashMap<String, String>();
private static final HashMap<String, String> MILE_MULTILINGUAL = new HashMap<String, String>();
public static String RELATIVE_STARTTIME_POSTFIX = ""; public static String RELATIVE_STARTTIME_POSTFIX = "";
public static String VIEW_MODE = ""; 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 Solo SOLO;
static Instrumentation instrumentation; static Instrumentation instrumentation;
@@ -82,32 +127,35 @@ public class EndToEndTestUtils {
// Android OS. // Android OS.
static boolean hasActionBar = false; static boolean hasActionBar = false;
static boolean isEmulator = true; static boolean isEmulator = true;
static boolean hasGpsSingal = true;
static boolean isCheckedFirstLaunch = false; static boolean isCheckedFirstLaunch = false;
public static final String LOG_TAG = "MyTracksTest";
private EndToEndTestUtils() {} private EndToEndTestUtils() {}
/**
* Checks the language, then sets the fields with right string.
*/
private static void checkLanguage() { private static void checkLanguage() {
RELATIVE_STARTTIME_POSTFIX_MULTILINGUAL.put("es", "mins ago"); Locale locale = null;
RELATIVE_STARTTIME_POSTFIX_MULTILINGUAL.put("de", "Minuten"); Configuration config=null;
RELATIVE_STARTTIME_POSTFIX_MULTILINGUAL.put("fr", "minute"); config = activityMytracks.getBaseContext().getResources().getConfiguration();
RELATIVE_STARTTIME_POSTFIX_MULTILINGUAL.put("ar", "دقيقة"); locale = new Locale("en");
RELATIVE_STARTTIME_POSTFIX_MULTILINGUAL.put("zh", "分钟前"); Locale.setDefault(locale);
config.locale = locale;
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", "模式");
// Check the language, test in Chinese or English. deviceLanguage = instrumentation.getContext().getResources().getConfiguration().locale.getLanguage();
String deviceLanguage = instrumentation.getContext().getResources().getConfiguration().locale.getLanguage();
if (RELATIVE_STARTTIME_POSTFIX_MULTILINGUAL.get(deviceLanguage) != null) { if (RELATIVE_STARTTIME_POSTFIX_MULTILINGUAL.get(deviceLanguage) != null) {
RELATIVE_STARTTIME_POSTFIX = RELATIVE_STARTTIME_POSTFIX_MULTILINGUAL.get(deviceLanguage); 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 { } else {
RELATIVE_STARTTIME_POSTFIX = RELATIVE_STARTTIME_POSTFIX_MULTILINGUAL.get("es"); 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; PrintStream out = null;
Socket socket = null; Socket socket = null;
try { try {
socket = new Socket(ANDROID_LOCAL_IP, ANDROID_LOCAL_PORT); socket = new Socket(ANDROID_LOCAL_IP, emulatorPort);
out = new PrintStream(socket.getOutputStream()); out = new PrintStream(socket.getOutputStream());
float longitude = START_LONGITUDE; float longitude = START_LONGITUDE;
float latitude = START_LATITUDE; float latitude = START_LATITUDE;
@@ -138,6 +186,8 @@ public class EndToEndTestUtils {
longitude += DELTA_LONGITUDE; longitude += DELTA_LONGITUDE;
latitude += DELTA_LADITUDE; latitude += DELTA_LADITUDE;
} }
// Wait the GPS signal can be obtained by MyTracks.
Thread.sleep(SHORT_WAIT_TIME);
} catch (UnknownHostException e) { } catch (UnknownHostException e) {
System.exit(-1); System.exit(-1);
} catch (IOException e) { } catch (IOException e) {
@@ -166,27 +216,39 @@ public class EndToEndTestUtils {
*/ */
static void setupForAllTest(Instrumentation instrumentation, TrackListActivity activityMyTracks) { static void setupForAllTest(Instrumentation instrumentation, TrackListActivity activityMyTracks) {
setIsEmulator(); setIsEmulator();
EndToEndTestUtils.instrumentation = instrumentation; EndToEndTestUtils.instrumentation = instrumentation;
EndToEndTestUtils.activityMytracks = activityMyTracks; EndToEndTestUtils.activityMytracks = activityMyTracks;
EndToEndTestUtils.SOLO = new Solo(EndToEndTestUtils.instrumentation, SOLO = new Solo(EndToEndTestUtils.instrumentation,
EndToEndTestUtils.activityMytracks); 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 // 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. // welcome view with accept buttons. And makes sure only check once.
if (!EndToEndTestUtils.isCheckedFirstLaunch) { if (!isCheckedFirstLaunch) {
if ((EndToEndTestUtils.getButtonOnScreen(EndToEndTestUtils.activityMytracks if ((getButtonOnScreen(EndToEndTestUtils.activityMytracks
.getString(R.string.eula_accept), false, false) != null)) { .getString(R.string.eula_accept), false, false) != null)) {
EndToEndTestUtils.verifyFirstLaunch(); verifyFirstLaunch();
} else if (EndToEndTestUtils.SOLO.waitForText( } else if (SOLO.waitForText(
// After reset setting, welcome page will show again. // 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(); resetPreferredUnits();
} }
hasActionBar = setHasActionBar();
checkLanguage(); checkLanguage();
EndToEndTestUtils.isCheckedFirstLaunch = true; isCheckedFirstLaunch = true;
EndToEndTestUtils.setHasActionBar(); deleteAllTracks();
} else if (EndToEndTestUtils.SOLO.waitForText( resetAllSettings(activityMyTracks, false);
} else if (SOLO.waitForText(
// After reset setting, welcome page will show again. // 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(); resetPreferredUnits();
} }
@@ -241,7 +303,7 @@ public class EndToEndTestUtils {
*/ */
static void createSimpleTrack(int numberOfGpsData) { static void createSimpleTrack(int numberOfGpsData) {
startRecording(); startRecording();
EndToEndTestUtils.sendGps(numberOfGpsData); sendGps(numberOfGpsData);
instrumentation.waitForIdleSync(); instrumentation.waitForIdleSync();
stopRecording(true); stopRecording(true);
} }
@@ -276,12 +338,14 @@ public class EndToEndTestUtils {
* activity * activity
*/ */
static void createTrackIfEmpty(int gpsNumber, boolean showTrackList) { static void createTrackIfEmpty(int gpsNumber, boolean showTrackList) {
if (EndToEndTestUtils.isTrackListEmpty(!showTrackList)) { if (isTrackListEmpty(!showTrackList)) {
// Create a simple track. // Create a simple track.
EndToEndTestUtils.createSimpleTrack(gpsNumber); createSimpleTrack(gpsNumber);
instrumentation.waitForIdleSync();
if (showTrackList) { if (showTrackList) {
EndToEndTestUtils.SOLO.goBack(); SOLO.goBack();
} }
instrumentation.waitForIdleSync();
} }
} }
@@ -290,7 +354,7 @@ public class EndToEndTestUtils {
*/ */
static void startRecording() { static void startRecording() {
if (hasActionBar) { 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. // In case a track is recording.
if (startButton == null) { if (startButton == null) {
stopRecording(true); stopRecording(true);
@@ -298,7 +362,7 @@ public class EndToEndTestUtils {
} }
SOLO.clickOnView(startButton); SOLO.clickOnView(startButton);
} else { } else {
showMoreMenuItem(); showMenuItem();
if (!SOLO.searchText(activityMytracks.getString(R.string.menu_record_track))) { if (!SOLO.searchText(activityMytracks.getString(R.string.menu_record_track))) {
// Check if in TrackDetailActivity. // Check if in TrackDetailActivity.
if (SOLO.searchText(activityMytracks.getString(R.string.menu_play))) { if (SOLO.searchText(activityMytracks.getString(R.string.menu_play))) {
@@ -306,12 +370,13 @@ public class EndToEndTestUtils {
} else { } else {
// In case a track is recording. // In case a track is recording.
stopRecording(true); stopRecording(true);
showMoreMenuItem(); showMenuItem();
} }
} }
instrumentation.waitForIdleSync(); instrumentation.waitForIdleSync();
SOLO.clickOnText(activityMytracks.getString(R.string.menu_record_track)); SOLO.clickOnText(activityMytracks.getString(R.string.menu_record_track));
} }
instrumentation.waitForIdleSync();
} }
/** /**
@@ -324,7 +389,7 @@ public class EndToEndTestUtils {
return getButtonOnScreen(activityMytracks return getButtonOnScreen(activityMytracks
.getString(R.string.menu_record_track), false, false) == null; .getString(R.string.menu_record_track), false, false) == null;
} }
showMoreMenuItem(); showMenuItem();
if (SOLO.searchText(activityMytracks.getString(R.string.menu_record_track)) if (SOLO.searchText(activityMytracks.getString(R.string.menu_record_track))
|| SOLO.searchText(activityMytracks.getString(R.string.menu_play))) { || SOLO.searchText(activityMytracks.getString(R.string.menu_play))) {
SOLO.goBack(); SOLO.goBack();
@@ -343,22 +408,21 @@ public class EndToEndTestUtils {
if (hasActionBar) { if (hasActionBar) {
getButtonOnScreen(activityMytracks.getString(R.string.menu_stop_recording), false, true); getButtonOnScreen(activityMytracks.getString(R.string.menu_stop_recording), false, true);
} else { } else {
showMoreMenuItem(); showMenuItem();
instrumentation.waitForIdleSync(); instrumentation.waitForIdleSync();
SOLO.clickOnText(activityMytracks.getString(R.string.menu_stop_recording)); SOLO.clickOnText(activityMytracks.getString(R.string.menu_stop_recording));
} }
if (isSave) { 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 // Make every track name is unique to make sure every check can be
// trusted. // trusted.
EndToEndTestUtils.trackName = EndToEndTestUtils.TRACK_NAME_PREFIX trackName = TRACK_NAME_PREFIX
+ System.currentTimeMillis(); + System.currentTimeMillis();
SOLO.enterText(0, trackName); SOLO.sendKey(KeyEvent.KEYCODE_DEL);
if(!EndToEndTestUtils.isEmulator) { enterTextAvoidSoftKeyBoard(0, trackName);
// Close soft keyboard. enterTextAvoidSoftKeyBoard(1, DEFAULTACTIVITY);
EndToEndTestUtils.SOLO.goBack(); SOLO.clickOnText(activityMytracks.getString(R.string.generic_save));
} instrumentation.waitForIdleSync();
SOLO.clickLongOnText(activityMytracks.getString(R.string.generic_save));
} }
} }
@@ -377,6 +441,15 @@ 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. * Gets a kind of exported files.
* *
@@ -417,22 +490,49 @@ public class EndToEndTestUtils {
* @param isClick whether click the button if find it * @param isClick whether click the button if find it
* @return the button to search, and null means can not find the button * @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(); instrumentation.waitForIdleSync();
if (isWait) { if (isWait) {
SOLO.waitForText(buttonName); SOLO.waitForText(buttonName);
} }
ArrayList<Button> currentButtons = SOLO.getCurrentButtons();
for (Button button : currentButtons) { // Find on action bar.
String title = (String) button.getText(); if (hasActionBar) {
if (title.equalsIgnoreCase(buttonName)) { ArrayList<View> allViews = SOLO.getViews();
if(isClick) { for (View view : allViews) {
SOLO.clickOnView(button); String className = view.getClass().getName();
if(className.indexOf("ActionMenuItemView") > 0) {
String menuItemNameString = view.getContentDescription().toString();
if(menuItemNameString.equalsIgnoreCase(buttonName)) {
button = view;
break;
}
} }
return button;
} }
} }
return null;
// Get all buttons and find.
if(button == null) {
ArrayList<Button> currentButtons = SOLO.getCurrentButtons();
for (Button oneButton : currentButtons) {
String title = (String) oneButton.getText();
if (title.equalsIgnoreCase(buttonName)) {
button = oneButton;
}
}
}
if(button != null && isClick) {
SOLO.clickOnView(button);
}
if (button == null && isClick) {
Log.d(LOG_TAG, "Don't find the button " + buttonName);
}
return button;
} }
/** /**
@@ -441,31 +541,19 @@ public class EndToEndTestUtils {
* @return false means can not check failed. * @return false means can not check failed.
*/ */
static boolean setHasActionBar() { static boolean setHasActionBar() {
instrumentation.waitForIdleSync(); try {
// If can find record button without pressing Menu, it should be an action return activityMytracks.getActionBar() == null ? false : true;
// bar. }catch (Throwable e) {
Button startButton = getButtonOnScreen(activityMytracks.getString(R.string.menu_record_track), false, false); // For in Android which does not has action bar, here will meet a error.
Button stopButton = getButtonOnScreen(activityMytracks.getString(R.string.menu_stop_recording), false, false); return false;
if (startButton != null || stopButton != null) {
hasActionBar = true;
} else {
showMoreMenuItem();
if (SOLO.searchText(activityMytracks.getString(R.string.menu_record_track))
|| SOLO.searchText(activityMytracks.getString(R.string.menu_stop_recording))) {
hasActionBar = false;
} else {
return false;
}
SOLO.goBack();
} }
return true;
} }
/** /**
* Rotates the current activity. * Rotates the current activity.
*/ */
static void rotateCurrentActivity() { static void rotateCurrentActivity() {
EndToEndTestUtils.rotateActivity(SOLO.getCurrentActivity()); rotateActivity(SOLO.getCurrentActivity());
instrumentation.waitForIdleSync(); instrumentation.waitForIdleSync();
} }
@@ -473,11 +561,12 @@ public class EndToEndTestUtils {
* Rotates all activities. * Rotates all activities.
*/ */
static void rotateAllActivities() { static void rotateAllActivities() {
ArrayList<Activity> allActivities = SOLO.getAllOpenedActivities(); if (hasActionBar) {
for (Activity activity : allActivities) { ArrayList<Activity> allActivities = SOLO.getAllOpenedActivities();
EndToEndTestUtils.rotateActivity(activity); for (Activity activity : allActivities) {
rotateActivity(activity);
}
} }
instrumentation.waitForIdleSync(); instrumentation.waitForIdleSync();
} }
@@ -490,47 +579,86 @@ public class EndToEndTestUtils {
*/ */
static boolean findMenuItem(String menuName, boolean click) { static boolean findMenuItem(String menuName, boolean click) {
boolean findResult = false; boolean findResult = false;
boolean isMoreMenuOpened = false;
// Firstly find in action bar. // ICS phone.
Button button = getButtonOnScreen(menuName, false, false); if (hasActionBar) {
if (button != null) { // Firstly find in action bar.
findResult = true; View button = getButtonOnScreen(menuName, false, false);
if (click) { if (button != null) {
SOLO.clickOnView(button); findResult = true;
instrumentation.waitForIdleSync(); if (click) {
SOLO.clickOnView(button);
instrumentation.waitForIdleSync();
}
return findResult;
} }
return findResult; showMenuItem();
}
showMoreMenuItem();
if (SOLO.searchText(menuName)) {
findResult = true;
} else if (SOLO.searchText(MENU_MORE)) {
SOLO.clickOnText(MENU_MORE);
findResult = SOLO.searchText(menuName); findResult = SOLO.searchText(menuName);
} else {
// Non-ICS phone.
SOLO.sendKey(KeyEvent.KEYCODE_MENU);
if (SOLO.searchText(menuName)) {
findResult = true;
} else if (SOLO.searchText(MENU_MORE)) {
SOLO.clickOnText(MENU_MORE);
findResult = SOLO.searchText(menuName);
isMoreMenuOpened = true;
}
} }
if (findResult && click) { if (findResult && click) {
SOLO.clickOnText(menuName); SOLO.clickOnText(menuName);
instrumentation.waitForIdleSync(); instrumentation.waitForIdleSync();
} else { } else {
// Quit more menu list if opened.
if (isMoreMenuOpened) {
SOLO.goBack();
}
// Quit menu list.
SOLO.goBack(); SOLO.goBack();
} }
return findResult; return findResult;
} }
/** /**
* Get more menu items operation is different for different Android OS. * Show menu item list.
*/ */
public static void showMoreMenuItem() { public static void showMenuItem() {
showMenuItem(0);
}
/**
* Gets more menu items operation is different for different Android OS. When
* get overflow button view on action, it usually be able to click. But in
* some situation, will meet an error when click it. So catch it and try
* again. In most situation, the second click will be pass.
*
* @param depth control the depth of recursion to prevent dead circulation
*/
private static void showMenuItem(int depth) {
// ICS phone.
if (hasActionBar) { if (hasActionBar) {
instrumentation.waitForIdleSync();
View moreButton = getMoreOptionView(); View moreButton = getMoreOptionView();
// ICS phone without menu key.
if (moreButton != null) { if (moreButton != null) {
SOLO.clickOnView(moreButton); try {
SOLO.clickOnView(moreButton);
} catch (Throwable e) {
if (depth < 5 && e.getMessage().indexOf("Click can not be completed") > -1) {
showMenuItem(depth++);
}
}
return; return;
} else {
// ICS phone with menu key.
SOLO.sendKey(KeyEvent.KEYCODE_MENU);
} }
} else {
// Non-ICS phone with menu key.
SOLO.sendKey(KeyEvent.KEYCODE_MENU);
} }
SOLO.sendKey(KeyEvent.KEYCODE_MENU);
} }
/** /**
@@ -559,7 +687,7 @@ public class EndToEndTestUtils {
* @return the text view, null means can not find it * @return the text view, null means can not find it
*/ */
static TextView findTextView(String findText, View parent) { static TextView findTextView(String findText, View parent) {
ArrayList<TextView> textViews = EndToEndTestUtils.SOLO.getCurrentTextViews(parent); ArrayList<TextView> textViews = SOLO.getCurrentTextViews(parent);
for (TextView textView : textViews) { for (TextView textView : textViews) {
String text = (String) textView.getText(); String text = (String) textView.getText();
if (textView.isShown() && text.endsWith(findText)) { if (textView.isShown() && text.endsWith(findText)) {
@@ -575,7 +703,7 @@ public class EndToEndTestUtils {
* @return the ChartView or null if not find * @return the ChartView or null if not find
*/ */
static ChartView getChartView() { static ChartView getChartView() {
ArrayList<View> views = EndToEndTestUtils.SOLO.getViews(); ArrayList<View> views = SOLO.getViews();
for (View view : views) { for (View view : views) {
if (view instanceof ChartView) { if (view instanceof ChartView) {
return (ChartView) view; return (ChartView) view;
@@ -591,13 +719,28 @@ public class EndToEndTestUtils {
* @param keepInSettingList whether keep in setting list or not * @param keepInSettingList whether keep in setting list or not
*/ */
public static void resetAllSettings(Activity activityMyTracks, boolean keepInSettingList) { public static void resetAllSettings(Activity activityMyTracks, boolean keepInSettingList) {
EndToEndTestUtils.findMenuItem(activityMyTracks.getString(R.string.menu_settings), true); findMenuItem(activityMyTracks.getString(R.string.menu_settings), true);
SOLO.waitForText(activityMyTracks.getString(R.string.settings_reset)); SOLO.waitForText(activityMyTracks.getString(R.string.settings_reset));
SOLO.clickOnText(activityMyTracks.getString(R.string.settings_reset)); SOLO.clickOnText(activityMyTracks.getString(R.string.settings_reset));
getButtonOnScreen(activityMytracks.getString(R.string.generic_ok), true, true); getButtonOnScreen(activityMytracks.getString(R.string.generic_ok), true, true);
instrumentation.waitForIdleSync(); instrumentation.waitForIdleSync();
if (!keepInSettingList) { if (!keepInSettingList) {
EndToEndTestUtils.SOLO.goBack(); SOLO.goBack();
} }
} }
/**
* Finds a edit text and enter text in it. This method can hides soft key
* board when input text.
*
* @param editTextIndex the index of edit text
* @param text to enter
*/
public static void enterTextAvoidSoftKeyBoard(int editTextIndex, String text) {
InputMethodManager imm = (InputMethodManager) activityMytracks.getApplicationContext().getSystemService(Context.INPUT_METHOD_SERVICE);
EditText editText = SOLO.getEditText(editTextIndex);
imm.hideSoftInputFromWindow(editText.getWindowToken(), InputMethodManager.HIDE_IMPLICIT_ONLY);
SOLO.enterText(editText, text);
}
} }
@@ -91,7 +91,6 @@ public class ExportAndImportTest extends ActivityInstrumentationTestCase2<TrackL
EndToEndTestUtils.SOLO EndToEndTestUtils.SOLO
.clickOnText(String.format(activityMyTracks.getString(R.string.menu_save_format), .clickOnText(String.format(activityMyTracks.getString(R.string.menu_save_format),
EndToEndTestUtils.GPX.toUpperCase())); EndToEndTestUtils.GPX.toUpperCase()));
EndToEndTestUtils.rotateAllActivities();
EndToEndTestUtils.SOLO.waitForText(activityMyTracks.getString(R.string.sd_card_save_success)); EndToEndTestUtils.SOLO.waitForText(activityMyTracks.getString(R.string.sd_card_save_success));
EndToEndTestUtils.getButtonOnScreen(activityMyTracks.getString(R.string.generic_ok), true, true); EndToEndTestUtils.getButtonOnScreen(activityMyTracks.getString(R.string.generic_ok), true, true);
// Check export file. // Check export file.
@@ -113,6 +112,7 @@ public class ExportAndImportTest extends ActivityInstrumentationTestCase2<TrackL
// gpxFilesNumber; // gpxFilesNumber;
EndToEndTestUtils.getButtonOnScreen(activityMyTracks.getString(R.string.generic_ok), true, true); EndToEndTestUtils.getButtonOnScreen(activityMyTracks.getString(R.string.generic_ok), true, true);
instrumentation.waitForIdleSync(); instrumentation.waitForIdleSync();
assertTrue(EndToEndTestUtils.SOLO.waitForText(EndToEndTestUtils.trackName));
assertEquals(trackNumber + gpxFilesNumber, EndToEndTestUtils.SOLO.getCurrentListViews().get(0) assertEquals(trackNumber + gpxFilesNumber, EndToEndTestUtils.SOLO.getCurrentListViews().get(0)
.getCount()); .getCount());
@@ -19,10 +19,13 @@ import com.google.android.apps.mytracks.TrackListActivity;
import com.google.android.apps.mytracks.util.GoogleLocationUtils; import com.google.android.apps.mytracks.util.GoogleLocationUtils;
import com.google.android.maps.mytracks.R; import com.google.android.maps.mytracks.R;
import android.annotation.TargetApi; import android.app.Activity;
import android.app.Instrumentation; import android.app.Instrumentation;
import android.test.ActivityInstrumentationTestCase2; import android.test.ActivityInstrumentationTestCase2;
import android.view.View; import android.view.View;
import android.widget.ImageButton;
import java.util.ArrayList;
/** /**
* Tests the function of go to my location. * Tests the function of go to my location.
@@ -34,7 +37,6 @@ public class GoToMyLocationTest extends ActivityInstrumentationTestCase2<TrackLi
private Instrumentation instrumentation; private Instrumentation instrumentation;
private TrackListActivity activityMyTracks; private TrackListActivity activityMyTracks;
@TargetApi(8)
public GoToMyLocationTest() { public GoToMyLocationTest() {
super(TrackListActivity.class); super(TrackListActivity.class);
} }
@@ -51,23 +53,45 @@ public class GoToMyLocationTest extends ActivityInstrumentationTestCase2<TrackLi
* Tests the menu My Location. * Tests the menu My Location.
*/ */
public void testGotoMyLocation() { public void testGotoMyLocation() {
EndToEndTestUtils.createTrackIfEmpty(1, false); findAndClickMyLocation(activityMyTracks);
EndToEndTestUtils.sendGps(30);
instrumentation.waitForIdleSync();
View myLocation = EndToEndTestUtils.SOLO.getCurrentActivity()
.findViewById(R.id.map_my_location);
EndToEndTestUtils.SOLO.clickOnView(myLocation);
if (EndToEndTestUtils.isEmulator) { if (EndToEndTestUtils.isEmulator) {
String setting = activityMyTracks.getString( String setting = activityMyTracks.getString(
GoogleLocationUtils.isAvailable(activityMyTracks) ? R.string.gps_google_location_settings GoogleLocationUtils.isAvailable(activityMyTracks) ? R.string.gps_google_location_settings
: R.string.gps_location_access); : R.string.gps_location_access);
EndToEndTestUtils.SOLO.waitForText( EndToEndTestUtils.SOLO.waitForText(
activityMyTracks.getString(R.string.my_location_no_gps, setting), 1, 1000); activityMyTracks.getString(R.string.my_location_no_gps, setting), 1,
EndToEndTestUtils.SHORT_WAIT_TIME);
} else { } else {
// TODO How to verify the location is shown on the map. // TODO How to verify the location is shown on the map.
} }
} }
/**
* Finds the My Location view and click it.
*
* @param activity
*/
public static void findAndClickMyLocation(Activity activity) {
EndToEndTestUtils.createTrackIfEmpty(1, false);
EndToEndTestUtils.sendGps(30);
View myLocation = EndToEndTestUtils.SOLO.getCurrentActivity()
.findViewById(R.id.map_my_location);
// Find the My Location button in another if null.
if (myLocation == null) {
ArrayList<ImageButton> aa = EndToEndTestUtils.SOLO.getCurrentImageButtons();
for (ImageButton imageButton : aa) {
if (imageButton.getContentDescription() != null
&& imageButton.getContentDescription().equals(
activity.getString(R.string.icon_my_location))) {
myLocation = imageButton;
break;
}
}
}
EndToEndTestUtils.SOLO.clickOnView(myLocation);
}
@Override @Override
protected void tearDown() throws Exception { protected void tearDown() throws Exception {
EndToEndTestUtils.SOLO.finishOpenedActivities(); EndToEndTestUtils.SOLO.finishOpenedActivities();
@@ -0,0 +1,365 @@
/*
* 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.endtoendtest;
import com.google.android.apps.mytracks.Constants;
import com.google.android.apps.mytracks.io.docs.SendDocsUtils;
import com.google.android.apps.mytracks.io.fusiontables.SendFusionTablesAsyncTask;
import com.google.android.apps.mytracks.io.fusiontables.SendFusionTablesUtils;
import com.google.android.apps.mytracks.io.gdata.GDataClientFactory;
import com.google.android.apps.mytracks.io.gdata.docs.DocumentsClient;
import com.google.android.apps.mytracks.io.gdata.docs.SpreadsheetsClient;
import com.google.android.apps.mytracks.io.gdata.docs.XmlDocsGDataParserFactory;
import com.google.android.apps.mytracks.io.gdata.maps.MapFeatureEntry;
import com.google.android.apps.mytracks.io.gdata.maps.MapsClient;
import com.google.android.apps.mytracks.io.gdata.maps.MapsConstants;
import com.google.android.apps.mytracks.io.gdata.maps.MapsGDataConverter;
import com.google.android.apps.mytracks.io.gdata.maps.MapsMapMetadata;
import com.google.android.apps.mytracks.io.gdata.maps.XmlMapsGDataParserFactory;
import com.google.android.apps.mytracks.util.ApiAdapterFactory;
import com.google.android.apps.mytracks.util.SystemUtils;
import com.google.android.common.gdata.AndroidXmlParserFactory;
import com.google.api.client.googleapis.GoogleHeaders;
import com.google.api.client.googleapis.MethodOverride;
import com.google.api.client.http.GenericUrl;
import com.google.api.client.http.HttpRequest;
import com.google.api.client.http.HttpResponse;
import com.google.api.client.http.InputStreamContent;
import com.google.api.client.util.Strings;
import com.google.wireless.gdata.data.Entry;
import com.google.wireless.gdata.data.Feed;
import com.google.wireless.gdata.parser.GDataParser;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.app.Activity;
import android.content.Context;
import android.util.Log;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Locale;
/**
* Provides utilities to access Google Maps, Google Documents, Google Fusion
* Tables.
*
* @author Youtao Liu
*/
public class GoogleUtils {
public static final String DOCUMENT_NAME_PREFIX = "My Tracks";
public static final String SPREADSHEET_NAME = DOCUMENT_NAME_PREFIX + "-" + EndToEndTestUtils.DEFAULTACTIVITY;
/**
* Gets the account to access Google Services.
*
* @param context context used to get account
* @return the first account which is bound with current device
*/
private static Account getAccount(Context context) {
return AccountManager.get(context).getAccountsByType(Constants.ACCOUNT_TYPE)[0];
}
/**
* Gets Google maps of a user.
*
* @param context used to get maps
* @param mapsClient the client to access Google Maps
* @return true means set successfully
*/
private static ArrayList<MapsMapMetadata> getMaps(Context context, MapsClient mapsClient) {
String authToken = null;
ArrayList<String> mapIds = new ArrayList<String>();
ArrayList<MapsMapMetadata> mapData = new ArrayList<MapsMapMetadata>();
try {
authToken = AccountManager.get(context).blockingGetAuthToken(getAccount(context),
MapsConstants.SERVICE_NAME, false);
} catch (Exception e) {
Log.d(EndToEndTestUtils.LOG_TAG, "Unable to get auth token", e);
return mapData;
}
GDataParser gDataParser = null;
try {
gDataParser = mapsClient.getParserForFeed(MapFeatureEntry.class, MapsClient.getMapsFeed(),
authToken);
gDataParser.init();
while (gDataParser.hasMoreData()) {
MapFeatureEntry entry = (MapFeatureEntry) gDataParser.readNextEntry(null);
mapIds.add(MapsGDataConverter.getMapidForEntry(entry));
mapData.add(MapsGDataConverter.getMapMetadataForEntry(entry));
}
} catch (Exception e) {
Log.d(EndToEndTestUtils.LOG_TAG, "Unable to get maps", e);
} finally {
if (gDataParser != null) {
gDataParser.close();
}
}
return mapData;
}
/**
* Searches a map in user's Google Maps.
*
* @param title the title of map
* @param activity activity to get context
* @param isDelete whether delete the map of this track in the Google Maps
* @return true means find the map
*/
private static boolean searchMapByTitle(String title, Activity activity, boolean isDelete) {
Context context = activity.getApplicationContext();
MapsClient mapsClient = new MapsClient(GDataClientFactory.getGDataClient(context),
new XmlMapsGDataParserFactory(new AndroidXmlParserFactory()));
ArrayList<MapsMapMetadata> mapData = getMaps(context, mapsClient);
for (MapsMapMetadata oneData : mapData) {
if (oneData.getDescription().indexOf(DOCUMENT_NAME_PREFIX) > -1
&& oneData.getTitle().equals(title)) {
if (isDelete) {
try {
mapsClient.deleteEntry(oneData.getGDataEditUri(), AccountManager.get(context).blockingGetAuthToken(getAccount(context),
MapsConstants.SERVICE_NAME, false));
return true;
} catch (Exception e) {
Log.d(EndToEndTestUtils.LOG_TAG, "Unable to drop map", e);
return false;
}
}
return true;
}
}
return false;
}
/**
* Searches a map in user's Google Maps.
*
* @param title the title of map
* @param activity activity to get context
* @return true means find the map
*/
public static boolean searchMap(String title, Activity activity) {
return searchMapByTitle(title, activity, false);
}
/**
* Searches a map in user's Google Maps and then delete it.
*
* @param title the title of map
* @param activity activity to get context
* @return true means find the map and delete it successfully
*/
public static boolean deleteMap(String title, Activity activity) {
return searchMapByTitle(title, activity, true);
}
/**
* Searches a doc in user's Google Documents.
*
* @param title the title of doc
* @param activity to get context
* @return the entry of the document, null means can not find the spreadsheet.
*/
private static Entry searchSpreadsheetByTitle(String title, Activity activity) {
Context context = activity.getApplicationContext();
DocumentsClient documentsClient = new DocumentsClient(
GDataClientFactory.getGDataClient(context),
new XmlDocsGDataParserFactory(new AndroidXmlParserFactory()));
try {
String documentsAuthToken = AccountManager.get(context)
.blockingGetAuthToken(getAccount(context), documentsClient.getServiceName(), false);
String uri = String.format(Locale.US, SendDocsUtils.GET_SPREADSHEET_BY_TITLE_URI,
URLEncoder.encode(title, "utf-8"));
GDataParser gDataParser = documentsClient.getParserForFeed(Entry.class, uri,
documentsAuthToken);
gDataParser.init();
while (gDataParser.hasMoreData()) {
Entry entry = gDataParser.readNextEntry(null);
String entryTitle = entry.getTitle();
if (entryTitle.equals(title)) {
return entry;
}
}
} catch (Exception e) {
Log.d(EndToEndTestUtils.LOG_TAG, "Unable to fetch spreadsheet.", e);
}
return null;
}
/**
* Searches a track title in a spreadsheet.
*
* @param title the track name to search
* @param activity to get context
* @param spreadsheetTitle the title of spreadsheet
* @param isDelete whether delete the information of this track in the document
* @return true means find the track name in the spreadsheet
*/
private static boolean searchTrackTitleInSpreadsheet(String title, Activity activity, String spreadsheetTitle, boolean isDelete) {
String spreadsheetId = searchSpreadsheetByTitle(spreadsheetTitle, activity).getId().replace(SendDocsUtils.SPREADSHEET_ID_PREFIX, "");
if(spreadsheetId == null) {
Log.d(EndToEndTestUtils.LOG_TAG, "Unable to find the spreadsheet -- " + spreadsheetTitle);
return false;
}
Context context = activity.getApplicationContext();
try {
SpreadsheetsClient spreadsheetsClient = new SpreadsheetsClient(
GDataClientFactory.getGDataClient(context), new XmlDocsGDataParserFactory(new AndroidXmlParserFactory()));
String spreadsheetsAuthToken = AccountManager.get(activity.getApplicationContext()).blockingGetAuthToken(
getAccount(context), spreadsheetsClient.getServiceName(), false);
String weekSheetId = SendDocsUtils.getWorksheetId(spreadsheetId, spreadsheetsClient, spreadsheetsAuthToken);
String worksheetUri = String.format(Locale.US, SendDocsUtils.GET_WORKSHEET_URI, URLEncoder.encode(spreadsheetId, "utf-8"),
weekSheetId);
GDataParser gDataParser = spreadsheetsClient.getParserForFeed(Feed.class, worksheetUri,
spreadsheetsAuthToken);
gDataParser.init();
while (gDataParser.hasMoreData()) {
Entry entry = gDataParser.readNextEntry(null);
String entryTitle = entry.getTitle();
if (entryTitle.indexOf(title) > -1) {
if (isDelete) {
spreadsheetsClient.deleteEntry(entry.getEditUri(), spreadsheetsAuthToken);
}
return true;
}
}
} catch (Exception e) {
Log.d(EndToEndTestUtils.LOG_TAG, "Unable to fetch content of spreadsheet.", e);
}
return false;
}
/**
* Searches a track in spreadsheet.
*
* @param title the track name to search
* @param activity to get context
* @return true means find the track name in the spreadsheet
*/
public static boolean searchTrackInSpreadSheet(String title, Activity activity) {
return searchTrackTitleInSpreadsheet(title, activity, GoogleUtils.SPREADSHEET_NAME, false);
}
/**
* Searches and deletes a track in spreadsheet.
*
* @param title the track name to search
* @param activity to get context
* @return true means find and delete successfully
*/
public static boolean deleteTrackInSpreadSheet(String title, Activity activity) {
return searchTrackTitleInSpreadsheet(title, activity, GoogleUtils.SPREADSHEET_NAME, true);
}
/**
* Searches a fusion table in user's Google tables.
*
* @param title the title of fusion table
* @param activity to get context
* @return true means find the fusion table
*/
public static boolean searchFusionTableByTitle(String title, Activity activity) {
Context context = activity.getApplicationContext();
try {
HttpResponse response = sendFusionTableQuery("SHOW TABLES", context);
// We can use index of method to check new table for every track name is unique.
if (response != null && response.parseAsString().indexOf(title) > 0) {
return true;
}
} catch (Exception e) {
Log.d(EndToEndTestUtils.LOG_TAG, "Unable to query fusion table.", e);
}
return false;
}
/**
* Drops one fusion table which contain the string in title of current user.
*
* @param title the title of a track to drop
* @param activity to get context
* @return the result of drop
*/
public static boolean dropFusionTables(String title, Activity activity) {
Context context = activity.getApplicationContext();
HttpResponse response = sendFusionTableQuery("SHOW TABLES", context);
String[] rowsTable;
try {
rowsTable = response.parseAsString().split("\n");
for (String row : rowsTable) {
String firstColumn = row.split(",")[0];
String secondColumn = row.split(",")[1];
// If the first column is all figure, it is the table id.
String regularExpression = "^[0-9]*$";
if (firstColumn.matches(regularExpression) && secondColumn.equals(title)) {
sendFusionTableQuery("DROP TABLE " + row.split(",")[0], context);
return true;
}
}
} catch (IOException e) {
Log.d(EndToEndTestUtils.LOG_TAG, "Failed when delete all fusion tables.", e);
}
return false;
}
/**
* Sends query to operate fusion tables.
*
* @param query to executed
* @param context application context
* @return the response of execution
*/
private static HttpResponse sendFusionTableQuery(String query, Context context) {
try {
String fusionTableAuthToken = AccountManager.get(context).blockingGetAuthToken(getAccount(context),
SendFusionTablesUtils.SERVICE, false);
GenericUrl url = new GenericUrl(SendFusionTablesAsyncTask.FUSION_TABLES_BASE_URL);
String sql = "sql=" + query;
ByteArrayInputStream inputStream = new ByteArrayInputStream(Strings.toBytesUtf8(sql));
InputStreamContent inputStreamContent = new InputStreamContent(null, inputStream);
HttpRequest request;
request = (ApiAdapterFactory.getApiAdapter().getHttpTransport()
.createRequestFactory(new MethodOverride())).buildPostRequest(url, inputStreamContent);
GoogleHeaders headers = new GoogleHeaders();
headers.setApplicationName(SendFusionTablesAsyncTask.APP_NAME_PREFIX
+ SystemUtils.getMyTracksVersion(context));
headers.gdataVersion = SendFusionTablesAsyncTask.GDATA_VERSION;
headers.setGoogleLogin(fusionTableAuthToken);
headers.setContentType(SendFusionTablesAsyncTask.CONTENT_TYPE);
request.setHeaders(headers);
HttpResponse response;
response = request.execute();
return response;
} catch (Exception e) {
Log.d(EndToEndTestUtils.LOG_TAG, "Failed when send fusion table query.", e);
return null;
}
}
}
@@ -22,9 +22,6 @@ import android.annotation.TargetApi;
import android.app.Instrumentation; import android.app.Instrumentation;
import android.test.ActivityInstrumentationTestCase2; import android.test.ActivityInstrumentationTestCase2;
import android.view.KeyEvent; import android.view.KeyEvent;
import android.widget.EditText;
import java.util.ArrayList;
/** /**
* Tests some menu items of MyTracks. * Tests some menu items of MyTracks.
@@ -67,13 +64,16 @@ public class MenuItemsTest extends ActivityInstrumentationTestCase2<TrackListAct
EndToEndTestUtils.createTrackIfEmpty(1, false); EndToEndTestUtils.createTrackIfEmpty(1, false);
instrumentation.waitForIdleSync(); instrumentation.waitForIdleSync();
// Menu in TrackDetailActivity. // Menu in TrackDetailActivity.
EndToEndTestUtils.findMenuItem(activityMyTracks.getString(R.string.menu_sensor_state), true); // When there is no sensor connected this menu will be hidden.
EndToEndTestUtils.SOLO.waitForText(activityMyTracks if (EndToEndTestUtils
.getString(R.string.sensor_state_last_sensor_time)); .findMenuItem(activityMyTracks.getString(R.string.menu_sensor_state), true)) {
EndToEndTestUtils.SOLO.waitForText(activityMyTracks
.getString(R.string.sensor_state_last_sensor_time));
}
EndToEndTestUtils.SOLO.goBack(); EndToEndTestUtils.SOLO.goBack();
EndToEndTestUtils.findMenuItem(activityMyTracks.getString(R.string.menu_help), true); EndToEndTestUtils.findMenuItem(activityMyTracks.getString(R.string.menu_help), true);
EndToEndTestUtils.SOLO.clickOnText(activityMyTracks.getString(R.string.help_about)); EndToEndTestUtils.getButtonOnScreen(activityMyTracks.getString(R.string.help_about), true, true);
EndToEndTestUtils.getButtonOnScreen(activityMyTracks.getString(R.string.generic_ok), true, true); EndToEndTestUtils.getButtonOnScreen(activityMyTracks.getString(R.string.generic_ok), true, true);
EndToEndTestUtils.getButtonOnScreen(activityMyTracks.getString(R.string.generic_ok), true, true); EndToEndTestUtils.getButtonOnScreen(activityMyTracks.getString(R.string.generic_ok), true, true);
} }
@@ -85,8 +85,7 @@ public class MenuItemsTest extends ActivityInstrumentationTestCase2<TrackListAct
EndToEndTestUtils.createSimpleTrack(1); EndToEndTestUtils.createSimpleTrack(1);
EndToEndTestUtils.SOLO.goBack(); EndToEndTestUtils.SOLO.goBack();
EndToEndTestUtils.findMenuItem(activityMyTracks.getString(R.string.menu_search), true); EndToEndTestUtils.findMenuItem(activityMyTracks.getString(R.string.menu_search), true);
ArrayList<EditText> editTexts = EndToEndTestUtils.SOLO.getCurrentEditTexts(); EndToEndTestUtils.enterTextAvoidSoftKeyBoard(0, EndToEndTestUtils.trackName);
EndToEndTestUtils.SOLO.enterText(editTexts.get(0), EndToEndTestUtils.trackName);
sendKeys(KeyEvent.KEYCODE_ENTER); sendKeys(KeyEvent.KEYCODE_ENTER);
instrumentation.waitForIdleSync(); instrumentation.waitForIdleSync();
assertEquals(1, EndToEndTestUtils.SOLO.getCurrentListViews().size()); assertEquals(1, EndToEndTestUtils.SOLO.getCurrentListViews().size());
@@ -33,8 +33,6 @@ import java.util.ArrayList;
*/ */
public class SettingTest extends ActivityInstrumentationTestCase2<TrackListActivity> { public class SettingTest extends ActivityInstrumentationTestCase2<TrackListActivity> {
private final String DEFAULTACTIVITY = "TestActivity";
private Instrumentation instrumentation; private Instrumentation instrumentation;
private TrackListActivity activityMyTracks; private TrackListActivity activityMyTracks;
@@ -78,14 +76,14 @@ public class SettingTest extends ActivityInstrumentationTestCase2<TrackListActiv
// Change a setting of sharing. // Change a setting of sharing.
EndToEndTestUtils.SOLO.clickOnText(activityMyTracks.getString(R.string.settings_sharing)); EndToEndTestUtils.SOLO.clickOnText(activityMyTracks.getString(R.string.settings_sharing));
assertTrue(EndToEndTestUtils.SOLO.waitForText( assertTrue(EndToEndTestUtils.SOLO.waitForText(
activityMyTracks.getString(R.string.settings_sharing_allow_access), 1, 5000)); activityMyTracks.getString(R.string.settings_sharing_allow_access), 1, EndToEndTestUtils.NORMAL_WAIT_TIME));
ArrayList<CheckBox> sharingCheckBoxs = EndToEndTestUtils.SOLO.getCurrentCheckBoxes(); ArrayList<CheckBox> sharingCheckBoxs = EndToEndTestUtils.SOLO.getCurrentCheckBoxes();
boolean newMapsPublic = sharingCheckBoxs.get(0).isChecked(); boolean newMapsPublic = sharingCheckBoxs.get(0).isChecked();
EndToEndTestUtils.SOLO.clickOnCheckBox(0); EndToEndTestUtils.SOLO.clickOnCheckBox(0);
instrumentation.waitForIdleSync(); instrumentation.waitForIdleSync();
assertTrue(EndToEndTestUtils.SOLO.waitForText( assertTrue(EndToEndTestUtils.SOLO.waitForText(
activityMyTracks.getString(R.string.settings_sharing_allow_access), 1, 5000)); activityMyTracks.getString(R.string.settings_sharing_allow_access), 1, EndToEndTestUtils.NORMAL_WAIT_TIME));
assertEquals(!newMapsPublic, sharingCheckBoxs.get(0).isChecked()); assertEquals(!newMapsPublic, EndToEndTestUtils.SOLO.getCurrentCheckBoxes().get(0).isChecked());
EndToEndTestUtils.SOLO.goBack(); EndToEndTestUtils.SOLO.goBack();
// Reset all settings. // Reset all settings.
@@ -93,9 +91,11 @@ public class SettingTest extends ActivityInstrumentationTestCase2<TrackListActiv
EndToEndTestUtils.getButtonOnScreen(activityMyTracks.getString(R.string.generic_ok), true, true); EndToEndTestUtils.getButtonOnScreen(activityMyTracks.getString(R.string.generic_ok), true, true);
// Check settings. // Check settings.
// Add following scroll up for a bug of Robotium.
EndToEndTestUtils.SOLO.scrollUp();
EndToEndTestUtils.SOLO.clickOnText(activityMyTracks.getString(R.string.track_detail_stats_tab)); EndToEndTestUtils.SOLO.clickOnText(activityMyTracks.getString(R.string.track_detail_stats_tab));
EndToEndTestUtils.SOLO.waitForText(activityMyTracks assertTrue(EndToEndTestUtils.SOLO.waitForText(activityMyTracks
.getString(R.string.settings_stats_units_title), 1, 20000); .getString(R.string.settings_stats_units_title), 1, EndToEndTestUtils.LONG_WAIT_TIME));
displayCheckBoxs = EndToEndTestUtils.SOLO.getCurrentCheckBoxes(); displayCheckBoxs = EndToEndTestUtils.SOLO.getCurrentCheckBoxes();
assertEquals(useMetric, displayCheckBoxs.get(0).isChecked()); assertEquals(useMetric, displayCheckBoxs.get(0).isChecked());
@@ -153,9 +153,11 @@ public class SettingTest extends ActivityInstrumentationTestCase2<TrackListActiv
EndToEndTestUtils.createTrackIfEmpty(5, false); EndToEndTestUtils.createTrackIfEmpty(5, false);
EndToEndTestUtils.resetAllSettings(activityMyTracks, true); EndToEndTestUtils.resetAllSettings(activityMyTracks, true);
ChangeStatsSettings(false, false, true, true, true); ChangeStatsSettings(false, false, true, true, true);
instrumentation.waitForIdleSync();
EndToEndTestUtils.SOLO.clickOnText(activityMyTracks.getString(R.string.track_detail_stats_tab)); EndToEndTestUtils.SOLO.clickOnText(activityMyTracks.getString(R.string.track_detail_stats_tab));
assertTrue(EndToEndTestUtils.SOLO.waitForText(activityMyTracks assertTrue(EndToEndTestUtils.SOLO.waitForText(
.getString(R.string.stats_min_elevation))); activityMyTracks.getString(R.string.stats_min_elevation), 1,
EndToEndTestUtils.NORMAL_WAIT_TIME, true));
assertTrue(EndToEndTestUtils.SOLO.waitForText(activityMyTracks assertTrue(EndToEndTestUtils.SOLO.waitForText(activityMyTracks
.getString(R.string.stats_min_grade))); .getString(R.string.stats_min_grade)));
assertTrue(EndToEndTestUtils.SOLO.waitForText(activityMyTracks assertTrue(EndToEndTestUtils.SOLO.waitForText(activityMyTracks
@@ -241,27 +243,28 @@ public class SettingTest extends ActivityInstrumentationTestCase2<TrackListActiv
// Write to SD card. // Write to SD card.
EndToEndTestUtils.findMenuItem(activityMyTracks.getString(R.string.menu_settings), true); EndToEndTestUtils.findMenuItem(activityMyTracks.getString(R.string.menu_settings), true);
EndToEndTestUtils.SOLO.clickOnText(activityMyTracks.getString(R.string.settings_backup)); EndToEndTestUtils.SOLO.clickOnText(activityMyTracks.getString(R.string.settings_backup));
EndToEndTestUtils.SOLO EndToEndTestUtils.SOLO.clickOnText(activityMyTracks.getString(R.string.settings_backup_now));
.clickOnText(activityMyTracks.getString(R.string.settings_backup_now));
assertTrue(EndToEndTestUtils.SOLO.waitForText( assertTrue(EndToEndTestUtils.SOLO.waitForText(
activityMyTracks.getString(R.string.sd_card_save_success), 0, 100000)); activityMyTracks.getString(R.string.sd_card_save_success), 0,
EndToEndTestUtils.SUPER_LONG_WAIT_TIME));
instrumentation.waitForIdleSync(); instrumentation.waitForIdleSync();
// Delete all tracks. // Delete all tracks.
EndToEndTestUtils.SOLO.goBack(); EndToEndTestUtils.SOLO.goBack();
EndToEndTestUtils.SOLO.goBack(); EndToEndTestUtils.SOLO.goBack();
EndToEndTestUtils.findMenuItem(activityMyTracks.getString(R.string.menu_delete_all), true); EndToEndTestUtils.deleteAllTracks();
EndToEndTestUtils.getButtonOnScreen(activityMyTracks.getString(R.string.generic_ok), true, true);
instrumentation.waitForIdleSync(); instrumentation.waitForIdleSync();
// Read from SD card. // Read from SD card.
EndToEndTestUtils.findMenuItem(activityMyTracks.getString(R.string.menu_settings), true); EndToEndTestUtils.findMenuItem(activityMyTracks.getString(R.string.menu_settings), true);
EndToEndTestUtils.SOLO.clickOnText(activityMyTracks.getString(R.string.settings_backup)); EndToEndTestUtils.SOLO.clickOnText(activityMyTracks.getString(R.string.settings_backup));
EndToEndTestUtils.SOLO.clickOnText(activityMyTracks EndToEndTestUtils.SOLO
.getString(R.string.settings_backup_restore)); .clickOnText(activityMyTracks.getString(R.string.settings_backup_restore));
EndToEndTestUtils.getButtonOnScreen(activityMyTracks.getString(R.string.generic_ok), true, true); EndToEndTestUtils
.getButtonOnScreen(activityMyTracks.getString(R.string.generic_ok), true, true);
assertTrue(EndToEndTestUtils.SOLO.waitForText( assertTrue(EndToEndTestUtils.SOLO.waitForText(
activityMyTracks.getString(R.string.sd_card_import_success), 0, 10000)); activityMyTracks.getString(R.string.sd_card_import_success), 0,
EndToEndTestUtils.SUPER_LONG_WAIT_TIME));
// Check restore track. // Check restore track.
assertTrue(EndToEndTestUtils.SOLO.searchText(EndToEndTestUtils.trackName)); assertTrue(EndToEndTestUtils.SOLO.searchText(EndToEndTestUtils.trackName));
} }
@@ -281,12 +284,9 @@ public class SettingTest extends ActivityInstrumentationTestCase2<TrackListActiv
// Changes the setting of default activity. // Changes the setting of default activity.
EndToEndTestUtils.SOLO.clickOnText(activityMyTracks EndToEndTestUtils.SOLO.clickOnText(activityMyTracks
.getString(R.string.settings_recording_default_activity)); .getString(R.string.settings_recording_default_activity));
EndToEndTestUtils.SOLO.enterText(0, DEFAULTACTIVITY); EndToEndTestUtils.enterTextAvoidSoftKeyBoard(0, EndToEndTestUtils.DEFAULTACTIVITY);
if(!EndToEndTestUtils.isEmulator) { EndToEndTestUtils
// Close soft keyboard. .getButtonOnScreen(activityMyTracks.getString(R.string.generic_ok), true, true);
EndToEndTestUtils.SOLO.goBack();
}
EndToEndTestUtils.getButtonOnScreen(activityMyTracks.getString(R.string.generic_ok), true, true);
EndToEndTestUtils.SOLO.goBack(); EndToEndTestUtils.SOLO.goBack();
EndToEndTestUtils.SOLO.goBack(); EndToEndTestUtils.SOLO.goBack();
@@ -297,9 +297,8 @@ public class SettingTest extends ActivityInstrumentationTestCase2<TrackListActiv
instrumentation.waitForIdleSync(); instrumentation.waitForIdleSync();
assertTrue(EndToEndTestUtils.SOLO.searchText(activityMyTracks.getString( assertTrue(EndToEndTestUtils.SOLO.searchText(activityMyTracks.getString(
R.string.track_name_format).split(" ")[0])); R.string.track_name_format).split(" ")[0]));
assertTrue(EndToEndTestUtils.SOLO.searchText(DEFAULTACTIVITY)); assertTrue(EndToEndTestUtils.SOLO.searchText(EndToEndTestUtils.DEFAULTACTIVITY));
EndToEndTestUtils.SOLO.clickOnText(activityMyTracks.getString(R.string.generic_save)); EndToEndTestUtils.SOLO.clickOnText(activityMyTracks.getString(R.string.generic_save));
} }
@Override @Override
@@ -23,7 +23,6 @@ import com.google.android.maps.mytracks.R;
import android.annotation.TargetApi; import android.annotation.TargetApi;
import android.app.Instrumentation; import android.app.Instrumentation;
import android.test.ActivityInstrumentationTestCase2; import android.test.ActivityInstrumentationTestCase2;
import android.view.KeyEvent;
import android.view.View; import android.view.View;
import java.util.ArrayList; import java.util.ArrayList;
@@ -57,38 +56,33 @@ public class ViewsTest extends ActivityInstrumentationTestCase2<TrackListActivit
* menus in these views. * menus in these views.
*/ */
public void testSwitchViewsAndMenusOfView() { public void testSwitchViewsAndMenusOfView() {
EndToEndTestUtils.createTrackIfEmpty(3, false); EndToEndTestUtils.createTrackIfEmpty(3, true);
EndToEndTestUtils.SOLO.goBack(); EndToEndTestUtils.SOLO.scrollUp();
EndToEndTestUtils.SOLO.clickOnText(EndToEndTestUtils.trackName); EndToEndTestUtils.SOLO.clickOnText(EndToEndTestUtils.trackName, 1, true);
EndToEndTestUtils.SOLO.clickOnText(activityMyTracks.getString(R.string.track_detail_chart_tab)); EndToEndTestUtils.SOLO.clickOnText(activityMyTracks.getString(R.string.track_detail_chart_tab));
EndToEndTestUtils.rotateAllActivities(); EndToEndTestUtils.rotateAllActivities();
EndToEndTestUtils.showMoreMenuItem(); assertTrue(EndToEndTestUtils.findMenuItem(activityMyTracks.getString(R.string.menu_settings),
assertTrue(EndToEndTestUtils.SOLO.waitForText( false));
activityMyTracks.getString(R.string.menu_settings), 1, 3000)); assertFalse(EndToEndTestUtils.findMenuItem(
assertFalse(EndToEndTestUtils.SOLO.waitForText( activityMyTracks.getString(R.string.menu_satellite_mode), false));
activityMyTracks.getString(R.string.menu_satellite_mode), 1, 0)); assertFalse(EndToEndTestUtils.findMenuItem(activityMyTracks.getString(R.string.menu_map_mode),
assertFalse(EndToEndTestUtils.SOLO.waitForText( false));
activityMyTracks.getString(R.string.menu_map_mode), 1, 0));
EndToEndTestUtils.SOLO.goBack();
EndToEndTestUtils.SOLO.clickOnText(activityMyTracks.getString(R.string.track_detail_stats_tab)); EndToEndTestUtils.SOLO.clickOnText(activityMyTracks.getString(R.string.track_detail_stats_tab));
EndToEndTestUtils.SOLO.sendKey(KeyEvent.KEYCODE_MENU); assertTrue(EndToEndTestUtils.findMenuItem(activityMyTracks.getString(R.string.menu_settings),
assertTrue(EndToEndTestUtils.SOLO.waitForText( false));
activityMyTracks.getString(R.string.menu_settings), 1, 3000)); assertFalse(EndToEndTestUtils.findMenuItem(
assertFalse(EndToEndTestUtils.SOLO.waitForText( activityMyTracks.getString(R.string.menu_satellite_mode), false));
activityMyTracks.getString(R.string.menu_satellite_mode), 1, 0)); assertFalse(EndToEndTestUtils.findMenuItem(activityMyTracks.getString(R.string.menu_map_mode),
assertFalse(EndToEndTestUtils.SOLO.waitForText( false));
activityMyTracks.getString(R.string.menu_map_mode), 1, 0));
EndToEndTestUtils.SOLO.goBack();
EndToEndTestUtils.SOLO.clickOnText(activityMyTracks.getString(R.string.track_detail_map_tab)); EndToEndTestUtils.SOLO.clickOnText(activityMyTracks.getString(R.string.track_detail_map_tab));
EndToEndTestUtils.showMoreMenuItem(); assertTrue(EndToEndTestUtils.findMenuItem(
assertTrue(EndToEndTestUtils.SOLO.waitForText( activityMyTracks.getString(R.string.menu_settings), false));
activityMyTracks.getString(R.string.menu_settings), 1, 3000)); assertTrue(EndToEndTestUtils.findMenuItem(EndToEndTestUtils.VIEW_MODE, false));
assertTrue(EndToEndTestUtils.SOLO.searchText(EndToEndTestUtils.VIEW_MODE, 1, true, false));
} }
/** /**
@@ -105,7 +99,7 @@ public class ViewsTest extends ActivityInstrumentationTestCase2<TrackListActivit
false)) { false)) {
isMapMode = false; isMapMode = false;
} }
EndToEndTestUtils.showMoreMenuItem(); EndToEndTestUtils.showMenuItem();
// Switch to satellite mode if it's map mode now.. // Switch to satellite mode if it's map mode now..
EndToEndTestUtils.SOLO.clickOnText(activityMyTracks EndToEndTestUtils.SOLO.clickOnText(activityMyTracks
.getString(isMapMode ? R.string.menu_satellite_mode : R.string.menu_map_mode)); .getString(isMapMode ? R.string.menu_satellite_mode : R.string.menu_map_mode));
@@ -125,7 +119,7 @@ public class ViewsTest extends ActivityInstrumentationTestCase2<TrackListActivit
EndToEndTestUtils.rotateAllActivities(); EndToEndTestUtils.rotateAllActivities();
// Switch back. // Switch back.
EndToEndTestUtils.showMoreMenuItem(); EndToEndTestUtils.showMenuItem();
EndToEndTestUtils.SOLO.clickOnText(activityMyTracks EndToEndTestUtils.SOLO.clickOnText(activityMyTracks
.getString(isMapMode ? R.string.menu_satellite_mode : R.string.menu_map_mode)); .getString(isMapMode ? R.string.menu_satellite_mode : R.string.menu_map_mode));
isMapMode = !isMapMode; isMapMode = !isMapMode;
@@ -139,7 +133,7 @@ public class ViewsTest extends ActivityInstrumentationTestCase2<TrackListActivit
// Wait for the TrackDetailActivity, or the stop button will can not be // Wait for the TrackDetailActivity, or the stop button will can not be
// found in next step. // found in next step.
EndToEndTestUtils.SOLO.waitForText(activityMyTracks.getString(R.string.track_detail_chart_tab), EndToEndTestUtils.SOLO.waitForText(activityMyTracks.getString(R.string.track_detail_chart_tab),
0, 1000); 0, EndToEndTestUtils.SHORT_WAIT_TIME);
} }
@Override @Override
@@ -148,7 +148,7 @@ public class GpxImporterTest extends AndroidTestCase {
expect(providerUtils.insertTrack(AndroidMock.capture(trackParam))) expect(providerUtils.insertTrack(AndroidMock.capture(trackParam)))
.andReturn(TRACK_ID_URI); .andReturn(TRACK_ID_URI);
expect(providerUtils.getLastLocationId(TRACK_ID)).andReturn(TRACK_POINT_ID_1).andReturn(TRACK_POINT_ID_2); expect(providerUtils.getLastTrackLocationId(TRACK_ID)).andReturn(TRACK_POINT_ID_1).andReturn(TRACK_POINT_ID_2);
// A flush happens after the first insertion to get the starting point ID, // A flush happens after the first insertion to get the starting point ID,
// which is why we get two calls // which is why we get two calls
@@ -225,7 +225,7 @@ public class GpxImporterTest extends AndroidTestCase {
expect(providerUtils.bulkInsertTrackPoints((Location[]) AndroidMock.anyObject(), expect(providerUtils.bulkInsertTrackPoints((Location[]) AndroidMock.anyObject(),
AndroidMock.anyInt(), AndroidMock.anyLong())).andStubReturn(1); AndroidMock.anyInt(), AndroidMock.anyLong())).andStubReturn(1);
expect(providerUtils.getLastLocationId(TRACK_ID)).andStubReturn(TRACK_POINT_ID_1); expect(providerUtils.getLastTrackLocationId(TRACK_ID)).andStubReturn(TRACK_POINT_ID_1);
providerUtils.deleteTrack(TRACK_ID); providerUtils.deleteTrack(TRACK_ID);
@@ -17,7 +17,7 @@ package com.google.android.apps.mytracks.io.file;
import com.google.android.apps.mytracks.content.DescriptionGenerator; import com.google.android.apps.mytracks.content.DescriptionGenerator;
import com.google.android.apps.mytracks.content.Track; import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.content.Waypoint; import com.google.android.apps.mytracks.stats.TripStatistics;
import android.annotation.TargetApi; import android.annotation.TargetApi;
import android.location.Location; import android.location.Location;
@@ -49,7 +49,7 @@ public class KmlTrackWriterTest extends TrackFormatWriterTest {
} }
@Override @Override
public String generateWaypointDescription(Waypoint waypoint) { public String generateWaypointDescription(TripStatistics tripStatistics) {
return null; return null;
} }
} }
@@ -57,35 +57,6 @@ public class DynamicSpeedTrackPathDescriptorTest extends AndroidTestCase {
} }
} }
/**
* Tests {@link DynamicSpeedTrackPathDescriptor#onSharedPreferenceChanged(SharedPreferences,
* String)} when the key is null.
*/
public void testOnSharedPreferenceChanged_nullKey() {
DynamicSpeedTrackPathDescriptor dynamicSpeedTrackPathDescriptor = new DynamicSpeedTrackPathDescriptor(
context);
int speedMargin = dynamicSpeedTrackPathDescriptor.getSpeedMargin();
// Change value in shared preferences.
PreferencesUtils.setInt(context, R.string.track_color_mode_percentage_key, speedMargin + 2);
dynamicSpeedTrackPathDescriptor.onSharedPreferenceChanged(sharedPreferences, null);
assertEquals(speedMargin, dynamicSpeedTrackPathDescriptor.getSpeedMargin());
}
/**
* Tests {@link DynamicSpeedTrackPathDescriptor#onSharedPreferenceChanged(SharedPreferences,
* String)} when the key is not null, and not trackColorModeDynamicVariation.
*/
public void testOnSharedPreferenceChanged_otherKey() {
DynamicSpeedTrackPathDescriptor dynamicSpeedTrackPathDescriptor = new DynamicSpeedTrackPathDescriptor(
context);
int speedMargin = dynamicSpeedTrackPathDescriptor.getSpeedMargin();
// Change value in shared preferences.
PreferencesUtils.setInt(context, R.string.track_color_mode_percentage_key, speedMargin + 2);
dynamicSpeedTrackPathDescriptor.onSharedPreferenceChanged(sharedPreferences, "anyKey");
assertEquals(speedMargin, dynamicSpeedTrackPathDescriptor.getSpeedMargin());
}
/** /**
* Tests {@link DynamicSpeedTrackPathDescriptor#onSharedPreferenceChanged(SharedPreferences, * Tests {@link DynamicSpeedTrackPathDescriptor#onSharedPreferenceChanged(SharedPreferences,
* String)} when the key is trackColorModeDynamicVariation. * String)} when the key is trackColorModeDynamicVariation.
@@ -120,7 +91,7 @@ public class DynamicSpeedTrackPathDescriptorTest extends AndroidTestCase {
} }
/** /**
* Tests {@link DynamicSpeedTrackPathDescriptor#needsRedraw()} by wrong track * Tests {@link DynamicSpeedTrackPathDescriptor#updateState()} by wrong track
* id. * id.
*/ */
public void testNeedsRedraw_WrongTrackId() { public void testNeedsRedraw_WrongTrackId() {
@@ -128,11 +99,11 @@ public class DynamicSpeedTrackPathDescriptorTest extends AndroidTestCase {
context, R.string.selected_track_id_key, PreferencesUtils.SELECTED_TRACK_ID_DEFAULT); context, R.string.selected_track_id_key, PreferencesUtils.SELECTED_TRACK_ID_DEFAULT);
DynamicSpeedTrackPathDescriptor dynamicSpeedTrackPathDescriptor = new DynamicSpeedTrackPathDescriptor( DynamicSpeedTrackPathDescriptor dynamicSpeedTrackPathDescriptor = new DynamicSpeedTrackPathDescriptor(
context); context);
assertEquals(false, dynamicSpeedTrackPathDescriptor.needsRedraw()); assertEquals(false, dynamicSpeedTrackPathDescriptor.updateState());
} }
/** /**
* Tests {@link DynamicSpeedTrackPathDescriptor#needsRedraw()} by different * Tests {@link DynamicSpeedTrackPathDescriptor#updateState()} by different
* averageMovingSpeed. * averageMovingSpeed.
*/ */
public void testIsDiffereceSignificant() { public void testIsDiffereceSignificant() {
@@ -145,8 +116,7 @@ public class DynamicSpeedTrackPathDescriptorTest extends AndroidTestCase {
// Difference is more than CRITICAL_DIFFERENCE_PERCENTAGE // Difference is more than CRITICAL_DIFFERENCE_PERCENTAGE
30 * (1 + (DynamicSpeedTrackPathDescriptor.CRITICAL_DIFFERENCE_PERCENTAGE / 100.00) * 2) }; 30 * (1 + (DynamicSpeedTrackPathDescriptor.CRITICAL_DIFFERENCE_PERCENTAGE / 100.00) * 2) };
boolean[] expectedValues = { true, false, false, true }; boolean[] expectedValues = { true, false, false, true };
double[] expectedAverageMovingSpeed = { 20, 30, 30, double[] expectedAverageMovingSpeed = { 0, 30, 30, 30 };
30 * (1 + (DynamicSpeedTrackPathDescriptor.CRITICAL_DIFFERENCE_PERCENTAGE / 100.00) * 2) };
// Test // Test
for (int i = 0; i < newAverageMovingSpeed.length; i++) { for (int i = 0; i < newAverageMovingSpeed.length; i++) {
dynamicSpeedTrackPathDescriptor.setAverageMovingSpeed(averageMovingSpeeds[i]); dynamicSpeedTrackPathDescriptor.setAverageMovingSpeed(averageMovingSpeeds[i]);
@@ -62,7 +62,7 @@ public class DynamicSpeedTrackPathPainterTest extends TrackPathPainterTestCase {
public void testUpdatePath_AllInvalidLocation() { public void testUpdatePath_AllInvalidLocation() {
List<CachedLocation> points = createCachedLocations(NUMBER_OF_LOCATIONS, INVALID_LATITUDE, -1); List<CachedLocation> points = createCachedLocations(NUMBER_OF_LOCATIONS, INVALID_LATITUDE, -1);
dynamicSpeedTrackPathPainter.updatePath(myTracksOverlay.getMapProjection(mockView), dynamicSpeedTrackPathPainter.updatePath(myTracksOverlay.getMapProjection(mockView),
myTracksOverlay.getMapViewRect(mockView), 1, true, points); myTracksOverlay.getMapViewRect(mockView), 1, points);
AndroidMock.verify(trackPathDescriptor); AndroidMock.verify(trackPathDescriptor);
// Should be zero for there is no valid locations. // Should be zero for there is no valid locations.
assertEquals(0, dynamicSpeedTrackPathPainter.getColoredPaths().size()); assertEquals(0, dynamicSpeedTrackPathPainter.getColoredPaths().size());
@@ -80,7 +80,7 @@ public class DynamicSpeedTrackPathPainterTest extends TrackPathPainterTestCase {
// Gets a number as the start index of points. // Gets a number as the start index of points.
int startLocationIdx = NUMBER_OF_LOCATIONS / 2; int startLocationIdx = NUMBER_OF_LOCATIONS / 2;
dynamicSpeedTrackPathPainter.updatePath(myTracksOverlay.getMapProjection(mockView), dynamicSpeedTrackPathPainter.updatePath(myTracksOverlay.getMapProjection(mockView),
myTracksOverlay.getMapViewRect(mockView), startLocationIdx, true, points); myTracksOverlay.getMapViewRect(mockView), startLocationIdx, points);
AndroidMock.verify(trackPathDescriptor); AndroidMock.verify(trackPathDescriptor);
assertEquals(NUMBER_OF_LOCATIONS - startLocationIdx, dynamicSpeedTrackPathPainter assertEquals(NUMBER_OF_LOCATIONS - startLocationIdx, dynamicSpeedTrackPathPainter
@@ -123,7 +123,7 @@ public class DynamicSpeedTrackPathPainterTest extends TrackPathPainterTestCase {
slowSpeed)); slowSpeed));
dynamicSpeedTrackPathPainter.updatePath(myTracksOverlay.getMapProjection(mockView), dynamicSpeedTrackPathPainter.updatePath(myTracksOverlay.getMapProjection(mockView),
myTracksOverlay.getMapViewRect(mockView), startLocationIdx, true, points); myTracksOverlay.getMapViewRect(mockView), startLocationIdx, points);
AndroidMock.verify(trackPathDescriptor); AndroidMock.verify(trackPathDescriptor);
List<ColoredPath> coloredPath = dynamicSpeedTrackPathPainter.getColoredPaths(); List<ColoredPath> coloredPath = dynamicSpeedTrackPathPainter.getColoredPaths();
@@ -31,9 +31,10 @@ import java.util.List;
*/ */
public class SingleColorTrackPathPainterTest extends TrackPathPainterTestCase { public class SingleColorTrackPathPainterTest extends TrackPathPainterTestCase {
private static final int NUMBER_OF_LOCATIONS = 10;
private SingleColorTrackPathPainter singleColorTrackPathPainter; private SingleColorTrackPathPainter singleColorTrackPathPainter;
private Path pathMock; private Path pathMock;
private static final int NUMBER_OF_LOCATIONS = 100;
/** /**
* Initials a mocked TrackPathDescriptor object and * Initials a mocked TrackPathDescriptor object and
@@ -44,8 +45,8 @@ public class SingleColorTrackPathPainterTest extends TrackPathPainterTestCase {
protected void setUp() throws Exception { protected void setUp() throws Exception {
super.setUp(); super.setUp();
pathMock = AndroidMock.createStrictMock(Path.class);
singleColorTrackPathPainter = new SingleColorTrackPathPainter(getContext()); singleColorTrackPathPainter = new SingleColorTrackPathPainter(getContext());
pathMock = AndroidMock.createStrictMock(Path.class);
} }
/** /**
@@ -54,19 +55,20 @@ public class SingleColorTrackPathPainterTest extends TrackPathPainterTestCase {
* method when all locations are valid. * method when all locations are valid.
*/ */
public void testUpdatePath_AllValidLocation() { public void testUpdatePath_AllValidLocation() {
pathMock.incReserve(NUMBER_OF_LOCATIONS);
List<CachedLocation> points = createCachedLocations(NUMBER_OF_LOCATIONS,
TrackStubUtils.INITIAL_LATITUDE, -1);
// Gets a number as the start index of points. // Gets a number as the start index of points.
int startLocationIdx = NUMBER_OF_LOCATIONS / 2; int startLocationIdx = NUMBER_OF_LOCATIONS / 2;
pathMock.incReserve(NUMBER_OF_LOCATIONS - startLocationIdx);
List<CachedLocation> points = createCachedLocations(NUMBER_OF_LOCATIONS,
TrackStubUtils.INITIAL_LATITUDE, -1);
for (int i = startLocationIdx; i < NUMBER_OF_LOCATIONS; i++) { for (int i = startLocationIdx; i < NUMBER_OF_LOCATIONS; i++) {
pathMock.lineTo(0, 0); pathMock.lineTo(0, 0);
} }
AndroidMock.replay(pathMock); AndroidMock.replay(pathMock);
singleColorTrackPathPainter.updatePath(myTracksOverlay.getMapProjection(mockView), singleColorTrackPathPainter.updatePath(myTracksOverlay.getMapProjection(mockView),
myTracksOverlay.getMapViewRect(mockView), startLocationIdx, true, points, pathMock); myTracksOverlay.getMapViewRect(mockView), startLocationIdx, points, pathMock);
AndroidMock.verify(pathMock); AndroidMock.verify(pathMock);
} }
@@ -76,20 +78,20 @@ public class SingleColorTrackPathPainterTest extends TrackPathPainterTestCase {
* method when all locations are invalid. * method when all locations are invalid.
*/ */
public void testUpdatePath_AllInvalidLocation() { public void testUpdatePath_AllInvalidLocation() {
pathMock.incReserve(NUMBER_OF_LOCATIONS); int startLocationIdx = NUMBER_OF_LOCATIONS / 2;
pathMock.incReserve(NUMBER_OF_LOCATIONS - startLocationIdx);
List<CachedLocation> points = createCachedLocations(NUMBER_OF_LOCATIONS, INVALID_LATITUDE, -1); List<CachedLocation> points = createCachedLocations(NUMBER_OF_LOCATIONS, INVALID_LATITUDE, -1);
// Gets a random number from 1 to numberOfLocations. // Gets a random number from 1 to numberOfLocations.
int startLocationIdx = NUMBER_OF_LOCATIONS / 2;
AndroidMock.replay(pathMock); AndroidMock.replay(pathMock);
singleColorTrackPathPainter.updatePath(myTracksOverlay.getMapProjection(mockView), singleColorTrackPathPainter.updatePath(myTracksOverlay.getMapProjection(mockView),
myTracksOverlay.getMapViewRect(mockView), startLocationIdx, true, points, pathMock); myTracksOverlay.getMapViewRect(mockView), startLocationIdx, points, pathMock);
AndroidMock.verify(pathMock); AndroidMock.verify(pathMock);
} }
/** /**
* Tests the * Tests the {@link SingleColorTrackPathPainter#updatePath(com.google.android.maps.Projection,
* {@link SingleColorTrackPathPainter#updatePath(com.google.android.maps.Projection, android.graphics.Rect, int, Boolean, List, Path)} * android.graphics.Rect, int, Boolean, List, Path)} method when there are
* method when there are three segments. * three segments.
*/ */
public void testUpdatePath_ThreeSegments() { public void testUpdatePath_ThreeSegments() {
// First segment. // First segment.
@@ -103,7 +105,7 @@ public class SingleColorTrackPathPainterTest extends TrackPathPainterTestCase {
points.addAll(createCachedLocations(NUMBER_OF_LOCATIONS, TrackStubUtils.INITIAL_LATITUDE, -1)); points.addAll(createCachedLocations(NUMBER_OF_LOCATIONS, TrackStubUtils.INITIAL_LATITUDE, -1));
// Gets a random number from 1 to numberOfLocations. // Gets a random number from 1 to numberOfLocations.
int startLocationIdx = NUMBER_OF_LOCATIONS / 2; int startLocationIdx = NUMBER_OF_LOCATIONS / 2;
pathMock.incReserve(NUMBER_OF_LOCATIONS *3 + 1 +1); pathMock.incReserve(NUMBER_OF_LOCATIONS *3 + 1 +1 - startLocationIdx);
for (int i = 0; i < NUMBER_OF_LOCATIONS - startLocationIdx; i++) { for (int i = 0; i < NUMBER_OF_LOCATIONS - startLocationIdx; i++) {
pathMock.lineTo(0, 0); pathMock.lineTo(0, 0);
} }
@@ -118,7 +120,7 @@ public class SingleColorTrackPathPainterTest extends TrackPathPainterTestCase {
AndroidMock.replay(pathMock); AndroidMock.replay(pathMock);
singleColorTrackPathPainter.updatePath(myTracksOverlay.getMapProjection(mockView), singleColorTrackPathPainter.updatePath(myTracksOverlay.getMapProjection(mockView),
myTracksOverlay.getMapViewRect(mockView), startLocationIdx, true, points, pathMock); myTracksOverlay.getMapViewRect(mockView), startLocationIdx, points, pathMock);
AndroidMock.verify(pathMock); AndroidMock.verify(pathMock);
} }
} }
@@ -40,13 +40,12 @@ public class TrackPathDescriptorDynamicSpeedTest extends TrackPathPainterTestCas
myTracksOverlay.setTrackPathPainter(painter); myTracksOverlay.setTrackPathPainter(painter);
int startLocationIdx = 0; int startLocationIdx = 0;
Boolean alwaysVisible = true;
assertNotNull(painter); assertNotNull(painter);
painter.updatePath(myTracksOverlay.getMapProjection(mockView), painter.updatePath(myTracksOverlay.getMapProjection(mockView),
myTracksOverlay.getMapViewRect(mockView), startLocationIdx, alwaysVisible, myTracksOverlay.getMapViewRect(mockView), startLocationIdx,
myTracksOverlay.getPoints()); myTracksOverlay.getPoints());
assertNotNull(myTracksOverlay.getTrackPathPainter().getLastPath()); assertNotNull(myTracksOverlay.getTrackPathPainter().hasPath());
painter.drawTrack(canvas); painter.drawPath(canvas);
} }
} }
@@ -40,13 +40,11 @@ public class TrackPathDescriptorFixedSpeedTest extends TrackPathPainterTestCase
myTracksOverlay.setTrackPathPainter(painter); myTracksOverlay.setTrackPathPainter(painter);
int startLocationIdx = 0; int startLocationIdx = 0;
Boolean alwaysVisible = true;
assertNotNull(painter); assertNotNull(painter);
painter.updatePath(myTracksOverlay.getMapProjection(mockView), painter.updatePath(myTracksOverlay.getMapProjection(mockView),
myTracksOverlay.getMapViewRect(mockView), startLocationIdx, alwaysVisible, myTracksOverlay.getMapViewRect(mockView), startLocationIdx, myTracksOverlay.getPoints());
myTracksOverlay.getPoints()); assertNotNull(myTracksOverlay.getTrackPathPainter().hasPath());
assertNotNull(myTracksOverlay.getTrackPathPainter().getLastPath()); painter.drawPath(canvas);
painter.drawTrack(canvas);
} }
} }
@@ -55,7 +55,6 @@ public class TrackPathPainterFactoryTest extends TrackPathPainterTestCase {
context, R.string.track_color_mode_key, context.getString(track_color_mode)); context, R.string.track_color_mode_key, context.getString(track_color_mode));
int startLocationIdx = 0; int startLocationIdx = 0;
Boolean alwaysVisible = true;
TrackPathPainter painter = TrackPathPainterFactory.getTrackPathPainter(context); TrackPathPainter painter = TrackPathPainterFactory.getTrackPathPainter(context);
myTracksOverlay.setTrackPathPainter(painter); myTracksOverlay.setTrackPathPainter(painter);
@@ -64,9 +63,9 @@ public class TrackPathPainterFactoryTest extends TrackPathPainterTestCase {
assertTrue(c.isInstance(painter)); assertTrue(c.isInstance(painter));
painter.updatePath(myTracksOverlay.getMapProjection(mockView), painter.updatePath(myTracksOverlay.getMapProjection(mockView),
myTracksOverlay.getMapViewRect(mockView), startLocationIdx, alwaysVisible, myTracksOverlay.getMapViewRect(mockView), startLocationIdx,
myTracksOverlay.getPoints()); myTracksOverlay.getPoints());
assertNotNull(myTracksOverlay.getTrackPathPainter().getLastPath()); assertNotNull(myTracksOverlay.getTrackPathPainter().hasPath());
painter.drawTrack(canvas); painter.drawPath(canvas);
} }
} }
@@ -39,13 +39,12 @@ public class TrackPathPainterSingleColorTest extends TrackPathPainterTestCase {
myTracksOverlay.setTrackPathPainter(painter); myTracksOverlay.setTrackPathPainter(painter);
int startLocationIdx = 0; int startLocationIdx = 0;
Boolean alwaysVisible = true;
assertNotNull(painter); assertNotNull(painter);
painter.updatePath(myTracksOverlay.getMapProjection(mockView), painter.updatePath(myTracksOverlay.getMapProjection(mockView),
myTracksOverlay.getMapViewRect(mockView), startLocationIdx, alwaysVisible, myTracksOverlay.getMapViewRect(mockView), startLocationIdx,
myTracksOverlay.getPoints()); myTracksOverlay.getPoints());
assertNotNull(myTracksOverlay.getTrackPathPainter().getLastPath()); assertNotNull(myTracksOverlay.getTrackPathPainter().hasPath());
painter.drawTrack(canvas); painter.drawPath(canvas);
} }
} }
@@ -13,20 +13,21 @@
* License for the specific language governing permissions and limitations under * License for the specific language governing permissions and limitations under
* the License. * the License.
*/ */
package com.google.android.apps.mytracks.services.tasks; package com.google.android.apps.mytracks.services.tasks;
import android.test.AndroidTestCase; import android.test.AndroidTestCase;
/** /**
* Tests for {@link StatusAnnouncerFactory}. * Tests for {@link AnnouncementPeriodicTaskFactory}.
* These tests require Donut+ to run. * These tests require Donut+ to run.
* *
* @author Rodrigo Damazio * @author Rodrigo Damazio
*/ */
public class StatusAnnouncerFactoryTest extends AndroidTestCase { public class AnnouncementPeriodicTaskFactoryTest extends AndroidTestCase {
public void testCreate() { public void testCreate() {
PeriodicTaskFactory factory = new StatusAnnouncerFactory(); PeriodicTaskFactory factory = new AnnouncementPeriodicTaskFactory();
PeriodicTask task = factory.create(getContext()); PeriodicTask task = factory.create(getContext());
assertTrue(task instanceof StatusAnnouncerTask); assertTrue(task instanceof AnnouncementPeriodicTask);
} }
} }
@@ -40,21 +40,21 @@ import java.util.concurrent.atomic.AtomicBoolean;
import org.easymock.Capture; import org.easymock.Capture;
/** /**
* Tests for {@link StatusAnnouncerTask}. * Tests for {@link AnnouncementPeriodicTask}.
* WARNING: I'm not responsible if your eyes start bleeding while reading this * WARNING: I'm not responsible if your eyes start bleeding while reading this
* code. You have been warned. It's still better than no test, though. * code. You have been warned. It's still better than no test, though.
* *
* @author Rodrigo Damazio * @author Rodrigo Damazio
*/ */
public class StatusAnnouncerTaskTest extends AndroidTestCase { public class AnnouncementPeriodicTaskTest extends AndroidTestCase {
// Use something other than our hardcoded value // Use something other than our hardcoded value
private static final Locale DEFAULT_LOCALE = Locale.KOREAN; private static final Locale DEFAULT_LOCALE = Locale.KOREAN;
private static final String ANNOUNCEMENT = "I can haz cheeseburger?"; private static final String ANNOUNCEMENT = "I can haz cheeseburger?";
private Locale oldDefaultLocale; private Locale oldDefaultLocale;
private StatusAnnouncerTask task; private AnnouncementPeriodicTask task;
private StatusAnnouncerTask mockTask; private AnnouncementPeriodicTask mockTask;
private Capture<OnInitListener> initListenerCapture; private Capture<OnInitListener> initListenerCapture;
private Capture<PhoneStateListener> phoneListenerCapture; private Capture<PhoneStateListener> phoneListenerCapture;
private TextToSpeechDelegate ttsDelegate; private TextToSpeechDelegate ttsDelegate;
@@ -252,7 +252,7 @@ public class StatusAnnouncerTaskTest extends AndroidTestCase {
} }
@UsesMocks({ @UsesMocks({
StatusAnnouncerTask.class, AnnouncementPeriodicTask.class,
StringUtils.class, StringUtils.class,
}) })
@Override @Override
@@ -291,8 +291,8 @@ public class StatusAnnouncerTaskTest extends AndroidTestCase {
phoneListenerCapture = new Capture<PhoneStateListener>(); phoneListenerCapture = new Capture<PhoneStateListener>();
// Create a partial forwarding mock // Create a partial forwarding mock
mockTask = AndroidMock.createMock(StatusAnnouncerTask.class, getContext()); mockTask = AndroidMock.createMock(AnnouncementPeriodicTask.class, getContext());
task = new StatusAnnouncerTask(getContext()) { task = new AnnouncementPeriodicTask(getContext()) {
@Override @Override
protected TextToSpeech newTextToSpeech(Context ctx, protected TextToSpeech newTextToSpeech(Context ctx,
OnInitListener onInitListener) { OnInitListener onInitListener) {
@@ -322,32 +322,6 @@ public class StatusAnnouncerTaskTest extends AndroidTestCase {
OnInitListener ttsInitListener = initListenerCapture.getValue(); OnInitListener ttsInitListener = initListenerCapture.getValue();
assertNotNull(ttsInitListener); assertNotNull(ttsInitListener);
expect(tts.isLanguageAvailable(DEFAULT_LOCALE))
.andStubReturn(TextToSpeech.LANG_AVAILABLE);
expect(tts.setLanguage(DEFAULT_LOCALE))
.andReturn(TextToSpeech.LANG_AVAILABLE);
expect(tts.setSpeechRate(StatusAnnouncerTask.TTS_SPEECH_RATE))
.andReturn(TextToSpeech.SUCCESS);
AndroidMock.replay(tts);
ttsInitListener.onInit(TextToSpeech.SUCCESS);
AndroidMock.verify(mockTask, tts);
}
public void testStart_languageNotSupported() {
doStart();
OnInitListener ttsInitListener = initListenerCapture.getValue();
assertNotNull(ttsInitListener);
expect(tts.isLanguageAvailable(DEFAULT_LOCALE))
.andStubReturn(TextToSpeech.LANG_NOT_SUPPORTED);
expect(tts.setLanguage(Locale.ENGLISH))
.andReturn(TextToSpeech.LANG_AVAILABLE);
expect(tts.setSpeechRate(StatusAnnouncerTask.TTS_SPEECH_RATE))
.andReturn(TextToSpeech.SUCCESS);
AndroidMock.replay(tts); AndroidMock.replay(tts);
ttsInitListener.onInit(TextToSpeech.SUCCESS); ttsInitListener.onInit(TextToSpeech.SUCCESS);
@@ -393,6 +367,10 @@ public class StatusAnnouncerTaskTest extends AndroidTestCase {
// Put task in "ready" state // Put task in "ready" state
startTask(TextToSpeech.SUCCESS); startTask(TextToSpeech.SUCCESS);
expect(tts.isLanguageAvailable(DEFAULT_LOCALE)).andStubReturn(TextToSpeech.LANG_AVAILABLE);
expect(tts.setLanguage(DEFAULT_LOCALE)).andReturn(TextToSpeech.LANG_AVAILABLE);
expect(tts.setSpeechRate(AnnouncementPeriodicTask.TTS_SPEECH_RATE)).andReturn(TextToSpeech.SUCCESS);
// Expect actual announcement call // Expect actual announcement call
expect(tts.speak( expect(tts.speak(
eq(ANNOUNCEMENT), eq(TextToSpeech.QUEUE_FLUSH), eq(ANNOUNCEMENT), eq(TextToSpeech.QUEUE_FLUSH),
@@ -401,7 +379,7 @@ public class StatusAnnouncerTaskTest extends AndroidTestCase {
// Run the announcement // Run the announcement
AndroidMock.replay(tts); AndroidMock.replay(tts);
task.runWithStatistics(stats); task.announce(stats);
AndroidMock.verify(mockTask, tts); AndroidMock.verify(mockTask, tts);
} }
@@ -411,7 +389,7 @@ public class StatusAnnouncerTaskTest extends AndroidTestCase {
// Run the announcement // Run the announcement
AndroidMock.replay(tts); AndroidMock.replay(tts);
task.runWithStatistics(null); task.run(null);
AndroidMock.verify(mockTask, tts); AndroidMock.verify(mockTask, tts);
} }
@@ -424,7 +402,7 @@ public class StatusAnnouncerTaskTest extends AndroidTestCase {
AndroidMock.replay(tts); AndroidMock.replay(tts);
PhoneStateListener phoneListener = phoneListenerCapture.getValue(); PhoneStateListener phoneListener = phoneListenerCapture.getValue();
phoneListener.onCallStateChanged(TelephonyManager.CALL_STATE_OFFHOOK, null); phoneListener.onCallStateChanged(TelephonyManager.CALL_STATE_OFFHOOK, null);
task.runWithStatistics(null); task.run(null);
AndroidMock.verify(mockTask, tts); AndroidMock.verify(mockTask, tts);
} }
@@ -441,7 +419,7 @@ public class StatusAnnouncerTaskTest extends AndroidTestCase {
phoneListener.onCallStateChanged(TelephonyManager.CALL_STATE_RINGING, null); phoneListener.onCallStateChanged(TelephonyManager.CALL_STATE_RINGING, null);
// Run the announcement - this should do nothing. // Run the announcement - this should do nothing.
task.runWithStatistics(null); task.run(null);
AndroidMock.verify(mockTask, tts); AndroidMock.verify(mockTask, tts);
} }
@@ -455,7 +433,7 @@ public class StatusAnnouncerTaskTest extends AndroidTestCase {
AndroidMock.replay(tts); AndroidMock.replay(tts);
PhoneStateListener phoneListener = phoneListenerCapture.getValue(); PhoneStateListener phoneListener = phoneListenerCapture.getValue();
phoneListener.onCallStateChanged(TelephonyManager.CALL_STATE_RINGING, null); phoneListener.onCallStateChanged(TelephonyManager.CALL_STATE_RINGING, null);
task.runWithStatistics(null); task.run(null);
AndroidMock.verify(mockTask, tts); AndroidMock.verify(mockTask, tts);
} }
@@ -475,12 +453,12 @@ public class StatusAnnouncerTaskTest extends AndroidTestCase {
// Run the announcement // Run the announcement
AndroidMock.replay(tts); AndroidMock.replay(tts);
task.runWithStatistics(null); task.run(null);
AndroidMock.verify(mockTask, tts); AndroidMock.verify(mockTask, tts);
} }
/** /**
* Tests {@link StatusAnnouncerTask#getAnnounceTime(long)} with time zero. * Tests {@link AnnouncementPeriodicTask#getAnnounceTime(long)} with time zero.
*/ */
public void testGetAnnounceTime_time_zero() { public void testGetAnnounceTime_time_zero() {
long time = 0; // 0 seconds long time = 0; // 0 seconds
@@ -488,7 +466,7 @@ public class StatusAnnouncerTaskTest extends AndroidTestCase {
} }
/** /**
* Tests {@link StatusAnnouncerTask#getAnnounceTime(long)} with time one. * Tests {@link AnnouncementPeriodicTask#getAnnounceTime(long)} with time one.
*/ */
public void testGetAnnounceTime_time_one() { public void testGetAnnounceTime_time_one() {
long time = 1 * 1000; // 1 second long time = 1 * 1000; // 1 second
@@ -496,7 +474,7 @@ public class StatusAnnouncerTaskTest extends AndroidTestCase {
} }
/** /**
* Tests {@link StatusAnnouncerTask#getAnnounceTime(long)} with singular * Tests {@link AnnouncementPeriodicTask#getAnnounceTime(long)} with singular
* numbers with the hour unit. * numbers with the hour unit.
*/ */
public void testGetAnnounceTime_singular_has_hour() { public void testGetAnnounceTime_singular_has_hour() {
@@ -505,7 +483,7 @@ public class StatusAnnouncerTaskTest extends AndroidTestCase {
} }
/** /**
* Tests {@link StatusAnnouncerTask#getAnnounceTime(long)} with plural numbers * Tests {@link AnnouncementPeriodicTask#getAnnounceTime(long)} with plural numbers
* with the hour unit. * with the hour unit.
*/ */
public void testGetAnnounceTime_plural_has_hour() { public void testGetAnnounceTime_plural_has_hour() {
@@ -514,7 +492,7 @@ public class StatusAnnouncerTaskTest extends AndroidTestCase {
} }
/** /**
* Tests {@link StatusAnnouncerTask#getAnnounceTime(long)} with singular * Tests {@link AnnouncementPeriodicTask#getAnnounceTime(long)} with singular
* numbers without the hour unit. * numbers without the hour unit.
*/ */
public void testGetAnnounceTime_singular_no_hour() { public void testGetAnnounceTime_singular_no_hour() {
@@ -523,7 +501,7 @@ public class StatusAnnouncerTaskTest extends AndroidTestCase {
} }
/** /**
* Tests {@link StatusAnnouncerTask#getAnnounceTime(long)} with plural numbers * Tests {@link AnnouncementPeriodicTask#getAnnounceTime(long)} with plural numbers
* without the hour unit. * without the hour unit.
*/ */
public void testGetAnnounceTime_plural_no_hour() { public void testGetAnnounceTime_plural_no_hour() {
@@ -25,55 +25,49 @@ public class TripStatisticsBuilderTest extends TestCase {
public void testAddLocationSimple() throws Exception { public void testAddLocationSimple() throws Exception {
builder = new TripStatisticsBuilder(1000); builder = new TripStatisticsBuilder(1000);
TripStatistics stats = builder.getStatistics(); TripStatistics tripStatistics = builder.getTripStatistics();
assertEquals(0.0, builder.getSmoothedElevation()); assertEquals(0.0, builder.getSmoothedElevation());
assertEquals(Double.POSITIVE_INFINITY, stats.getMinElevation()); assertEquals(Double.POSITIVE_INFINITY, tripStatistics.getMinElevation());
assertEquals(Double.NEGATIVE_INFINITY, stats.getMaxElevation()); assertEquals(Double.NEGATIVE_INFINITY, tripStatistics.getMaxElevation());
assertEquals(0.0, stats.getMaxSpeed()); assertEquals(0.0, tripStatistics.getMaxSpeed());
assertEquals(Double.POSITIVE_INFINITY, stats.getMinGrade()); assertEquals(Double.POSITIVE_INFINITY, tripStatistics.getMinGrade());
assertEquals(Double.NEGATIVE_INFINITY, stats.getMaxGrade()); assertEquals(Double.NEGATIVE_INFINITY, tripStatistics.getMaxGrade());
assertEquals(0.0, stats.getTotalElevationGain()); assertEquals(0.0, tripStatistics.getTotalElevationGain());
assertEquals(0, stats.getMovingTime()); assertEquals(0, tripStatistics.getMovingTime());
assertEquals(0.0, stats.getTotalDistance()); assertEquals(0.0, tripStatistics.getTotalDistance());
for (int i = 0; i < 100; i++) { for (int i = 0; i < 100; i++) {
Location l = new Location("test"); Location location = new Location("test");
l.setAccuracy(1.0f); location.setAccuracy(1.0f);
l.setLongitude(45.0); location.setLongitude(45.0);
// Going up by 5 meters each time. // Going up by 1 meter each time.
l.setAltitude(i); location.setAltitude(i);
// Moving by .1% of a degree latitude. location.setLatitude(i * .001); // Moving by .001 degree latitude (111 meters)
l.setLatitude(i * .001); location.setSpeed(11.1f);
l.setSpeed(11.1f);
// Each time slice is 10 seconds. // Each time slice is 10 seconds.
long time = 1000 + 10000 * i; long time = 1000 + 10000 * i;
l.setTime(time); location.setTime(time);
boolean moving = builder.addLocation(l, time); boolean moving = builder.addLocation(location, time);
assertEquals((i != 0), moving); assertEquals((i != 0), moving);
stats = builder.getStatistics(); tripStatistics = builder.getTripStatistics();
assertEquals(10000 * i, stats.getTotalTime()); assertEquals(10000 * i, tripStatistics.getTotalTime());
assertEquals(10000 * i, stats.getMovingTime()); assertEquals(10000 * i, tripStatistics.getMovingTime());
assertEquals(i, builder.getSmoothedElevation(), assertEquals(i, builder.getSmoothedElevation(), Constants.ELEVATION_SMOOTHING_FACTOR / 2);
Constants.ELEVATION_SMOOTHING_FACTOR / 2); assertEquals(0.0, tripStatistics.getMinElevation());
assertEquals(0.0, stats.getMinElevation()); assertEquals(i, tripStatistics.getMaxElevation(), Constants.ELEVATION_SMOOTHING_FACTOR / 2);
assertEquals(i, stats.getMaxElevation(), assertEquals(i, tripStatistics.getTotalElevationGain(), Constants.ELEVATION_SMOOTHING_FACTOR);
Constants.ELEVATION_SMOOTHING_FACTOR / 2);
assertEquals(i, stats.getTotalElevationGain(),
Constants.ELEVATION_SMOOTHING_FACTOR);
if (i > Constants.SPEED_SMOOTHING_FACTOR) { if (i > Constants.SPEED_SMOOTHING_FACTOR) {
assertEquals(11.1f, stats.getMaxSpeed(), 0.1); assertEquals(11.1f, tripStatistics.getMaxSpeed(), 0.1);
} }
if ((i > Constants.GRADE_SMOOTHING_FACTOR) if (i > Constants.DISTANCE_SMOOTHING_FACTOR && i > Constants.ELEVATION_SMOOTHING_FACTOR) {
&& (i > Constants.ELEVATION_SMOOTHING_FACTOR)) { // 1 m / 111 m = .009
assertEquals(0.009, stats.getMinGrade(), 0.0001); assertEquals(0.009, tripStatistics.getMinGrade(), 0.0001);
assertEquals(0.009, stats.getMaxGrade(), 0.0001); assertEquals(0.009, tripStatistics.getMaxGrade(), 0.0001);
} }
// 1 degree = 111 km assertEquals(111.0 * i, tripStatistics.getTotalDistance(), 100);
// 1 timeslice = 0.001 degree = 111 m
assertEquals(111.0 * i, stats.getTotalDistance(), 100);
} }
} }
@@ -86,7 +80,7 @@ public class TripStatisticsBuilderTest extends TestCase {
for (int j = 0; j < 100; j++) { for (int j = 0; j < 100; j++) {
assertEquals(0.0, builder.updateElevation(elevation)); assertEquals(0.0, builder.updateElevation(elevation));
assertEquals(elevation, builder.getSmoothedElevation()); assertEquals(elevation, builder.getSmoothedElevation());
TripStatistics data = builder.getStatistics(); TripStatistics data = builder.getTripStatistics();
assertEquals(elevation, data.getMinElevation()); assertEquals(elevation, data.getMinElevation());
assertEquals(elevation, data.getMaxElevation()); assertEquals(elevation, data.getMaxElevation());
assertEquals(0.0, data.getTotalElevationGain()); assertEquals(0.0, data.getTotalElevationGain());
@@ -104,80 +98,71 @@ public class TripStatisticsBuilderTest extends TestCase {
} else { } else {
expectedGain = 1.0; expectedGain = 1.0;
} }
assertEquals(expectedGain, assertEquals(expectedGain, builder.updateElevation(i));
builder.updateElevation(i));
assertEquals(i, builder.getSmoothedElevation(), 20); assertEquals(i, builder.getSmoothedElevation(), 20);
TripStatistics data = builder.getStatistics(); TripStatistics data = builder.getTripStatistics();
assertEquals(0.0, data.getMinElevation(), 0.0); assertEquals(0.0, data.getMinElevation(), 0.0);
assertEquals(i, data.getMaxElevation(), assertEquals(i, data.getMaxElevation(), Constants.ELEVATION_SMOOTHING_FACTOR);
Constants.ELEVATION_SMOOTHING_FACTOR); assertEquals(i, data.getTotalElevationGain(), Constants.ELEVATION_SMOOTHING_FACTOR);
assertEquals(i, data.getTotalElevationGain(),
Constants.ELEVATION_SMOOTHING_FACTOR);
} }
} }
public void testGradeSimple() throws Exception { public void testGradeSimple() throws Exception {
for (double i = 0; i < 1000; i++) { for (double i = 0; i < 1000; i++) {
// The value of the elevation does not matter. This is just to fill the // The value of the elevation does not matter. This is just to fill the
// buffer. // buffer.
builder.updateElevation(i); builder.updateElevation(i);
builder.updateGrade(100, 100); builder.updateGrade(100, 100);
if ((i > Constants.GRADE_SMOOTHING_FACTOR) if ((i > Constants.GRADE_SMOOTHING_FACTOR) && (i > Constants.ELEVATION_SMOOTHING_FACTOR)) {
&& (i > Constants.ELEVATION_SMOOTHING_FACTOR)) { assertEquals(1.0, builder.getTripStatistics().getMaxGrade());
assertEquals(1.0, builder.getStatistics().getMaxGrade()); assertEquals(1.0, builder.getTripStatistics().getMinGrade());
assertEquals(1.0, builder.getStatistics().getMinGrade());
} }
} }
for (double i = 0; i < 1000; i++) { for (double i = 0; i < 1000; i++) {
// The value of the elevation does not matter. This is just to fill the // The value of the elevation does not matter. This is just to fill the
// buffer. // buffer.
builder.updateElevation(i); builder.updateElevation(i);
builder.updateGrade(100, -100); builder.updateGrade(100, -100);
if ((i > Constants.GRADE_SMOOTHING_FACTOR) if ((i > Constants.GRADE_SMOOTHING_FACTOR) && (i > Constants.ELEVATION_SMOOTHING_FACTOR)) {
&& (i > Constants.ELEVATION_SMOOTHING_FACTOR)) { assertEquals(1.0, builder.getTripStatistics().getMaxGrade());
assertEquals(1.0, builder.getStatistics().getMaxGrade()); assertEquals(-1.0, builder.getTripStatistics().getMinGrade());
assertEquals(-1.0, builder.getStatistics().getMinGrade());
} }
} }
} }
public void testGradeIgnoreShort() throws Exception { public void testGradeIgnoreShort() throws Exception {
for (double i = 0; i < 100; i++) { for (double i = 0; i < 100; i++) {
// The value of the elevation does not matter. This is just to fill the // The value of the elevation does not matter. This is just to fill the
// buffer. // buffer.
builder.updateElevation(i); builder.updateElevation(i);
builder.updateGrade(1, 100); builder.updateGrade(1, 100);
assertEquals(Double.NEGATIVE_INFINITY, builder.getStatistics().getMaxGrade()); assertEquals(Double.NEGATIVE_INFINITY, builder.getTripStatistics().getMaxGrade());
assertEquals(Double.POSITIVE_INFINITY, builder.getStatistics().getMinGrade()); assertEquals(Double.POSITIVE_INFINITY, builder.getTripStatistics().getMinGrade());
} }
} }
public void testUpdateSpeedIncludeZero() { public void testUpdateSpeedIncludeZero() {
for (int i = 0; i < 1000; i++) { for (int i = 0; i < 1000; i++) {
builder.updateSpeed(i + 1000, 0.0, i, 4.0); builder.updateSpeed(i + 1000, 0.0, i, 4.0);
assertEquals(0.0, builder.getStatistics().getMaxSpeed()); assertEquals(0.0, builder.getTripStatistics().getMaxSpeed());
assertEquals((i + 1) * 1000, builder.getStatistics().getMovingTime());
} }
} }
public void testUpdateSpeedIngoreErrorCode() { public void testUpdateSpeedIngoreErrorCode() {
builder.updateSpeed(12345000, 128.0, 12344000, 0.0); builder.updateSpeed(12345000, 128.0, 12344000, 0.0);
assertEquals(0.0, builder.getStatistics().getMaxSpeed()); assertEquals(0.0, builder.getTripStatistics().getMaxSpeed());
assertEquals(1000, builder.getStatistics().getMovingTime());
} }
public void testUpdateSpeedIngoreLargeAcceleration() { public void testUpdateSpeedIngoreLargeAcceleration() {
builder.updateSpeed(12345000, 100.0, 12344000, 1.0); builder.updateSpeed(12345000, 100.0, 12344000, 1.0);
assertEquals(0.0, builder.getStatistics().getMaxSpeed()); assertEquals(0.0, builder.getTripStatistics().getMaxSpeed());
assertEquals(1000, builder.getStatistics().getMovingTime());
} }
public void testUpdateSpeed() { public void testUpdateSpeed() {
for (int i = 0; i < 1000; i++) { for (int i = 0; i < 1000; i++) {
builder.updateSpeed(i + 1000, 4.0, i, 4.0); builder.updateSpeed(i + 1000, 4.0, i, 4.0);
assertEquals((i + 1) * 1000, builder.getStatistics().getMovingTime());
if (i > Constants.SPEED_SMOOTHING_FACTOR) { if (i > Constants.SPEED_SMOOTHING_FACTOR) {
assertEquals(4.0, builder.getStatistics().getMaxSpeed()); assertEquals(4.0, builder.getTripStatistics().getMaxSpeed());
} }
} }
} }