From ad01b379c7a317b962fa5fde0de61e156e03f5d9 Mon Sep 17 00:00:00 2001 From: youtaol Date: Wed, 30 May 2012 09:49:31 +0800 Subject: [PATCH] Merge heads --- MyTracks/AndroidManifest.xml | 2 +- MyTracks/res/layout/sensor_state.xml | 12 +- MyTracks/res/values/colors.xml | 4 + MyTracks/res/values/strings.xml | 58 +- .../apps/mytracks/ChartValueSeries.java | 447 +++--- .../android/apps/mytracks/ChartView.java | 1272 +++++++++-------- .../android/apps/mytracks/ImportActivity.java | 3 +- .../apps/mytracks/MarkerDetailActivity.java | 1 + .../apps/mytracks/SensorStateActivity.java | 46 +- .../apps/mytracks/TrackDetailActivity.java | 1 + .../content/DataSourcesWrapperImpl.java | 2 +- .../mytracks/fragments/ChartFragment.java | 67 +- .../apps/mytracks/io/file/CsvTrackWriter.java | 4 +- .../SendFusionTablesAsyncTask.java | 10 +- .../io/sendtogoogle/UploadResultActivity.java | 3 +- .../services/TrackRecordingService.java | 10 +- .../sensors/BluetoothConnectionManager.java | 376 ++--- .../sensors/BluetoothSensorManager.java | 210 ++- .../services/sensors/PolarSensorManager.java | 6 +- .../services/sensors/SensorManager.java | 130 +- .../sensors/SensorManagerFactory.java | 122 +- .../services/sensors/ZephyrSensorManager.java | 7 +- .../sensors/ant/AntSensorManager.java | 4 +- .../settings/StatsSettingsActivity.java | 10 +- .../apps/mytracks/util/Api10Adapter.java | 4 +- .../apps/mytracks/util/Api7Adapter.java | 2 +- .../apps/mytracks/ChartValueSeriesTest.java | 102 +- .../mytracks/fragments/ChartFragmentTest.java | 74 +- .../UploadResultActivityTest.java | 3 + .../sensors/SensorManagerFactoryTest.java | 6 +- 30 files changed, 1444 insertions(+), 1554 deletions(-) diff --git a/MyTracks/AndroidManifest.xml b/MyTracks/AndroidManifest.xml index 6b9aa585a..fbf179b96 100755 --- a/MyTracks/AndroidManifest.xml +++ b/MyTracks/AndroidManifest.xml @@ -16,7 +16,7 @@ limitations under the License. --> diff --git a/MyTracks/res/layout/sensor_state.xml b/MyTracks/res/layout/sensor_state.xml index 7a4088436..e1b066f5b 100644 --- a/MyTracks/res/layout/sensor_state.xml +++ b/MyTracks/res/layout/sensor_state.xml @@ -34,12 +34,12 @@ limitations under the License. - + - + #FF0066FF #40C8E0FF + + #FF0066FF + #40C8E0FF + #FFCC3300 #40E88A70 diff --git a/MyTracks/res/values/strings.xml b/MyTracks/res/values/strings.xml index da27b7b8d..c8e70d00c 100644 --- a/MyTracks/res/values/strings.xml +++ b/MyTracks/res/values/strings.xml @@ -451,6 +451,10 @@ limitations under the License. %2$.1f speed in miles per hour"> Average speed: %1$.2f km/h (%2$.1f mi/h) + + Elevation (ft) + Elevation gain: %1$d m (%2$d ft) + + Elevation (m) + Speed (m/s) - - Time - Moving time: %1$s + + Pace (hr/mi) + + + Pace (hr/km) + Power (W) + + Speed (mi/hr) + + + Speed (km/hr) + + + Time + Point @@ -1721,6 +1745,12 @@ limitations under the License. %1$d: sensor id number. E.g., '1'."> Paired with: %1$d. + + Connected to %1$s + Grade - - Pace (hr/mi) - - - Pace (hr/km) - - - Speed (mi/hr) - - - Speed (km/hr) - diff --git a/MyTracks/src/com/google/android/apps/mytracks/ChartValueSeries.java b/MyTracks/src/com/google/android/apps/mytracks/ChartValueSeries.java index 7a054fe6f..5e6a8f01c 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/ChartValueSeries.java +++ b/MyTracks/src/com/google/android/apps/mytracks/ChartValueSeries.java @@ -13,293 +13,252 @@ * License for the specific language governing permissions and limitations under * the License. */ + package com.google.android.apps.mytracks; import com.google.android.apps.mytracks.stats.ExtremityMonitor; +import com.google.common.annotations.VisibleForTesting; import android.content.Context; import android.graphics.Canvas; import android.graphics.Paint; +import android.graphics.Paint.Align; import android.graphics.Paint.Style; import android.graphics.Path; import java.text.NumberFormat; /** - * This class encapsulates meta data about one series of values for a chart. - * + * This class encapsulates the meta data for one series of the chart values. + * * @author Sandor Dornbush */ public class ChartValueSeries { - private final ExtremityMonitor monitor = new ExtremityMonitor(); - private final NumberFormat format; - private final Path path = new Path(); + private static final float STROKE_WIDTH = 2f; + + private final int absoluteMin; + private final int absoluteMax; + private final int[] intervalValues; + private final int metricTitleId; + private final int imperialTitleId; private final Paint fillPaint; private final Paint strokePaint; - private final Paint labelPaint; - private final ZoomSettings zoomSettings; - - private String title; - private double min; - private double max = 1.0; - private int effectiveMax; - private int effectiveMin; - private double spread; - private int interval; + private final Paint titlePaint; + private final Paint markerPaint; + private final ExtremityMonitor extremityMonitor; + private final NumberFormat numberFormat; + private final Path path; + + private int interval = 1; + private int minMarkerValue = 0; + private int maxMarkerValue = interval * ChartView.Y_AXIS_INTERVALS; private boolean enabled = true; /** - * This class controls how effective min/max values of a {@link ChartValueSeries} are calculated. + * Constructor. + * + * @param context the context + * @param absoluteMin the absolute min value + * @param absoluteMax the absolute max value + * @param intervalValues the list of interval values + * @param metricTitleId the metric title id + * @param imperialTitleId the imperial title id + * @param fillColor the fill color + * @param strokeColor the stroke color */ - public static class ZoomSettings { - private int intervals; - private final int absoluteMin; - private final int absoluteMax; - private final int[] zoomLevels; + public ChartValueSeries(Context context, int absoluteMin, int absoluteMax, int[] intervalValues, + int metricTitleId, int imperialTitleId, int fillColor, int strokeColor) { + this.absoluteMin = absoluteMin; + this.absoluteMax = absoluteMax; + this.intervalValues = intervalValues; + this.metricTitleId = metricTitleId; + this.imperialTitleId = imperialTitleId; - public ZoomSettings(int intervals, int[] zoomLevels) { - this.intervals = intervals; - this.absoluteMin = Integer.MAX_VALUE; - this.absoluteMax = Integer.MIN_VALUE; - this.zoomLevels = zoomLevels; - checkArgs(); - } - - public ZoomSettings(int intervals, int absoluteMin, int absoluteMax, int[] zoomLevels) { - this.intervals = intervals; - this.absoluteMin = absoluteMin; - this.absoluteMax = absoluteMax; - this.zoomLevels = zoomLevels; - checkArgs(); - } - - private void checkArgs() { - if (intervals <= 0 || zoomLevels == null || zoomLevels.length == 0) { - throw new IllegalArgumentException("Expecing positive intervals and non-empty zoom levels"); - } - for (int i = 1; i < zoomLevels.length; ++i) { - if (zoomLevels[i] <= zoomLevels[i - 1]) { - throw new IllegalArgumentException("Expecting zoom levels in ascending order"); - } - } - } - - public int getIntervals() { - return intervals; - } - - public int getAbsoluteMin() { - return absoluteMin; - } - - public int getAbsoluteMax() { - return absoluteMax; - } - - public int[] getZoomLevels() { - return zoomLevels; - } - - /** - * Calculates the interval between markings given the min and max values. - * This function attempts to find the smallest zoom level that fits [min,max] after rounding - * it to the current zoom level. - * - * @param min the minimum value in the series - * @param max the maximum value in the series - * @return the calculated interval for the given range - */ - public int calculateInterval(double min, double max) { - min = Math.min(min, absoluteMin); - max = Math.max(max, absoluteMax); - for (int i = 0; i < zoomLevels.length; ++i) { - int zoomLevel = zoomLevels[i]; - int roundedMin = (int)(min / zoomLevel) * zoomLevel; - if (roundedMin > min) { - roundedMin -= zoomLevel; - } - double interval = (max - roundedMin) / intervals; - if (zoomLevel >= interval) { - return zoomLevel; - } - } - return zoomLevels[zoomLevels.length - 1]; - } - } - - /** - * Constructs a new chart value series. - * - * @param context The context for the chart - * @param fillColor The paint for filling the chart - * @param strokeColor The paint for stroking the outside the chart, optional - * @param zoomSettings The settings related to zooming - * @param titleId The title ID - * - * TODO: Get rid of Context and inject appropriate values instead. - */ - public ChartValueSeries( - Context context, int fillColor, int strokeColor, ZoomSettings zoomSettings, int titleId) { - this.format = NumberFormat.getIntegerInstance(); fillPaint = new Paint(); fillPaint.setStyle(Style.FILL); fillPaint.setColor(context.getResources().getColor(fillColor)); fillPaint.setAntiAlias(true); - if (strokeColor != -1) { - strokePaint = new Paint(); - strokePaint.setStyle(Style.STROKE); - strokePaint.setColor(context.getResources().getColor(strokeColor)); - strokePaint.setAntiAlias(true); - // Make a copy of the stroke paint with the default thickness. - labelPaint = new Paint(strokePaint); - strokePaint.setStrokeWidth(2f); - } else { - strokePaint = null; - labelPaint = fillPaint; - } - this.zoomSettings = zoomSettings; - this.title = context.getString(titleId); + + strokePaint = new Paint(); + strokePaint.setStyle(Style.STROKE); + strokePaint.setColor(context.getResources().getColor(strokeColor)); + strokePaint.setAntiAlias(true); + + float scale = context.getResources().getDisplayMetrics().density; + + // Make copies of the stroke paint with the default thickness + titlePaint = new Paint(strokePaint); + titlePaint.setTextSize(ChartView.MEDIUM_TEXT_SIZE * scale); + titlePaint.setTextAlign(Align.CENTER); + + markerPaint = new Paint(strokePaint); + markerPaint.setTextSize(ChartView.SMALL_TEXT_SIZE * scale); + markerPaint.setTextAlign(Align.RIGHT); + + // Set stroke paint thickness + strokePaint.setStrokeWidth(STROKE_WIDTH); + + extremityMonitor = new ExtremityMonitor(); + numberFormat = NumberFormat.getIntegerInstance(); + path = new Path(); } /** - * Draws the path of the chart + * Sets the series enabled value. + * + * @param enabled true to enable */ - public void drawPath(Canvas c) { - c.drawPath(path, fillPaint); - if (strokePaint != null) { - c.drawPath(path, strokePaint); - } + public void setEnabled(boolean enabled) { + this.enabled = enabled; } /** - * Resets this series - */ - public void reset() { - monitor.reset(); - } - - /** - * Updates this series with a new value - */ - public void update(double d) { - monitor.update(d); - } - - /** - * @return The interval between markers - */ - public int getInterval() { - return interval; - } - - /** - * Determines what the min and max of the chart will be. - * This will round down and up the min and max respectively. - */ - public void updateDimension() { - if (monitor.getMax() == Double.NEGATIVE_INFINITY) { - min = 0; - max = 1; - } else { - min = monitor.getMin(); - max = monitor.getMax(); - } - min = Math.min(min, zoomSettings.getAbsoluteMin()); - max = Math.max(max, zoomSettings.getAbsoluteMax()); - - this.interval = zoomSettings.calculateInterval(min, max); - // Round it up. - effectiveMax = ((int) (max / interval)) * interval + interval; - // Round it down. - effectiveMin = ((int) (min / interval)) * interval; - if (min < 0) { - effectiveMin -= interval; - } - spread = effectiveMax - effectiveMin; - } - - /** - * @return The length of the longest string from the series - */ - public int getMaxLabelLength() { - String minS = format.format(effectiveMin); - String maxS = format.format(getMax()); - return Math.max(minS.length(), maxS.length()); - } - - /** - * @return The rounded down minimum value - */ - public int getMin() { - return effectiveMin; - } - - /** - * @return The rounded up maximum value - */ - public int getMax() { - return effectiveMax; - } - - /** - * @return The difference between the min and max values in the series - */ - public double getSpread() { - return spread; - } - - /** - * @return The number format for this series - */ - NumberFormat getFormat() { - return format; - } - - /** - * @return The path for this series - */ - Path getPath() { - return path; - } - - /** - * @return The paint for this series - */ - Paint getPaint() { - return strokePaint == null ? fillPaint : strokePaint; - } - - public Paint getLabelPaint() { - return labelPaint; - } - - /** - * @return The title of the series - */ - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - /** - * @return is this series enabled + * Returns true if the series is enabled. */ public boolean isEnabled() { return enabled; } /** - * Sets the series enabled flag. + * Returns true if the series has data. */ - public void setEnabled(boolean enabled) { - this.enabled = enabled; + public boolean hasData() { + return extremityMonitor.hasData(); } - public boolean hasData() { - return monitor.hasData(); + /** + * Updates the series with a new value. + * + * @param value the new value + */ + public void update(double value) { + extremityMonitor.update(value); + } + + /** + * Gets the path. + */ + public Path getPath() { + return path; + } + + /** + * Draws the path on canvas. + * + * @param canvas the canvas + */ + public void drawPath(Canvas canvas) { + canvas.drawPath(path, fillPaint); + canvas.drawPath(path, strokePaint); + } + + /** + * Updates the y axis dimension. + */ + public void updateDimension() { + double min = hasData() ? extremityMonitor.getMin() : 0.0; + double max = hasData() ? extremityMonitor.getMax() : 1.0; + min = Math.max(min, absoluteMin); + max = Math.min(max, absoluteMax); + interval = getInterval(min, max); + minMarkerValue = getMinMarkerValue(min, interval); + maxMarkerValue = minMarkerValue + interval * ChartView.Y_AXIS_INTERVALS; + } + + /** + * Gets the interval value. + * + * @param min the min value + * @param max the max value + */ + private int getInterval(double min, double max) { + for (int i = 0; i < intervalValues.length; i++) { + int intervalValue = intervalValues[i]; + int minValue = getMinMarkerValue(min, intervalValue); + double targetInterval = (max - minValue) / ChartView.Y_AXIS_INTERVALS; + if (intervalValue >= targetInterval) { + return intervalValue; + } + } + // Return the largest interval + return intervalValues[intervalValues.length - 1]; + } + + /** + * Gets the min marker value. + * + * @param min the min series value + * @param intervalValue the interval value + */ + private int getMinMarkerValue(double min, int intervalValue) { + // Round down to the nearest intervalValue + int value = ((int) (min / intervalValue)) * intervalValue; + // value > min if min is negative + if (value > min) { + return value - intervalValue; + } + return value; + } + + /** + * Gets the interval value. + */ + public int getInterval() { + return interval; + } + + /** + * Gets the min marker value. + */ + public int getMinMarkerValue() { + return minMarkerValue; + } + + /** + * Gets the max marker value. + */ + @VisibleForTesting + int getMaxMarkerValue() { + return maxMarkerValue; + } + + /** + * Gets the title id. + */ + public int getTitleId(boolean metricUnits) { + return metricUnits ? metricTitleId : imperialTitleId; + } + + /** + * Gets the title paint. + */ + public Paint getTitlePaint() { + return titlePaint; + } + + /** + * Gets the marker paint. + */ + public Paint getMarkerPaint() { + return markerPaint; + } + + /** + * Gets the largest marker. + */ + public String getLargestMarker() { + String minMarker = numberFormat.format(getMinMarkerValue()); + String maxMarker = numberFormat.format(getMaxMarkerValue()); + return minMarker.length() >= maxMarker.length() ? minMarker : maxMarker; + } + + /** + * Formats a marker value. + * + * @param value the value + */ + public String formatMarker(int value) { + return numberFormat.format(value); } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/ChartView.java b/MyTracks/src/com/google/android/apps/mytracks/ChartView.java index af6c785fd..880e146e8 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/ChartView.java +++ b/MyTracks/src/com/google/android/apps/mytracks/ChartView.java @@ -13,16 +13,15 @@ * License for the specific language governing permissions and limitations under * the License. */ + package com.google.android.apps.mytracks; -import com.google.android.apps.mytracks.ChartValueSeries.ZoomSettings; import com.google.android.apps.mytracks.content.Waypoint; import com.google.android.apps.mytracks.stats.ExtremityMonitor; import com.google.android.apps.mytracks.util.IntentUtils; 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.content.Intent; @@ -33,6 +32,7 @@ import android.graphics.Paint; import android.graphics.Paint.Align; import android.graphics.Paint.Style; import android.graphics.Path; +import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.view.MotionEvent; import android.view.VelocityTracker; @@ -45,151 +45,169 @@ import java.util.ArrayList; /** * Visualization of the chart. - * + * * @author Sandor Dornbush * @author Leif Hendrik Wilden */ public class ChartView extends View { - private static final int MIN_ZOOM_LEVEL = 1; - /* - * Scrolling logic: - */ + public static final float MEDIUM_TEXT_SIZE = 18f; + public static final float SMALL_TEXT_SIZE = 12f; + + public static final int Y_AXIS_INTERVALS = 5; + + public static final int NUM_SERIES = 6; + public static final int ELEVATION_SERIES = 0; + public static final int SPEED_SERIES = 1; + public static final int PACE_SERIES = 2; + public static final int HEART_RATE_SERIES = 3; + public static final int CADENCE_SERIES = 4; + public static final int POWER_SERIES = 5; + + private static final int TARGET_X_AXIS_INTERVALS = 4; + + private static final int MIN_ZOOM_LEVEL = 1; + private static final int MAX_ZOOM_LEVEL = 10; + + private static final NumberFormat X_NUMBER_FORMAT = NumberFormat.getIntegerInstance(); + private static final NumberFormat X_FRACTION_FORMAT = NumberFormat.getNumberInstance(); + static { + X_FRACTION_FORMAT.setMaximumFractionDigits(1); + X_FRACTION_FORMAT.setMinimumFractionDigits(1); + } + + private static final int BORDER = 8; + private static final int SPACER = 4; + private static final int Y_AXIS_OFFSET = 16; + + // Marker pin x position / marker icon width + private static final double MARKER_PIN_X_POSITION_PERCENTAGE = (double) 10 / 32; + + private final ChartValueSeries[] series = new ChartValueSeries[NUM_SERIES]; + private final ArrayList chartData = new ArrayList(); + private final ArrayList waypoints = new ArrayList(); + private final ExtremityMonitor xExtremityMonitor = new ExtremityMonitor(); + private double maxX = 1.0; + + private final Paint axisPaint; + private final Paint xAxisMarkerPaint; + private final Paint gridPaint; + private final Paint markerPaint; + + private final Drawable pointer; + private final Drawable statisticsMarker; + private final Drawable waypointMarker; + private final int markerWidth; + private final int markerHeight; + private final Scroller scroller; private VelocityTracker velocityTracker = null; - /** Position of the last motion event */ - private float lastMotionX; - - /* - * Zoom logic: - */ + private float lastMotionEventX = -1; private int zoomLevel = 1; - private int maxZoomLevel = 10; - private static final int MAX_INTERVALS = 5; + private int leftBorder = BORDER; + private int topBorder = BORDER; + private int bottomBorder = BORDER; + private int rightBorder = BORDER; + private int spacer = SPACER; + private int yAxisOffset = Y_AXIS_OFFSET; - /* - * Borders, margins, dimensions (in pixels): - */ - private int leftBorder = -1; - - /** - * Unscaled top border of the chart. - */ - private static final int TOP_BORDER = 16; - - /** - * Device scaled top border of the chart. - */ - private int topBorder; - - /** - * Unscaled bottom border of the chart. - */ - private static final float BOTTOM_BORDER = 8; - - /** - * Device scaled bottom border of the chart. - */ - private int bottomBorder; - - private static final int RIGHT_BORDER = 40; - - /** Space to leave for drawing the unit labels */ - private static final int UNIT_BORDER = 15; - private static final int FONT_HEIGHT = 10; - - private int w = 0; - private int h = 0; + private int width = 0; + private int height = 0; private int effectiveWidth = 0; private int effectiveHeight = 0; - /* - * Ranges (in data units): - */ - private double maxX = 1; - - /** - * The various series. - */ - public static final int ELEVATION_SERIES = 0; - public static final int SPEED_SERIES = 1; - public static final int POWER_SERIES = 2; - public static final int CADENCE_SERIES = 3; - public static final int HEART_RATE_SERIES = 4; - public static final int NUM_SERIES = 5; - private ChartValueSeries[] series; - - private final ExtremityMonitor xMonitor = new ExtremityMonitor(); - private static final NumberFormat X_FORMAT = NumberFormat.getIntegerInstance(); - private static final NumberFormat X_SHORT_FORMAT = NumberFormat.getNumberInstance(); - - static { - X_SHORT_FORMAT.setMaximumFractionDigits(1); - X_SHORT_FORMAT.setMinimumFractionDigits(1); - } - - /* - * Paints etc. used when drawing the chart: - */ - private final Paint borderPaint = new Paint(); - private final Paint labelPaint = new Paint(); - private final Paint gridPaint = new Paint(); - private final Paint gridBarPaint = new Paint(); - private final Paint clearPaint = new Paint(); - private final Drawable pointer; - private final Drawable statsMarker; - private final Drawable waypointMarker; - private final int markerWidth, markerHeight; - - /** - * The chart data stored as an array of double arrays. Each one dimensional - * array is composed of [x, y]. - */ - private final ArrayList data = new ArrayList(); - - /** - * List of way points to be displayed. - */ - private final ArrayList waypoints = new ArrayList(); - + private boolean chartByDistance = true; private boolean metricUnits = true; + private boolean reportSpeed = true; private boolean showPointer = false; - private boolean chartByDistance = true; - + /** + * Constructor. + * + * @param context the context + */ public ChartView(Context context) { super(context); - setUpChartValueSeries(context); + series[ELEVATION_SERIES] = new ChartValueSeries(context, + Integer.MIN_VALUE, + Integer.MAX_VALUE, + new int[] { 5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000 }, + R.string.description_elevation_metric, + R.string.description_elevation_imperial, + R.color.elevation_fill, + R.color.elevation_border); + series[SPEED_SERIES] = new ChartValueSeries(context, + 0, + Integer.MAX_VALUE, + new int[] {1, 5, 10, 20, 50 }, + R.string.description_speed_metric, + R.string.description_speed_imperial, + R.color.speed_fill, + R.color.speed_border); + series[PACE_SERIES] = new ChartValueSeries(context, + 0, + Integer.MAX_VALUE, + new int[] {1, 2, 5, 10, 15, 20, 30, 60, 120 }, + R.string.description_pace_metric, + R.string.description_pace_imperial, + R.color.pace_fill, + R.color.pace_border); + series[HEART_RATE_SERIES] = new ChartValueSeries(context, + 0, + Integer.MAX_VALUE, + new int[] {25, 50 }, + R.string.description_sensor_heart_rate, + R.string.description_sensor_heart_rate, + R.color.heartrate_fill, + R.color.heartrate_border); + series[CADENCE_SERIES] = new ChartValueSeries(context, + 0, + Integer.MAX_VALUE, + new int[] {5, 10, 25, 50 }, + R.string.description_sensor_cadence, + R.string.description_sensor_cadence, + R.color.cadence_fill, + R.color.cadence_border); + series[POWER_SERIES] = new ChartValueSeries(context, + 0, + 1000, + new int[] { 5, 50, 100, 200 }, + R.string.description_sensor_power, + R.string.description_sensor_power, + R.color.power_fill, + R.color.power_border); - labelPaint.setStyle(Style.STROKE); - labelPaint.setColor(context.getResources().getColor(R.color.black)); - labelPaint.setAntiAlias(true); + float scale = context.getResources().getDisplayMetrics().density; - borderPaint.setStyle(Style.STROKE); - borderPaint.setColor(context.getResources().getColor(R.color.black)); - borderPaint.setAntiAlias(true); + axisPaint = new Paint(); + axisPaint.setStyle(Style.STROKE); + axisPaint.setColor(context.getResources().getColor(R.color.black)); + axisPaint.setAntiAlias(true); + axisPaint.setTextSize(SMALL_TEXT_SIZE * scale); + xAxisMarkerPaint = new Paint(axisPaint); + xAxisMarkerPaint.setTextAlign(Align.CENTER); + + gridPaint = new Paint(); gridPaint.setStyle(Style.STROKE); gridPaint.setColor(context.getResources().getColor(R.color.gray)); gridPaint.setAntiAlias(false); + gridPaint.setPathEffect(new DashPathEffect(new float[] { 3, 2 }, 0)); - gridBarPaint.set(gridPaint); - gridBarPaint.setPathEffect(new DashPathEffect(new float[] {3, 2}, 0)); - - clearPaint.setStyle(Style.FILL); - clearPaint.setColor(context.getResources().getColor(R.color.white)); - clearPaint.setAntiAlias(false); + markerPaint = new Paint(); + markerPaint.setStyle(Style.STROKE); + markerPaint.setColor(context.getResources().getColor(R.color.gray)); + markerPaint.setAntiAlias(false); pointer = context.getResources().getDrawable(R.drawable.arrow_180); - pointer.setBounds(0, 0, - pointer.getIntrinsicWidth(), pointer.getIntrinsicHeight()); + pointer.setBounds(0, 0, pointer.getIntrinsicWidth(), pointer.getIntrinsicHeight()); - statsMarker = getResources().getDrawable(R.drawable.yellow_pushpin); - markerWidth = statsMarker.getIntrinsicWidth(); - markerHeight = statsMarker.getIntrinsicHeight(); - statsMarker.setBounds(0, 0, markerWidth, markerHeight); + statisticsMarker = getResources().getDrawable(R.drawable.yellow_pushpin); + markerWidth = statisticsMarker.getIntrinsicWidth(); + markerHeight = statisticsMarker.getIntrinsicHeight(); + statisticsMarker.setBounds(0, 0, markerWidth, markerHeight); waypointMarker = getResources().getDrawable(R.drawable.blue_pushpin); waypointMarker.setBounds(0, 0, markerWidth, markerHeight); @@ -200,105 +218,71 @@ public class ChartView extends View { updateDimensions(); } - private void setUpChartValueSeries(Context context) { - series = new ChartValueSeries[NUM_SERIES]; - - // Create the value series. - series[ELEVATION_SERIES] = - new ChartValueSeries(context, - R.color.elevation_fill, - R.color.elevation_border, - new ZoomSettings(MAX_INTERVALS, - new int[] {5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000}), - R.string.stats_elevation); - - series[SPEED_SERIES] = - new ChartValueSeries(context, - R.color.speed_fill, - R.color.speed_border, - new ZoomSettings(MAX_INTERVALS, 0, Integer.MIN_VALUE, - new int[] {1, 5, 10, 20, 50}), - R.string.stats_speed); - series[POWER_SERIES] = - new ChartValueSeries(context, - R.color.power_fill, - R.color.power_border, - new ZoomSettings(MAX_INTERVALS, 0, 1000, new int[] {5, 50, 100, 200}), - R.string.sensor_state_power); - series[CADENCE_SERIES] = - new ChartValueSeries(context, - R.color.cadence_fill, - R.color.cadence_border, - new ZoomSettings(MAX_INTERVALS, 0, Integer.MIN_VALUE, - new int[] {5, 10, 25, 50}), - R.string.sensor_state_cadence); - series[HEART_RATE_SERIES] = - new ChartValueSeries(context, - R.color.heartrate_fill, - R.color.heartrate_border, - new ZoomSettings(MAX_INTERVALS, 0, Integer.MIN_VALUE, - new int[] {25, 50}), - R.string.sensor_state_heart_rate); - } - - public void clearWaypoints() { - waypoints.clear(); - } - - public void addWaypoint(Waypoint waypoint) { - waypoints.add(waypoint); + /** + * Sets the enabled value for a chart value series. + * + * @param index the chart value series index + */ + public void setChartValueSeriesEnabled(int index, boolean enabled) { + series[index].setEnabled(enabled); } /** - * Determines whether the pointer icon is shown on the last data point. + * Sets chart by distance. It is expected that after changing this value, data + * will be reloaded. + * + * @param value true for by distance, false for by time */ - public void setShowPointer(boolean showPointer) { - this.showPointer = showPointer; + public void setChartByDistance(boolean value) { + chartByDistance = value; } /** - * Sets whether metric units are used or not. + * Sets metric units. + * + * @param value true to use metric units */ - public void setMetricUnits(boolean metricUnits) { - this.metricUnits = metricUnits; - } - - public void setReportSpeed(boolean reportSpeed, Context c) { - series[SPEED_SERIES].setTitle(c.getString(reportSpeed - ? R.string.stats_speed - : R.string.stats_pace)); - } - - private void addDataPointInternal(double[] theData) { - xMonitor.update(theData[0]); - int min = Math.min(series.length, theData.length - 1); - for (int i = 1; i <= min; i++) { - if (!Double.isNaN(theData[i])) { - series[i - 1].update(theData[i]); - } - } - // Fill in the extra's if needed. - for (int i = theData.length; i < series.length; i++) { - if (series[i].hasData()) { - series[i].update(0); - } - } + public void setMetricUnits(boolean value) { + metricUnits = value; } /** - * Adds multiple data points to the chart. - * - * @param theData an array list of data points to be added + * Sets report speed. + * + * @param value true to report speed */ - public void addDataPoints(ArrayList theData) { - synchronized (data) { - data.addAll(theData); - for (int i = 0; i < theData.size(); i++) { - double d[] = theData.get(i); - addDataPointInternal(d); + public void setReportSpeed(boolean value) { + reportSpeed = value; + } + + /** + * Sets show pointer. + * + * @param value true to show pointer + */ + public void setShowPointer(boolean value) { + showPointer = value; + } + + /** + * Adds data points. + * + * @param dataPoints an array of data points to be added + */ + public void addDataPoints(ArrayList dataPoints) { + synchronized (chartData) { + chartData.addAll(dataPoints); + for (int i = 0; i < dataPoints.size(); i++) { + double[] dataPoint = dataPoints.get(i); + xExtremityMonitor.update(dataPoint[0]); + for (int j = 0; j < series.length; j++) { + if (!Double.isNaN(dataPoint[j])) { + series[j].update(dataPoint[j + 1]); + } + } } updateDimensions(); - setUpPath(); + updatePaths(); } } @@ -306,107 +290,108 @@ public class ChartView extends View { * Clears all data. */ public void reset() { - synchronized (data) { - data.clear(); - xMonitor.reset(); + synchronized (chartData) { + chartData.clear(); + xExtremityMonitor.reset(); zoomLevel = 1; updateDimensions(); } } + /** + * Resets scroll. To be called on the UI thread. + */ public void resetScroll() { scrollTo(0, 0); } /** - * @return true if the chart can be zoomed into. + * Adds a waypoint. + * + * @param waypoint the waypoint */ - public boolean canZoomIn() { - return zoomLevel < maxZoomLevel; + public void addWaypoint(Waypoint waypoint) { + waypoints.add(waypoint); } /** - * @return true if the chart can be zoomed out + * Clears the waypoints. + */ + public void clearWaypoints() { + waypoints.clear(); + } + + /** + * Returns true if can zoom in. + */ + public boolean canZoomIn() { + return zoomLevel < MAX_ZOOM_LEVEL; + } + + /** + * Returns true if can zoom out. */ public boolean canZoomOut() { return zoomLevel > MIN_ZOOM_LEVEL; } /** - * Zooms in one level (factor 2). + * Zooms in one level. */ public void zoomIn() { if (canZoomIn()) { zoomLevel++; - setUpPath(); + updatePaths(); invalidate(); } } /** - * Zooms out one level (factor 2). + * Zooms out one level. */ public void zoomOut() { if (canZoomOut()) { zoomLevel--; scroller.abortAnimation(); int scrollX = getScrollX(); - if (scrollX > effectiveWidth * (zoomLevel - 1)) { - scrollX = effectiveWidth * (zoomLevel - 1); + int maxWidth = effectiveWidth * (zoomLevel - 1); + if (scrollX > maxWidth) { + scrollX = maxWidth; scrollTo(scrollX, 0); } - setUpPath(); + updatePaths(); invalidate(); } } /** * Initiates flinging. - * - * @param velocityX start velocity (pixels per second) + * + * @param velocityX velocity of fling in pixels per second */ public void fling(int velocityX) { - scroller.fling(getScrollX(), 0, velocityX, 0, 0, - effectiveWidth * (zoomLevel - 1), 0, 0); + int maxWidth = effectiveWidth * (zoomLevel - 1); + scroller.fling(getScrollX(), 0, velocityX, 0, 0, maxWidth, 0, 0); invalidate(); } /** - * Scrolls the view horizontally by the given amount. - * - * @param deltaX number of pixels to scroll + * Scrolls the view horizontally by a given amount. + * + * @param deltaX the number of pixels to scroll */ public void scrollBy(int deltaX) { int scrollX = getScrollX() + deltaX; if (scrollX < 0) { scrollX = 0; } - int available = effectiveWidth * (zoomLevel - 1); - if (scrollX > available) { - scrollX = available; + int maxWidth = effectiveWidth * (zoomLevel - 1); + if (scrollX > maxWidth) { + scrollX = maxWidth; } scrollTo(scrollX, 0); } - /** - * Sets chart by distance value. It is expected that after changing this - * value, data will be reloaded. - * - * @param value true for by distance, false for by time. - */ - public void setChartByDistance(boolean value) { - this.chartByDistance = value; - } - - private int getWaypointX(Waypoint waypoint) { - if (chartByDistance) { - double lenghtInKm = waypoint.getLength() * UnitConversions.M_TO_KM; - return getX(metricUnits ? lenghtInKm : lenghtInKm * UnitConversions.KM_TO_MI); - } else { - return getX(waypoint.getDuration()); - } - } - /** * Called by the parent to indicate that the mScrollX/Y values need to be * updated. Triggers a redraw during flinging. @@ -430,52 +415,48 @@ public class ChartView extends View { velocityTracker = VelocityTracker.obtain(); } velocityTracker.addMovement(event); - final int action = event.getAction(); - final float x = event.getX(); - switch (action) { + float x = event.getX(); + switch (event.getAction()) { case MotionEvent.ACTION_DOWN: - /* - * If being flinged and user touches, stop the fling. isFinished will be - * false if being flinged. - */ + // Stop the fling if (!scroller.isFinished()) { scroller.abortAnimation(); } - // Remember where the motion event started - lastMotionX = x; + lastMotionEventX = x; break; case MotionEvent.ACTION_MOVE: + if (lastMotionEventX == -1) { + break; + } // Scroll to follow the motion event - final int deltaX = (int) (lastMotionX - x); - lastMotionX = x; + int deltaX = (int) (lastMotionEventX - x); + lastMotionEventX = x; if (deltaX < 0) { if (getScrollX() > 0) { scrollBy(deltaX); } } else if (deltaX > 0) { - final int availableToScroll = - effectiveWidth * (zoomLevel - 1) - getScrollX(); + int availableToScroll = effectiveWidth * (zoomLevel - 1) - getScrollX(); if (availableToScroll > 0) { scrollBy(Math.min(availableToScroll, deltaX)); } } break; case MotionEvent.ACTION_UP: - // Check if top area with waypoint markers was touched and find the - // touched marker if any: - if (event.getY() < 100) { - int dmin = Integer.MAX_VALUE; + // Check if the y event is within markerHeight of the marker center + if (Math.abs(event.getY() - topBorder - spacer - markerHeight / 2) < markerHeight) { + int minDistance = Integer.MAX_VALUE; Waypoint nearestWaypoint = null; for (int i = 0; i < waypoints.size(); i++) { - final Waypoint waypoint = waypoints.get(i); - final int d = Math.abs(getWaypointX(waypoint) - (int) event.getX() - - getScrollX()); - if (d < dmin) { - dmin = d; + Waypoint waypoint = waypoints.get(i); + int distance = Math.abs( + getX(getWaypointXValue(waypoint)) - (int) event.getX() - getScrollX()); + if (distance < minDistance) { + minDistance = distance; nearestWaypoint = waypoint; } } - if (nearestWaypoint != null && dmin < 100) { + if (nearestWaypoint != null && minDistance < markerWidth) { Intent intent = IntentUtils.newIntent(getContext(), MarkerDetailActivity.class) .putExtra(MarkerDetailActivity.EXTRA_MARKER_ID, nearestWaypoint.getId()); getContext().startActivity(intent); @@ -486,8 +467,7 @@ public class ChartView extends View { VelocityTracker myVelocityTracker = velocityTracker; myVelocityTracker.computeCurrentVelocity(1000); int initialVelocity = (int) myVelocityTracker.getXVelocity(); - if (Math.abs(initialVelocity) > - ViewConfiguration.getMinimumFlingVelocity()) { + if (Math.abs(initialVelocity) > ViewConfiguration.getMinimumFlingVelocity()) { fling(-initialVelocity); } if (velocityTracker != null) { @@ -505,159 +485,351 @@ public class ChartView extends View { View.MeasureSpec.getSize(widthMeasureSpec), View.MeasureSpec.getSize(heightMeasureSpec)); super.onMeasure(widthMeasureSpec, heightMeasureSpec); } - + @Override - protected void onDraw(Canvas c) { - synchronized (data) { + protected void onDraw(Canvas canvas) { + synchronized (chartData) { - // Keep original state. - c.save(); + canvas.save(); + + canvas.drawColor(Color.WHITE); - c.drawColor(Color.WHITE); + canvas.save(); + + clipToGraphArea(canvas); + drawDataSeries(canvas); + drawWaypoints(canvas); + drawGrid(canvas); + + canvas.restore(); - if (data.isEmpty()) { - // No data, draw only axes - drawXAxis(c); - drawYAxis(c); - c.restore(); - return; - } + drawSeriesTitles(canvas); + drawXAxis(canvas); + drawYAxis(canvas); + + canvas.restore(); - // Clip to graph drawing space - c.save(); - clipToGraphSpace(c); - - // Draw the grid and the data on it. - drawGrid(c); - drawDataSeries(c); - drawWaypoints(c); - - // Go back to full canvas drawing. - c.restore(); - - // Draw the axes and their labels. - drawAxesAndLabels(c); - - // Go back to original state. - c.restore(); - - // Draw the pointer if (showPointer) { - drawPointer(c); - } - } - } - - /** Clips the given canvas to the area where the graph lines should be drawn. */ - private void clipToGraphSpace(Canvas c) { - c.clipRect(leftBorder + 1 + getScrollX(), topBorder + 1, - w - RIGHT_BORDER + getScrollX() - 1, h - bottomBorder - 1); - } - - /** Draws the axes and their labels into th e given canvas. */ - private void drawAxesAndLabels(Canvas c) { - drawXLabels(c); - drawXAxis(c); - drawSeriesTitles(c); - - c.translate(getScrollX(), 0); - drawYAxis(c); - float density = getContext().getResources().getDisplayMetrics().density; - final int spacer = (int) (5 * density); - int x = leftBorder - spacer; - for (ChartValueSeries cvs : series) { - if (cvs.isEnabled() && cvs.hasData()) { - x -= drawYLabels(cvs, c, x) + spacer; - } - } - } - - /** Draws the current pointer into the given canvas. */ - private void drawPointer(Canvas c) { - c.translate(getX(maxX) - pointer.getIntrinsicWidth() / 2, - getY(series[0], data.get(data.size() - 1)[1]) - - pointer.getIntrinsicHeight() / 2 - 12); - pointer.draw(c); - } - - /** Draws the waypoints into the given canvas. */ - private void drawWaypoints(Canvas c) { - for (int i = 1; i < waypoints.size(); i++) { - final Waypoint waypoint = waypoints.get(i); - if (waypoint.getLocation() == null) { - continue; - } - c.save(); - - final float x = getWaypointX(waypoint); - c.drawLine(x, h - bottomBorder, x, topBorder, gridPaint); - c.translate(x - (float) markerWidth / 2.0f, (float) markerHeight); - if (waypoints.get(i).getType() == Waypoint.TYPE_STATISTICS) { - statsMarker.draw(c); - } else { - waypointMarker.draw(c); - } - c.restore(); - } - } - - /** Draws the data series into the given canvas. */ - private void drawDataSeries(Canvas c) { - for (ChartValueSeries cvs : series) { - if (cvs.isEnabled() && cvs.hasData()) { - cvs.drawPath(c); - } - } - } - - /** Draws the colored titles for the data series. */ - private void drawSeriesTitles(Canvas c) { - int sections = 1; - for (ChartValueSeries cvs : series) { - if (cvs.isEnabled() && cvs.hasData()) { - sections++; - } - } - int j = 0; - for (ChartValueSeries cvs : series) { - if (cvs.isEnabled() && cvs.hasData()) { - int x = (int) (w * (double) ++j / sections) + getScrollX(); - c.drawText(cvs.getTitle(), x, topBorder, cvs.getLabelPaint()); + drawPointer(canvas); } } } /** - * Sets up the path that is used to draw the chart in onDraw(). The path - * needs to be updated any time after the data or histogram dimensions change. + * Clips a canvas to the graph area. + * + * @param canvas the canvas */ - private void setUpPath() { - synchronized (data) { - for (ChartValueSeries cvs : series) { - cvs.getPath().reset(); - } + private void clipToGraphArea(Canvas canvas) { + int x = getScrollX() + leftBorder; + int y = topBorder; + canvas.clipRect(x, y, x + effectiveWidth, y + effectiveHeight); + } - if (!data.isEmpty()) { - drawPaths(); - closePaths(); + /** + * Draws the data series. + * + * @param canvas the canvas + */ + private void drawDataSeries(Canvas canvas) { + for (ChartValueSeries chartValueSeries : series) { + if (chartValueSeries.isEnabled() && chartValueSeries.hasData()) { + chartValueSeries.drawPath(canvas); } } } - /** Actually draws the data points as a path. */ + /** + * Draws the waypoints. + * + * @param canvas the canvas + */ + private void drawWaypoints(Canvas canvas) { + for (int i = 0; i < waypoints.size(); i++) { + final Waypoint waypoint = waypoints.get(i); + if (waypoint.getLocation() == null) { + continue; + } + double xValue = getWaypointXValue(waypoint); + if (xValue > maxX) { + continue; + } + canvas.save(); + float x = getX(getWaypointXValue(waypoint)); + canvas.drawLine( + x, topBorder + spacer + markerHeight / 2, x, topBorder + effectiveHeight, markerPaint); + canvas.translate( + x - (float) (markerWidth * MARKER_PIN_X_POSITION_PERCENTAGE), topBorder + spacer); + if (waypoints.get(i).getType() == Waypoint.TYPE_STATISTICS) { + statisticsMarker.draw(canvas); + } else { + waypointMarker.draw(canvas); + } + canvas.restore(); + } + } + + /** + * Draws the grid. + * + * @param canvas the canvas + */ + private void drawGrid(Canvas canvas) { + // X axis grid + ArrayList xAxisMarkerPositions = getXAxisMarkerPositions(getXAxisInterval()); + for (int i = 0; i < xAxisMarkerPositions.size(); i++) { + int x = getX(xAxisMarkerPositions.get(i)); + canvas.drawLine(x, topBorder, x, topBorder + effectiveHeight, gridPaint); + } + // Y axis grid + float rightEdge = getX(maxX); + for (int i = 0; i <= Y_AXIS_INTERVALS; i++) { + double percentage = (double) i / Y_AXIS_INTERVALS; + int range = effectiveHeight - 2 * yAxisOffset; + int y = topBorder + yAxisOffset + (int) (percentage * range); + canvas.drawLine(leftBorder, y, rightEdge, y, gridPaint); + } + } + + /** + * Draws series titles. + * + * @param canvas the canvas + */ + private void drawSeriesTitles(Canvas canvas) { + int[] titleDimensions = getTitleDimenions(); + int lines = titleDimensions[0]; + int lineHeight = titleDimensions[1]; + int count = 0; + for (int i = 0; i < series.length; i++) { + ChartValueSeries chartValueSeries = series[i]; + if (chartValueSeries.isEnabled() && chartValueSeries.hasData() || allowIfEmpty(i)) { + count++; + String title = getContext().getString(chartValueSeries.getTitleId(metricUnits)); + Paint paint = chartValueSeries.getTitlePaint(); + int x = (int) (0.5 * width) + getScrollX(); + int y = topBorder - spacer - (lines - count) * (lineHeight + spacer); + canvas.drawText(title, x, y, paint); + } + } + } + + /** + * Gets the title dimensions. Returns an array of 2 integers, first element is + * the number of lines and the second element is the line height. + */ + private int[] getTitleDimenions() { + int lines = 0; + int lineHeight = 0; + for (int i = 0; i < series.length; i++) { + ChartValueSeries chartValueSeries = series[i]; + if (chartValueSeries.isEnabled() && chartValueSeries.hasData() || allowIfEmpty(i)) { + lines++; + String title = getContext().getString(chartValueSeries.getTitleId(metricUnits)); + Rect rect = getRect(chartValueSeries.getTitlePaint(), title); + if (rect.height() > lineHeight) { + lineHeight = rect.height(); + } + } + } + return new int[] { lines, lineHeight }; + } + + /** + * Draws the x axis. + * + * @param canvas the canvas + */ + private void drawXAxis(Canvas canvas) { + int x = getScrollX() + leftBorder; + int y = topBorder + effectiveHeight; + canvas.drawLine(x, y, x + effectiveWidth, y, axisPaint); + String label = getXAxisLabel(); + Rect rect = getRect(axisPaint, label); + canvas.drawText(label, x + effectiveWidth + spacer, y + ((int) rect.height() / 2), axisPaint); + + double interval = getXAxisInterval(); + ArrayList markerPositions = getXAxisMarkerPositions(interval); + NumberFormat numberFormat = interval < 1 ? X_FRACTION_FORMAT : X_NUMBER_FORMAT; + for (int i = 0; i < markerPositions.size(); i++) { + drawXAxisMarker(canvas, markerPositions.get(i), numberFormat); + } + } + + /** + * Gets the x axis label. + */ + private String getXAxisLabel() { + Context context = getContext(); + if (chartByDistance) { + return metricUnits ? context.getString(R.string.unit_kilometer) + : context.getString(R.string.unit_mile); + } else { + return context.getString(R.string.description_time); + } + } + + /** + * Draws a x axis marker. + * + * @param canvas + * @param value value + * @param numberFormat the number format + */ + private void drawXAxisMarker(Canvas canvas, double value, NumberFormat numberFormat) { + String marker = chartByDistance ? numberFormat.format(value) + : StringUtils.formatElapsedTime((long) value); + Rect rect = getRect(xAxisMarkerPaint, marker); + canvas.drawText(marker, getX(value), topBorder + effectiveHeight + spacer + rect.height(), + xAxisMarkerPaint); + } + + /** + * Gets the x axis interval. + */ + private double getXAxisInterval() { + double interval = maxX / zoomLevel / TARGET_X_AXIS_INTERVALS; + if (interval < 1) { + interval = .5; + } else if (interval < 5) { + interval = 2; + } else if (interval < 10) { + interval = 5; + } else { + interval = (interval / 10) * 10; + } + return interval; + } + + /** + * Gets the x axis marker positions. + */ + private ArrayList getXAxisMarkerPositions(double interval) { + ArrayList markers = new ArrayList(); + markers.add(0d); + for (int i = 1; i * interval < maxX; i++) { + markers.add(i * interval); + } + // At least 2 markers + if (markers.size() < 2) { + markers.add(maxX); + } + return markers; + } + + /** + * Draws the y axis. + * + * @param canvas the canvas + */ + private void drawYAxis(Canvas canvas) { + int x = getScrollX() + leftBorder; + int y = topBorder; + canvas.drawLine(x, y, x, y + effectiveHeight, axisPaint); + + int markerXPosition = x - spacer; + for (int i = 0; i < series.length; i++) { + int index = series.length - 1 - i; + ChartValueSeries chartValueSeries = series[index]; + if (chartValueSeries.isEnabled() && chartValueSeries.hasData() || allowIfEmpty(index)) { + markerXPosition -= drawYAxisMarkers(chartValueSeries, canvas, markerXPosition) + spacer; + } + } + } + + /** + * Draws the y axis markers for a chart value series. + * + * @param chartValueSeries the chart value series + * @param canvas the canvas + * @param xPosition the right most x position + * @return the maximum marker width. + */ + private float drawYAxisMarkers(ChartValueSeries chartValueSeries, Canvas canvas, int xPosition) { + int interval = chartValueSeries.getInterval(); + float maxMarkerWidth = 0; + for (int i = 0; i <= Y_AXIS_INTERVALS; i++) { + maxMarkerWidth = Math.max(maxMarkerWidth, drawYAxisMarker(chartValueSeries, canvas, xPosition, + i * interval + chartValueSeries.getMinMarkerValue())); + } + return maxMarkerWidth; + } + + /** + * Draws a y axis marker. + * + * @param chartValueSeries the chart value series + * @param canvas the canvas + * @param xPosition the right most x position + * @param yValue the y value + * @return the marker width. + */ + private float drawYAxisMarker( + ChartValueSeries chartValueSeries, Canvas canvas, int xPosition, int yValue) { + String marker = chartValueSeries.formatMarker(yValue); + Paint paint = chartValueSeries.getMarkerPaint(); + Rect rect = getRect(paint, marker); + int yPosition = getY(chartValueSeries, yValue) + (int) (rect.height() / 2); + canvas.drawText(marker, xPosition, yPosition, paint); + return paint.measureText(marker); + } + + /** + * Draws the current pointer. + * + * @param canvas the canvas + */ + private void drawPointer(Canvas canvas) { + int index = -1; + for (int i = 0; i < series.length; i++) { + ChartValueSeries chartValueSeries = series[i]; + if (chartValueSeries.isEnabled() && chartValueSeries.hasData()) { + index = i; + break; + } + } + if (index != -1) { + int dx = getX(maxX) - pointer.getIntrinsicWidth() / 2; + int dy = getY(series[index], chartData.get(chartData.size() - 1)[index + 1]) + - pointer.getIntrinsicHeight(); + canvas.translate(dx, dy); + pointer.draw(canvas); + } + } + + /** + * Updates paths. The path needs to be updated any time after the data or the + * dimensions change. + */ + private void updatePaths() { + synchronized (chartData) { + for (ChartValueSeries chartValueSeries : series) { + chartValueSeries.getPath().reset(); + } + drawPaths(); + closePaths(); + } + } + + /** + * Draws all paths. + */ private void drawPaths() { - // All of the data points to the respective series. - // TODO: Come up with a better sampling than Math.max(1, (maxZoomLevel - zoomLevel + 1) / 2); - int sampling = 1; - for (int i = 0; i < data.size(); i += sampling) { - double[] d = data.get(i); - int min = Math.min(series.length, d.length - 1); - for (int j = 0; j < min; ++j) { - ChartValueSeries cvs = series[j]; - Path path = cvs.getPath(); - int x = getX(d[0]); - int y = getY(cvs, d[j + 1]); - if (i == 0) { + boolean[] hasMoved = new boolean[series.length]; + + for (int i = 0; i < chartData.size(); i++) { + double[] dataPoint = chartData.get(i); + for (int j = 0; j < series.length; j++) { + double value = dataPoint[j + 1]; + if (Double.isNaN(value)) { + continue; + } + ChartValueSeries chartValueSeries = series[j]; + Path path = chartValueSeries.getPath(); + int x = getX(dataPoint[0]); + int y = getY(chartValueSeries, value); + if (!hasMoved[j]) { + hasMoved[j] = true; path.moveTo(x, y); } else { path.lineTo(x, y); @@ -666,37 +838,37 @@ public class ChartView extends View { } } - /** Closes the drawn path so it looks like a solid graph. */ + /** + * Closes all paths. + */ private void closePaths() { - // Close the path. - int yCorner = topBorder + effectiveHeight; - int xCorner = getX(data.get(0)[0]); - int min = series.length; - for (int j = 0; j < min; j++) { - ChartValueSeries cvs = series[j]; - Path path = cvs.getPath(); - int first = getFirstPointPopulatedIndex(j + 1); + for (int i = 0; i < series.length; i++) { + int first = getFirstPopulatedChartDataIndex(i); + if (first != -1) { + int xCorner = getX(chartData.get(first)[0]); + int yCorner = topBorder + effectiveHeight; + ChartValueSeries chartValueSeries = series[i]; + Path path = chartValueSeries.getPath(); // Bottom right corner - path.lineTo(getX(data.get(data.size() - 1)[0]), yCorner); + path.lineTo(getX(chartData.get(chartData.size() - 1)[0]), yCorner); // Bottom left corner path.lineTo(xCorner, yCorner); // Top right corner - path.lineTo(xCorner, getY(cvs, data.get(first)[j + 1])); + path.lineTo(xCorner, getY(chartValueSeries, chartData.get(first)[i + 1])); } } } /** - * Finds the index of the first point which has a series populated. - * - * @param seriesIndex The index of the value series to search for - * @return The index in the first data for the point in the series that has series - * index value populated or -1 if none is found + * Finds the index of the first data point containing data for a series. + * Returns -1 if no data point contains data for the series. + * + * @param seriesIndex the series's index */ - private int getFirstPointPopulatedIndex(int seriesIndex) { - for (int i = 0; i < data.size(); i++) { - if (data.get(i).length > seriesIndex) { + private int getFirstPopulatedChartDataIndex(int seriesIndex) { + for (int i = 0; i < chartData.size(); i++) { + if (!Double.isNaN(chartData.get(i)[seriesIndex + 1])) { return i; } } @@ -707,180 +879,124 @@ public class ChartView extends View { * Updates the chart dimensions. */ private void updateDimensions() { - maxX = xMonitor.getMax(); - if (data.size() <= 1) { - maxX = 1; - } - for (ChartValueSeries cvs : series) { - cvs.updateDimension(); - } - // TODO: This is totally broken. Make sure that we calculate based on measureText for each - // grid line, as the labels may vary across intervals. - int maxLength = 0; - for (ChartValueSeries cvs : series) { - if (cvs.isEnabled() && cvs.hasData()) { - maxLength += cvs.getMaxLabelLength(); - } + maxX = xExtremityMonitor.hasData() ? xExtremityMonitor.getMax() : 1.0; + for (ChartValueSeries chartValueSeries : series) { + chartValueSeries.updateDimension(); } float density = getContext().getResources().getDisplayMetrics().density; - maxLength = Math.max(maxLength, 1); - leftBorder = (int) (density * (4 + 8 * maxLength)); - bottomBorder = (int) (density * BOTTOM_BORDER); - topBorder = (int) (density * TOP_BORDER); + spacer = (int) (density * SPACER); + yAxisOffset = (int) (density * Y_AXIS_OFFSET); + + int markerLength = 0; + for (int i = 0; i < series.length; i ++) { + ChartValueSeries chartValueSeries = series[i]; + if (chartValueSeries.isEnabled() && chartValueSeries.hasData() || allowIfEmpty(i)) { + Rect rect = getRect(chartValueSeries.getMarkerPaint(), chartValueSeries.getLargestMarker()); + markerLength += rect.width() + spacer; + } + } + + leftBorder = (int) (density * BORDER + markerLength); + int[] titleDimensions = getTitleDimenions(); + topBorder = (int) (density * BORDER + titleDimensions[0] * (titleDimensions[1] + spacer)); + bottomBorder = (int) (density * BORDER + getRect(xAxisMarkerPaint, "1").height() + spacer); + rightBorder = (int) (density * BORDER + getRect(axisPaint, getXAxisLabel()).width() + spacer); updateEffectiveDimensions(); } - /** Updates the effective dimensions where the graph will be drawn. */ + /** + * Updates the effective dimensions. + */ private void updateEffectiveDimensions() { - effectiveWidth = Math.max(0, w - leftBorder - RIGHT_BORDER); - effectiveHeight = Math.max(0, h - topBorder - bottomBorder); + effectiveWidth = Math.max(0, width - leftBorder - rightBorder); + effectiveHeight = Math.max(0, height - topBorder - bottomBorder); } /** - * Updates the effective dimensions where the graph will be drawn, only if the - * dimensions have changed since the last call. - * + * Updates the effective dimensions if changed. + * * @param newWidth the new width * @param newHeight the new height */ private void updateEffectiveDimensionsIfChanged(int newWidth, int newHeight) { - if (w != newWidth || h != newHeight) { - // Dimensions have changed (for example due to orientation change). - w = newWidth; - h = newHeight; + if (width != newWidth || height != newHeight) { + width = newWidth; + height = newHeight; updateEffectiveDimensions(); - setUpPath(); - } - } - - private int getX(double distance) { - return leftBorder + (int) ((distance * effectiveWidth / maxX) * zoomLevel); - } - - private int getY(ChartValueSeries cvs, double y) { - int effectiveSpread = cvs.getInterval() * MAX_INTERVALS; - return topBorder + effectiveHeight - - (int) ((y - cvs.getMin()) * effectiveHeight / effectiveSpread); - } - - /** Draws the labels on the X axis into the given canvas. */ - private void drawXLabels(Canvas c) { - double interval = (int) (maxX / zoomLevel / 4); - boolean shortFormat = false; - if (interval < 1) { - interval = .5; - shortFormat = true; - } else if (interval < 5) { - interval = 2; - } else if (interval < 10) { - interval = 5; - } else { - interval = (interval / 10) * 10; - } - drawXLabel(c, 0, shortFormat); - int numLabels = 1; - for (int i = 1; i * interval < maxX; i++) { - drawXLabel(c, i * interval, shortFormat); - numLabels++; - } - if (numLabels < 2) { - drawXLabel(c, (int) maxX, shortFormat); - } - } - - /** Draws the labels on the Y axis into the given canvas. */ - private float drawYLabels(ChartValueSeries cvs, Canvas c, int x) { - int interval = cvs.getInterval(); - float maxTextWidth = 0; - for (int i = 0; i < MAX_INTERVALS; ++i) { - maxTextWidth = Math.max(maxTextWidth, drawYLabel(cvs, c, x, i * interval + cvs.getMin())); - } - return maxTextWidth; - } - - /** Draws a single label on the X axis. */ - private void drawXLabel(Canvas c, double x, boolean shortFormat) { - if (x < 0) { - return; - } - String s = chartByDistance - ? (shortFormat ? X_SHORT_FORMAT.format(x) : X_FORMAT.format(x)) - : StringUtils.formatElapsedTime((long) x); - c.drawText(s, - getX(x), - effectiveHeight + UNIT_BORDER + topBorder, - labelPaint); - } - - /** Draws a single label on the Y axis. */ - private float drawYLabel(ChartValueSeries cvs, Canvas c, int x, int y) { - int desiredY = (int) ((y - cvs.getMin()) * effectiveHeight / - (cvs.getInterval() * MAX_INTERVALS)); - desiredY = topBorder + effectiveHeight + FONT_HEIGHT / 2 - desiredY - 1; - Paint p = new Paint(cvs.getLabelPaint()); - p.setTextAlign(Align.RIGHT); - String text = cvs.getFormat().format(y); - c.drawText(text, x, desiredY, p); - return p.measureText(text); - } - - /** Draws the actual X axis line and its label. */ - private void drawXAxis(Canvas canvas) { - float rightEdge = getX(maxX); - final int y = effectiveHeight + topBorder; - canvas.drawLine(leftBorder, y, rightEdge, y, borderPaint); - Context c = getContext(); - String s = chartByDistance - ? (metricUnits ? c.getString(R.string.unit_kilometer) : c.getString(R.string.unit_mile)) - : c.getString(R.string.unit_minute); - canvas.drawText(s, rightEdge, effectiveHeight + .2f * UNIT_BORDER + topBorder, labelPaint); - } - - /** Draws the actual Y axis line and its label. */ - private void drawYAxis(Canvas canvas) { - canvas.drawRect(0, 0, - leftBorder - 1, effectiveHeight + topBorder + UNIT_BORDER + 1, - clearPaint); - canvas.drawLine(leftBorder, UNIT_BORDER + topBorder, - leftBorder, effectiveHeight + topBorder, - borderPaint); - for (int i = 1; i < MAX_INTERVALS; ++i) { - int y = i * effectiveHeight / MAX_INTERVALS + topBorder; - canvas.drawLine(leftBorder - 5, y, leftBorder, y, gridPaint); - } - - Context c = getContext(); - // TODO: This should really show units for all series. - String s = metricUnits ? c.getString(R.string.unit_meter) : c.getString(R.string.unit_feet); - canvas.drawText(s, leftBorder - UNIT_BORDER * .2f, UNIT_BORDER * .8f + topBorder, labelPaint); - } - - /** Draws the grid for the graph. */ - private void drawGrid(Canvas c) { - float rightEdge = getX(maxX); - for (int i = 1; i < MAX_INTERVALS; ++i) { - int y = i * effectiveHeight / MAX_INTERVALS + topBorder; - c.drawLine(leftBorder, y, rightEdge, y, gridBarPaint); + updatePaths(); } } /** - * Sets whether a given time series will be enabled for drawing. - * - * @param index the time series, one of {@link #ELEVATION_SERIES}, - * {@link #SPEED_SERIES}, {@link #POWER_SERIES}, etc. + * Gets the x position for a value. + * + * @param value the value */ - public void setChartValueSeriesEnabled(int index, boolean enabled) { - series[index].setEnabled(enabled); + private int getX(double value) { + if (value > maxX) { + value = maxX; + } + double percentage = value / maxX; + return leftBorder + (int) (percentage * effectiveWidth * zoomLevel); } - - @VisibleForTesting - int getZoomLevel() { - return zoomLevel; + + /** + * Gets the y position for a value in a chart value series + * + * @param chartValueSeries the chart value series + * @param value the value + */ + private int getY(ChartValueSeries chartValueSeries, double value) { + int effectiveSpread = chartValueSeries.getInterval() * Y_AXIS_INTERVALS; + double percentage = (value - chartValueSeries.getMinMarkerValue()) / effectiveSpread; + int rangeHeight = effectiveHeight - 2 * yAxisOffset; + return topBorder + yAxisOffset + (int) ((1 - percentage) * rangeHeight); } - - @VisibleForTesting - int getMaxZoomLevel() { - return maxZoomLevel; + + /** + * Gets a waypoint's x value. + * + * @param waypoint the waypoint + */ + private double getWaypointXValue(Waypoint waypoint) { + if (chartByDistance) { + double lenghtInKm = waypoint.getLength() * UnitConversions.M_TO_KM; + return metricUnits ? lenghtInKm : lenghtInKm * UnitConversions.KM_TO_MI; + } else { + return waypoint.getDuration(); + } + } + + /** + * Gets a paint's Rect for a string. + * + * @param paint the paint + * @param string the string + */ + private Rect getRect(Paint paint, String string) { + Rect rect = new Rect(); + paint.getTextBounds(string, 0, string.length(), rect); + return rect; + } + + /** + * Returns true if the index is allowed when the chartData is empty. + * + * @param index the index + */ + private boolean allowIfEmpty(int index) { + if (!chartData.isEmpty()) { + return false; + } + switch (index) { + case ELEVATION_SERIES: + return true; + case SPEED_SERIES: + return reportSpeed; + case PACE_SERIES: + return !reportSpeed; + default: + return false; + } } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/ImportActivity.java b/MyTracks/src/com/google/android/apps/mytracks/ImportActivity.java index 228d07ffb..aa49e76f0 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/ImportActivity.java +++ b/MyTracks/src/com/google/android/apps/mytracks/ImportActivity.java @@ -172,8 +172,7 @@ public class ImportActivity extends Activity { if (success) { showDialog(DIALOG_RESULT_ID); } else { - Toast.makeText(this, getString(R.string.sd_card_import_error), Toast.LENGTH_LONG) - .show(); + Toast.makeText(this, R.string.sd_card_import_error, Toast.LENGTH_LONG).show(); finish(); } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/MarkerDetailActivity.java b/MyTracks/src/com/google/android/apps/mytracks/MarkerDetailActivity.java index 79b3a8f28..f293a494a 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/MarkerDetailActivity.java +++ b/MyTracks/src/com/google/android/apps/mytracks/MarkerDetailActivity.java @@ -101,6 +101,7 @@ public class MarkerDetailActivity extends AbstractMyTracksActivity { Intent intent = IntentUtils.newIntent(this, MarkerListActivity.class) .putExtra(MarkerListActivity.EXTRA_TRACK_ID, waypoint.getTrackId()); startActivity(intent); + finish(); } @Override diff --git a/MyTracks/src/com/google/android/apps/mytracks/SensorStateActivity.java b/MyTracks/src/com/google/android/apps/mytracks/SensorStateActivity.java index 339f81180..adf2e03f0 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/SensorStateActivity.java +++ b/MyTracks/src/com/google/android/apps/mytracks/SensorStateActivity.java @@ -126,7 +126,7 @@ public class SensorStateActivity extends AbstractMyTracksActivity { */ private void stopTempSensorManager() { if (tempSensorManager != null) { - SensorManagerFactory.getInstance().releaseSensorManager(tempSensorManager); + SensorManagerFactory.releaseTempSensorManager(); tempSensorManager = null; } } @@ -139,7 +139,7 @@ public class SensorStateActivity extends AbstractMyTracksActivity { Sensor.SensorDataSet sensorDataSet = null; if (tempSensorManager == null) { - tempSensorManager = SensorManagerFactory.getInstance().getSensorManager(this); + tempSensorManager = SensorManagerFactory.getTempSensorManager(this); } if (tempSensorManager != null) { @@ -199,19 +199,19 @@ public class SensorStateActivity extends AbstractMyTracksActivity { String lastSensorTime = sensorDataSet == null ? getString(R.string.value_unknown) : getLastSensorTime(sensorDataSet); - String power = sensorDataSet == null ? getString(R.string.value_unknown) - : getPower(sensorDataSet); - String cadence = sensorDataSet == null ? getString(R.string.value_unknown) - : getCadence(sensorDataSet); String heartRate = sensorDataSet == null ? getString(R.string.value_unknown) : getHeartRate(sensorDataSet); + String cadence = sensorDataSet == null ? getString(R.string.value_unknown) + : getCadence(sensorDataSet); + String power = sensorDataSet == null ? getString(R.string.value_unknown) + : getPower(sensorDataSet); String battery = sensorDataSet == null ? getString(R.string.value_unknown) : getBattery(sensorDataSet); ((TextView) findViewById(R.id.sensor_state_last_sensor_time)).setText(lastSensorTime); - ((TextView) findViewById(R.id.sensor_state_power)).setText(power); - ((TextView) findViewById(R.id.sensor_state_cadence)).setText(cadence); ((TextView) findViewById(R.id.sensor_state_heart_rate)).setText(heartRate); + ((TextView) findViewById(R.id.sensor_state_cadence)).setText(cadence); + ((TextView) findViewById(R.id.sensor_state_power)).setText(power); ((TextView) findViewById(R.id.sensor_state_battery)).setText(battery); } @@ -225,19 +225,20 @@ public class SensorStateActivity extends AbstractMyTracksActivity { } /** - * Gets the power. + * Gets the heart rate. * * @param sensorDataSet sensor data set */ - private String getPower(Sensor.SensorDataSet sensorDataSet) { + private String getHeartRate(Sensor.SensorDataSet sensorDataSet) { String value; - if (sensorDataSet.hasPower() && sensorDataSet.getPower().hasValue() - && sensorDataSet.getPower().getState() == Sensor.SensorState.SENDING) { - value = getString(R.string.sensor_state_power_value, sensorDataSet.getPower().getValue()); + if (sensorDataSet.hasHeartRate() && sensorDataSet.getHeartRate().hasValue() + && sensorDataSet.getHeartRate().getState() == Sensor.SensorState.SENDING) { + value = getString( + R.string.sensor_state_heart_rate_value, sensorDataSet.getHeartRate().getValue()); } else { value = SensorUtils.getStateAsString( - sensorDataSet.hasPower() ? sensorDataSet.getPower().getState() : Sensor.SensorState.NONE, - this); + sensorDataSet.hasHeartRate() ? sensorDataSet.getHeartRate().getState() + : Sensor.SensorState.NONE, this); } return value; } @@ -261,20 +262,19 @@ public class SensorStateActivity extends AbstractMyTracksActivity { } /** - * Gets the heart rate. + * Gets the power. * * @param sensorDataSet sensor data set */ - private String getHeartRate(Sensor.SensorDataSet sensorDataSet) { + private String getPower(Sensor.SensorDataSet sensorDataSet) { String value; - if (sensorDataSet.hasHeartRate() && sensorDataSet.getHeartRate().hasValue() - && sensorDataSet.getHeartRate().getState() == Sensor.SensorState.SENDING) { - value = getString( - R.string.sensor_state_heart_rate_value, sensorDataSet.getHeartRate().getValue()); + if (sensorDataSet.hasPower() && sensorDataSet.getPower().hasValue() + && sensorDataSet.getPower().getState() == Sensor.SensorState.SENDING) { + value = getString(R.string.sensor_state_power_value, sensorDataSet.getPower().getValue()); } else { value = SensorUtils.getStateAsString( - sensorDataSet.hasHeartRate() ? sensorDataSet.getHeartRate().getState() - : Sensor.SensorState.NONE, this); + sensorDataSet.hasPower() ? sensorDataSet.getPower().getState() : Sensor.SensorState.NONE, + this); } return value; } diff --git a/MyTracks/src/com/google/android/apps/mytracks/TrackDetailActivity.java b/MyTracks/src/com/google/android/apps/mytracks/TrackDetailActivity.java index 3cfb8963c..071dd8cca 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/TrackDetailActivity.java +++ b/MyTracks/src/com/google/android/apps/mytracks/TrackDetailActivity.java @@ -230,6 +230,7 @@ public class TrackDetailActivity extends AbstractMyTracksActivity implements Del protected void onHomeSelected() { Intent intent = IntentUtils.newIntent(this, TrackListActivity.class); startActivity(intent); + finish(); } @Override diff --git a/MyTracks/src/com/google/android/apps/mytracks/content/DataSourcesWrapperImpl.java b/MyTracks/src/com/google/android/apps/mytracks/content/DataSourcesWrapperImpl.java index 6e4e51de9..9bc57f08c 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/content/DataSourcesWrapperImpl.java +++ b/MyTracks/src/com/google/android/apps/mytracks/content/DataSourcesWrapperImpl.java @@ -154,7 +154,7 @@ class DataSourcesWrapperImpl implements DataSourcesWrapper { } // Let the user know we have only an approximate location: - Toast.makeText(context, context.getString(toastResId), Toast.LENGTH_LONG).show(); + Toast.makeText(context, toastResId, Toast.LENGTH_LONG).show(); } return loc; } diff --git a/MyTracks/src/com/google/android/apps/mytracks/fragments/ChartFragment.java b/MyTracks/src/com/google/android/apps/mytracks/fragments/ChartFragment.java index 9a7f87b8d..fb8d108dd 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/fragments/ChartFragment.java +++ b/MyTracks/src/com/google/android/apps/mytracks/fragments/ChartFragment.java @@ -77,15 +77,15 @@ public class ChartFragment extends Fragment implements TrackDataListener { private boolean reportSpeed = true; private boolean chartByDistance = true; - private boolean[] chartShow = new boolean[] {true, true, true, true, true}; + private boolean[] chartShow = new boolean[] {true, true, true, true, true, true}; // UI elements private ChartView chartView; private ZoomControls zoomControls; /** - * A runnable that will remove the spinner (if any), enable/disable zoom - * controls and orange pointer as appropriate and redraw. + * A runnable that will enable/disable zoom controls and orange pointer as + * appropriate and redraw. */ private final Runnable updateChart = new Runnable() { @Override @@ -215,7 +215,7 @@ public class ChartFragment extends Fragment implements TrackDataListener { @Override public void onNewTrackPoint(Location location) { if (LocationUtils.isValidLocation(location)) { - double[] data = new double[6]; + double[] data = new double[ChartView.NUM_SERIES + 1]; fillDataPoint(location, data); pendingPoints.add(data); } @@ -280,7 +280,11 @@ public class ChartFragment extends Fragment implements TrackDataListener { return false; } reportSpeed = speed; - chartView.setReportSpeed(speed, getActivity()); + chartView.setReportSpeed(reportSpeed); + boolean chartShowSpeed = PreferencesUtils.getBoolean( + getActivity(), R.string.chart_show_speed_key, PreferencesUtils.CHART_SHOW_SPEED_DEFAULT); + setSeriesEnabled(ChartView.SPEED_SERIES, chartShowSpeed && reportSpeed); + setSeriesEnabled(ChartView.PACE_SERIES, chartShowSpeed && !reportSpeed); getActivity().runOnUiThread(new Runnable() { @Override public void run() { @@ -306,8 +310,13 @@ public class ChartFragment extends Fragment implements TrackDataListener { R.string.chart_show_elevation_key, PreferencesUtils.CHART_SHOW_ELEVATION_DEFAULT))) { needUpdate = true; } - if (setSeriesEnabled(ChartView.SPEED_SERIES, PreferencesUtils.getBoolean( - getActivity(), R.string.chart_show_speed_key, PreferencesUtils.CHART_SHOW_SPEED_DEFAULT))) { + + boolean chartShowSpeed = PreferencesUtils.getBoolean( + getActivity(), R.string.chart_show_speed_key, PreferencesUtils.CHART_SHOW_SPEED_DEFAULT); + if (setSeriesEnabled(ChartView.SPEED_SERIES, chartShowSpeed && reportSpeed)) { + needUpdate = true; + } + if (setSeriesEnabled(ChartView.PACE_SERIES, chartShowSpeed && !reportSpeed)) { needUpdate = true; } if (setSeriesEnabled(ChartView.POWER_SERIES, PreferencesUtils.getBoolean( @@ -405,13 +414,14 @@ public class ChartFragment extends Fragment implements TrackDataListener { } /** - * Given a location, fill in a data point, an array of double[6].
+ * Given a location, fill in a data point, an array of double[].
* data[0] = time/distance
* data[1] = elevation
* data[2] = speed
- * data[3] = power
- * data[4] = cadence
- * data[5] = heart rate
+ * data[3] = pace
+ * data[4] = heart rate
+ * data[5] = cadence
+ * data[6] = power
* * @param location the location * @param data the data point to fill in, can be null @@ -421,9 +431,10 @@ public class ChartFragment extends Fragment implements TrackDataListener { double timeOrDistance = Double.NaN; double elevation = Double.NaN; double speed = Double.NaN; - double power = Double.NaN; + double pace = Double.NaN; + double heartRate = Double.NaN; double cadence = Double.NaN; - double heartRate = Double.NaN; + double power = Double.NaN; // TODO: Use TripStatisticsBuilder if (chartByDistance) { @@ -460,36 +471,36 @@ public class ChartFragment extends Fragment implements TrackDataListener { if (!metricUnits) { speed *= UnitConversions.KM_TO_MI; } - if (!reportSpeed) { - speed = speed == 0 ? 0.0 : 60.0 / speed; - } - + pace = speed == 0 ? 0.0 : 60.0 / speed; + if (location instanceof MyTracksLocation && ((MyTracksLocation) location).getSensorDataSet() != null) { SensorDataSet sensorDataSet = ((MyTracksLocation) location).getSensorDataSet(); - if (sensorDataSet.hasPower() && sensorDataSet.getPower().getState() == Sensor.SensorState.SENDING - && sensorDataSet.getPower().hasValue()) { - power = sensorDataSet.getPower().getValue(); + if (sensorDataSet.hasHeartRate() + && sensorDataSet.getHeartRate().getState() == Sensor.SensorState.SENDING + && sensorDataSet.getHeartRate().hasValue()) { + heartRate = sensorDataSet.getHeartRate().getValue(); } if (sensorDataSet.hasCadence() && sensorDataSet.getCadence().getState() == Sensor.SensorState.SENDING && sensorDataSet.getCadence().hasValue()) { cadence = sensorDataSet.getCadence().getValue(); } - if (sensorDataSet.hasHeartRate() - && sensorDataSet.getHeartRate().getState() == Sensor.SensorState.SENDING - && sensorDataSet.getHeartRate().hasValue()) { - heartRate = sensorDataSet.getHeartRate().getValue(); + if (sensorDataSet.hasPower() + && sensorDataSet.getPower().getState() == Sensor.SensorState.SENDING + && sensorDataSet.getPower().hasValue()) { + power = sensorDataSet.getPower().getValue(); } } - + if (data != null) { data[0] = timeOrDistance; data[1] = elevation; data[2] = speed; - data[3] = power; - data[4] = cadence; - data[5] = heartRate; + data[3] = pace; + data[4] = heartRate; + data[5] = cadence; + data[6] = power; } lastLocation = location; } diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/file/CsvTrackWriter.java b/MyTracks/src/com/google/android/apps/mytracks/io/file/CsvTrackWriter.java index d05e3da05..98c0d769a 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/file/CsvTrackWriter.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/file/CsvTrackWriter.java @@ -110,7 +110,7 @@ public class CsvTrackWriter implements TrackFormatWriter { context.getString(R.string.description_location_bearing), context.getString(R.string.description_location_accuracy), context.getString(R.string.description_location_speed), - context.getString(R.string.description_location_time)); + context.getString(R.string.description_time)); } @Override @@ -143,7 +143,7 @@ public class CsvTrackWriter implements TrackFormatWriter { context.getString(R.string.description_location_bearing), context.getString(R.string.description_location_accuracy), context.getString(R.string.description_location_speed), - context.getString(R.string.description_location_time), + context.getString(R.string.description_time), context.getString(R.string.description_sensor_power), context.getString(R.string.description_sensor_cadence), context.getString(R.string.description_sensor_heart_rate), diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/fusiontables/SendFusionTablesAsyncTask.java b/MyTracks/src/com/google/android/apps/mytracks/io/fusiontables/SendFusionTablesAsyncTask.java index 7795fe3ee..f921a2452 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/fusiontables/SendFusionTablesAsyncTask.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/fusiontables/SendFusionTablesAsyncTask.java @@ -70,8 +70,9 @@ public class SendFusionTablesAsyncTask extends AbstractSendAsyncTask { // See http://support.google.com/fusiontables/bin/answer.py?hl=en&answer=185991 private static final String MARKER_TYPE_START = "large_green"; private static final String MARKER_TYPE_END = "large_red"; - private static final String MARKER_TYPE_WAYPOINT = "large_yellow"; - + private static final String MARKER_TYPE_WAYPOINT = "large_blue"; + private static final String MARKER_TYPE_STATISTICS = "large_yellow"; + private static final String TAG = SendFusionTablesAsyncTask.class.getSimpleName(); private final Context context; @@ -336,8 +337,9 @@ public class SendFusionTablesAsyncTask extends AbstractSendAsyncTask { // track). while (cursor.moveToNext()) { Waypoint wpt = myTracksProviderUtils.createWaypoint(cursor); - if (!createNewPoint( - wpt.getName(), wpt.getDescription(), wpt.getLocation(), MARKER_TYPE_WAYPOINT)) { + String type = wpt.getType() == Waypoint.TYPE_STATISTICS ? MARKER_TYPE_STATISTICS + : MARKER_TYPE_WAYPOINT; + if (!createNewPoint(wpt.getName(), wpt.getDescription(), wpt.getLocation(), type)) { Log.d(TAG, "Upload waypoints failed"); return false; } diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/sendtogoogle/UploadResultActivity.java b/MyTracks/src/com/google/android/apps/mytracks/io/sendtogoogle/UploadResultActivity.java index 01369bb09..354a389f9 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/sendtogoogle/UploadResultActivity.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/sendtogoogle/UploadResultActivity.java @@ -44,7 +44,8 @@ import android.widget.TextView; public class UploadResultActivity extends FragmentActivity { private static final String TAG = UploadResultActivity.class.getSimpleName(); - private static final int DIALOG_RESULT_ID = 0; + @VisibleForTesting + static final int DIALOG_RESULT_ID = 0; private SendRequest sendRequest; private String shareUrl; diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/TrackRecordingService.java b/MyTracks/src/com/google/android/apps/mytracks/services/TrackRecordingService.java index f0dbffa2e..08051f268 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/services/TrackRecordingService.java +++ b/MyTracks/src/com/google/android/apps/mytracks/services/TrackRecordingService.java @@ -359,7 +359,7 @@ public class TrackRecordingService extends Service { unregisterLocationListener(); shutdownTaskExecutors(); if (sensorManager != null) { - SensorManagerFactory.getInstance().releaseSensorManager(sensorManager); + SensorManagerFactory.releaseSystemSensorManager(); sensorManager = null; } @@ -595,7 +595,7 @@ public class TrackRecordingService extends Service { length = 0; showNotification(); registerLocationListener(); - sensorManager = SensorManagerFactory.getInstance().getSensorManager(this); + sensorManager = SensorManagerFactory.getSystemSensorManager(this); // Reset the number of auto-resume retries. setAutoResumeTrackRetries(0); @@ -724,7 +724,7 @@ public class TrackRecordingService extends Service { boolean hasSensorData = sensorManager != null && sensorManager.isEnabled() && sensorManager.getSensorDataSet() != null - && sensorManager.isDataValid(); + && sensorManager.isSensorDataSetValid(); // If the user has been stationary for two recording just record the first // two and ignore the rest. This code will only have an effect if the @@ -823,7 +823,7 @@ public class TrackRecordingService extends Service { Location locationToInsert = location; if (sensorManager != null && sensorManager.isEnabled()) { SensorDataSet sd = sensorManager.getSensorDataSet(); - if (sd != null && sensorManager.isDataValid()) { + if (sd != null && sensorManager.isSensorDataSetValid()) { locationToInsert = new MyTracksLocation(location, sd); } } @@ -1042,7 +1042,7 @@ public class TrackRecordingService extends Service { PreferencesUtils.setLong(this, R.string.recording_track_id_key, recordingTrackId); if (sensorManager != null) { - SensorManagerFactory.getInstance().releaseSensorManager(sensorManager); + SensorManagerFactory.releaseSystemSensorManager(); sensorManager = null; } diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/sensors/BluetoothConnectionManager.java b/MyTracks/src/com/google/android/apps/mytracks/services/sensors/BluetoothConnectionManager.java index 558bfe423..e22679bac 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/services/sensors/BluetoothConnectionManager.java +++ b/MyTracks/src/com/google/android/apps/mytracks/services/sensors/BluetoothConnectionManager.java @@ -16,8 +16,8 @@ package com.google.android.apps.mytracks.services.sensors; -import com.google.android.apps.mytracks.Constants; import com.google.android.apps.mytracks.content.Sensor; +import com.google.android.apps.mytracks.content.Sensor.SensorState; import com.google.android.apps.mytracks.util.ApiAdapterFactory; import android.bluetooth.BluetoothAdapter; @@ -30,382 +30,264 @@ import android.util.Log; import java.io.IOException; import java.io.InputStream; -import java.io.OutputStream; import java.util.UUID; /** - * This class does all the work for setting up and managing Bluetooth - * connections with other devices. It has a thread that listens for incoming - * connections, a thread for connecting with a device, and a thread for - * performing data transmissions when connected. - * + * Manages bluetooth connection. It has a thread for connecting with a bluetooth + * device and a thread for performing data transmission when connected. + * * @author Sandor Dornbush */ public class BluetoothConnectionManager { - // Unique Bluetooth UUID for My Tracks - public static final UUID SPP_UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB"); + // My Tracks UUID + public static final UUID MY_TRACKS_UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB"); - private MessageParser parser; + // Message types sent to hander + public static final int MESSAGE_DEVICE_NAME = 1; + public static final int MESSAGE_READ = 2; - // Member fields - private final BluetoothAdapter adapter; + // Key for storing the device name + public static final String KEY_DEVICE_NAME = "device_name"; + + private static final String TAG = BluetoothConnectionManager.class.getSimpleName(); + + private final BluetoothAdapter bluetoothAdapter; private final Handler handler; + private final MessageParser messageParser; + private SensorState sensorState; + private ConnectThread connectThread; private ConnectedThread connectedThread; - private Sensor.SensorState state; - - // Message types sent from the BluetoothSenorService Handler - public static final int MESSAGE_STATE_CHANGE = 1; - public static final int MESSAGE_READ = 2; - public static final int MESSAGE_WRITE = 3; - public static final int MESSAGE_DEVICE_NAME = 4; - - // Key names received from the BluetoothSenorService Handler - public static final String DEVICE_NAME = "device_name"; - /** - * Constructor. Prepares a new BluetoothSensor session. - * - * @param handler A Handler to send messages back to the UI Activity - * @param parser A message parser + * Constructor. + * + * @param bluetoothAdapter the bluetooth adapter + * @param handler a hander for sending messages back to the UI activity + * @param messageParser a message parser */ - public BluetoothConnectionManager(Handler handler, MessageParser parser) { - this.adapter = BluetoothAdapter.getDefaultAdapter(); - this.state = Sensor.SensorState.NONE; + public BluetoothConnectionManager( + BluetoothAdapter bluetoothAdapter, Handler handler, MessageParser messageParser) { + this.bluetoothAdapter = bluetoothAdapter; this.handler = handler; - this.parser = parser; + this.messageParser = messageParser; + this.sensorState = SensorState.NONE; } /** - * Set the current state of the sensor connection - * - * @param state An integer defining the current connection state + * Gets the sensor state. */ - private synchronized void setState(Sensor.SensorState state) { - // TODO pretty print this. - Log.d(Constants.TAG, "setState(" + state + ")"); - this.state = state; - - // Give the new state to the Handler so the UI Activity can update - handler.obtainMessage(MESSAGE_STATE_CHANGE, state.getNumber(), -1).sendToTarget(); + public synchronized SensorState getSensorState() { + return sensorState; } /** - * Return the current connection state. + * Sets the sensor state. + * + * @param sensorState the sensor state */ - public synchronized Sensor.SensorState getState() { - return state; + private synchronized void setState(Sensor.SensorState sensorState) { + this.sensorState = sensorState; } /** - * Start the sensor service. Specifically start AcceptThread to begin a session - * in listening (server) mode. Called by the Activity onResume() + * Resets the bluetooth connection manager. */ - public synchronized void start() { - Log.d(Constants.TAG, "BluetoothConnectionManager.start()"); - - // Cancel any thread attempting to make a connection - if (connectThread != null) { - connectThread.cancel(); - connectThread = null; - } - - // Cancel any thread currently running a connection - if (connectedThread != null) { - connectedThread.cancel(); - connectedThread = null; - } - + public synchronized void reset() { + cancelThreads(); setState(Sensor.SensorState.NONE); } /** - * Start the ConnectThread to initiate a connection to a remote device. - * - * @param device The BluetoothDevice to connect + * Cancels all the threads. */ - public synchronized void connect(BluetoothDevice device) { - Log.d(Constants.TAG, "connect to: " + device); - - // Cancel any thread attempting to make a connection - if (state == Sensor.SensorState.CONNECTING) { - if (connectThread != null) { - connectThread.cancel(); - connectThread = null; - } + private void cancelThreads() { + if (connectThread != null) { + connectThread.cancel(); + connectThread = null; } - - // Cancel any thread currently running a connection if (connectedThread != null) { connectedThread.cancel(); connectedThread = null; } + } - // Start the thread to connect with the given device - connectThread = new ConnectThread(device); + /** + * Connects to a bluetooth device. + * + * @param bluetoothDevice the bluetooth device + */ + public synchronized void connect(BluetoothDevice bluetoothDevice) { + Log.d(TAG, "connect to: " + bluetoothDevice); + cancelThreads(); + + connectThread = new ConnectThread(bluetoothDevice); connectThread.start(); setState(Sensor.SensorState.CONNECTING); } /** - * Start the ConnectedThread to begin managing a Bluetooth connection - * - * @param socket The BluetoothSocket on which the connection was made - * @param device The BluetoothDevice that has been connected + * Starts the ConnectedThread to read data. + * + * @param bluetoothSocket the bluetooth socket + * @param bluetoothDevice the bluetooth device */ - public synchronized void connected(BluetoothSocket socket, - BluetoothDevice device) { - Log.d(Constants.TAG, "connected"); + private synchronized void connected( + BluetoothSocket bluetoothSocket, BluetoothDevice bluetoothDevice) { + cancelThreads(); - // Cancel the thread that completed the connection - if (connectThread != null) { - connectThread.cancel(); - connectThread = null; - } - - // Cancel any thread currently running a connection - if (connectedThread != null) { - connectedThread.cancel(); - connectedThread = null; - } - - // Start the thread to manage the connection and perform transmissions - connectedThread = new ConnectedThread(socket); + connectedThread = new ConnectedThread(bluetoothSocket); connectedThread.start(); - // Send the name of the connected device back to the UI Activity - Message msg = handler.obtainMessage(MESSAGE_DEVICE_NAME); + // Send the device name to the handler + Message message = handler.obtainMessage(MESSAGE_DEVICE_NAME); Bundle bundle = new Bundle(); - bundle.putString(DEVICE_NAME, device.getName()); - msg.setData(bundle); - handler.sendMessage(msg); + bundle.putString(KEY_DEVICE_NAME, bluetoothDevice.getName()); + message.setData(bundle); + handler.sendMessage(message); setState(Sensor.SensorState.CONNECTED); } /** - * Stop all threads - */ - public synchronized void stop() { - Log.d(Constants.TAG, "stop()"); - if (connectThread != null) { - connectThread.cancel(); - connectThread = null; - } - if (connectedThread != null) { - connectedThread.cancel(); - connectedThread = null; - } - setState(Sensor.SensorState.NONE); - } - - /** - * Write to the ConnectedThread in an unsynchronized manner - * - * @param out The bytes to write - * @see ConnectedThread#write(byte[]) - */ - public void write(byte[] out) { - // Create temporary object - ConnectedThread r; - // Synchronize a copy of the ConnectedThread - synchronized (this) { - if (state != Sensor.SensorState.CONNECTED) { - return; - } - r = connectedThread; - } - // Perform the write unsynchronized - r.write(out); - } - - /** - * Indicate that the connection attempt failed and notify the UI Activity. - */ - private void connectionFailed() { - setState(Sensor.SensorState.DISCONNECTED); - Log.i(Constants.TAG, "Bluetooth connection failed."); - } - - /** - * Indicate that the connection was lost and notify the UI Activity. - */ - private void connectionLost() { - setState(Sensor.SensorState.DISCONNECTED); - Log.i(Constants.TAG, "Bluetooth connection lost."); - } - - /** - * This thread runs while attempting to make an outgoing connection with a - * device. It runs straight through; the connection either succeeds or fails. + * A thread to connect to a bluetooth device. */ private class ConnectThread extends Thread { - private final BluetoothSocket socket; - private final BluetoothDevice device; + private final BluetoothSocket bluetoothSocket; + private final BluetoothDevice bluetoothDevice; public ConnectThread(BluetoothDevice device) { setName("ConnectThread-" + device.getName()); - this.device = device; + this.bluetoothDevice = device; BluetoothSocket tmp = null; - - // Get a BluetoothSocket for a connection with the - // given BluetoothDevice try { tmp = ApiAdapterFactory.getApiAdapter().getBluetoothSocket(device); } catch (IOException e) { - Log.e(Constants.TAG, "create() failed", e); + Log.e(TAG, "Unable to get blueooth socket.", e); } - socket = tmp; + bluetoothSocket = tmp; } @Override public void run() { - Log.d(Constants.TAG, "BEGIN mConnectThread"); + if (bluetoothAdapter == null) { + BluetoothConnectionManager.this.reset(); + return; + } + // Cancel discovery to prevent slow down + bluetoothAdapter.cancelDiscovery(); - // Always cancel discovery because it will slow down a connection - adapter.cancelDiscovery(); - - // Make a connection to the BluetoothSocket try { - // This is a blocking call and will only return on a - // successful connection or an exception - socket.connect(); - } catch (IOException e) { - connectionFailed(); - // Close the socket + bluetoothSocket.connect(); + } catch (IOException connectException) { + Log.i(TAG, "Unable to connect.", connectException); + setState(Sensor.SensorState.DISCONNECTED); try { - socket.close(); - } catch (IOException e2) { - Log.e(Constants.TAG, - "unable to close() socket during connection failure", e2); + bluetoothSocket.close(); + } catch (IOException e) { + Log.e(TAG, "Unable to close blueooth socket.", e); } - // Start the service over to restart listening mode - BluetoothConnectionManager.this.start(); + // Reset the bluetooth connection manager + BluetoothConnectionManager.this.reset(); return; } - // Reset the ConnectThread because we're done + // Reset the ConnectThread since we are done synchronized (BluetoothConnectionManager.this) { connectThread = null; } // Start the connected thread - connected(socket, device); + connected(bluetoothSocket, bluetoothDevice); } + /** + * Cancels this thread. + */ public void cancel() { try { - socket.close(); + bluetoothSocket.close(); } catch (IOException e) { - Log.e(Constants.TAG, "close() of connect socket failed", e); + Log.e(TAG, "Unable to close bluetooth socket.", e); } } } /** - * This thread runs during a connection with a remote device. It handles all - * incoming and outgoing transmissions. + * This thread handles data transmission when connected. */ private class ConnectedThread extends Thread { - private final BluetoothSocket btSocket; - private final InputStream mmInStream; - private final OutputStream mmOutStream; + private final BluetoothSocket bluetoothSSocket; + private final InputStream inputStream; - public ConnectedThread(BluetoothSocket socket) { - Log.d(Constants.TAG, "create ConnectedThread"); - btSocket = socket; - InputStream tmpIn = null; - OutputStream tmpOut = null; + public ConnectedThread(BluetoothSocket bluetoothSocket) { + this.bluetoothSSocket = bluetoothSocket; + InputStream tmp = null; - // Get the BluetoothSocket input and output streams try { - tmpIn = socket.getInputStream(); - tmpOut = socket.getOutputStream(); + tmp = bluetoothSocket.getInputStream(); } catch (IOException e) { - Log.e(Constants.TAG, "temp sockets not created", e); + Log.e(TAG, "Unable to get input stream.", e); } - - mmInStream = tmpIn; - mmOutStream = tmpOut; + inputStream = tmp; } @Override public void run() { - Log.i(Constants.TAG, "BEGIN mConnectedThread"); - byte[] buffer = new byte[parser.getFrameSize()]; - int bytes; + byte[] buffer = new byte[messageParser.getFrameSize()]; + int bytes; // bytes read int offset = 0; - // Keep listening to the InputStream while connected + // Keep listening to the inputStream while connected while (true) { try { - // Read from the InputStream - bytes = mmInStream.read(buffer, offset, parser.getFrameSize() - offset); + // Read from the inputStream + bytes = inputStream.read(buffer, offset, messageParser.getFrameSize() - offset); - if (bytes < 0) { - throw new IOException("EOF reached"); - } + if (bytes == -1) { throw new IOException("EOF reached."); } offset += bytes; - if (offset != parser.getFrameSize()) { - // partial frame received, call read() again to receive the rest + if (offset != messageParser.getFrameSize()) { + // Partial frame received. Call read again to receive the rest. continue; } - // check if its a valid frame - if (!parser.isValid(buffer)) { - int index = parser.findNextAlignment(buffer); - if (index > 0) { - // re-align - offset = parser.getFrameSize() - index; - System.arraycopy(buffer, index, buffer, 0, offset); - Log.w(Constants.TAG, "Misaligned data, found new message at " + - index + " recovering..."); + if (!messageParser.isValid(buffer)) { + int index = messageParser.findNextAlignment(buffer); + if (index == -1) { + Log.w(TAG, "Could not find any valid data. Drop data."); + offset = 0; continue; } - Log.w(Constants.TAG, "Could not find valid data, dropping data"); - offset = 0; + Log.w(TAG, "Misaligned data. Found new message at " + index + ". Recovering..."); + offset = messageParser.getFrameSize() - index; + System.arraycopy(buffer, index, buffer, 0, offset); continue; } offset = 0; - // Send copy of the obtained bytes to the UI Activity. - // Avoids memory inconsistency issues. - handler.obtainMessage(MESSAGE_READ, bytes, -1, buffer.clone()) - .sendToTarget(); + // Send a copy of the obtained bytes to the handler to avoid memory + // inconsistency issues + handler.obtainMessage(MESSAGE_READ, bytes, -1, buffer.clone()).sendToTarget(); } catch (IOException e) { - Log.e(Constants.TAG, "disconnected", e); - connectionLost(); + Log.i(TAG, "Bluetooth connection lost.", e); + setState(Sensor.SensorState.DISCONNECTED); break; } } } /** - * Write to the connected OutStream. - * - * @param buffer The bytes to write + * Cancels this thread. */ - public void write(byte[] buffer) { - try { - mmOutStream.write(buffer); - - // Share the sent message back to the UI Activity - handler.obtainMessage(MESSAGE_WRITE, -1, -1, buffer).sendToTarget(); - } catch (IOException e) { - Log.e(Constants.TAG, "Exception during write", e); - } - } - public void cancel() { try { - btSocket.close(); + bluetoothSSocket.close(); } catch (IOException e) { - Log.e(Constants.TAG, "close() of connect socket failed", e); + Log.e(TAG, "Unable to close bluetooth socket.", e); } } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/sensors/BluetoothSensorManager.java b/MyTracks/src/com/google/android/apps/mytracks/services/sensors/BluetoothSensorManager.java index 759e4daa4..a8b8ce0af 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/services/sensors/BluetoothSensorManager.java +++ b/MyTracks/src/com/google/android/apps/mytracks/services/sensors/BluetoothSensorManager.java @@ -13,12 +13,11 @@ * License for the specific language governing permissions and limitations under * the License. */ + package com.google.android.apps.mytracks.services.sensors; -import static com.google.android.apps.mytracks.Constants.TAG; - -import com.google.android.apps.mytracks.Constants; -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.SensorState; import com.google.android.apps.mytracks.util.PreferencesUtils; import com.google.android.maps.mytracks.R; @@ -34,66 +33,31 @@ import android.widget.Toast; import java.util.ArrayList; /** - * Manage the connection to a bluetooth sensor. - * + * Bluetooth sensor manager. + * * @author Sandor Dornbush */ public class BluetoothSensorManager extends SensorManager { - // Local Bluetooth adapter private static final BluetoothAdapter bluetoothAdapter = getDefaultBluetoothAdapter(); - - // Member object for the sensor threads and connections. - private BluetoothConnectionManager connectionManager = null; - - // Name of the connected device - private String connectedDeviceName = null; - - private Context context = null; - - private Sensor.SensorDataSet sensorDataSet = null; - - private MessageParser parser; - - public BluetoothSensorManager( - Context context, MessageParser parser) { - this.context = context; - this.parser = parser; - // If BT is not available or not enabled quit. - if (!isEnabled()) { - return; - } - - setupSensor(); - } - - private void setupSensor() { - Log.d(Constants.TAG, "setupSensor()"); - - // Initialize the BluetoothSensorAdapter to perform bluetooth connections. - connectionManager = new BluetoothConnectionManager(messageHandler, parser); - } + private static final String TAG = BluetoothConnectionManager.class.getSimpleName(); /** - * Code for assigning the local bluetooth adapter - * - * @return The default bluetooth adapter, if one is available, NULL if it isn't. + * Gets the default bluetooth adapter. */ private static BluetoothAdapter getDefaultBluetoothAdapter() { - // Check if the calling thread is the main application thread, - // if it is, do it directly. + // If from the main application thread, return directly if (Thread.currentThread().equals(Looper.getMainLooper().getThread())) { return BluetoothAdapter.getDefaultAdapter(); } - - // If the calling thread, isn't the main application thread, - // then get the main application thread to return the default adapter. + + // Get the default adapter from the main application thread final ArrayList adapters = new ArrayList(1); final Object mutex = new Object(); - + Handler handler = new Handler(Looper.getMainLooper()); handler.post(new Runnable() { - @Override + @Override public void run() { adapters.add(BluetoothAdapter.getDefaultAdapter()); synchronized (mutex) { @@ -101,7 +65,7 @@ public class BluetoothSensorManager extends SensorManager { } } }); - + while (adapters.isEmpty()) { synchronized (mutex) { try { @@ -111,106 +75,104 @@ public class BluetoothSensorManager extends SensorManager { } } } - + if (adapters.get(0) == null) { - Log.w(TAG, "No bluetooth adapter found!"); + Log.w(TAG, "No bluetooth adapter found."); + return null; } return adapters.get(0); } - + + private final Context context; + private final MessageParser messageParser; + private final BluetoothConnectionManager bluetoothConnectionManager; + private SensorDataSet sensorDataSet = null; + + // Handler that gets information back from the bluetoothConnectionManager + private final Handler messageHandler = new Handler(Looper.getMainLooper()) { + @Override + public void handleMessage(Message message) { + switch (message.what) { + case BluetoothConnectionManager.MESSAGE_DEVICE_NAME: + String deviceName = message.getData() + .getString(BluetoothConnectionManager.KEY_DEVICE_NAME); + Toast.makeText(context, + context.getString(R.string.settings_sensor_bluetooth_connected, deviceName), + Toast.LENGTH_SHORT).show(); + break; + case BluetoothConnectionManager.MESSAGE_READ: + try { + byte[] readBuf = (byte[]) message.obj; + sensorDataSet = messageParser.parseBuffer(readBuf); + Log.d(TAG, "MESSAGE_READ: " + sensorDataSet); + } catch (IllegalArgumentException e) { + sensorDataSet = null; + Log.i(TAG, "Unexpected exception on read", e); + } catch (RuntimeException e) { + sensorDataSet = null; + Log.i(TAG, "Unexpected exception on read.", e); + } + break; + default: + break; + } + } + }; + + /** + * Constructor. + * + * @param context the context + * @param messageParser the message parser + */ + public BluetoothSensorManager(Context context, MessageParser messageParser) { + this.context = context; + this.messageParser = messageParser; + bluetoothConnectionManager = new BluetoothConnectionManager( + bluetoothAdapter, messageHandler, messageParser); + } + + @Override public boolean isEnabled() { return bluetoothAdapter != null && bluetoothAdapter.isEnabled(); } - public void setupChannel() { - if (!isEnabled() || connectionManager == null) { - Log.w(Constants.TAG, "Disabled manager onStartTrack"); + @Override + protected void setUpChannel() { + if (!isEnabled()) { + Log.w(TAG, "Bluetooth not enabled."); return; } String address = PreferencesUtils.getString( context, R.string.bluetooth_sensor_key, PreferencesUtils.BLUETOOTH_SENSOR_DEFAULT); if (PreferencesUtils.BLUETOOTH_SENSOR_DEFAULT.equals(address)) { + Log.w(TAG, "No blueooth address."); return; } - Log.w(Constants.TAG, "Connecting to bluetooth sensor: " + address); - // Get the BluetoothDevice object + Log.w(TAG, "Connecting to bluetooth address: " + address); + BluetoothDevice device; try { device = bluetoothAdapter.getRemoteDevice(address); } catch (IllegalArgumentException e) { - Log.d(Constants.TAG, "Invalid address " + address, e); + Log.d(TAG, "Unable to get remote device for: " + address, e); return; } - // Attempt to connect to the device - connectionManager.connect(device); - - // Performing this check in onResume() covers the case in which BT was - // not enabled during onStart(), so we were paused to enable it... - // onResume() will be called when ACTION_REQUEST_ENABLE activity returns. - if (connectionManager != null) { - // Only if the state is STATE_NONE, do we know that we haven't started - // already - if (connectionManager.getState() == Sensor.SensorState.NONE) { - // Start the Bluetooth sensor services - Log.w(Constants.TAG, "Disabled manager onStartTrack"); - connectionManager.start(); - } - } + bluetoothConnectionManager.connect(device); } - public void onDestroy() { - // Stop the Bluetooth sensor services - if (connectionManager != null) { - connectionManager.stop(); - } + @Override + protected void tearDownChannel() { + bluetoothConnectionManager.reset(); } - public Sensor.SensorDataSet getSensorDataSet() { + @Override + public SensorState getSensorState() { + return bluetoothConnectionManager.getSensorState(); + } + + @Override + public SensorDataSet getSensorDataSet() { return sensorDataSet; } - - public Sensor.SensorState getSensorState() { - return (connectionManager == null) - ? Sensor.SensorState.NONE - : connectionManager.getState(); - } - - // The Handler that gets information back from the BluetoothSensorService - private final Handler messageHandler = new Handler(Looper.getMainLooper()) { - @Override - public void handleMessage(Message msg) { - switch (msg.what) { - case BluetoothConnectionManager.MESSAGE_STATE_CHANGE: - // TODO should we update the SensorManager state var? - Log.i(Constants.TAG, "MESSAGE_STATE_CHANGE: " + msg.arg1); - break; - case BluetoothConnectionManager.MESSAGE_WRITE: - break; - case BluetoothConnectionManager.MESSAGE_READ: - byte[] readBuf = null; - try { - readBuf = (byte[]) msg.obj; - sensorDataSet = parser.parseBuffer(readBuf); - Log.d(Constants.TAG, "MESSAGE_READ: " + sensorDataSet.toString()); - } catch (IllegalArgumentException iae) { - sensorDataSet = null; - Log.i(Constants.TAG, - "Got bad sensor data: " + new String(readBuf, 0, readBuf.length), - iae); - } catch (RuntimeException re) { - sensorDataSet = null; - Log.i(Constants.TAG, "Unexpected exception on read.", re); - } - break; - case BluetoothConnectionManager.MESSAGE_DEVICE_NAME: - // save the connected device's name - connectedDeviceName = - msg.getData().getString(BluetoothConnectionManager.DEVICE_NAME); - Toast.makeText(context.getApplicationContext(), - "Connected to " + connectedDeviceName, Toast.LENGTH_SHORT) - .show(); - break; - } - } - }; } diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/sensors/PolarSensorManager.java b/MyTracks/src/com/google/android/apps/mytracks/services/sensors/PolarSensorManager.java index d1d1f7441..a9d901065 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/services/sensors/PolarSensorManager.java +++ b/MyTracks/src/com/google/android/apps/mytracks/services/sensors/PolarSensorManager.java @@ -13,14 +13,16 @@ * License for the specific language governing permissions and limitations under * the License. */ + package com.google.android.apps.mytracks.services.sensors; import android.content.Context; /** - * PolarSensorManager - A sensor manager for Polar heart rate monitors. + * A Polar heart rate sensor manager. + * + * @author Jimmy Shih */ - public class PolarSensorManager extends BluetoothSensorManager { public PolarSensorManager(Context context) { super(context, new PolarMessageParser()); diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/sensors/SensorManager.java b/MyTracks/src/com/google/android/apps/mytracks/services/sensors/SensorManager.java index cb217e590..eb63ce6a0 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/services/sensors/SensorManager.java +++ b/MyTracks/src/com/google/android/apps/mytracks/services/sensors/SensorManager.java @@ -13,126 +13,118 @@ * License for the specific language governing permissions and limitations under * the License. */ + package com.google.android.apps.mytracks.services.sensors; +import com.google.android.apps.mytracks.content.Sensor.SensorDataSet; +import com.google.android.apps.mytracks.content.Sensor.SensorState; + +import android.util.Log; + import java.util.Timer; import java.util.TimerTask; -import android.util.Log; - -import com.google.android.apps.mytracks.Constants; -import com.google.android.apps.mytracks.content.Sensor; - /** * Manage the connection to a sensor. - * + * * @author Sandor Dornbush */ public abstract class SensorManager { - /** - * The maximum age where the data is considered valid. - */ - public static final long MAX_AGE = 5000; + private static final String TAG = SensorManager.class.getSimpleName(); + private static final long MAX_SENSOR_DATE_SET_AGE = 5000; + private static final long MAX_SENSOR_STATE_AGE = 30000; + private static final int RETRY_PERIOD = 30000; + + private SensorState sensorState = SensorState.NONE; + private long sensorStateTimestamp = System.currentTimeMillis(); /** - * Time to wait after a time out to retry. + * A time task to check sensor connection. */ - public static final int RETRY_PERIOD = 30000; - - private Sensor.SensorState sensorState = Sensor.SensorState.NONE; - - private long sensorStateTimestamp = 0; - - /** - * A task to run periodically to check to see if connection was lost. - */ - private TimerTask checkSensorManager = new TimerTask() { - @Override + private TimerTask checkSensorConnectionTimeTask = new TimerTask() { + @Override public void run() { - Log.i(Constants.TAG, - "SensorManager state: " + getSensorState()); switch (getSensorState()) { case CONNECTING: - long age = System.currentTimeMillis() - getSensorStateTimestamp(); - if (age > 2 * RETRY_PERIOD) { - Log.i(Constants.TAG, "Retrying connecting SensorManager."); - setupChannel(); + if (System.currentTimeMillis() - sensorStateTimestamp > MAX_SENSOR_STATE_AGE) { + Log.i(TAG, "Retry setUpChannel"); + setUpChannel(); } break; + case NONE: case DISCONNECTED: - Log.i(Constants.TAG, - "Re-registering disconnected SensoManager."); - setupChannel(); + setUpChannel(); + break; + default: break; } } }; - /** - * This timer invokes periodically the checkLocationListener timer task. - */ private final Timer timer = new Timer(); /** - * Is the sensor that this manages enabled. - * @return true if the sensor is enabled + * Returns true if the sensor is enabled. */ public abstract boolean isEnabled(); /** - * This is called when my tracks starts recording a new track. - * This is the place to open connections to the sensor. + * Sets up the sensor channel. */ - public void onStartTrack() { - setupChannel(); - timer.schedule(checkSensorManager, RETRY_PERIOD, RETRY_PERIOD); + protected abstract void setUpChannel(); + + /** + * Tears down the sensor channel. + */ + protected abstract void tearDownChannel(); + + /** + * Gets the sensor data set. + */ + public abstract SensorDataSet getSensorDataSet(); + + /** + * Starts the sensor. + */ + public void startSensor() { + setUpChannel(); + timer.schedule(checkSensorConnectionTimeTask, RETRY_PERIOD, RETRY_PERIOD); } /** - * This method is used to set up any necessary connections to underlying - * sensor hardware. + * Stops the sensor. */ - protected abstract void setupChannel(); - - public void shutdown() { + public void stopSensor() { timer.cancel(); - onDestroy(); + tearDownChannel(); } /** - * This is called when my tracks stops recording. - * This is the place to shutdown any open connections. + * Sets the sensor state. + * + * @param sensorState the sensor state */ - public abstract void onDestroy(); - - /** - * Return the last sensor reading. - * @return The last reading from the sensor. - */ - public abstract Sensor.SensorDataSet getSensorDataSet(); - - public void setSensorState(Sensor.SensorState sensorState) { + public void setSensorState(SensorState sensorState) { + sensorStateTimestamp = System.currentTimeMillis(); this.sensorState = sensorState; } /** - * Return the current sensor state. - * @return The current sensor state. + * Gets the sensor state. */ - public Sensor.SensorState getSensorState() { + public SensorState getSensorState() { return sensorState; } - public long getSensorStateTimestamp() { - return sensorStateTimestamp; - } - /** - * @return True if the data is recent enough to be considered valid. + * Returns true if the sensor data set is valid. */ - public boolean isDataValid() { - return (System.currentTimeMillis() - getSensorDataSet().getCreationTime()) < MAX_AGE; + public boolean isSensorDataSetValid() { + SensorDataSet sensorDataSet = getSensorDataSet(); + if (sensorDataSet == null) { + return false; + } + return (System.currentTimeMillis() - sensorDataSet.getCreationTime()) < MAX_SENSOR_DATE_SET_AGE; } - } diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/sensors/SensorManagerFactory.java b/MyTracks/src/com/google/android/apps/mytracks/services/sensors/SensorManagerFactory.java index 40fa7b2b6..0f651fe70 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/services/sensors/SensorManagerFactory.java +++ b/MyTracks/src/com/google/android/apps/mytracks/services/sensors/SensorManagerFactory.java @@ -13,101 +13,99 @@ * License for the specific language governing permissions and limitations under * the License. */ + package com.google.android.apps.mytracks.services.sensors; -import com.google.android.apps.mytracks.Constants; import com.google.android.apps.mytracks.services.sensors.ant.AntDirectSensorManager; import com.google.android.apps.mytracks.services.sensors.ant.AntSrmBridgeSensorManager; import com.google.android.apps.mytracks.util.PreferencesUtils; import com.google.android.maps.mytracks.R; import android.content.Context; -import android.util.Log; /** - * A factory of SensorManagers. + * A factory of {@link SensorManager}. * * @author Sandor Dornbush */ public class SensorManagerFactory { - private String activeSensorType; - private SensorManager activeSensorManager; - private int refCount; + private static SensorManager systemSensorManager = null; + private static SensorManager tempSensorManager = null; - private static SensorManagerFactory instance = new SensorManagerFactory(); + private SensorManagerFactory() {} - private SensorManagerFactory() { + /** + * Gets the system sensor manager. + * + * @param context the context + */ + public static SensorManager getSystemSensorManager(Context context) { + releaseTempSensorManager(); + releaseSystemSensorManager(); + systemSensorManager = getSensorManager(context); + if (systemSensorManager != null) { + systemSensorManager.startSensor(); + } + return systemSensorManager; } /** - * Get the factory instance. + * Releases the system sensor manager. */ - public static SensorManagerFactory getInstance() { - return instance; + public static void releaseSystemSensorManager() { + if (systemSensorManager != null) { + systemSensorManager.stopSensor(); + } + systemSensorManager = null; } /** - * Get and start a new sensor manager. - * @param context Context to fetch system preferences. - * @return The sensor manager that corresponds to the sensor type setting. + * Gets the temp sensor manager. + * + * @param context */ - public SensorManager getSensorManager(Context context) { - context = context.getApplicationContext(); + public static SensorManager getTempSensorManager(Context context) { + releaseTempSensorManager(); + if (systemSensorManager != null) { + return null; + } + tempSensorManager = getSensorManager(context); + if (tempSensorManager != null) { + tempSensorManager.startSensor(); + } + return tempSensorManager; + } + + /** + * Releases the temp sensor manager. + */ + public static void releaseTempSensorManager() { + if (tempSensorManager != null) { + tempSensorManager.stopSensor(); + } + tempSensorManager = null; + } + + /** + * Gets the sensor manager. + * + * @param context the context + */ + private static SensorManager getSensorManager(Context context) { String sensorTypeValueNone = context.getString(R.string.sensor_type_value_none); String sensorType = PreferencesUtils.getString( context, R.string.sensor_type_key, sensorTypeValueNone); - Log.i(Constants.TAG, "Creating sensor of type: " + sensorType); - - if (sensorTypeValueNone.equals(sensorType)) { - reset(); - return null; - } - if (sensorType.equals(activeSensorType)) { - Log.i(Constants.TAG, "Returning existing sensor manager."); - refCount++; - return activeSensorManager; - } - reset(); if (sensorType.equals(context.getString(R.string.sensor_type_value_ant))) { - activeSensorManager = new AntDirectSensorManager(context); + return new AntDirectSensorManager(context); } else if (sensorType.equals(context.getString(R.string.sensor_type_value_srm_ant_bridge))) { - activeSensorManager = new AntSrmBridgeSensorManager(context); + return new AntSrmBridgeSensorManager(context); } else if (sensorType.equals(context.getString(R.string.sensor_type_value_zephyr))) { - activeSensorManager = new ZephyrSensorManager(context); + return new ZephyrSensorManager(context); } else if (sensorType.equals(context.getString(R.string.sensor_type_value_polar))) { - activeSensorManager = new PolarSensorManager(context); - } else { - Log.w(Constants.TAG, "Unable to find sensor type: " + sensorType); - return null; + return new PolarSensorManager(context); } - activeSensorType = sensorType; - refCount = 1; - activeSensorManager.onStartTrack(); - return activeSensorManager; - } - - /** - * Finish using a sensor manager. - */ - public void releaseSensorManager(SensorManager sensorManager) { - Log.i(Constants.TAG, "releaseSensorManager: " + activeSensorType + " " + refCount); - if (sensorManager != activeSensorManager) { - Log.e(Constants.TAG, "invalid parameter to releaseSensorManager"); - } - if (--refCount > 0) { - return; - } - reset(); - } - - private void reset() { - activeSensorType = null; - if (activeSensorManager != null) { - activeSensorManager.shutdown(); - } - activeSensorManager = null; - refCount = 0; + return null; } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/sensors/ZephyrSensorManager.java b/MyTracks/src/com/google/android/apps/mytracks/services/sensors/ZephyrSensorManager.java index c5ff64839..b818c3cba 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/services/sensors/ZephyrSensorManager.java +++ b/MyTracks/src/com/google/android/apps/mytracks/services/sensors/ZephyrSensorManager.java @@ -13,14 +13,19 @@ * License for the specific language governing permissions and limitations under * the License. */ + package com.google.android.apps.mytracks.services.sensors; import android.content.Context; +/** + * A Zephyr heart rate sensor manager. + * + * @author Jimmy Shih + */ public class ZephyrSensorManager extends BluetoothSensorManager { public ZephyrSensorManager(Context context) { super(context, new ZephyrMessageParser()); } - } diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/sensors/ant/AntSensorManager.java b/MyTracks/src/com/google/android/apps/mytracks/services/sensors/ant/AntSensorManager.java index 3d8bce09b..d174a6138 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/services/sensors/ant/AntSensorManager.java +++ b/MyTracks/src/com/google/android/apps/mytracks/services/sensors/ant/AntSensorManager.java @@ -133,7 +133,7 @@ public abstract class AntSensorManager extends SensorManager { } @Override - public void onDestroy() { + protected void tearDownChannel() { Log.i(TAG, "destroying AntSensorManager"); cleanAntInterface(); @@ -161,7 +161,7 @@ public abstract class AntSensorManager extends SensorManager { * {@link #setupAntSensorChannels}. */ @Override - protected final void setupChannel() { + protected void setUpChannel() { setup(); } diff --git a/MyTracks/src/com/google/android/apps/mytracks/settings/StatsSettingsActivity.java b/MyTracks/src/com/google/android/apps/mytracks/settings/StatsSettingsActivity.java index 7b5781756..89e5214f7 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/settings/StatsSettingsActivity.java +++ b/MyTracks/src/com/google/android/apps/mytracks/settings/StatsSettingsActivity.java @@ -71,11 +71,9 @@ public class StatsSettingsActivity extends AbstractSettingsActivity { getString(R.string.report_speed_key)); boolean metric = PreferencesUtils.getBoolean( this, R.string.metric_units_key, PreferencesUtils.METRIC_UNITS_DEFAULT); - reportSpeedCheckBoxPreference.setSummaryOn( - metric ? getString(R.string.settings_stats_rate_speed_metric) - : getString(R.string.settings_stats_rate_speed_imperial)); - reportSpeedCheckBoxPreference.setSummaryOff( - metric ? getString(R.string.settings_stats_rate_pace_metric) - : getString(R.string.settings_stats_rate_pace_imperial)); + reportSpeedCheckBoxPreference.setSummaryOn(metric ? getString(R.string.description_speed_metric) + : getString(R.string.description_speed_imperial)); + reportSpeedCheckBoxPreference.setSummaryOff(metric ? getString(R.string.description_pace_metric) + : getString(R.string.description_pace_imperial)); } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/util/Api10Adapter.java b/MyTracks/src/com/google/android/apps/mytracks/util/Api10Adapter.java index b346a802e..9a8d16ae4 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/util/Api10Adapter.java +++ b/MyTracks/src/com/google/android/apps/mytracks/util/Api10Adapter.java @@ -37,10 +37,10 @@ public class Api10Adapter extends Api9Adapter { public BluetoothSocket getBluetoothSocket(BluetoothDevice bluetoothDevice) throws IOException { try { return bluetoothDevice.createInsecureRfcommSocketToServiceRecord( - BluetoothConnectionManager.SPP_UUID); + BluetoothConnectionManager.MY_TRACKS_UUID); } catch (IOException e) { Log.d(Constants.TAG, "Unable to create insecure connection", e); } - return bluetoothDevice.createRfcommSocketToServiceRecord(BluetoothConnectionManager.SPP_UUID); + return bluetoothDevice.createRfcommSocketToServiceRecord(BluetoothConnectionManager.MY_TRACKS_UUID); }; } diff --git a/MyTracks/src/com/google/android/apps/mytracks/util/Api7Adapter.java b/MyTracks/src/com/google/android/apps/mytracks/util/Api7Adapter.java index 78233be64..13b9963ee 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/util/Api7Adapter.java +++ b/MyTracks/src/com/google/android/apps/mytracks/util/Api7Adapter.java @@ -104,7 +104,7 @@ public class Api7Adapter implements ApiAdapter { } catch (InvocationTargetException e) { Log.d(Constants.TAG, "Unable to create insecure connection", e); } - return bluetoothDevice.createRfcommSocketToServiceRecord(BluetoothConnectionManager.SPP_UUID); + return bluetoothDevice.createRfcommSocketToServiceRecord(BluetoothConnectionManager.MY_TRACKS_UUID); } @Override diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/ChartValueSeriesTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/ChartValueSeriesTest.java index cbc5b3c71..39e69f67c 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/ChartValueSeriesTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/ChartValueSeriesTest.java @@ -15,36 +15,34 @@ */ package com.google.android.apps.mytracks; -import com.google.android.apps.mytracks.ChartValueSeries.ZoomSettings; import com.google.android.maps.mytracks.R; -import android.graphics.Paint.Style; import android.test.AndroidTestCase; /** + * Tests {@link ChartValueSeries}. + * * @author Sandor Dornbush */ public class ChartValueSeriesTest extends AndroidTestCase { private ChartValueSeries series; - + @Override protected void setUp() throws Exception { series = new ChartValueSeries(getContext(), + Integer.MIN_VALUE, + Integer.MAX_VALUE, + new int[] {100, 1000 }, + R.string.description_elevation_metric, + R.string.description_elevation_imperial, R.color.elevation_fill, - R.color.elevation_border, - new ZoomSettings(5, new int[] {100}), - R.string.stats_elevation); + R.color.elevation_border); } public void testInitialConditions() { - assertEquals(0, series.getInterval()); - assertEquals(1, series.getMaxLabelLength()); - assertEquals(0, series.getMin()); - assertEquals(0, series.getMax()); - assertEquals(0.0, series.getSpread()); - assertEquals(Style.STROKE, series.getPaint().getStyle()); - assertEquals(getContext().getString(R.string.stats_elevation), - series.getTitle()); + assertEquals(1, series.getInterval()); + assertEquals(0, series.getMinMarkerValue()); + assertEquals(5, series.getMaxMarkerValue()); assertTrue(series.isEnabled()); } @@ -52,87 +50,31 @@ public class ChartValueSeriesTest extends AndroidTestCase { series.setEnabled(false); assertFalse(series.isEnabled()); } - + public void testSmallUpdates() { series.update(0); series.update(10); series.updateDimension(); assertEquals(100, series.getInterval()); - assertEquals(3, series.getMaxLabelLength()); - assertEquals(0, series.getMin()); - assertEquals(100, series.getMax()); - assertEquals(100.0, series.getSpread()); + assertEquals(0, series.getMinMarkerValue()); + assertEquals(500, series.getMaxMarkerValue()); } public void testBigUpdates() { series.update(0); series.update(901); series.updateDimension(); - assertEquals(100, series.getInterval()); - assertEquals(5, series.getMaxLabelLength()); - assertEquals(0, series.getMin()); - assertEquals(1000, series.getMax()); - assertEquals(1000.0, series.getSpread()); + assertEquals(1000, series.getInterval()); + assertEquals(0, series.getMinMarkerValue()); + assertEquals(5000, series.getMaxMarkerValue()); } public void testNotZeroBasedUpdates() { - series.update(500); - series.update(1401); + series.update(220); + series.update(250); series.updateDimension(); assertEquals(100, series.getInterval()); - assertEquals(5, series.getMaxLabelLength()); - assertEquals(500, series.getMin()); - assertEquals(1500, series.getMax()); - assertEquals(1000.0, series.getSpread()); - } - - public void testZoomSettings_invalidArgs() { - try { - new ZoomSettings(0, new int[] {10, 50, 100}); - fail("Expected IllegalArgumentException"); - } catch (IllegalArgumentException e) { - // OK. - } - try { - new ZoomSettings(1, null); - fail("Expected IllegalArgumentException"); - } catch (IllegalArgumentException e) { - // OK. - } - try { - new ZoomSettings(1, new int[] {}); - fail("Expected IllegalArgumentException"); - } catch (IllegalArgumentException e) { - // OK. - } - try { - new ZoomSettings(1, new int[] {1, 3, 2}); - fail("Expected IllegalArgumentException"); - } catch (IllegalArgumentException e) { - // OK. - } - } - - public void testZoomSettings_minAligned() { - ZoomSettings settings = new ZoomSettings(5, new int[] {10, 50, 100}); - assertEquals(10, settings.calculateInterval(0, 15)); - assertEquals(10, settings.calculateInterval(0, 50)); - assertEquals(50, settings.calculateInterval(0, 111)); - assertEquals(50, settings.calculateInterval(0, 250)); - assertEquals(100, settings.calculateInterval(0, 251)); - assertEquals(100, settings.calculateInterval(0, 10000)); - } - - public void testZoomSettings_minNotAligned() { - ZoomSettings settings = new ZoomSettings(5, new int[] {10, 50, 100}); - assertEquals(50, settings.calculateInterval(5, 55)); - assertEquals(10, settings.calculateInterval(10, 60)); - assertEquals(50, settings.calculateInterval(7, 250)); - assertEquals(100, settings.calculateInterval(7, 257)); - assertEquals(100, settings.calculateInterval(11, 10000)); - - // A regression test. - settings = new ZoomSettings(5, new int[] {5, 10, 20}); - assertEquals(10, settings.calculateInterval(-37.14, -11.89)); + assertEquals(200, series.getMinMarkerValue()); + assertEquals(700, series.getMaxMarkerValue()); } } diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/fragments/ChartFragmentTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/fragments/ChartFragmentTest.java index f171510a6..8ba25ceac 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/fragments/ChartFragmentTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/fragments/ChartFragmentTest.java @@ -52,31 +52,32 @@ public class ChartFragmentTest extends AndroidTestCase { // No input. double[] point = fillDataPointTestHelper(myTracksLocation); - assertEquals(Double.NaN, point[3]); - assertEquals(Double.NaN, point[4]); - assertEquals(Double.NaN, point[5]); + assertEquals(Double.NaN, point[ChartView.HEART_RATE_SERIES + 1]); + assertEquals(Double.NaN, point[ChartView.CADENCE_SERIES + 1]); + assertEquals(Double.NaN, point[ChartView.POWER_SERIES + 1]); // Input incorrect state. // Creates SensorData. - Sensor.SensorData.Builder powerData = Sensor.SensorData.newBuilder() - .setValue(20).setState(Sensor.SensorState.NONE); - Sensor.SensorData.Builder cadenceData = Sensor.SensorData.newBuilder() - .setValue(20).setState(Sensor.SensorState.NONE); Sensor.SensorData.Builder heartRateData = Sensor.SensorData.newBuilder() - .setValue(20).setState(Sensor.SensorState.NONE); + .setValue(100).setState(Sensor.SensorState.NONE); + Sensor.SensorData.Builder cadenceData = Sensor.SensorData.newBuilder() + .setValue(101).setState(Sensor.SensorState.NONE); + Sensor.SensorData.Builder powerData = Sensor.SensorData.newBuilder() + .setValue(102).setState(Sensor.SensorState.NONE); + // Creates SensorDataSet. SensorDataSet sensorDataSet = myTracksLocation.getSensorDataSet(); sensorDataSet = sensorDataSet.toBuilder() - .setPower(powerData) - .setCadence(cadenceData) .setHeartRate(heartRateData) + .setCadence(cadenceData) + .setPower(powerData) .build(); myTracksLocation.setSensorData(sensorDataSet); // Test. point = fillDataPointTestHelper(myTracksLocation); - assertEquals(Double.NaN, point[3]); - assertEquals(Double.NaN, point[4]); - assertEquals(Double.NaN, point[5]); + assertEquals(Double.NaN, point[ChartView.HEART_RATE_SERIES + 1]); + assertEquals(Double.NaN, point[ChartView.CADENCE_SERIES + 1]); + assertEquals(Double.NaN, point[ChartView.POWER_SERIES + 1]); } /** @@ -87,31 +88,31 @@ public class ChartFragmentTest extends AndroidTestCase { MyTracksLocation myTracksLocation = TrackStubUtils.createMyTracksLocation(); // No input. double[] point = fillDataPointTestHelper(myTracksLocation); - assertEquals(Double.NaN, point[3]); - assertEquals(Double.NaN, point[4]); - assertEquals(Double.NaN, point[5]); + assertEquals(Double.NaN, point[ChartView.HEART_RATE_SERIES + 1]); + assertEquals(Double.NaN, point[ChartView.CADENCE_SERIES + 1]); + assertEquals(Double.NaN, point[ChartView.POWER_SERIES + 1]); // Creates SensorData. - Sensor.SensorData.Builder powerData = Sensor.SensorData.newBuilder() - .setValue(20).setState(Sensor.SensorState.SENDING); - Sensor.SensorData.Builder cadenceData = Sensor.SensorData.newBuilder() - .setValue(20).setState(Sensor.SensorState.SENDING); Sensor.SensorData.Builder heartRateData = Sensor.SensorData.newBuilder() - .setValue(20).setState(Sensor.SensorState.SENDING); + .setValue(100).setState(Sensor.SensorState.SENDING); + Sensor.SensorData.Builder cadenceData = Sensor.SensorData.newBuilder() + .setValue(101).setState(Sensor.SensorState.SENDING); + Sensor.SensorData.Builder powerData = Sensor.SensorData.newBuilder() + .setValue(102).setState(Sensor.SensorState.SENDING); // Creates SensorDataSet. SensorDataSet sensorDataSet = myTracksLocation.getSensorDataSet(); sensorDataSet = sensorDataSet.toBuilder() - .setPower(powerData) - .setCadence(cadenceData) .setHeartRate(heartRateData) + .setCadence(cadenceData) + .setPower(powerData) .build(); myTracksLocation.setSensorData(sensorDataSet); // Test. point = fillDataPointTestHelper(myTracksLocation); - assertEquals(20.0, point[3]); - assertEquals(20.0, point[4]); - assertEquals(20.0, point[5]); + assertEquals(100.0, point[ChartView.HEART_RATE_SERIES + 1]); + assertEquals(101.0, point[ChartView.CADENCE_SERIES + 1]); + assertEquals(102.0, point[ChartView.POWER_SERIES + 1]); } /** @@ -230,7 +231,7 @@ public class ChartFragmentTest extends AndroidTestCase { * parameter. Then only one value INITIALLONGTITUDE in buffer. */ double[] point = fillDataPointTestHelper(myTracksLocation1); - assertEquals(TrackStubUtils.INITIAL_ALTITUDE, point[1]); + assertEquals(TrackStubUtils.INITIAL_ALTITUDE, point[ChartView.ELEVATION_SERIES + 1]); /* * Send another value to buffer, now there are two values, INITIALALTITUDE @@ -239,8 +240,8 @@ public class ChartFragmentTest extends AndroidTestCase { MyTracksLocation myTracksLocation2 = TrackStubUtils.createMyTracksLocation(); myTracksLocation2.setAltitude(TrackStubUtils.INITIAL_ALTITUDE * 2); point = fillDataPointTestHelper(myTracksLocation2); - assertEquals( - (TrackStubUtils.INITIAL_ALTITUDE + TrackStubUtils.INITIAL_ALTITUDE * 2) / 2.0, point[1]); + assertEquals((TrackStubUtils.INITIAL_ALTITUDE + TrackStubUtils.INITIAL_ALTITUDE * 2) / 2.0, + point[ChartView.ELEVATION_SERIES + 1]); } /** @@ -260,7 +261,7 @@ public class ChartFragmentTest extends AndroidTestCase { MyTracksLocation myTracksLocation1 = TrackStubUtils.createMyTracksLocation(); myTracksLocation1.setSpeed(129); double[] point = fillDataPointTestHelper(myTracksLocation1); - assertEquals(0.0, point[2]); + assertEquals(0.0, point[ChartView.SPEED_SERIES + 1]); /* * Tests the logic when both metricUnits and reportSpeed are true.This @@ -275,7 +276,7 @@ public class ChartFragmentTest extends AndroidTestCase { myTracksLocation2.setTime(myTracksLocation1.getTime() + 222); myTracksLocation2.setSpeed(130); point = fillDataPointTestHelper(myTracksLocation2); - assertEquals(130.0 * UnitConversions.MS_TO_KMH, point[2]); + assertEquals(130.0 * UnitConversions.MS_TO_KMH, point[ChartView.SPEED_SERIES + 1]); } /** @@ -287,7 +288,8 @@ public class ChartFragmentTest extends AndroidTestCase { MyTracksLocation myTracksLocation = TrackStubUtils.createMyTracksLocation(); myTracksLocation.setSpeed(132); double[] point = fillDataPointTestHelper(myTracksLocation); - assertEquals(132.0 * UnitConversions.MS_TO_KMH * UnitConversions.KM_TO_MI, point[2]); + assertEquals(132.0 * UnitConversions.MS_TO_KMH * UnitConversions.KM_TO_MI, + point[ChartView.SPEED_SERIES + 1]); } /** @@ -299,8 +301,8 @@ public class ChartFragmentTest extends AndroidTestCase { MyTracksLocation myTracksLocation = TrackStubUtils.createMyTracksLocation(); myTracksLocation.setSpeed(134); double[] point = fillDataPointTestHelper(myTracksLocation); - assertEquals(HOURS_PER_UNIT / (134.0 * UnitConversions.MS_TO_KMH), point[2]); - + assertEquals( + HOURS_PER_UNIT / (134.0 * UnitConversions.MS_TO_KMH), point[ChartView.PACE_SERIES + 1]); } /** @@ -313,7 +315,7 @@ public class ChartFragmentTest extends AndroidTestCase { MyTracksLocation myTracksLocation = TrackStubUtils.createMyTracksLocation(); myTracksLocation.setSpeed(0); double[] point = fillDataPointTestHelper(myTracksLocation); - assertEquals(0.0, point[2]); + assertEquals(0.0, point[ChartView.PACE_SERIES + 1]); } /** @@ -323,7 +325,7 @@ public class ChartFragmentTest extends AndroidTestCase { * @return data of this location */ private double[] fillDataPointTestHelper(Location location) { - double[] point = new double[6]; + double[] point = new double[ChartView.NUM_SERIES + 1]; chartFragment.fillDataPoint(location, point); return point; } diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/io/sendtogoogle/UploadResultActivityTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/io/sendtogoogle/UploadResultActivityTest.java index f56de3b42..6ec62b942 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/io/sendtogoogle/UploadResultActivityTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/io/sendtogoogle/UploadResultActivityTest.java @@ -46,6 +46,7 @@ public class UploadResultActivityTest */ public void testAllSuccess() { initialActivity(true, true, true, true, true, true); + uploadResultActivity.showDialog(UploadResultActivity.DIALOG_RESULT_ID); Dialog dialog = uploadResultActivity.getDialog(); TextView textView = (TextView) dialog.findViewById(R.id.upload_result_success_footer); assertTrue(textView.isShown()); @@ -57,6 +58,7 @@ public class UploadResultActivityTest public void testAllFailed() { // Send all kinds but all failed. initialActivity(true, true, true, false, false, false); + uploadResultActivity.showDialog(UploadResultActivity.DIALOG_RESULT_ID); Dialog dialog = uploadResultActivity.getDialog(); TextView textView = (TextView) dialog.findViewById(R.id.upload_result_error_footer); assertTrue(textView.isShown()); @@ -72,6 +74,7 @@ public class UploadResultActivityTest */ public void testPartialSuccess() { initialActivity(true, false, true, true, false, false); + uploadResultActivity.showDialog(UploadResultActivity.DIALOG_RESULT_ID); Dialog dialog = uploadResultActivity.getDialog(); TextView textView = (TextView) dialog.findViewById(R.id.upload_result_error_footer); assertTrue(textView.isShown()); diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/services/sensors/SensorManagerFactoryTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/services/sensors/SensorManagerFactoryTest.java index 936f7f3ec..e1da6a22c 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/services/sensors/SensorManagerFactoryTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/services/sensors/SensorManagerFactoryTest.java @@ -23,7 +23,7 @@ public class SensorManagerFactoryTest extends AndroidTestCase { @SmallTest public void testDefaultSettings() throws Exception { - assertNull(SensorManagerFactory.getInstance().getSensorManager(getContext())); + assertNull(SensorManagerFactory.getSystemSensorManager(getContext())); } @SmallTest @@ -38,9 +38,9 @@ public class SensorManagerFactoryTest extends AndroidTestCase { private void assertClassForName(Class c, int i) { PreferencesUtils.setString(getContext(), R.string.sensor_type_key, getContext().getString(i)); - SensorManager sm = SensorManagerFactory.getInstance().getSensorManager(getContext()); + SensorManager sm = SensorManagerFactory.getSystemSensorManager(getContext()); assertNotNull(sm); assertTrue(c.isInstance(sm)); - SensorManagerFactory.getInstance().releaseSensorManager(sm); + SensorManagerFactory.releaseSystemSensorManager(); } }