diff --git a/MyTracks/.classpath b/MyTracks/.classpath index 43ae28c20..3d5ddb422 100644 --- a/MyTracks/.classpath +++ b/MyTracks/.classpath @@ -7,5 +7,6 @@ + diff --git a/MyTracks/AndroidManifest.xml b/MyTracks/AndroidManifest.xml index 318a6e1ed..578bd1e48 100755 --- a/MyTracks/AndroidManifest.xml +++ b/MyTracks/AndroidManifest.xml @@ -83,6 +83,9 @@ limitations under the License. android:label="@string/statistics" android:exported="false" /> + + @@ -113,6 +116,8 @@ limitations under the License. + + diff --git a/MyTracks/res/drawable/chart_settings.png b/MyTracks/res/drawable/chart_settings.png index d608eedf3..63147af45 100644 Binary files a/MyTracks/res/drawable/chart_settings.png and b/MyTracks/res/drawable/chart_settings.png differ diff --git a/MyTracks/res/drawable/menu_sensor_status.png b/MyTracks/res/drawable/menu_sensor_status.png new file mode 100644 index 000000000..80e68b847 Binary files /dev/null and b/MyTracks/res/drawable/menu_sensor_status.png differ diff --git a/MyTracks/res/layout/chart_settings.xml b/MyTracks/res/layout/chart_settings.xml index e2f13303d..6178579bc 100644 --- a/MyTracks/res/layout/chart_settings.xml +++ b/MyTracks/res/layout/chart_settings.xml @@ -103,6 +103,21 @@ limitations under the License. android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="@string/speed"/> + + + diff --git a/MyTracks/res/layout/sensor_state.xml b/MyTracks/res/layout/sensor_state.xml new file mode 100644 index 000000000..a6cd7f810 --- /dev/null +++ b/MyTracks/res/layout/sensor_state.xml @@ -0,0 +1,131 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + android:layout_column="5" + android:text="@string/watts" /> + + + + + + + + + + android:layout_column="2" + android:text="@string/revolutions_per_minute" /> + + + + + + + + + + + + + + + diff --git a/MyTracks/res/layout/stats.xml b/MyTracks/res/layout/stats.xml index 342e3b59f..af30d0875 100644 --- a/MyTracks/res/layout/stats.xml +++ b/MyTracks/res/layout/stats.xml @@ -42,15 +42,10 @@ limitations under the License. + android:layout_span="4"/> + style="@style/StatsUnit" /> + style="@style/StatsDivider" /> + style="@style/StatsUnit" /> @@ -100,43 +83,25 @@ limitations under the License. + android:layout_column="1" /> + style="@style/StatsUnitSolo" /> + style="@style/StatsSpacer" + android:layout_column="3"/> + style="@style/StatsRegister" + android:layout_column="4" /> + style="@style/StatsUnit" /> + style="@style/StatsDivider" /> + style="@style/StatsUnit" /> @@ -182,33 +135,19 @@ limitations under the License. + style="@style/StatsRegister" /> + style="@style/StatsSpacer" + android:layout_column="3"/> + style="@style/StatsRegister" /> + style="@style/StatsUnit" /> + style="@style/StatsDivider" /> + style="@style/StatsUnit" /> @@ -254,33 +181,18 @@ limitations under the License. + style="@style/StatsRegister" /> + style="@style/StatsSpacer" + android:layout_column="3"/> + style="@style/StatsRegister" /> + style="@style/StatsUnit" /> android:layout_height="3dip" android:layout_width="fill_parent" /> + style="@style/StatsUnit" /> @@ -326,46 +228,24 @@ limitations under the License. + style="@style/StatsRegister" /> + style="@style/StatsUnitSolo" /> + style="@style/StatsSpacer" + android:layout_column="3"/> + style="@style/StatsRegister" /> + style="@style/StatsUnitSolo" /> @@ -384,46 +264,24 @@ limitations under the License. + style="@style/StatsRegister" /> + style="@style/StatsUnitSolo" /> + style="@style/StatsSpacer" + android:layout_column="3"/> + style="@style/StatsRegister" /> + style="@style/StatsUnitSolo" /> @@ -442,36 +300,18 @@ limitations under the License. + style="@style/StatsRegister" /> + style="@style/StatsSpacer" + android:layout_column="3"/> + style="@style/StatsRegister" /> @@ -488,36 +328,19 @@ limitations under the License. + style="@style/StatsRegister.Small" /> + style="@style/StatsRegister.Small" /> diff --git a/MyTracks/res/menu/main.xml b/MyTracks/res/menu/main.xml index 9acffc9b3..8ff5a9e71 100644 --- a/MyTracks/res/menu/main.xml +++ b/MyTracks/res/menu/main.xml @@ -22,10 +22,14 @@ android:title="@string/settings"/> + + CSV TCX + + + None + Zephyr Bluetooth + + + + + @string/none + @string/zephyr_sensor_type + + diff --git a/MyTracks/res/values/colors.xml b/MyTracks/res/values/colors.xml index 911148cc0..3ce80b119 100644 --- a/MyTracks/res/values/colors.xml +++ b/MyTracks/res/values/colors.xml @@ -15,17 +15,22 @@ See the License for the specific language governing permissions and limitations under the License. --> - #FFffd700 - #FFFFFFFF #FF000000 - #FF00FFFF #FF0000FF #400000FF - #FFFF0000 - #FF00AA00 - #FFAAAAAA #FF444444 + #FFffd700 + #FFAAAAAA + #FF00AA00 + #FF00FFFF #70000000 + #FF401465 + #40401465 + #FFFF0000 + #40FF0000 #FFF0F0F0 #FF0F0F0F + #FFFFFFFF + #FFF8FF00 + #40F8FF00 diff --git a/MyTracks/res/values/keys.xml b/MyTracks/res/values/keys.xml index a4ec6dc48..ebf7cdf07 100644 --- a/MyTracks/res/values/keys.xml +++ b/MyTracks/res/values/keys.xml @@ -33,7 +33,6 @@ limitations under the License. sendStatsAndPoints sendToDocs sendToMyMaps - sensorId sensorType shareUrlOnly splitFrequency diff --git a/MyTracks/res/values/strings.xml b/MyTracks/res/values/strings.xml index e52af7809..b837dcc82 100644 --- a/MyTracks/res/values/strings.xml +++ b/MyTracks/res/values/strings.xml @@ -32,6 +32,7 @@ limitations under the License. Please select the track to view Current track Last segment + Sensor State Aggregated Statistics @@ -454,9 +455,7 @@ limitations under the License. NONE - ANT - SRM - ATHLOSOFT + ZEPHYR Connected to %s @@ -467,6 +466,14 @@ limitations under the License. Sensor Data Time Power meter enabled Heart rate monitor enabled + No Sensor Manager + Enabled + Connecting + Connected + Disconnected + No Sensor Service + Searching + Sending receive track recording notifications diff --git a/MyTracks/res/values/styles.xml b/MyTracks/res/values/styles.xml new file mode 100644 index 000000000..b9e0bb68f --- /dev/null +++ b/MyTracks/res/values/styles.xml @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/MyTracks/res/xml/preferences.xml b/MyTracks/res/xml/preferences.xml index 98d34845a..c0b519386 100644 --- a/MyTracks/res/xml/preferences.xml +++ b/MyTracks/res/xml/preferences.xml @@ -101,6 +101,29 @@ limitations under the License. + + + + + + + + + + + + data = new ArrayList(); // Need two locations so we can keep track of the last location. - Location location = new Location(""); + Location location = new MyTracksLocation(""); do { lastSeenLocationId = cursor.getLong(idColumnIdx); providerUtils.fillLocation(cursor, location); if (MyTracksUtils.isValidLocation(location)) { - double[] point = new double[3]; + double[] point = new double[6]; location = getDataPoint(location, track, point); data.add(point); } @@ -464,10 +467,14 @@ public class ChartActivity extends Activity implements /** * Given a location, creates a new data point for the chart. A data point is - * an array double[2], where: + * an array double[3 or 6], where: * data[0] = the time or distance * data[1] = the elevation * data[2] = the speed + * and possibly: + * data[3] = power + * data[4] = cadence + * data[5] = heart rate * * This must be called in order for each point. * @@ -477,6 +484,35 @@ public class ChartActivity extends Activity implements * @return the previous location, now available for reuse */ private Location getDataPoint(Location location, Track track, double[] result) { + if (location instanceof MyTracksLocation && + ((MyTracksLocation) location).getSensorDataSet() != null) { + SensorDataSet sensorData = ((MyTracksLocation) location).getSensorDataSet(); + if (sensorData.hasPower() + && sensorData.getPower().getState() == Sensor.SensorState.SENDING + && sensorData.getPower().hasValue()) { + result[3] = sensorData.getPower().getValue(); + } else { + result[3] = Double.NaN; + } + if (sensorData.hasCadence() + && sensorData.getCadence().getState() == Sensor.SensorState.SENDING + && sensorData.getCadence().hasValue()) { + result[4] = sensorData.getCadence().getValue(); + } else { + result[4] = Double.NaN; + } + if (sensorData.hasHeartRate() + && sensorData.getHeartRate().getState() == Sensor.SensorState.SENDING + && sensorData.getHeartRate().hasValue()) { + result[5] = sensorData.getHeartRate().getValue(); + } else { + result[5] = Double.NaN; + } + } else { + result[3] = Double.NaN; + result[4] = Double.NaN; + result[5] = Double.NaN; + } switch (mode) { case BY_DISTANCE: result[0] = profileLength; @@ -529,7 +565,7 @@ public class ChartActivity extends Activity implements if (oldLastLocation == null) { // No previous location, but return a blank one for reuse - return new Location(""); + return new MyTracksLocation(""); } return oldLastLocation; @@ -576,7 +612,7 @@ public class ChartActivity extends Activity implements final ArrayList theData = new ArrayList(); int points = 0; // Need two locations so we can keep track of the last location. - Location location = new Location(""); + Location location = new MyTracksLocation(""); while (lastLocationRead < track.getStopId()) { cursor = providerUtils.getLocationsCursor( selectedTrackId, lastLocationRead, bufferSize, false); @@ -592,7 +628,8 @@ public class ChartActivity extends Activity implements if (MyTracksUtils.isValidLocation(location)) { lastLocationRead = lastSeenLocationId = cursor.getLong(idColumnIdx); - double[] point = new double[3]; + // TODO Can we be smarter about choosing 3 or 6 entries? + double[] point = new double[6]; location = getDataPoint(location, track, point); if (points % chartSamplingFrequency == 0) { theData.add(point); diff --git a/MyTracks/src/com/google/android/apps/mytracks/ChartSettingsDialog.java b/MyTracks/src/com/google/android/apps/mytracks/ChartSettingsDialog.java index 0610e0671..34d64e82f 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/ChartSettingsDialog.java +++ b/MyTracks/src/com/google/android/apps/mytracks/ChartSettingsDialog.java @@ -35,8 +35,7 @@ import android.widget.RadioGroup; */ public class ChartSettingsDialog extends Dialog { private RadioButton distance; - private CheckBox elevation; - private CheckBox speed; + private CheckBox[] series; public ChartSettingsDialog(Context context) { super(context); @@ -61,8 +60,18 @@ public class ChartSettingsDialog extends Dialog { }); distance = (RadioButton) findViewById(R.id.chart_settings_by_distance); - elevation = (CheckBox) findViewById(R.id.chart_settings_elevation); - speed = (CheckBox) findViewById(R.id.chart_settings_speed); + + series = new CheckBox[ChartView.NUM_SERIES]; + series[ChartView.ELEVATION_SERIES] = + (CheckBox) findViewById(R.id.chart_settings_elevation); + series[ChartView.SPEED_SERIES] = + (CheckBox) findViewById(R.id.chart_settings_speed); + series[ChartView.POWER_SERIES] = + (CheckBox) findViewById(R.id.chart_settings_power); + series[ChartView.CADENCE_SERIES] = + (CheckBox) findViewById(R.id.chart_settings_cadence); + series[ChartView.HEART_RATE_SERIES] = + (CheckBox) findViewById(R.id.chart_settings_heart_rate); } public void setup(ChartActivity chart) { @@ -74,8 +83,9 @@ public class ChartSettingsDialog extends Dialog { rd.check(chart.getMode() == Mode.BY_DISTANCE ? R.id.chart_settings_by_distance : R.id.chart_settings_by_time); - elevation.setChecked(chart.isSeriesEnabled(ChartView.ELEVATION_SERIES)); - speed.setChecked(chart.isSeriesEnabled(ChartView.SPEED_SERIES)); + for (int i = 0; i < ChartView.NUM_SERIES; i++) { + series[i].setChecked(chart.isSeriesEnabled(i)); + } } private void handleOk() { @@ -83,8 +93,9 @@ public class ChartSettingsDialog extends Dialog { chart.setMode(distance.isChecked() ? Mode.BY_DISTANCE : Mode.BY_TIME); // TODO: check that something is visible. - chart.setSeriesEnabled(ChartView.ELEVATION_SERIES, elevation.isChecked()); - chart.setSeriesEnabled(ChartView.SPEED_SERIES, speed.isChecked()); + for (int i = 0; i < ChartView.NUM_SERIES; i++) { + chart.setSeriesEnabled(i, series[i].isChecked()); + } dismiss(); } diff --git a/MyTracks/src/com/google/android/apps/mytracks/ChartValueSeries.java b/MyTracks/src/com/google/android/apps/mytracks/ChartValueSeries.java index d5ff24834..acf61ab16 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/ChartValueSeries.java +++ b/MyTracks/src/com/google/android/apps/mytracks/ChartValueSeries.java @@ -42,6 +42,7 @@ public class ChartValueSeries { private double min = 0; private double max = 1; private int effectiveMax = 0; + private int absoluteMax = Integer.MIN_VALUE; private int effectiveMin = 0; private double spread = 0; private boolean enabled = true; @@ -131,7 +132,7 @@ public class ChartValueSeries { if (min < 0) { effectiveMin -= rounding; } - spread = effectiveMax - effectiveMin; + spread = getMax() - effectiveMin; } /** @@ -139,7 +140,7 @@ public class ChartValueSeries { */ public int getMaxLabelLength() { String minS = format.format(effectiveMin); - String maxS = format.format(effectiveMax); + String maxS = format.format(getMax()); return Math.max(minS.length(), maxS.length()); } @@ -151,10 +152,16 @@ public class ChartValueSeries { } /** - * @return The rounded up maximum value + * @return The minimum of the rounded up max value and the effective max. */ public int getMax() { - return effectiveMax; + // Return the effective max if no absolute is set + // or the effective max is less than the absolute. + if ((absoluteMax == Integer.MIN_VALUE) || (effectiveMax < absoluteMax)) { + return effectiveMax; + } else { + return absoluteMax; + } } /** @@ -211,4 +218,16 @@ public class ChartValueSeries { public void setEnabled(boolean enabled) { this.enabled = enabled; } + + public int getAbsoluteMax() { + return absoluteMax; + } + + public void setAbsoluteMax(int absoluteMax) { + this.absoluteMax = absoluteMax; + } + + public boolean hasData() { + return monitor.hasData(); + } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/ChartView.java b/MyTracks/src/com/google/android/apps/mytracks/ChartView.java index 4ff195a76..06cc848bf 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/ChartView.java +++ b/MyTracks/src/com/google/android/apps/mytracks/ChartView.java @@ -87,7 +87,10 @@ public class ChartView extends View { */ public static final int ELEVATION_SERIES = 0; public static final int SPEED_SERIES = 1; - public static final int NUM_SERIES = 2; + 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(); @@ -172,38 +175,44 @@ public class ChartView extends View { series = new ChartValueSeries[NUM_SERIES]; // Create the paints - Paint fillPaint1 = new Paint(); - fillPaint1.setStyle(Style.FILL); - fillPaint1.setColor(context.getResources().getColor(R.color.green)); - fillPaint1.setAntiAlias(true); - - Paint fillPaint2 = new Paint(); - fillPaint2.setStyle(Style.FILL); - fillPaint2.setColor( - context.getResources().getColor(R.color.blue_transparent)); - fillPaint2.setAntiAlias(true); - - Paint strokePaint = new Paint(); - strokePaint.setStyle(Style.STROKE); - strokePaint.setColor(context.getResources().getColor(R.color.blue)); - strokePaint.setAntiAlias(true); + Paint fillPaint = new Paint(); + fillPaint.setStyle(Style.FILL); + fillPaint.setColor(context.getResources().getColor(R.color.green)); + fillPaint.setAntiAlias(true); // Create the value series series[ELEVATION_SERIES] = new ChartValueSeries(context, "###,###", - fillPaint1, + fillPaint, null, 100, R.string.elevation); - series[SPEED_SERIES] = - new ChartValueSeries(context, - "###,###.0", - fillPaint2, - strokePaint, - 5, - R.string.speed); + series[SPEED_SERIES] = getSeries(context, R.color.blue_transparent, + R.color.blue, "###,###.0", R.string.speed); + series[POWER_SERIES] = getSeries(context, R.color.red_transparent, + R.color.red, "###,###", R.string.power); + series[POWER_SERIES].setAbsoluteMax(1500); + series[CADENCE_SERIES] = getSeries(context, R.color.purple_transparent, + R.color.purple, "###,###", R.string.cadence); + series[HEART_RATE_SERIES] = getSeries(context, R.color.yellow_transparent, + R.color.yellow, "###,###", R.string.heart_rate); + } + + private ChartValueSeries getSeries(Context context, + int fillColor, int strokeColor, String format, int seriesName) { + Paint fillPaint = new Paint(); + fillPaint.setStyle(Style.FILL); + fillPaint.setColor(context.getResources().getColor(fillColor)); + fillPaint.setAntiAlias(true); + + Paint strokePaint = new Paint(); + strokePaint.setStyle(Style.STROKE); + strokePaint.setColor(context.getResources().getColor(strokeColor)); + strokePaint.setAntiAlias(true); + return new ChartValueSeries(context, format, fillPaint, strokePaint, 5, + seriesName); } public void clearWaypoints() { @@ -266,13 +275,25 @@ public class ChartView extends View { */ public synchronized void addDataPoint(double[] theData) { data.add(theData); + addDataPointInternal(theData); + updateDimensions(); + setupPath(); + } + + private void addDataPointInternal(double[] theData) { xMonitor.update(theData[0]); int min = Math.min(series.length, theData.length - 1); for (int i = 0; i < min; i++) { - series[i].update(theData[i + 1]); + if (!Double.isNaN(theData[i + 1])) { + series[i].update(theData[i + 1]); + } + } + // Fill in the extra's if needed. + for (int i = theData.length; i < series.length; i++) { + if (series[i].hasData()) { + series[i].update(0); + } } - updateDimensions(); - setupPath(); } /** @@ -284,11 +305,7 @@ public class ChartView extends View { data.addAll(theData); for (int i = 0; i < theData.size(); i++) { double d[] = theData.get(i); - xMonitor.update(d[0]); - int min = Math.min(series.length, d.length - 1); - for (int j = 0; j < min; j++) { - series[j].update(d[j + 1]); - } + addDataPointInternal(d); } updateDimensions(); setupPath(); @@ -526,7 +543,7 @@ public class ChartView extends View { // Draw the data series for (ChartValueSeries cvs : series) { - if (cvs.isEnabled()) { + if (cvs.isEnabled() && cvs.hasData()) { cvs.drawPath(c); } } @@ -560,7 +577,7 @@ public class ChartView extends View { c.translate(getScrollX(), 0); drawYAxis(c); for (ChartValueSeries cvs : series) { - if (cvs.isEnabled()) { + if (cvs.isEnabled() && cvs.hasData()) { drawYLabels(cvs, c); } } @@ -576,13 +593,13 @@ public class ChartView extends View { private void drawSeriesTitles(Canvas c) { int sections = 1; for (ChartValueSeries cvs : series) { - if (cvs.isEnabled()) { + if (cvs.isEnabled() && cvs.hasData()) { sections++; } } int j = 0; for (ChartValueSeries cvs : series) { - if (cvs.isEnabled()) { + if (cvs.isEnabled() && cvs.hasData()) { int y = (int) (w * zoomLevel * ((double) (++j) / sections)); c.drawText(cvs.getTitle(), y, TOP_BORDER, cvs.getPaint()); } @@ -601,11 +618,11 @@ public class ChartView extends View { return; } - int min = Math.min(series.length, data.get(0).length - 1); - // All of the data points to the respective series. for (int i = 0; i < data.size(); i++) { 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(); @@ -616,17 +633,36 @@ public class ChartView extends View { // Close the path. int yCorner = TOP_BORDER + 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(); - // Bottom right corner - path.lineTo(getX(data.get(data.size() - 1)[0]), yCorner); - // Bottom left corner - path.lineTo(xCorner, yCorner); - // Top right corner - path.lineTo(xCorner, getY(cvs, data.get(0)[j + 1])); + int first = getFirstPointPopulatedIndex(j + 1); + if (first != -1) { + // Bottom right corner + path.lineTo(getX(data.get(data.size() - 1)[0]), yCorner); + // Bottom left corner + path.lineTo(xCorner, yCorner); + // Top right corner + path.lineTo(xCorner, getY(cvs, data.get(first)[j + 1])); + } } } + + /** + * Find 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. + */ + private int getFirstPointPopulatedIndex(int seriesIndex) { + for (int i = 0; i < data.size(); i++) { + if (data.get(i).length > seriesIndex) { + return i; + } + } + return -1; + } /** * Update the histogram dimensions. diff --git a/MyTracks/src/com/google/android/apps/mytracks/MenuManager.java b/MyTracks/src/com/google/android/apps/mytracks/MenuManager.java index 374e4d945..aecda4b3d 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/MenuManager.java +++ b/MyTracks/src/com/google/android/apps/mytracks/MenuManager.java @@ -17,6 +17,7 @@ package com.google.android.apps.mytracks; import com.google.android.maps.mytracks.R; +import android.app.Activity; import android.content.Intent; import android.view.Menu; import android.view.MenuItem; @@ -63,8 +64,8 @@ class MenuManager { return true; } case R.id.menu_list_tracks: { - Intent startIntent = new Intent(activity, MyTracksList.class); - activity.startActivityForResult(startIntent, MyTracksConstants.SHOW_TRACK); + activity.startActivityForResult(new Intent(activity, MyTracksList.class), + MyTracksConstants.SHOW_TRACK); return true; } case R.id.menu_list_markers: { @@ -73,20 +74,17 @@ class MenuManager { activity.startActivityForResult(startIntent, MyTracksConstants.SHOW_WAYPOINT); return true; } + case R.id.menu_sensor_state: { + return startActivity(SensorStateActivity.class); + } case R.id.menu_settings: { - Intent startIntent = new Intent(activity, MyTracksSettings.class); - activity.startActivity(startIntent); - return true; + return startActivity(MyTracksSettings.class); } case R.id.menu_aggregated_stats: { - Intent startIntent = new Intent(activity, AggregatedStatsActivity.class); - activity.startActivity(startIntent); - return true; + return startActivity(AggregatedStatsActivity.class); } case R.id.menu_help: { - Intent startIntent = new Intent(activity, WelcomeActivity.class); - activity.startActivity(startIntent); - return true; + return startActivity(WelcomeActivity.class); } case MyTracksConstants.MENU_CLEAR_MAP: { activity.setSelectedTrackId(-1); @@ -95,4 +93,9 @@ class MenuManager { } return false; } + + private boolean startActivity(Class activityClass) { + activity.startActivity(new Intent(activity, activityClass)); + return true; + } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/MyTracks.java b/MyTracks/src/com/google/android/apps/mytracks/MyTracks.java index ae5d9ab5e..e86357dba 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/MyTracks.java +++ b/MyTracks/src/com/google/android/apps/mytracks/MyTracks.java @@ -1128,7 +1128,7 @@ public class MyTracks extends TabActivity implements OnTouchListener, long getSelectedTrackId() { return selectedTrackId; } - + /** * Binds to track recording service if it is running. */ diff --git a/MyTracks/src/com/google/android/apps/mytracks/MyTracksSettings.java b/MyTracks/src/com/google/android/apps/mytracks/MyTracksSettings.java index e58c99388..c47365ae1 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/MyTracksSettings.java +++ b/MyTracks/src/com/google/android/apps/mytracks/MyTracksSettings.java @@ -15,12 +15,17 @@ */ package com.google.android.apps.mytracks; +import java.util.ArrayList; +import java.util.List; + import com.google.android.apps.mytracks.io.backup.BackupActivityHelper; import com.google.android.apps.mytracks.io.backup.BackupPreferencesListener; import com.google.android.apps.mytracks.services.StatusAnnouncerFactory; import com.google.android.apps.mytracks.util.ApiFeatures; +import com.google.android.apps.mytracks.util.BluetoothDeviceUtils; import com.google.android.maps.mytracks.R; +import android.content.Intent; import android.content.SharedPreferences; import android.os.Bundle; import android.preference.CheckBoxPreference; @@ -30,6 +35,7 @@ import android.preference.PreferenceActivity; import android.preference.PreferenceManager; import android.preference.Preference.OnPreferenceChangeListener; import android.preference.Preference.OnPreferenceClickListener; +import android.provider.Settings; /** * An activity that let's the user see and edit the settings. @@ -53,6 +59,7 @@ public class MyTracksSettings extends PreferenceActivity { public static final int DEFAULT_SPLIT_FREQUENCY = 0; private BackupPreferencesListener backupListener; + private SharedPreferences preferences; /** Called when the activity is first created. */ @@ -96,6 +103,19 @@ public class MyTracksSettings extends PreferenceActivity { }); updatePreferenceUnits(metricUnitsPreference.isChecked()); + ListPreference sensorTypePreference = + (ListPreference) findPreference(getString(R.string.sensor_type_key)); + sensorTypePreference.setOnPreferenceChangeListener( + new OnPreferenceChangeListener() { + @Override + public boolean onPreferenceChange(Preference preference, + Object newValue) { + updateSensorSettings((String) newValue); + return true; + } + }); + updateSensorSettings(sensorTypePreference.getValue()); + // Disable TTS announcement preference if not available if (!apiFeatures.hasTextToSpeech()) { IntegerListPreference announcementFrequency = @@ -112,6 +132,7 @@ public class MyTracksSettings extends PreferenceActivity { protected void onResume() { super.onResume(); + configureBluetoothPreferences(); Preference backupNowPreference = findPreference(getString(R.string.backup_to_sd_key)); Preference restoreNowPreference = @@ -158,7 +179,15 @@ public class MyTracksSettings extends PreferenceActivity { getPreferenceManager().getSharedPreferences() .unregisterOnSharedPreferenceChangeListener(backupListener); - super.onDestroy(); + super.onPause(); + } + private void updateSensorSettings(String sensorType) { + boolean usesBluetooth = + getString(R.string.zephyr_sensor_type).equals(sensorType); + findPreference( + getString(R.string.bluetooth_sensor_key)).setEnabled(usesBluetooth); + findPreference( + getString(R.string.bluetooth_pairing_key)).setEnabled(usesBluetooth); } /** @@ -194,4 +223,44 @@ public class MyTracksSettings extends PreferenceActivity { ? R.array.split_frequency_options : R.array.split_frequency_options_ft); } + + + /** + * Configures preference actions related to bluetooth. + */ + private void configureBluetoothPreferences() { + if (BluetoothDeviceUtils.isBluetoothMethodSupported()) { + // Populate the list of bluetooth devices + populateBluetoothDeviceList(); + + // Make the pair devices preference go to the system preferences + findPreference(getString(R.string.bluetooth_pairing_key)) + .setOnPreferenceClickListener(new OnPreferenceClickListener() { + public boolean onPreferenceClick(Preference preference) { + Intent settingsIntent = new Intent(Settings.ACTION_BLUETOOTH_SETTINGS); + startActivity(settingsIntent); + return false; + } + }); + } + } + + /** + * Populates the list preference with all available bluetooth devices. + */ + private void populateBluetoothDeviceList() { + // Build the list of entries and their values + List entries = new ArrayList(); + List entryValues = new ArrayList(); + + // The actual devices + BluetoothDeviceUtils.getInstance().populateDeviceLists(entries, entryValues); + + CharSequence[] entriesArray = entries.toArray(new CharSequence[entries.size()]); + CharSequence[] entryValuesArray = entryValues.toArray(new CharSequence[entryValues.size()]); + ListPreference devicesPreference = + (ListPreference) findPreference(getString(R.string.bluetooth_sensor_key)); + devicesPreference.setEntryValues(entryValuesArray); + devicesPreference.setEntries(entriesArray); + } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/SensorStateActivity.java b/MyTracks/src/com/google/android/apps/mytracks/SensorStateActivity.java new file mode 100644 index 000000000..fb9559d26 --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/SensorStateActivity.java @@ -0,0 +1,193 @@ +/* + * Copyright 2010 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.google.android.apps.mytracks; + +import static com.google.android.apps.mytracks.MyTracksConstants.TAG; +import com.google.android.apps.mytracks.content.Sensor; +import com.google.android.apps.mytracks.services.ITrackRecordingService; +import com.google.android.apps.mytracks.services.sensors.SensorUtils; +import com.google.android.maps.mytracks.R; +import com.google.protobuf.InvalidProtocolBufferException; + +import android.app.Activity; +import android.os.Bundle; +import android.os.RemoteException; +import android.util.Log; +import android.widget.TextView; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Timer; +import java.util.TimerTask; + +/** + * An activity that displays information about sensors. + * + * @author Sandor Dornbush + */ +public class SensorStateActivity extends Activity { + + private static final SimpleDateFormat TIMESTAMP_FORMAT = + new SimpleDateFormat("HH:mm:ss"); + private static final long REFRESH_PERIOD_MS = 250; + + private final StatsUtilities utils; + + /** + * This timer periodically invokes the refresh timer task. + */ + private Timer timer; + + private final Runnable stateUpdater = new Runnable() { + public void run() { + updateState(); + } + }; + + /** + * A task which will update the U/I. + */ + private class RefreshTask extends TimerTask { + @Override + public void run() { + runOnUiThread(stateUpdater); + } + }; + + public SensorStateActivity() { + utils = new StatsUtilities(this); + Log.w(TAG, "SensorStateActivity()"); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + Log.w(TAG, "SensorStateActivity.onCreate"); + + setContentView(R.layout.sensor_state); + + updateState(); + } + + @Override + protected void onResume() { + super.onResume(); + timer = new Timer(); + timer.schedule(new RefreshTask(), REFRESH_PERIOD_MS, REFRESH_PERIOD_MS); + } + + @Override + protected void onPause() { + super.onPause(); + timer.cancel(); + timer.purge(); + timer = null; + } + + protected void updateState() { + MyTracks mt = MyTracks.getInstance(); + + ITrackRecordingService service = + mt == null ? null : mt.getTrackRecordingService(); + if (service == null) { + Log.d(MyTracksConstants.TAG, "Could not get track recording service."); + updateSensorState(Sensor.SensorState.NONE); + updateSensorData(null); + return; + } + Sensor.SensorDataSet sds = null; + try { + byte[] buff = service.getSensorData(); + if (buff != null) { + sds = Sensor.SensorDataSet.parseFrom(buff); + updateSensorData(sds); + } + } catch (RemoteException e) { + Log.e(MyTracksConstants.TAG, "Could not read sensor data.", e); + } catch (InvalidProtocolBufferException e) { + Log.e(MyTracksConstants.TAG, "Could not read sensor data.", e); + } + updateSensorData(sds); + + try { + int i = service.getSensorState(); + updateSensorState(Sensor.SensorState.valueOf(i)); + } catch (RemoteException e) { + Log.e(MyTracksConstants.TAG, "Could not read sensor state.", e); + updateSensorState(Sensor.SensorState.NONE); + } + } + + private void updateSensorState(Sensor.SensorState state) { + TextView sensorTime = + ((TextView) findViewById(R.id.sensor_state_register)); + sensorTime.setText(SensorUtils.getStateAsString(state, this)); + } + + protected void updateSensorData(Sensor.SensorDataSet sds) { + if (sds == null) { + utils.setUnknown(R.id.sensor_data_time_register); + utils.setUnknown(R.id.cadence_state_register); + utils.setUnknown(R.id.power_state_register); + utils.setUnknown(R.id.heart_rate_register); + return; + } + + TextView sensorTime = + ((TextView) findViewById(R.id.sensor_data_time_register)); + sensorTime.setText( + TIMESTAMP_FORMAT.format(new Date(sds.getCreationTime()))); + + if (sds.hasPower() && sds.getPower().hasValue() + && sds.getPower().getState() == Sensor.SensorState.SENDING) { + utils.setText(R.id.power_state_register, + Integer.toString(sds.getPower().getValue())); + } else { + utils.setText(R.id.power_state_register, + SensorUtils.getStateAsString( + sds.hasPower() + ? sds.getPower().getState() + : Sensor.SensorState.NONE, + this)); + } + + if (sds.hasCadence() && sds.getCadence().hasValue() + && sds.getCadence().getState() == Sensor.SensorState.SENDING) { + utils.setText(R.id.cadence_state_register, + Integer.toString(sds.getCadence().getValue())); + } else { + utils.setText(R.id.cadence_state_register, + SensorUtils.getStateAsString( + sds.hasCadence() + ? sds.getCadence().getState() + : Sensor.SensorState.NONE, + this)); + } + + if (sds.hasHeartRate() && sds.getHeartRate().hasValue() + && sds.getHeartRate().getState() == Sensor.SensorState.SENDING) { + utils.setText(R.id.heart_rate_register, + Integer.toString(sds.getHeartRate().getValue())); + } else { + utils.setText(R.id.heart_rate_register, + SensorUtils.getStateAsString( + sds.hasHeartRate() + ? sds.getHeartRate().getState() + : Sensor.SensorState.NONE, + this)); + } + } +} diff --git a/MyTracks/src/com/google/android/apps/mytracks/content/MyTracksProvider.java b/MyTracks/src/com/google/android/apps/mytracks/content/MyTracksProvider.java index 010e553f8..89604e4b2 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/content/MyTracksProvider.java +++ b/MyTracks/src/com/google/android/apps/mytracks/content/MyTracksProvider.java @@ -15,6 +15,8 @@ */ package com.google.android.apps.mytracks.content; +import com.google.android.apps.mytracks.MyTracksConstants; + import android.content.ContentProvider; import android.content.ContentUris; import android.content.ContentValues; @@ -38,7 +40,7 @@ import android.util.Log; public class MyTracksProvider extends ContentProvider { private static final String DATABASE_NAME = "mytracks.db"; - private static final int DATABASE_VERSION = 17; + private static final int DATABASE_VERSION = 18; private static final int TRACKPOINTS = 1; private static final int TRACKPOINTS_ID = 2; private static final int TRACKS = 3; @@ -70,7 +72,8 @@ public class MyTracksProvider extends ContentProvider { + TrackPointsColumns.ALTITUDE + " FLOAT, " + TrackPointsColumns.ACCURACY + " FLOAT, " + TrackPointsColumns.SPEED + " FLOAT, " - + TrackPointsColumns.BEARING + " FLOAT);"); + + TrackPointsColumns.BEARING + " FLOAT, " + + TrackPointsColumns.SENSOR + " BLOB);"); db.execSQL("CREATE TABLE " + TRACKS_TABLE + " (" + TracksColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " + TracksColumns.NAME + " STRING, " @@ -132,12 +135,19 @@ public class MyTracksProvider extends ContentProvider { @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { - Log.w(TAG, "Upgrading database from version " + oldVersion + " to " - + newVersion + ", which will destroy all old data"); - db.execSQL("DROP TABLE IF EXISTS " + TRACKPOINTS_TABLE); - db.execSQL("DROP TABLE IF EXISTS " + TRACKS_TABLE); - db.execSQL("DROP TABLE IF EXISTS " + WAYPOINTS_TABLE); - onCreate(db); + if (oldVersion < 17) { + Log.w(TAG, "Upgrading database from version " + oldVersion + " to " + + newVersion + ", which will destroy all old data"); + db.execSQL("DROP TABLE IF EXISTS " + TRACKPOINTS_TABLE); + db.execSQL("DROP TABLE IF EXISTS " + TRACKS_TABLE); + db.execSQL("DROP TABLE IF EXISTS " + WAYPOINTS_TABLE); + onCreate(db); + } else if (oldVersion >= 17) { + Log.w(TAG, "Upgrading database from version " + oldVersion + " to " + + newVersion + ", adding sensor column."); + db.execSQL("ALTER TABLE " + TRACKPOINTS_TABLE + + " ADD " + TrackPointsColumns.SENSOR + " BLOB"); + } } } @@ -235,6 +245,7 @@ public class MyTracksProvider extends ContentProvider { } } + @Override public int bulkInsert(Uri url, ContentValues[] valuesBulk) { Log.d(MyTracksProvider.TAG, "MyTracksProvider.bulkInsert"); @@ -346,6 +357,8 @@ public class MyTracksProvider extends ContentProvider { throw new IllegalArgumentException("Unknown URL " + url); } + Log.i(MyTracksConstants.TAG, + "Build query: " + qb.buildQuery(projection, selection, selectionArgs, null, null, sortOrder, null)); Cursor c = qb.query(db, projection, selection, selectionArgs, null, null, sortOrder); c.setNotificationUri(getContext().getContentResolver(), url); diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/GpxImporter.java b/MyTracks/src/com/google/android/apps/mytracks/io/GpxImporter.java index 571859cc9..e1d58b354 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/GpxImporter.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/GpxImporter.java @@ -342,8 +342,14 @@ public class GpxImporter extends DefaultHandler { // create new location and set attributes Location loc = new Location(LocationManager.GPS_PROVIDER); - loc.setLatitude(Double.parseDouble(latitude)); - loc.setLongitude(Double.parseDouble(longitude)); + try { + loc.setLatitude(Double.parseDouble(latitude)); + loc.setLongitude(Double.parseDouble(longitude)); + } catch (NumberFormatException e) { + String msg = createErrorMessage( + "Unable to parse lat/long: " + latitude + "/" + longitude); + throw new SAXException(msg, e); + } return loc; } @@ -472,9 +478,14 @@ public class GpxImporter extends DefaultHandler { } } - private void onAltitudeElementEnd() { + private void onAltitudeElementEnd() throws SAXException { if (location != null) { - location.setAltitude(Double.parseDouble(content)); + try { + location.setAltitude(Double.parseDouble(content)); + } catch (NumberFormatException e) { + String msg = createErrorMessage("Unable to parse altitude: " + content); + throw new SAXException(msg, e); + } } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/TcxTrackWriter.java b/MyTracks/src/com/google/android/apps/mytracks/io/TcxTrackWriter.java index 7ad325f41..a09e2fd60 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/TcxTrackWriter.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/TcxTrackWriter.java @@ -15,8 +15,11 @@ */ package com.google.android.apps.mytracks.io; +import com.google.android.apps.mytracks.content.MyTracksLocation; +import com.google.android.apps.mytracks.content.Sensor; import com.google.android.apps.mytracks.content.Track; import com.google.android.apps.mytracks.content.Waypoint; +import com.google.android.apps.mytracks.content.Sensor.SensorDataSet; import com.google.android.apps.mytracks.io.TrackWriterFactory.TrackFileFormat; import android.location.Location; @@ -141,6 +144,37 @@ public class TcxTrackWriter implements TrackFormatWriter { pw.print(location.getAltitude()); pw.println(""); + if (location instanceof MyTracksLocation) { + SensorDataSet sensorData = ((MyTracksLocation) location).getSensorDataSet(); + if (sensorData != null) { + if (sensorData.hasHeartRate() + && sensorData.getHeartRate().getState() == Sensor.SensorState.SENDING + && sensorData.getHeartRate().hasValue()) { + pw.print(" "); + pw.print(""); + pw.print(sensorData.getHeartRate().getValue()); + pw.print(""); + pw.println(""); + } + if (sensorData.hasPower() + && sensorData.getPower().getState() == Sensor.SensorState.SENDING + && sensorData.getPower().hasValue()) { + pw.print(" "); + pw.print(""); + pw.print(""); + pw.print(sensorData.getPower().getValue()); + pw.print(""); + pw.println(""); + } + if (sensorData.hasCadence() + && sensorData.getCadence().getState() == Sensor.SensorState.SENDING + && sensorData.getCadence().hasValue()) { + pw.print(" "); + pw.print(sensorData.getCadence().getValue()); + pw.println(""); + } + } + } pw.println(" "); } 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 e97ddc73c..b3610bd44 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/services/TrackRecordingService.java +++ b/MyTracks/src/com/google/android/apps/mytracks/services/TrackRecordingService.java @@ -20,11 +20,16 @@ import static com.google.android.apps.mytracks.MyTracksConstants.RESUME_TRACK_EX import com.google.android.apps.mytracks.MyTracks; import com.google.android.apps.mytracks.MyTracksConstants; import com.google.android.apps.mytracks.MyTracksSettings; +import com.google.android.apps.mytracks.content.MyTracksLocation; import com.google.android.apps.mytracks.content.MyTracksProviderUtils; +import com.google.android.apps.mytracks.content.Sensor; import com.google.android.apps.mytracks.content.Track; import com.google.android.apps.mytracks.content.TracksColumns; import com.google.android.apps.mytracks.content.Waypoint; import com.google.android.apps.mytracks.content.WaypointsColumns; +import com.google.android.apps.mytracks.content.Sensor.SensorDataSet; +import com.google.android.apps.mytracks.services.sensors.SensorManager; +import com.google.android.apps.mytracks.services.sensors.SensorManagerFactory; import com.google.android.apps.mytracks.stats.TripStatistics; import com.google.android.apps.mytracks.stats.TripStatisticsBuilder; import com.google.android.apps.mytracks.util.ApiFeatures; @@ -111,6 +116,8 @@ public class TrackRecordingService extends Service implements LocationListener { private PeriodicTaskExecuter announcementExecuter; private SplitManager splitManager; + private SensorManager sensorManager; + private PreferenceManager prefManager; /** @@ -214,7 +221,14 @@ public class TrackRecordingService extends Service implements LocationListener { // Insert the new location: try { - Uri pointUri = providerUtils.insertTrackPoint(location, trackId); + Location locationToInsert = location; + if (sensorManager != null && sensorManager.isEnabled()) { + SensorDataSet sd = sensorManager.getSensorDataSet(); + if (sd != null && sensorManager.isDataValid()) { + locationToInsert = new MyTracksLocation(location, sd); + } + } + Uri pointUri = providerUtils.insertTrackPoint(locationToInsert, trackId); int pointId = Integer.parseInt(pointUri.getLastPathSegment()); // Update the current track: @@ -523,11 +537,15 @@ public class TrackRecordingService extends Service implements LocationListener { if (lastLocation != null) { distanceToLast = location.distanceTo(lastLocation); } + boolean hasSensorData = sensorManager != null + && sensorManager.isEnabled() + && sensorManager.getSensorDataSet() != null + && sensorManager.isDataValid(); // If the user has been stationary for two recording just record the first // two and ignore the rest. This code will only have an effect if the // maxRecordingDistance = 0 - if (distanceToLast == 0) { + if (distanceToLast == 0 && !hasSensorData) { if (isMoving) { Log.d(MyTracksConstants.TAG, "Found two identical locations."); isMoving = false; @@ -547,7 +565,8 @@ public class TrackRecordingService extends Service implements LocationListener { Log.d(MyTracksConstants.TAG, "Not recording. More than two identical locations."); } - } else if (distanceToLastRecorded > minRecordingDistance) { + } else if (distanceToLastRecorded > minRecordingDistance + || hasSensorData) { if (lastLocation != null && !isMoving) { // Last location was the last stationary location. Need to go back and // add it. @@ -627,9 +646,11 @@ public class TrackRecordingService extends Service implements LocationListener { locationManager = (LocationManager) getSystemService(LOCATION_SERVICE); splitManager = new SplitManager(this); + sensorManager = SensorManagerFactory.getSensorManager(this); prefManager = new PreferenceManager(this); registerLocationListener(); - /** + + /* * After 5 min, check every minute that location listener still is * registered and spit out additional debugging info to the logs: */ @@ -711,6 +732,10 @@ public class TrackRecordingService extends Service implements LocationListener { shutdownAnnouncer(); splitManager.shutdown(); splitManager = null; + if (sensorManager != null) { + sensorManager.onDestroy(); + sensorManager = null; + } // Make sure we have no indirect references to this service. locationManager = null; @@ -1022,6 +1047,28 @@ public class TrackRecordingService extends Service implements LocationListener { } service.onLocationChanged(loc); } + + @Override + public byte[] getSensorData() { + if (service.sensorManager == null) { + Log.d(MyTracksConstants.TAG, "No sensor manager for data."); + return null; + } + if (service.sensorManager.getSensorDataSet() == null) { + Log.d(MyTracksConstants.TAG, "Sensor data set is null."); + return null; + } + return service.sensorManager.getSensorDataSet().toByteArray(); + } + + @Override + public int getSensorState() { + if (service.sensorManager == null) { + Log.d(MyTracksConstants.TAG, "No sensor manager for data."); + return Sensor.SensorState.NONE.getNumber(); + } + return service.sensorManager.getSensorState().getNumber(); + } } public long startNewTrack() { @@ -1054,6 +1101,9 @@ public class TrackRecordingService extends Service implements LocationListener { showNotification(); registerLocationListener(); splitManager.restore(); + if (sensorManager != null) { + sensorManager.onStartTrack(); + } // Reset the number of auto-resume retries. setAutoResumeTrackRetries( @@ -1096,6 +1146,11 @@ public class TrackRecordingService extends Service implements LocationListener { showNotification(); long recordedTrackId = recordingTrackId; prefManager.setRecordingTrack(recordingTrackId = -1); + + if (sensorManager != null) { + sensorManager.shutdown(); + } + releaseWakeLock(); // Notify the world that we're no longer recording. 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 new file mode 100644 index 000000000..986ae773b --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/services/sensors/BluetoothConnectionManager.java @@ -0,0 +1,432 @@ +/* + * Copyright (C) 2010 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.android.apps.mytracks.services.sensors; + +import com.google.android.apps.mytracks.MyTracksConstants; +import com.google.android.apps.mytracks.content.Sensor; + +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothSocket; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.util.Log; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +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. + * + * @author Sandor Dornbush + */ +public class BluetoothConnectionManager { + // Unique UUID for this application + private static final UUID SPP_UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB"); + + private MessageParser parser; + + // Member fields + private final BluetoothAdapter adapter; + private final Handler handler; + 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 context The UI Activity Context + * @param handler A Handler to send messages back to the UI Activity + */ + public BluetoothConnectionManager(Handler handler, MessageParser parser) { + this.adapter = BluetoothAdapter.getDefaultAdapter(); + this.state = Sensor.SensorState.NONE; + this.handler = handler; + this.parser = parser; + } + + /** + * Set the current state of the sensor connection + * + * @param state An integer defining the current connection state + */ + private synchronized void setState(Sensor.SensorState state) { + // TODO pretty print this. + Log.d(MyTracksConstants.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(); + } + + /** + * Return the current connection state. + */ + public synchronized Sensor.SensorState getState() { + return state; + } + + /** + * Start the sensor service. Specifically start AcceptThread to begin a session + * in listening (server) mode. Called by the Activity onResume() + */ + public synchronized void start() { + Log.d(MyTracksConstants.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; + } + + setState(Sensor.SensorState.NONE); + } + + /** + * Start the ConnectThread to initiate a connection to a remote device. + * + * @param device The BluetoothDevice to connect + */ + public synchronized void connect(BluetoothDevice device) { + Log.d(MyTracksConstants.TAG, "connect to: " + device); + + // Cancel any thread attempting to make a connection + if (state == Sensor.SensorState.CONNECTING) { + 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); + 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 + */ + public synchronized void connected(BluetoothSocket socket, + BluetoothDevice device) { + Log.d(MyTracksConstants.TAG, "connected"); + + // 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.start(); + + // Send the name of the connected device back to the UI Activity + Message msg = handler.obtainMessage(MESSAGE_DEVICE_NAME); + Bundle bundle = new Bundle(); + bundle.putString(DEVICE_NAME, device.getName()); + msg.setData(bundle); + handler.sendMessage(msg); + + setState(Sensor.SensorState.CONNECTED); + } + + /** + * Stop all threads + */ + public synchronized void stop() { + Log.d(MyTracksConstants.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(MyTracksConstants.TAG, "Bluetooth connection failed."); + } + + /** + * Indicate that the connection was lost and notify the UI Activity. + */ + private void connectionLost() { + setState(Sensor.SensorState.DISCONNECTED); + Log.i(MyTracksConstants.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. + */ + private class ConnectThread extends Thread { + private final BluetoothSocket socket; + private final BluetoothDevice device; + + public ConnectThread(BluetoothDevice device) { + setName("ConnectThread-" + device.getName()); + this.device = device; + BluetoothSocket tmp = null; + + // Get a BluetoothSocket for a connection with the + // given BluetoothDevice + try { + tmp = getSocket(); + } catch (IOException e) { + Log.e(MyTracksConstants.TAG, "create() failed", e); + } + socket = tmp; + } + + private BluetoothSocket getSocket() throws IOException { + try { + Class c = device.getClass(); + Method insecure = c.getMethod("createInsecureRfcommSocket", + Integer.class); + insecure.setAccessible(true); + return (BluetoothSocket) insecure.invoke(device, 1); + } catch (SecurityException e) { + Log.e(MyTracksConstants.TAG, "Unable to get insecure connect.", e); + } catch (NoSuchMethodException e) { + Log.e(MyTracksConstants.TAG, "Unable to get insecure connect.", e); + } catch (IllegalArgumentException e) { + Log.e(MyTracksConstants.TAG, "Unable to get insecure connect.", e); + } catch (IllegalAccessException e) { + Log.e(MyTracksConstants.TAG, "Unable to get insecure connect.", e); + } catch (InvocationTargetException e) { + Log.e(MyTracksConstants.TAG, "Unable to get insecure connect.", e); + } + return device.createRfcommSocketToServiceRecord(SPP_UUID); + } + + @Override + public void run() { + Log.d(MyTracksConstants.TAG, "BEGIN mConnectThread"); + + // 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 + try { + socket.close(); + } catch (IOException e2) { + Log.e(MyTracksConstants.TAG, + "unable to close() socket during connection failure", e2); + } + // Start the service over to restart listening mode + BluetoothConnectionManager.this.start(); + return; + } + + // Reset the ConnectThread because we're done + synchronized (BluetoothConnectionManager.this) { + connectThread = null; + } + + // Start the connected thread + connected(socket, device); + } + + public void cancel() { + try { + socket.close(); + } catch (IOException e) { + Log.e(MyTracksConstants.TAG, "close() of connect socket failed", e); + } + } + } + + /** + * This thread runs during a connection with a remote device. It handles all + * incoming and outgoing transmissions. + */ + private class ConnectedThread extends Thread { + private final BluetoothSocket btSocket; + private final InputStream mmInStream; + private final OutputStream mmOutStream; + + public ConnectedThread(BluetoothSocket socket) { + Log.d(MyTracksConstants.TAG, "create ConnectedThread"); + btSocket = socket; + InputStream tmpIn = null; + OutputStream tmpOut = null; + + // Get the BluetoothSocket input and output streams + try { + tmpIn = socket.getInputStream(); + tmpOut = socket.getOutputStream(); + } catch (IOException e) { + Log.e(MyTracksConstants.TAG, "temp sockets not created", e); + } + + mmInStream = tmpIn; + mmOutStream = tmpOut; + } + + @Override + public void run() { + Log.i(MyTracksConstants.TAG, "BEGIN mConnectedThread"); + byte[] buffer = new byte[parser.getFrameSize()]; + int bytes; + int offset = 0; + + // Keep listening to the InputStream while connected + while (true) { + try { + // Read from the InputStream + bytes = mmInStream.read(buffer, offset, parser.getFrameSize() - offset); + + if (bytes < 0) { + throw new IOException("EOF reached"); + } + + offset += bytes; + + if (offset != parser.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(MyTracksConstants.TAG, "Misaligned data, found new message at " + + index + " recovering..."); + continue; + } + Log.w(MyTracksConstants.TAG, "Could not find valid data, dropping data"); + offset = 0; + continue; + } + + offset = 0; + + // Send the obtained bytes to the UI Activity + handler.obtainMessage(MESSAGE_READ, bytes, -1, buffer) + .sendToTarget(); + } catch (IOException e) { + Log.e(MyTracksConstants.TAG, "disconnected", e); + connectionLost(); + break; + } + } + } + + /** + * Write to the connected OutStream. + * + * @param buffer The bytes to write + */ + 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(MyTracksConstants.TAG, "Exception during write", e); + } + } + + public void cancel() { + try { + btSocket.close(); + } catch (IOException e) { + Log.e(MyTracksConstants.TAG, "close() of connect socket failed", 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 new file mode 100644 index 000000000..32b16d1e4 --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/services/sensors/BluetoothSensorManager.java @@ -0,0 +1,166 @@ +/* + * Copyright 2010 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.google.android.apps.mytracks.services.sensors; + +import com.google.android.apps.mytracks.MyTracksConstants; +import com.google.android.apps.mytracks.MyTracksSettings; +import com.google.android.apps.mytracks.content.Sensor; +import com.google.android.maps.mytracks.R; + +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.content.Context; +import android.content.SharedPreferences; +import android.os.Handler; +import android.os.Message; +import android.util.Log; +import android.widget.Toast; + +/** + * Manage the connection to a bluetooth sensor. + * + * @author Sandor Dornbush + */ +public class BluetoothSensorManager extends SensorManager { + + // Local Bluetooth adapter + private BluetoothAdapter bluetoothAdapter = null; + + // 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; + // Get local Bluetooth adapter + bluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); + // If BT is not available or not enabled quit. + if (!isEnabled()) { + return; + } + + setupSensor(); + } + + private void setupSensor() { + Log.d(MyTracksConstants.TAG, "setupSensor()"); + + // Initialize the BluetoothSensorAdapter to perform bluetooth connections. + connectionManager = new BluetoothConnectionManager(messageHandler, parser); + } + + public boolean isEnabled() { + return bluetoothAdapter != null && bluetoothAdapter.isEnabled(); + } + + public void setupChannel() { + if (!isEnabled() || connectionManager == null) { + Log.w(MyTracksConstants.TAG, "Disabled manager onStartTrack"); + return; + } + SharedPreferences prefs = + context.getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0); + String address = + prefs.getString(context.getString(R.string.bluetooth_sensor_key), null); + if (address == null) { + return; + } + Log.w(MyTracksConstants.TAG, "Connecting to bluetooth sensor: " + address); + // Get the BluetoothDevice object + BluetoothDevice device = bluetoothAdapter.getRemoteDevice(address); + // 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(MyTracksConstants.TAG, "Disabled manager onStartTrack"); + connectionManager.start(); + } + } + } + + public void onDestroy() { + // Stop the Bluetooth sensor services + if (connectionManager != null) { + connectionManager.stop(); + } + } + + public Sensor.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() { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case BluetoothConnectionManager.MESSAGE_STATE_CHANGE: + // TODO should we update the SensorManager state var? + Log.i(MyTracksConstants.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(MyTracksConstants.TAG, "MESSAGE_READ: " + sensorDataSet.toString()); + } catch (IllegalArgumentException iae) { + sensorDataSet = null; + Log.i(MyTracksConstants.TAG, + "Got bad sensor data: " + new String(readBuf, 0, readBuf.length), + iae); + } catch (RuntimeException re) { + sensorDataSet = null; + Log.i(MyTracksConstants.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/MessageParser.java b/MyTracks/src/com/google/android/apps/mytracks/services/sensors/MessageParser.java new file mode 100644 index 000000000..f490df048 --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/services/sensors/MessageParser.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2010 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.android.apps.mytracks.services.sensors; + +import com.google.android.apps.mytracks.content.Sensor; + +/** + * An interface for parsing a byte array to a SensorData object. + * + * @author Sandor Dornbush + */ +public interface MessageParser { + + public int getFrameSize(); + + public Sensor.SensorDataSet parseBuffer(byte[] readBuff); + + public boolean isValid(byte[] buffer); + + public int findNextAlignment(byte[] buffer); +} 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 new file mode 100644 index 000000000..49b421b50 --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/services/sensors/SensorManager.java @@ -0,0 +1,134 @@ +/* + * Copyright 2010 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.google.android.apps.mytracks.services.sensors; + +import java.util.Timer; +import java.util.TimerTask; + +import android.util.Log; + +import com.google.android.apps.mytracks.MyTracksConstants; +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; + + /** + * Time to wait after a time out to retry. + */ + 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 + public void run() { + Log.i(MyTracksConstants.TAG, + "SensorManager state: " + getSensorState()); + switch (getSensorState()) { + case CONNECTING: + long age = System.currentTimeMillis() - getSensorStateTimestamp(); + if (age > 2 * RETRY_PERIOD) { + Log.i(MyTracksConstants.TAG, "Retrying connecting SensorManager."); + setupChannel(); + } + break; + case DISCONNECTED: + Log.i(MyTracksConstants.TAG, + "Re-registering disconnected SensoManager."); + setupChannel(); + 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 + */ + 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. + */ + public void onStartTrack() { + setupChannel(); + timer.schedule(checkSensorManager, RETRY_PERIOD, RETRY_PERIOD); + } + + public abstract void setupChannel(); + + public void shutdown() { + timer.cancel(); + onDestroy(); + } + + /** + * This is called when my tracks stops recording. + * This is the place to shutdown any open connections. + */ + 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) { + this.sensorState = sensorState; + } + + /** + * Return the current sensor state. + * @return The current sensor state. + */ + public Sensor.SensorState getSensorState() { + return sensorState; + } + + public long getSensorStateTimestamp() { + return sensorStateTimestamp; + } + + /** + * @return True if the data is recent enough to be considered valid. + */ + public boolean isDataValid() { + return (System.currentTimeMillis() - getSensorDataSet().getCreationTime()) < MAX_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 new file mode 100644 index 000000000..01a8d8bf5 --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/services/sensors/SensorManagerFactory.java @@ -0,0 +1,61 @@ +/* + * Copyright 2009 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.android.apps.mytracks.services.sensors; + +import com.google.android.apps.mytracks.MyTracksConstants; +import com.google.android.apps.mytracks.MyTracksSettings; +import com.google.android.maps.mytracks.R; + +import android.content.Context; +import android.content.SharedPreferences; +import android.util.Log; + +/** + * A factory of SensorManagers. + * + * @author Sandor Dornbush + */ +public class SensorManagerFactory { + + private SensorManagerFactory() { + } + + /** + * Get a new sensor manager. + * @param context Context to fetch system preferences. + * @return The sensor manager that corresponds to the sensor type setting. + */ + public static SensorManager getSensorManager(Context context) { + SharedPreferences prefs = + context.getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0); + if (prefs == null) { + return null; + } + + String sensor = prefs.getString(context.getString(R.string.sensor_type_key), + null); + Log.i(MyTracksConstants.TAG, "Creating sensor of type: " + sensor); + + if (sensor == null) { + return null; + } else if (sensor.equals(context.getString(R.string.zephyr_sensor_type))) { + return new ZephyrSensorManager(context); + } else { + return null; + } + } +} diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/sensors/SensorUtils.java b/MyTracks/src/com/google/android/apps/mytracks/services/sensors/SensorUtils.java new file mode 100644 index 000000000..28f2f4fb8 --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/services/sensors/SensorUtils.java @@ -0,0 +1,61 @@ +/* + * Copyright 2010 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.google.android.apps.mytracks.services.sensors; + +import android.content.Context; + +import com.google.android.apps.mytracks.content.Sensor; +import com.google.android.maps.mytracks.R; + +/** + * A collection of methods for message parsers. + * + * @author Sandor Dornbush + */ +public class SensorUtils { + + private SensorUtils() { + } + + /** + * Extract one unsigned short from a big endian byte array. + * @param buffer the buffer to extract the short from + * @param index the first byte to be interpreted as part of the short + * @return The unsigned short at the given index in the buffer + */ + public static int unsignedShortToInt(byte[] buffer, int index) { + int r = (buffer[index] & 0xFF) << 8; + r |= buffer[index + 1] & 0xFF; + return r; + } + + public static String getStateAsString(Sensor.SensorState state, Context c) { + switch (state) { + case NONE: + return c.getString(R.string.none); + case CONNECTING: + return c.getString(R.string.connecting); + case CONNECTED: + return c.getString(R.string.connected); + case DISCONNECTED: + return c.getString(R.string.disconnected); + case SENDING: + return c.getString(R.string.sending); + default: + return ""; + } + } +} diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/sensors/ZephyrMessageParser.java b/MyTracks/src/com/google/android/apps/mytracks/services/sensors/ZephyrMessageParser.java new file mode 100644 index 000000000..8cf8f951f --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/services/sensors/ZephyrMessageParser.java @@ -0,0 +1,76 @@ +/* + * Copyright 2010 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.google.android.apps.mytracks.services.sensors; + +import android.util.Log; + +import com.google.android.apps.mytracks.MyTracksConstants; +import com.google.android.apps.mytracks.content.Sensor; + +/** + * An implementation of a SensorData parser for Zephyr HRM. + * + * @author Sandor Dornbush + */ +public class ZephyrMessageParser implements MessageParser { + + @Override + public Sensor.SensorDataSet parseBuffer(byte[] buffer) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < buffer.length; i++) { + sb.append(String.format("%02X", buffer[i])); + } + Log.w(MyTracksConstants.TAG, "Got zephyr data: " + sb); + // The provided units are 1/16 strides per minute. + int cadence = SensorUtils.unsignedShortToInt(buffer, 56); + // Heart Rate + Sensor.SensorData.Builder b = Sensor.SensorData.newBuilder() + .setValue(buffer[12] & 0xFF) + .setState(Sensor.SensorState.SENDING); + // Cadence + //.setCadence(cadence / 16) + //.build(); + Sensor.SensorDataSet sds = + Sensor.SensorDataSet.newBuilder() + .setCreationTime(System.currentTimeMillis()) + .setHeartRate(b) + .build(); + + return sds; + } + + @Override + public boolean isValid(byte[] buffer) { + // TODO crc etc. + return buffer[0] == 0x02 && buffer[59] == 0x03; + } + + @Override + public int getFrameSize() { + return 60; + } + + @Override + public int findNextAlignment(byte[] buffer) { + // TODO test or understand this code. + for (int i = 0; i < buffer.length - 1; i++) { + if (buffer[i] == 0x03 && buffer[i+1] == 0x02) { + return i; + } + } + return -1; + } +} 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 new file mode 100644 index 000000000..c5ff64839 --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/services/sensors/ZephyrSensorManager.java @@ -0,0 +1,26 @@ +/* + * Copyright 2010 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.google.android.apps.mytracks.services.sensors; + +import android.content.Context; + +public class ZephyrSensorManager extends BluetoothSensorManager { + + public ZephyrSensorManager(Context context) { + super(context, new ZephyrMessageParser()); + } + +} diff --git a/MyTracks/src/com/google/android/apps/mytracks/util/BluetoothDeviceUtils.java b/MyTracks/src/com/google/android/apps/mytracks/util/BluetoothDeviceUtils.java new file mode 100644 index 000000000..a819a8473 --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/util/BluetoothDeviceUtils.java @@ -0,0 +1,200 @@ +/* + * Copyright 2010 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.google.android.apps.mytracks.util; + +import com.google.android.apps.mytracks.MyTracksConstants; + +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothClass; +import android.bluetooth.BluetoothDevice; +import android.os.Build; +import android.util.Log; + +import java.util.List; +import java.util.Set; + +/** + * Utilities for dealing with bluetooth devices. + * This can be used safely even in systems that don't support bluetooth, + * in which case a dummy implementation will be used. + * + * @author Rodrigo Damazio + */ +public abstract class BluetoothDeviceUtils { + public static final String ANY_DEVICE = "any"; + private static BluetoothDeviceUtils instance; + + /** + * Dummy implementation, for systems that don't support bluetooth. + */ + private static class DummyImpl extends BluetoothDeviceUtils { + @Override + public void populateDeviceLists(List deviceNames, List deviceAddresses) { + // Do nothing - no devices to add + } + + @Override + public BluetoothDevice findDeviceMatching(String targetDeviceAddress) { + return null; + } + } + + /** + * Real implementation, for systems that DO support bluetooth. + */ + private static class RealImpl extends BluetoothDeviceUtils { + private final BluetoothAdapter bluetoothAdapter; + + public RealImpl() { + bluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); + + if (bluetoothAdapter == null) { + throw new IllegalStateException("Unable to get bluetooth adapter"); + } + } + + @Override + public void populateDeviceLists(List deviceNames, List deviceAddresses) { + ensureNotDiscovering(); + + Set pairedDevices = bluetoothAdapter.getBondedDevices(); + for (BluetoothDevice device : pairedDevices) { + BluetoothClass bluetoothClass = device.getBluetoothClass(); + if (bluetoothClass != null) { + // Not really sure what we want, but I know what we don't want. + switch(bluetoothClass.getMajorDeviceClass()) { + case BluetoothClass.Device.Major.COMPUTER: + case BluetoothClass.Device.Major.PHONE: + break; + default: + deviceAddresses.add(device.getAddress()); + deviceNames.add(device.getName()); + } + } + } + } + + @Override + public BluetoothDevice findDeviceMatching(String targetDeviceAddress) { + if (targetDeviceAddress.equals(ANY_DEVICE)) { + return findAnyDevice(); + } else { + return findDeviceByAddress(targetDeviceAddress); + } + } + + /** + * Finds and returns the first suitable bluetooth sensor. + */ + private BluetoothDevice findAnyDevice() { + ensureNotDiscovering(); + + Set pairedDevices = bluetoothAdapter.getBondedDevices(); + for (BluetoothDevice device : pairedDevices) { + // Look for the first paired computer device + if (isSuitableDevice(device)) { + return device; + } + } + + return null; + } + + /** + * Finds and returns a device with the given address, or null if it's not + * a suitable sensor. + */ + private BluetoothDevice findDeviceByAddress(String targetDeviceAddress) { + ensureNotDiscovering(); + + BluetoothDevice device = bluetoothAdapter.getRemoteDevice(targetDeviceAddress); + if (isSuitableDevice(device)) { + return device; + } + + return null; + } + + /** + * Ensures the bluetooth adapter is not in discovery mode. + */ + private void ensureNotDiscovering() { + // If it's in discovery mode, cancel that for now. + bluetoothAdapter.cancelDiscovery(); + } + + /** + * Checks whether the given device is a suitable sensor. + * + * @param device the device to check + * @return true if it's suitable, false otherwise + */ + private boolean isSuitableDevice(BluetoothDevice device) { + // Check that the device is bonded + if (device.getBondState() != BluetoothDevice.BOND_BONDED) { + return false; + } + + return true; + } + } + + /** + * Returns the proper (singleton) instance of this class. + */ + public static BluetoothDeviceUtils getInstance() { + if (instance == null) { + if (!isBluetoothMethodSupported()) { + Log.d(MyTracksConstants.TAG, "Using dummy bluetooth utils"); + instance = new DummyImpl(); + } else { + Log.d(MyTracksConstants.TAG, "Using real bluetooth utils"); + try { + instance = new RealImpl(); + } catch (IllegalStateException ise) { + Log.w(MyTracksConstants.TAG, "Oops, I mean, using dummy bluetooth utils", ise); + instance = new DummyImpl(); + } + } + } + return instance; + } + + /** + * Populates the given lists with the names and addresses of all suitable + * bluetooth devices. + * + * @param deviceNames the list to populate with user-visible names + * @param deviceAddresses the list to populate with device addresses + */ + public abstract void populateDeviceLists(List deviceNames, List deviceAddresses); + + /** + * Finds the bluetooth device with the given address. + * + * @param targetDeviceAddress the address of the device, or + * {@link #ANY_DEVICE} for using the first suitable device + * @return the device's descriptor, or null if not found + */ + public abstract BluetoothDevice findDeviceMatching(String targetDeviceAddress); + + /** + * @return whether the bluetooth method is supported on this device + */ + public static boolean isBluetoothMethodSupported() { + return Integer.parseInt(Build.VERSION.SDK) >= 5; + } +} diff --git a/MyTracksLib/.classpath b/MyTracksLib/.classpath index 609aa00eb..67679b6b2 100644 --- a/MyTracksLib/.classpath +++ b/MyTracksLib/.classpath @@ -2,6 +2,7 @@ + diff --git a/MyTracksLib/libs/protobuf-java-2.3.0-lite.jar b/MyTracksLib/libs/protobuf-java-2.3.0-lite.jar new file mode 100644 index 000000000..9b6605da7 Binary files /dev/null and b/MyTracksLib/libs/protobuf-java-2.3.0-lite.jar differ diff --git a/MyTracksLib/proto/sensor.proto b/MyTracksLib/proto/sensor.proto new file mode 100644 index 000000000..b0ea1ac11 --- /dev/null +++ b/MyTracksLib/proto/sensor.proto @@ -0,0 +1,53 @@ +// Copyright 2010 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. + +package com.google.android.apps.mytracks.content; + +option optimize_for = LITE_RUNTIME; + +// An enumeration of the state of a single sensor. +enum SensorState { + // There is no sensor. + NONE = 1; + + // Now initiating an outgoing connection. + CONNECTING = 2; + + // Now connected to a remote device. + CONNECTED = 3; + + // A state where the sensor is not connected. + DISCONNECTED = 4; + + // This is the only state where the sensor data can be used. + SENDING = 5; +}; + +// The state and possible reading for one integer sensor. +message SensorData { + required SensorState state = 1 [default = NONE]; + + // Value is only defined if state = SENDING. + optional int32 value = 2; +}; + +// A message to encapsulate sensor readings associated with a track point. +message SensorDataSet { + // This is a required field. + optional uint64 creation_time = 1 [default = 0]; + + optional SensorData heart_rate = 2; + optional SensorData cadence = 3; + optional SensorData power = 4; +} \ No newline at end of file diff --git a/MyTracksLib/src/com/google/android/apps/mytracks/content/MyTracksLocation.java b/MyTracksLib/src/com/google/android/apps/mytracks/content/MyTracksLocation.java new file mode 100644 index 000000000..4566225d5 --- /dev/null +++ b/MyTracksLib/src/com/google/android/apps/mytracks/content/MyTracksLocation.java @@ -0,0 +1,64 @@ +/* + * Copyright 2010 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.google.android.apps.mytracks.content; + +import android.location.Location; + +/** + * This class extends the standard Android location with extra information. + * + * @author Sandor Dornbush + */ +public class MyTracksLocation extends Location { + + private Sensor.SensorDataSet sensorDataSet = null; + + /** + * The id of this location from the provider. + */ + private int id = -1; + + public MyTracksLocation(Location location, Sensor.SensorDataSet sd) { + super(location); + this.sensorDataSet = sd; + } + + public MyTracksLocation(String string) { + super(string); + } + + public Sensor.SensorDataSet getSensorDataSet() { + return sensorDataSet; + } + + public void setSensorData(Sensor.SensorDataSet sensorData) { + this.sensorDataSet = sensorData; + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public void reset() { + super.reset(); + sensorDataSet = null; + id = -1; + } +} diff --git a/MyTracksLib/src/com/google/android/apps/mytracks/content/MyTracksProviderUtilsImpl.java b/MyTracksLib/src/com/google/android/apps/mytracks/content/MyTracksProviderUtilsImpl.java index c3d8a5233..f9e466dce 100644 --- a/MyTracksLib/src/com/google/android/apps/mytracks/content/MyTracksProviderUtilsImpl.java +++ b/MyTracksLib/src/com/google/android/apps/mytracks/content/MyTracksProviderUtilsImpl.java @@ -16,7 +16,9 @@ package com.google.android.apps.mytracks.content; import static com.google.android.apps.mytracks.lib.MyTracksLibConstants.*; +import com.google.android.apps.mytracks.content.Sensor; import com.google.android.apps.mytracks.stats.TripStatistics; +import com.google.protobuf.InvalidProtocolBufferException; import android.content.ContentResolver; import android.content.ContentValues; @@ -75,6 +77,12 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { if (location.hasSpeed()) { values.put(TrackPointsColumns.SPEED, location.getSpeed()); } + if (location instanceof MyTracksLocation) { + MyTracksLocation mtLocation = (MyTracksLocation) location; + if (mtLocation.getSensorDataSet() != null) { + values.put(TrackPointsColumns.SENSOR, mtLocation.getSensorDataSet().toByteArray()); + } + } return values; } @@ -172,7 +180,7 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { @Override public Location createLocation(Cursor cursor) { - Location location = new Location(""); + Location location = new MyTracksLocation(""); fillLocation(cursor, location); return location; } @@ -189,6 +197,7 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { int idxBearing = cursor.getColumnIndexOrThrow(TrackPointsColumns.BEARING); int idxAccuracy = cursor.getColumnIndexOrThrow(TrackPointsColumns.ACCURACY); int idxSpeed = cursor.getColumnIndexOrThrow(TrackPointsColumns.SPEED); + int idxSensor = cursor.getColumnIndexOrThrow(TrackPointsColumns.SENSOR); if (!cursor.isNull(idxLatitude)) { location.setLatitude(1. * cursor.getInt(idxLatitude) / 1E6); @@ -211,6 +220,18 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { if (!cursor.isNull(idxAccuracy)) { location.setAccuracy(cursor.getFloat(idxAccuracy)); } + if (location instanceof MyTracksLocation && + !cursor.isNull(idxSensor)) { + MyTracksLocation mtLocation = (MyTracksLocation) location; + // TODO get the right buffer. + Sensor.SensorDataSet sensorData; + try { + sensorData = Sensor.SensorDataSet.parseFrom(cursor.getBlob(idxSensor)); + mtLocation.setSensorData(sensorData); + } catch (InvalidProtocolBufferException e) { + Log.w(TAG, "Failed to parse sensor data.", e); + } + } } @Override @@ -450,7 +471,8 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { @Override public void deleteAllTracks() { contentResolver.delete(TracksColumns.CONTENT_URI, null, null); - contentResolver.delete(TrackPointsColumns.CONTENT_URI, null, null); + contentResolver.delete(TrackPointsColumns.CONTENT_URI, + null, null); contentResolver.delete(WaypointsColumns.CONTENT_URI, null, null); } @@ -464,7 +486,8 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { } contentResolver.delete(WaypointsColumns.CONTENT_URI, WaypointsColumns.TRACKID + "=" + trackId, null); - contentResolver.delete(TracksColumns.CONTENT_URI, "_id=" + trackId, null); + contentResolver.delete( + TracksColumns.CONTENT_URI, "_id=" + trackId, null); } @Override diff --git a/MyTracksLib/src/com/google/android/apps/mytracks/content/Sensor.java b/MyTracksLib/src/com/google/android/apps/mytracks/content/Sensor.java new file mode 100644 index 000000000..54a9df31d --- /dev/null +++ b/MyTracksLib/src/com/google/android/apps/mytracks/content/Sensor.java @@ -0,0 +1,816 @@ +// Generated by the protocol buffer compiler. DO NOT EDIT! +// source: proto/sensor.proto + +package com.google.android.apps.mytracks.content; + +public final class Sensor { + private Sensor() {} + public static void registerAllExtensions( + com.google.protobuf.ExtensionRegistryLite registry) { + } + public enum SensorState + implements com.google.protobuf.Internal.EnumLite { + NONE(0, 1), + CONNECTING(1, 2), + CONNECTED(2, 3), + DISCONNECTED(3, 4), + SENDING(4, 5), + ; + + + public final int getNumber() { return value; } + + public static SensorState valueOf(int value) { + switch (value) { + case 1: return NONE; + case 2: return CONNECTING; + case 3: return CONNECTED; + case 4: return DISCONNECTED; + case 5: return SENDING; + default: return null; + } + } + + public static com.google.protobuf.Internal.EnumLiteMap + internalGetValueMap() { + return internalValueMap; + } + private static com.google.protobuf.Internal.EnumLiteMap + internalValueMap = + new com.google.protobuf.Internal.EnumLiteMap() { + public SensorState findValueByNumber(int number) { + return SensorState.valueOf(number) + ; } + }; + + private final int index; + private final int value; + private SensorState(int index, int value) { + this.index = index; + this.value = value; + } + + // @@protoc_insertion_point(enum_scope:com.google.android.apps.mytracks.content.SensorState) + } + + public static final class SensorData extends + com.google.protobuf.GeneratedMessageLite { + // Use SensorData.newBuilder() to construct. + private SensorData() { + initFields(); + } + private SensorData(boolean noInit) {} + + private static final SensorData defaultInstance; + public static SensorData getDefaultInstance() { + return defaultInstance; + } + + public SensorData getDefaultInstanceForType() { + return defaultInstance; + } + + // required .com.google.android.apps.mytracks.content.SensorState state = 1 [default = NONE]; + public static final int STATE_FIELD_NUMBER = 1; + private boolean hasState; + private com.google.android.apps.mytracks.content.Sensor.SensorState state_; + public boolean hasState() { return hasState; } + public com.google.android.apps.mytracks.content.Sensor.SensorState getState() { return state_; } + + // optional int32 value = 2; + public static final int VALUE_FIELD_NUMBER = 2; + private boolean hasValue; + private int value_ = 0; + public boolean hasValue() { return hasValue; } + public int getValue() { return value_; } + + private void initFields() { + state_ = com.google.android.apps.mytracks.content.Sensor.SensorState.NONE; + } + public final boolean isInitialized() { + if (!hasState) return false; + return true; + } + + public void writeTo(com.google.protobuf.CodedOutputStream output) + throws java.io.IOException { + getSerializedSize(); + if (hasState()) { + output.writeEnum(1, getState().getNumber()); + } + if (hasValue()) { + output.writeInt32(2, getValue()); + } + } + + private int memoizedSerializedSize = -1; + public int getSerializedSize() { + int size = memoizedSerializedSize; + if (size != -1) return size; + + size = 0; + if (hasState()) { + size += com.google.protobuf.CodedOutputStream + .computeEnumSize(1, getState().getNumber()); + } + if (hasValue()) { + size += com.google.protobuf.CodedOutputStream + .computeInt32Size(2, getValue()); + } + memoizedSerializedSize = size; + return size; + } + + public static com.google.android.apps.mytracks.content.Sensor.SensorData parseFrom( + com.google.protobuf.ByteString data) + throws com.google.protobuf.InvalidProtocolBufferException { + return newBuilder().mergeFrom(data).buildParsed(); + } + public static com.google.android.apps.mytracks.content.Sensor.SensorData parseFrom( + com.google.protobuf.ByteString data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return newBuilder().mergeFrom(data, extensionRegistry) + .buildParsed(); + } + public static com.google.android.apps.mytracks.content.Sensor.SensorData parseFrom(byte[] data) + throws com.google.protobuf.InvalidProtocolBufferException { + return newBuilder().mergeFrom(data).buildParsed(); + } + public static com.google.android.apps.mytracks.content.Sensor.SensorData parseFrom( + byte[] data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return newBuilder().mergeFrom(data, extensionRegistry) + .buildParsed(); + } + public static com.google.android.apps.mytracks.content.Sensor.SensorData parseFrom(java.io.InputStream input) + throws java.io.IOException { + return newBuilder().mergeFrom(input).buildParsed(); + } + public static com.google.android.apps.mytracks.content.Sensor.SensorData parseFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return newBuilder().mergeFrom(input, extensionRegistry) + .buildParsed(); + } + public static com.google.android.apps.mytracks.content.Sensor.SensorData parseDelimitedFrom(java.io.InputStream input) + throws java.io.IOException { + Builder builder = newBuilder(); + if (builder.mergeDelimitedFrom(input)) { + return builder.buildParsed(); + } else { + return null; + } + } + public static com.google.android.apps.mytracks.content.Sensor.SensorData parseDelimitedFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + Builder builder = newBuilder(); + if (builder.mergeDelimitedFrom(input, extensionRegistry)) { + return builder.buildParsed(); + } else { + return null; + } + } + public static com.google.android.apps.mytracks.content.Sensor.SensorData parseFrom( + com.google.protobuf.CodedInputStream input) + throws java.io.IOException { + return newBuilder().mergeFrom(input).buildParsed(); + } + public static com.google.android.apps.mytracks.content.Sensor.SensorData parseFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return newBuilder().mergeFrom(input, extensionRegistry) + .buildParsed(); + } + + public static Builder newBuilder() { return Builder.create(); } + public Builder newBuilderForType() { return newBuilder(); } + public static Builder newBuilder(com.google.android.apps.mytracks.content.Sensor.SensorData prototype) { + return newBuilder().mergeFrom(prototype); + } + public Builder toBuilder() { return newBuilder(this); } + + public static final class Builder extends + com.google.protobuf.GeneratedMessageLite.Builder< + com.google.android.apps.mytracks.content.Sensor.SensorData, Builder> { + private com.google.android.apps.mytracks.content.Sensor.SensorData result; + + // Construct using com.google.android.apps.mytracks.content.Sensor.SensorData.newBuilder() + private Builder() {} + + private static Builder create() { + Builder builder = new Builder(); + builder.result = new com.google.android.apps.mytracks.content.Sensor.SensorData(); + return builder; + } + + protected com.google.android.apps.mytracks.content.Sensor.SensorData internalGetResult() { + return result; + } + + public Builder clear() { + if (result == null) { + throw new IllegalStateException( + "Cannot call clear() after build()."); + } + result = new com.google.android.apps.mytracks.content.Sensor.SensorData(); + return this; + } + + public Builder clone() { + return create().mergeFrom(result); + } + + public com.google.android.apps.mytracks.content.Sensor.SensorData getDefaultInstanceForType() { + return com.google.android.apps.mytracks.content.Sensor.SensorData.getDefaultInstance(); + } + + public boolean isInitialized() { + return result.isInitialized(); + } + public com.google.android.apps.mytracks.content.Sensor.SensorData build() { + if (result != null && !isInitialized()) { + throw newUninitializedMessageException(result); + } + return buildPartial(); + } + + private com.google.android.apps.mytracks.content.Sensor.SensorData buildParsed() + throws com.google.protobuf.InvalidProtocolBufferException { + if (!isInitialized()) { + throw newUninitializedMessageException( + result).asInvalidProtocolBufferException(); + } + return buildPartial(); + } + + public com.google.android.apps.mytracks.content.Sensor.SensorData buildPartial() { + if (result == null) { + throw new IllegalStateException( + "build() has already been called on this Builder."); + } + com.google.android.apps.mytracks.content.Sensor.SensorData returnMe = result; + result = null; + return returnMe; + } + + public Builder mergeFrom(com.google.android.apps.mytracks.content.Sensor.SensorData other) { + if (other == com.google.android.apps.mytracks.content.Sensor.SensorData.getDefaultInstance()) return this; + if (other.hasState()) { + setState(other.getState()); + } + if (other.hasValue()) { + setValue(other.getValue()); + } + return this; + } + + public Builder mergeFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + while (true) { + int tag = input.readTag(); + switch (tag) { + case 0: + return this; + default: { + if (!parseUnknownField(input, extensionRegistry, tag)) { + return this; + } + break; + } + case 8: { + int rawValue = input.readEnum(); + com.google.android.apps.mytracks.content.Sensor.SensorState value = com.google.android.apps.mytracks.content.Sensor.SensorState.valueOf(rawValue); + if (value != null) { + setState(value); + } + break; + } + case 16: { + setValue(input.readInt32()); + break; + } + } + } + } + + + // required .com.google.android.apps.mytracks.content.SensorState state = 1 [default = NONE]; + public boolean hasState() { + return result.hasState(); + } + public com.google.android.apps.mytracks.content.Sensor.SensorState getState() { + return result.getState(); + } + public Builder setState(com.google.android.apps.mytracks.content.Sensor.SensorState value) { + if (value == null) { + throw new NullPointerException(); + } + result.hasState = true; + result.state_ = value; + return this; + } + public Builder clearState() { + result.hasState = false; + result.state_ = com.google.android.apps.mytracks.content.Sensor.SensorState.NONE; + return this; + } + + // optional int32 value = 2; + public boolean hasValue() { + return result.hasValue(); + } + public int getValue() { + return result.getValue(); + } + public Builder setValue(int value) { + result.hasValue = true; + result.value_ = value; + return this; + } + public Builder clearValue() { + result.hasValue = false; + result.value_ = 0; + return this; + } + + // @@protoc_insertion_point(builder_scope:com.google.android.apps.mytracks.content.SensorData) + } + + static { + defaultInstance = new SensorData(true); + com.google.android.apps.mytracks.content.Sensor.internalForceInit(); + defaultInstance.initFields(); + } + + // @@protoc_insertion_point(class_scope:com.google.android.apps.mytracks.content.SensorData) + } + + public static final class SensorDataSet extends + com.google.protobuf.GeneratedMessageLite { + // Use SensorDataSet.newBuilder() to construct. + private SensorDataSet() { + initFields(); + } + private SensorDataSet(boolean noInit) {} + + private static final SensorDataSet defaultInstance; + public static SensorDataSet getDefaultInstance() { + return defaultInstance; + } + + public SensorDataSet getDefaultInstanceForType() { + return defaultInstance; + } + + // optional uint64 creation_time = 1 [default = 0]; + public static final int CREATION_TIME_FIELD_NUMBER = 1; + private boolean hasCreationTime; + private long creationTime_ = 0L; + public boolean hasCreationTime() { return hasCreationTime; } + public long getCreationTime() { return creationTime_; } + + // optional .com.google.android.apps.mytracks.content.SensorData heart_rate = 2; + public static final int HEART_RATE_FIELD_NUMBER = 2; + private boolean hasHeartRate; + private com.google.android.apps.mytracks.content.Sensor.SensorData heartRate_; + public boolean hasHeartRate() { return hasHeartRate; } + public com.google.android.apps.mytracks.content.Sensor.SensorData getHeartRate() { return heartRate_; } + + // optional .com.google.android.apps.mytracks.content.SensorData cadence = 3; + public static final int CADENCE_FIELD_NUMBER = 3; + private boolean hasCadence; + private com.google.android.apps.mytracks.content.Sensor.SensorData cadence_; + public boolean hasCadence() { return hasCadence; } + public com.google.android.apps.mytracks.content.Sensor.SensorData getCadence() { return cadence_; } + + // optional .com.google.android.apps.mytracks.content.SensorData power = 4; + public static final int POWER_FIELD_NUMBER = 4; + private boolean hasPower; + private com.google.android.apps.mytracks.content.Sensor.SensorData power_; + public boolean hasPower() { return hasPower; } + public com.google.android.apps.mytracks.content.Sensor.SensorData getPower() { return power_; } + + private void initFields() { + heartRate_ = com.google.android.apps.mytracks.content.Sensor.SensorData.getDefaultInstance(); + cadence_ = com.google.android.apps.mytracks.content.Sensor.SensorData.getDefaultInstance(); + power_ = com.google.android.apps.mytracks.content.Sensor.SensorData.getDefaultInstance(); + } + public final boolean isInitialized() { + if (hasHeartRate()) { + if (!getHeartRate().isInitialized()) return false; + } + if (hasCadence()) { + if (!getCadence().isInitialized()) return false; + } + if (hasPower()) { + if (!getPower().isInitialized()) return false; + } + return true; + } + + public void writeTo(com.google.protobuf.CodedOutputStream output) + throws java.io.IOException { + getSerializedSize(); + if (hasCreationTime()) { + output.writeUInt64(1, getCreationTime()); + } + if (hasHeartRate()) { + output.writeMessage(2, getHeartRate()); + } + if (hasCadence()) { + output.writeMessage(3, getCadence()); + } + if (hasPower()) { + output.writeMessage(4, getPower()); + } + } + + private int memoizedSerializedSize = -1; + public int getSerializedSize() { + int size = memoizedSerializedSize; + if (size != -1) return size; + + size = 0; + if (hasCreationTime()) { + size += com.google.protobuf.CodedOutputStream + .computeUInt64Size(1, getCreationTime()); + } + if (hasHeartRate()) { + size += com.google.protobuf.CodedOutputStream + .computeMessageSize(2, getHeartRate()); + } + if (hasCadence()) { + size += com.google.protobuf.CodedOutputStream + .computeMessageSize(3, getCadence()); + } + if (hasPower()) { + size += com.google.protobuf.CodedOutputStream + .computeMessageSize(4, getPower()); + } + memoizedSerializedSize = size; + return size; + } + + public static com.google.android.apps.mytracks.content.Sensor.SensorDataSet parseFrom( + com.google.protobuf.ByteString data) + throws com.google.protobuf.InvalidProtocolBufferException { + return newBuilder().mergeFrom(data).buildParsed(); + } + public static com.google.android.apps.mytracks.content.Sensor.SensorDataSet parseFrom( + com.google.protobuf.ByteString data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return newBuilder().mergeFrom(data, extensionRegistry) + .buildParsed(); + } + public static com.google.android.apps.mytracks.content.Sensor.SensorDataSet parseFrom(byte[] data) + throws com.google.protobuf.InvalidProtocolBufferException { + return newBuilder().mergeFrom(data).buildParsed(); + } + public static com.google.android.apps.mytracks.content.Sensor.SensorDataSet parseFrom( + byte[] data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return newBuilder().mergeFrom(data, extensionRegistry) + .buildParsed(); + } + public static com.google.android.apps.mytracks.content.Sensor.SensorDataSet parseFrom(java.io.InputStream input) + throws java.io.IOException { + return newBuilder().mergeFrom(input).buildParsed(); + } + public static com.google.android.apps.mytracks.content.Sensor.SensorDataSet parseFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return newBuilder().mergeFrom(input, extensionRegistry) + .buildParsed(); + } + public static com.google.android.apps.mytracks.content.Sensor.SensorDataSet parseDelimitedFrom(java.io.InputStream input) + throws java.io.IOException { + Builder builder = newBuilder(); + if (builder.mergeDelimitedFrom(input)) { + return builder.buildParsed(); + } else { + return null; + } + } + public static com.google.android.apps.mytracks.content.Sensor.SensorDataSet parseDelimitedFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + Builder builder = newBuilder(); + if (builder.mergeDelimitedFrom(input, extensionRegistry)) { + return builder.buildParsed(); + } else { + return null; + } + } + public static com.google.android.apps.mytracks.content.Sensor.SensorDataSet parseFrom( + com.google.protobuf.CodedInputStream input) + throws java.io.IOException { + return newBuilder().mergeFrom(input).buildParsed(); + } + public static com.google.android.apps.mytracks.content.Sensor.SensorDataSet parseFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return newBuilder().mergeFrom(input, extensionRegistry) + .buildParsed(); + } + + public static Builder newBuilder() { return Builder.create(); } + public Builder newBuilderForType() { return newBuilder(); } + public static Builder newBuilder(com.google.android.apps.mytracks.content.Sensor.SensorDataSet prototype) { + return newBuilder().mergeFrom(prototype); + } + public Builder toBuilder() { return newBuilder(this); } + + public static final class Builder extends + com.google.protobuf.GeneratedMessageLite.Builder< + com.google.android.apps.mytracks.content.Sensor.SensorDataSet, Builder> { + private com.google.android.apps.mytracks.content.Sensor.SensorDataSet result; + + // Construct using com.google.android.apps.mytracks.content.Sensor.SensorDataSet.newBuilder() + private Builder() {} + + private static Builder create() { + Builder builder = new Builder(); + builder.result = new com.google.android.apps.mytracks.content.Sensor.SensorDataSet(); + return builder; + } + + protected com.google.android.apps.mytracks.content.Sensor.SensorDataSet internalGetResult() { + return result; + } + + public Builder clear() { + if (result == null) { + throw new IllegalStateException( + "Cannot call clear() after build()."); + } + result = new com.google.android.apps.mytracks.content.Sensor.SensorDataSet(); + return this; + } + + public Builder clone() { + return create().mergeFrom(result); + } + + public com.google.android.apps.mytracks.content.Sensor.SensorDataSet getDefaultInstanceForType() { + return com.google.android.apps.mytracks.content.Sensor.SensorDataSet.getDefaultInstance(); + } + + public boolean isInitialized() { + return result.isInitialized(); + } + public com.google.android.apps.mytracks.content.Sensor.SensorDataSet build() { + if (result != null && !isInitialized()) { + throw newUninitializedMessageException(result); + } + return buildPartial(); + } + + private com.google.android.apps.mytracks.content.Sensor.SensorDataSet buildParsed() + throws com.google.protobuf.InvalidProtocolBufferException { + if (!isInitialized()) { + throw newUninitializedMessageException( + result).asInvalidProtocolBufferException(); + } + return buildPartial(); + } + + public com.google.android.apps.mytracks.content.Sensor.SensorDataSet buildPartial() { + if (result == null) { + throw new IllegalStateException( + "build() has already been called on this Builder."); + } + com.google.android.apps.mytracks.content.Sensor.SensorDataSet returnMe = result; + result = null; + return returnMe; + } + + public Builder mergeFrom(com.google.android.apps.mytracks.content.Sensor.SensorDataSet other) { + if (other == com.google.android.apps.mytracks.content.Sensor.SensorDataSet.getDefaultInstance()) return this; + if (other.hasCreationTime()) { + setCreationTime(other.getCreationTime()); + } + if (other.hasHeartRate()) { + mergeHeartRate(other.getHeartRate()); + } + if (other.hasCadence()) { + mergeCadence(other.getCadence()); + } + if (other.hasPower()) { + mergePower(other.getPower()); + } + return this; + } + + public Builder mergeFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + while (true) { + int tag = input.readTag(); + switch (tag) { + case 0: + return this; + default: { + if (!parseUnknownField(input, extensionRegistry, tag)) { + return this; + } + break; + } + case 8: { + setCreationTime(input.readUInt64()); + break; + } + case 18: { + com.google.android.apps.mytracks.content.Sensor.SensorData.Builder subBuilder = com.google.android.apps.mytracks.content.Sensor.SensorData.newBuilder(); + if (hasHeartRate()) { + subBuilder.mergeFrom(getHeartRate()); + } + input.readMessage(subBuilder, extensionRegistry); + setHeartRate(subBuilder.buildPartial()); + break; + } + case 26: { + com.google.android.apps.mytracks.content.Sensor.SensorData.Builder subBuilder = com.google.android.apps.mytracks.content.Sensor.SensorData.newBuilder(); + if (hasCadence()) { + subBuilder.mergeFrom(getCadence()); + } + input.readMessage(subBuilder, extensionRegistry); + setCadence(subBuilder.buildPartial()); + break; + } + case 34: { + com.google.android.apps.mytracks.content.Sensor.SensorData.Builder subBuilder = com.google.android.apps.mytracks.content.Sensor.SensorData.newBuilder(); + if (hasPower()) { + subBuilder.mergeFrom(getPower()); + } + input.readMessage(subBuilder, extensionRegistry); + setPower(subBuilder.buildPartial()); + break; + } + } + } + } + + + // optional uint64 creation_time = 1 [default = 0]; + public boolean hasCreationTime() { + return result.hasCreationTime(); + } + public long getCreationTime() { + return result.getCreationTime(); + } + public Builder setCreationTime(long value) { + result.hasCreationTime = true; + result.creationTime_ = value; + return this; + } + public Builder clearCreationTime() { + result.hasCreationTime = false; + result.creationTime_ = 0L; + return this; + } + + // optional .com.google.android.apps.mytracks.content.SensorData heart_rate = 2; + public boolean hasHeartRate() { + return result.hasHeartRate(); + } + public com.google.android.apps.mytracks.content.Sensor.SensorData getHeartRate() { + return result.getHeartRate(); + } + public Builder setHeartRate(com.google.android.apps.mytracks.content.Sensor.SensorData value) { + if (value == null) { + throw new NullPointerException(); + } + result.hasHeartRate = true; + result.heartRate_ = value; + return this; + } + public Builder setHeartRate(com.google.android.apps.mytracks.content.Sensor.SensorData.Builder builderForValue) { + result.hasHeartRate = true; + result.heartRate_ = builderForValue.build(); + return this; + } + public Builder mergeHeartRate(com.google.android.apps.mytracks.content.Sensor.SensorData value) { + if (result.hasHeartRate() && + result.heartRate_ != com.google.android.apps.mytracks.content.Sensor.SensorData.getDefaultInstance()) { + result.heartRate_ = + com.google.android.apps.mytracks.content.Sensor.SensorData.newBuilder(result.heartRate_).mergeFrom(value).buildPartial(); + } else { + result.heartRate_ = value; + } + result.hasHeartRate = true; + return this; + } + public Builder clearHeartRate() { + result.hasHeartRate = false; + result.heartRate_ = com.google.android.apps.mytracks.content.Sensor.SensorData.getDefaultInstance(); + return this; + } + + // optional .com.google.android.apps.mytracks.content.SensorData cadence = 3; + public boolean hasCadence() { + return result.hasCadence(); + } + public com.google.android.apps.mytracks.content.Sensor.SensorData getCadence() { + return result.getCadence(); + } + public Builder setCadence(com.google.android.apps.mytracks.content.Sensor.SensorData value) { + if (value == null) { + throw new NullPointerException(); + } + result.hasCadence = true; + result.cadence_ = value; + return this; + } + public Builder setCadence(com.google.android.apps.mytracks.content.Sensor.SensorData.Builder builderForValue) { + result.hasCadence = true; + result.cadence_ = builderForValue.build(); + return this; + } + public Builder mergeCadence(com.google.android.apps.mytracks.content.Sensor.SensorData value) { + if (result.hasCadence() && + result.cadence_ != com.google.android.apps.mytracks.content.Sensor.SensorData.getDefaultInstance()) { + result.cadence_ = + com.google.android.apps.mytracks.content.Sensor.SensorData.newBuilder(result.cadence_).mergeFrom(value).buildPartial(); + } else { + result.cadence_ = value; + } + result.hasCadence = true; + return this; + } + public Builder clearCadence() { + result.hasCadence = false; + result.cadence_ = com.google.android.apps.mytracks.content.Sensor.SensorData.getDefaultInstance(); + return this; + } + + // optional .com.google.android.apps.mytracks.content.SensorData power = 4; + public boolean hasPower() { + return result.hasPower(); + } + public com.google.android.apps.mytracks.content.Sensor.SensorData getPower() { + return result.getPower(); + } + public Builder setPower(com.google.android.apps.mytracks.content.Sensor.SensorData value) { + if (value == null) { + throw new NullPointerException(); + } + result.hasPower = true; + result.power_ = value; + return this; + } + public Builder setPower(com.google.android.apps.mytracks.content.Sensor.SensorData.Builder builderForValue) { + result.hasPower = true; + result.power_ = builderForValue.build(); + return this; + } + public Builder mergePower(com.google.android.apps.mytracks.content.Sensor.SensorData value) { + if (result.hasPower() && + result.power_ != com.google.android.apps.mytracks.content.Sensor.SensorData.getDefaultInstance()) { + result.power_ = + com.google.android.apps.mytracks.content.Sensor.SensorData.newBuilder(result.power_).mergeFrom(value).buildPartial(); + } else { + result.power_ = value; + } + result.hasPower = true; + return this; + } + public Builder clearPower() { + result.hasPower = false; + result.power_ = com.google.android.apps.mytracks.content.Sensor.SensorData.getDefaultInstance(); + return this; + } + + // @@protoc_insertion_point(builder_scope:com.google.android.apps.mytracks.content.SensorDataSet) + } + + static { + defaultInstance = new SensorDataSet(true); + com.google.android.apps.mytracks.content.Sensor.internalForceInit(); + defaultInstance.initFields(); + } + + // @@protoc_insertion_point(class_scope:com.google.android.apps.mytracks.content.SensorDataSet) + } + + + static { + } + + public static void internalForceInit() {} + + // @@protoc_insertion_point(outer_class_scope) +} diff --git a/MyTracksLib/src/com/google/android/apps/mytracks/content/TrackPointsColumns.java b/MyTracksLib/src/com/google/android/apps/mytracks/content/TrackPointsColumns.java index 206fd0eb8..eba090517 100644 --- a/MyTracksLib/src/com/google/android/apps/mytracks/content/TrackPointsColumns.java +++ b/MyTracksLib/src/com/google/android/apps/mytracks/content/TrackPointsColumns.java @@ -42,4 +42,5 @@ public interface TrackPointsColumns extends BaseColumns { public static final String TIME = "time"; public static final String ACCURACY = "accuracy"; public static final String SPEED = "speed"; + public static final String SENSOR = "sensor"; } diff --git a/MyTracksLib/src/com/google/android/apps/mytracks/services/ITrackRecordingService.aidl b/MyTracksLib/src/com/google/android/apps/mytracks/services/ITrackRecordingService.aidl index d0e492563..39484aef0 100644 --- a/MyTracksLib/src/com/google/android/apps/mytracks/services/ITrackRecordingService.aidl +++ b/MyTracksLib/src/com/google/android/apps/mytracks/services/ITrackRecordingService.aidl @@ -84,4 +84,18 @@ interface ITrackRecordingService { * Deletes all the stored tracks. */ void deleteAllTracks(); + + /** + * The current sensor data. + * The data is returned as a byte array which is a binary version of a + * Sensor.SensorDataSet object. + * @return the current sensor data or null if there is none. + */ + byte[] getSensorData(); + + /** + * The current state of the sensor manager. + * The value is the value of a Sensor.SensorState enum. + */ + int getSensorState(); } diff --git a/MyTracksLib/src/com/google/android/apps/mytracks/stats/ExtremityMonitor.java b/MyTracksLib/src/com/google/android/apps/mytracks/stats/ExtremityMonitor.java index fafcf9ae2..bb65ee76d 100644 --- a/MyTracksLib/src/com/google/android/apps/mytracks/stats/ExtremityMonitor.java +++ b/MyTracksLib/src/com/google/android/apps/mytracks/stats/ExtremityMonitor.java @@ -103,6 +103,11 @@ public class ExtremityMonitor { public void setMax(double max) { this.max = max; } + + public boolean hasData() { + return min != Double.POSITIVE_INFINITY + && max != Double.NEGATIVE_INFINITY; + } @Override public String toString() { diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/io/GpxImporterTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/io/GpxImporterTest.java index 0c293b7d0..b819f5c2a 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/io/GpxImporterTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/io/GpxImporterTest.java @@ -19,9 +19,9 @@ import static com.google.android.testing.mocking.AndroidMock.eq; import static com.google.android.testing.mocking.AndroidMock.expect; import com.google.android.apps.mytracks.content.MyTracksProviderUtils; -import com.google.android.apps.mytracks.content.MyTracksProviderUtils.Factory; import com.google.android.apps.mytracks.content.Track; import com.google.android.apps.mytracks.content.TracksColumns; +import com.google.android.apps.mytracks.content.MyTracksProviderUtils.Factory; import com.google.android.apps.mytracks.testing.TestingProviderUtilsFactory; import com.google.android.testing.mocking.AndroidMock; import com.google.android.testing.mocking.UsesMocks; @@ -77,7 +77,7 @@ public class GpxImporterTest extends AndroidTestCase { DATE_FORMAT2.setTimeZone(utc); } - // TODO use real files from different sources with more track points + // TODO: use real files from different sources with more track points. private static final String VALID_TEST_GPX = "" + " trackParam = new Capture(); @@ -171,7 +177,7 @@ public class GpxImporterTest extends AndroidTestCase { } /** - * Test with invalid location - track should be deleted + * Test with invalid location - track should be deleted. */ public void testImportLocationFailure() throws ParserConfigurationException, SAXException, IOException { @@ -179,7 +185,7 @@ public class GpxImporterTest extends AndroidTestCase { } /** - * Test with invalid time - track should be deleted + * Test with invalid time - track should be deleted. */ public void testImportTimeFailure() throws ParserConfigurationException, SAXException, IOException { @@ -187,13 +193,37 @@ public class GpxImporterTest extends AndroidTestCase { } /** - * Test with invalid xml - track should be deleted + * Test with invalid xml - track should be deleted. */ public void testImportXMLFailure() throws ParserConfigurationException, SAXException, IOException { testInvalidXML(INVALID_XML_TEST_GPX); } + + /** + * Test with invalid altitude - track should be deleted. + */ + public void testImportInvalidAltitude() throws ParserConfigurationException, + SAXException, IOException { + testInvalidXML(INVALID_ALTITUDE_TEST_GPX); + } + /** + * Test with invalid latitude - track should be deleted. + */ + public void testImportInvalidLatitude() throws ParserConfigurationException, + SAXException, IOException { + testInvalidXML(INVALID_LATITUDE_TEST_GPX); + } + + /** + * Test with invalid longitude - track should be deleted. + */ + public void testImportInvalidLongitude() throws ParserConfigurationException, + SAXException, IOException { + testInvalidXML(INVALID_LONGITUDE_TEST_GPX); + } + private void testInvalidXML(String xml) throws ParserConfigurationException, IOException { expect(providerUtils.insertTrack((Track) AndroidMock.anyObject())) diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/io/TcxTrackWriterTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/io/TcxTrackWriterTest.java index ec132e0db..897df2792 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/io/TcxTrackWriterTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/io/TcxTrackWriterTest.java @@ -1,7 +1,8 @@ // Copyright 2010 Google Inc. All Rights Reserved. package com.google.android.apps.mytracks.io; -import android.location.Location; +import com.google.android.apps.mytracks.content.MyTracksLocation; +import com.google.android.apps.mytracks.content.Sensor; import org.w3c.dom.Document; import org.w3c.dom.Element; @@ -38,11 +39,11 @@ public class TcxTrackWriterTest extends TrackFormatWriterTest { /** * Asserts that the given tags describe the given points, in the same order. */ - private void assertTagsMatchPoints(List tags, Location... locs) { + private void assertTagsMatchPoints(List tags, MyTracksLocation... locs) { assertEquals(locs.length, tags.size()); for (int i = 0; i < locs.length; i++) { Element tag = tags.get(i); - Location loc = locs[i]; + MyTracksLocation loc = locs[i]; assertTagMatchesLocation(tag, loc); } @@ -51,7 +52,7 @@ public class TcxTrackWriterTest extends TrackFormatWriterTest { /** * Asserts that the given tag describes the given location. */ - private void assertTagMatchesLocation(Element tag, Location loc) { + private void assertTagMatchesLocation(Element tag, MyTracksLocation loc) { Element posTag = getChildElement(tag, "Position"); assertEquals(Double.toString(loc.getLatitude()), getChildTextValue(posTag, "LatitudeDegrees")); @@ -62,5 +63,16 @@ public class TcxTrackWriterTest extends TrackFormatWriterTest { getChildTextValue(tag, "Time")); assertEquals(Double.toString(loc.getAltitude()), getChildTextValue(tag, "AltitudeMeters")); + assertTrue(loc.getSensorDataSet() != null); + Sensor.SensorDataSet sds = loc.getSensorDataSet(); + + List bpm = getChildElements(tag, "HeartRateBpm", 1); + assertEquals(Integer.toString(sds.getHeartRate().getValue()), + getChildTextValue(bpm.get(0), "Value")); + + List ext = getChildElements(tag, "Extensions", 1); + List tpx = getChildElements(ext.get(0), "TPX", 1); + assertEquals(Integer.toString(sds.getPower().getValue()), + getChildTextValue(tpx.get(0), "Watts")); } } diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/io/TrackFormatWriterTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/io/TrackFormatWriterTest.java index 8e4bd0e44..89d38ad55 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/io/TrackFormatWriterTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/io/TrackFormatWriterTest.java @@ -1,10 +1,11 @@ // Copyright 2010 Google Inc. All Rights Reserved. package com.google.android.apps.mytracks.io; +import com.google.android.apps.mytracks.content.MyTracksLocation; +import com.google.android.apps.mytracks.content.Sensor; import com.google.android.apps.mytracks.content.Track; import com.google.android.apps.mytracks.content.Waypoint; -import android.location.Location; import android.test.AndroidTestCase; import org.w3c.dom.Document; @@ -46,7 +47,7 @@ public abstract class TrackFormatWriterTest extends AndroidTestCase { private static final int BUFFER_SIZE = 10240; protected static final long TRACK_ID = 12345L; protected Track track; - protected Location location1, location2, location3, location4; + protected MyTracksLocation location1, location2, location3, location4; protected Waypoint wp1, wp2; @Override @@ -58,10 +59,10 @@ public abstract class TrackFormatWriterTest extends AndroidTestCase { track.setName(TRACK_NAME); track.setDescription(TRACK_DESCRIPTION); - location1 = new Location("mock"); - location2 = new Location("mock"); - location3 = new Location("mock"); - location4 = new Location("mock"); + location1 = new MyTracksLocation("mock"); + location2 = new MyTracksLocation("mock"); + location3 = new MyTracksLocation("mock"); + location4 = new MyTracksLocation("mock"); populateLocations(location1, location2, location3, location4); wp1 = new Waypoint(); @@ -77,13 +78,24 @@ public abstract class TrackFormatWriterTest extends AndroidTestCase { /** * Populates the given locations with coordinates and time. */ - protected void populateLocations(Location... locs) { + protected void populateLocations(MyTracksLocation... locs) { for (int i = 0; i < locs.length; i++) { - Location loc = locs[i]; + MyTracksLocation loc = locs[i]; loc.setAltitude(i * 1000); loc.setLatitude(i); loc.setLongitude(-i); loc.setTime(10000000 + i * 1000); + Sensor.SensorData.Builder hr = Sensor.SensorData.newBuilder() + .setValue(100 + i) + .setState(Sensor.SensorState.SENDING); + Sensor.SensorData.Builder power = Sensor.SensorData.newBuilder() + .setValue(400 + i) + .setState(Sensor.SensorState.SENDING); + Sensor.SensorDataSet sds = + Sensor.SensorDataSet.newBuilder().setHeartRate(hr.build()) + .setPower(power) + .build(); + loc.setSensorData(sds); } } @@ -139,7 +151,7 @@ public abstract class TrackFormatWriterTest extends AndroidTestCase { } /** - * Returns all child elements of a given parent which gave the given name. + * Returns all child elements of a given parent which have the given name. * * @param parent the parent to get children from * @param elementName the element name to look for diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/services/TrackRecordingServiceTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/services/TrackRecordingServiceTest.java index ab891e42a..f54f6d6c1 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/services/TrackRecordingServiceTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/services/TrackRecordingServiceTest.java @@ -692,6 +692,17 @@ public class TrackRecordingServiceTest functionalTest(R.string.min_required_accuracy_key, 500); } + @MediumTest + public void testWithProperties_noSensorType() throws Exception { + functionalTest(R.string.sensor_type_key, (Object) null); + } + + @MediumTest + public void testWithProperties_zephyrSensorType() throws Exception { + functionalTest(R.string.sensor_type_key, + context.getString(R.string.zephyr_sensor_type)); + } + private ITrackRecordingService bindAndGetService(Intent intent) { ITrackRecordingService service = ITrackRecordingService.Stub.asInterface( bindService(intent)); 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 new file mode 100644 index 000000000..4dcc0b9d9 --- /dev/null +++ b/MyTracksTest/src/com/google/android/apps/mytracks/services/sensors/SensorManagerFactoryTest.java @@ -0,0 +1,39 @@ +package com.google.android.apps.mytracks.services.sensors; + +import com.google.android.apps.mytracks.MyTracksSettings; +import com.google.android.maps.mytracks.R; + +import android.content.SharedPreferences; +import android.test.AndroidTestCase; +import android.test.suitebuilder.annotation.SmallTest; + +public class SensorManagerFactoryTest extends AndroidTestCase { + + private SharedPreferences sharedPreferences; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + sharedPreferences = getContext().getSharedPreferences( + MyTracksSettings.SETTINGS_NAME, 0); + // Let's use default values. + sharedPreferences.edit().clear().commit(); + } + + @SmallTest + public void testDefaultSettings() throws Exception { + assertNull(SensorManagerFactory.getSensorManager(getContext())); + } + + @SmallTest + public void testCreateZephyr() throws Exception { + sharedPreferences.edit() + .putString(getContext().getString(R.string.sensor_type_key), + getContext().getString(R.string.zephyr_sensor_type)) + .commit(); + SensorManager sm = SensorManagerFactory.getSensorManager(getContext()); + assertNotNull(sm); + assertTrue(sm instanceof ZephyrSensorManager); + } +} diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/services/sensors/ZephyrMessageParserTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/services/sensors/ZephyrMessageParserTest.java new file mode 100644 index 000000000..6cebf3692 --- /dev/null +++ b/MyTracksTest/src/com/google/android/apps/mytracks/services/sensors/ZephyrMessageParserTest.java @@ -0,0 +1,36 @@ +package com.google.android.apps.mytracks.services.sensors; + +import com.google.android.apps.mytracks.content.Sensor; + +import junit.framework.TestCase; + +public class ZephyrMessageParserTest extends TestCase { + + ZephyrMessageParser parser = new ZephyrMessageParser(); + + public void testIsValid() { + byte[] buf = new byte[60]; + assertFalse(parser.isValid(buf)); + buf[0] = 0x02; + assertFalse(parser.isValid(buf)); + buf[59] = 0x03; + assertTrue(parser.isValid(buf)); + } + + public void testParseBuffer() { + byte[] buf = new byte[60]; + buf[12] = 50; + Sensor.SensorDataSet sds = parser.parseBuffer(buf); + assertTrue(sds.hasHeartRate()); + assertTrue(sds.getHeartRate().getState() == Sensor.SensorState.SENDING); + assertEquals(50, sds.getHeartRate().getValue()); + } + + public void testFindNextAlignment() { + byte[] buf = new byte[60]; + assertEquals(-1, parser.findNextAlignment(buf)); + buf[10] = 0x03; + buf[11] = 0x02; + assertEquals(10, parser.findNextAlignment(buf)); + } +} diff --git a/SignalStrengthSampler/.classpath b/SignalStrengthSampler/.classpath index efe4c9c16..57972d993 100644 --- a/SignalStrengthSampler/.classpath +++ b/SignalStrengthSampler/.classpath @@ -4,5 +4,6 @@ +