Merging sensor and library changes together.

This commit is contained in:
Rodrigo Damazio
2010-12-07 18:04:28 -02:00
51 changed files with 3241 additions and 349 deletions
+1
View File
@@ -7,5 +7,6 @@
<classpathentry kind="src" path="src"/>
<classpathentry kind="src" path="gen"/>
<classpathentry kind="src" path="MyTracksLib_src"/>
<classpathentry exported="true" kind="lib" path="/MyTracksLib/libs/protobuf-java-2.3.0-lite.jar"/>
<classpathentry kind="output" path="bin"/>
</classpath>
+5
View File
@@ -83,6 +83,9 @@ limitations under the License.
android:label="@string/statistics"
android:exported="false" />
<activity android:name="com.google.android.apps.mytracks.SensorStateActivity"
android:label="@string/sensor_state" />
<activity android:name="com.google.android.apps.mytracks.ChartActivity"
android:label="@string/elevation_label"
android:exported="false" />
@@ -113,6 +116,8 @@ limitations under the License.
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<!-- Permissions for uploading tracks -->
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
Binary file not shown.

Before

Width:  |  Height:  |  Size: 358 B

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

+15
View File
@@ -103,6 +103,21 @@ limitations under the License.
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="@string/speed"/>
<CheckBox android:id="@+id/chart_settings_power"
android:checked="true"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="@string/power"/>
<CheckBox android:id="@+id/chart_settings_cadence"
android:checked="true"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="@string/cadence"/>
<CheckBox android:id="@+id/chart_settings_heart_rate"
android:checked="true"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="@string/heart_rate"/>
</LinearLayout>
</TableRow>
</TableLayout>
+131
View File
@@ -0,0 +1,131 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_width="fill_parent">
<ScrollView
android:layout_height="wrap_content"
android:layout_width="fill_parent">
<LinearLayout
android:layout_height="wrap_content"
android:layout_width="fill_parent">
<TableLayout
android:layout_height="wrap_content"
android:stretchColumns="1,4"
android:layout_width="fill_parent">
<!-- Layout the sensor state on one line. -->
<TableRow>
<TextView
android:layout_span="5"
android:text="@string/sensor_state" />
</TableRow>
<TableRow
android:background="@color/stat_label_background">
<TextView
android:id="@+id/sensor_state_register"
android:layout_column="1"
android:layout_span="5"
style="@style/StatsRegister" />
</TableRow>
<!-- Layout the sensor data age on one line. -->
<TableRow>
<TextView
android:layout_span="5"
android:text="@string/sensor_data_time" />
</TableRow>
<TableRow
android:background="@color/stat_label_background">
<TextView
android:id="@+id/sensor_data_time_register"
android:layout_column="1"
android:layout_span="5"
style="@style/StatsRegister" />
</TableRow>
<!-- Power meter -->
<TableRow
android:padding="1dip">
<TextView
android:layout_column="1"
android:layout_span="3"
android:text="@string/power" />
</TableRow>
<TableRow
android:background="@color/stat_label_background">
<TextView
style="@style/StatsRegister.Big"
android:id="@+id/power_state_register"
android:layout_column="1"
android:layout_span="4" />
<TextView
style="@style/StatsUnitSolo" />
android:layout_column="5"
android:text="@string/watts" />
</TableRow>
<!-- Cadence -->
<TableRow
android:padding="1dip">
<TextView
android:layout_column="1"
android:layout_span="3"
android:text="@string/cadence" />
</TableRow>
<TableRow
android:background="@color/stat_label_background">
<TextView
style="@style/StatsRegister.Big"
android:id="@+id/cadence_state_register"
android:layout_column="4" />
<TextView
style="@style/StatsUnitSolo" />
android:layout_column="2"
android:text="@string/revolutions_per_minute" />
</TableRow>
<!-- Heart Rate -->
<TableRow
android:padding="1dip">
<TextView
android:layout_column="1"
android:layout_span="3"
android:text="@string/heart_rate" />
</TableRow>
<TableRow
android:background="@color/stat_label_background">
<TextView
style="@style/StatsRegister.Big"
android:id="@+id/heart_rate_register"
android:layout_column="1"
android:layout_span="4" />
<TextView
style="@style/StatsUnitSolo"
android:layout_column="5"
android:text="@string/bpm" />
</TableRow>
</TableLayout>
</LinearLayout>
</ScrollView>
</RelativeLayout>
+48 -225
View File
@@ -42,15 +42,10 @@ limitations under the License.
<TableRow
android:background="@color/stat_label_background">
<TextView
android:gravity="right"
style="@style/StatsRegister.Big"
android:id="@+id/speed_register"
android:layout_column="1"
android:layout_marginBottom="-13dip"
android:layout_marginTop="-13dip"
android:layout_span="4"
android:text="@string/unknown"
android:textColor="@color/stat_label_text"
android:textSize="50sp" />
android:layout_span="4"/>
<LinearLayout
android:gravity="bottom"
android:layout_column="5"
@@ -62,24 +57,12 @@ limitations under the License.
android:layout_width="fill_parent" />
<TextView
android:id="@+id/speed_unit_label_top"
android:gravity="bottom"
android:layout_height="wrap_content"
android:layout_marginBottom="2dip"
android:layout_marginTop="2dip"
android:layout_width="wrap_content"
android:textColor="@color/stat_label_text" />
style="@style/StatsUnit" />
<TextView
android:background="@color/black"
android:layout_height="3dip"
android:layout_width="fill_parent" />
style="@style/StatsDivider" />
<TextView
android:id="@+id/speed_unit_label_bottom"
android:gravity="bottom"
android:layout_height="wrap_content"
android:layout_marginBottom="2dip"
android:layout_marginTop="2dip"
android:layout_width="wrap_content"
android:textColor="@color/stat_label_text" />
style="@style/StatsUnit" />
</LinearLayout>
</TableRow>
@@ -100,43 +83,25 @@ limitations under the License.
<TableRow
android:background="@color/stat_label_background">
<TextView
android:gravity="right"
style="@style/StatsRegister"
android:id="@+id/total_distance_register"
android:layout_column="1"
android:layout_marginBottom="-9dip"
android:layout_marginTop="-9dip"
android:text="@string/unknown"
android:textColor="@color/stat_label_text"
android:textSize="40sp" />
android:layout_column="1" />
<TextView
android:gravity="left"
android:id="@+id/total_distance_unit_label"
android:layout_column="2"
android:layout_marginBottom="-9dip"
android:layout_marginTop="-9dip"
android:text="@string/mile"
android:textColor="@color/stat_label_text" />
style="@style/StatsUnitSolo" />
<!-- TODO 4/08/2009
The following is a bad hack to get a spacer the same size as the
text. Ideally this should just be a View, however I don't know how to
set the view height properly. fill_parent on this element causes as
NullPointerException. This same filler is repeated on each line.-->
<TextView
android:background="@color/black"
android:layout_column="3"
android:layout_marginBottom="-9dip"
android:layout_marginTop="-9dip"
android:layout_width="5dip"
android:textSize="40sp" />
style="@style/StatsSpacer"
android:layout_column="3"/>
<TextView
android:gravity="right"
android:id="@+id/max_speed_register"
android:layout_column="4"
android:layout_marginBottom="-9dip"
android:layout_marginTop="-9dip"
android:text="@string/unknown"
android:textColor="@color/stat_label_text"
android:textSize="40sp" />
style="@style/StatsRegister"
android:layout_column="4" />
<LinearLayout
android:gravity="bottom"
android:layout_column="5"
@@ -145,24 +110,12 @@ limitations under the License.
android:orientation="vertical">
<TextView
android:id="@+id/max_speed_unit_label_top"
android:gravity="bottom"
android:layout_height="wrap_content"
android:layout_marginBottom="-1dip"
android:layout_marginTop="-1dip"
android:layout_width="wrap_content"
android:textColor="@color/stat_label_text" />
style="@style/StatsUnit" />
<TextView
android:background="@color/black"
android:layout_height="3dip"
android:layout_width="fill_parent" />
style="@style/StatsDivider" />
<TextView
android:id="@+id/max_speed_unit_label_bottom"
android:gravity="bottom"
android:layout_height="wrap_content"
android:layout_marginBottom="-1dip"
android:layout_marginTop="-1dip"
android:layout_width="wrap_content"
android:textColor="@color/stat_label_text" />
style="@style/StatsUnit" />
</LinearLayout>
</TableRow>
@@ -182,33 +135,19 @@ limitations under the License.
<TableRow
android:background="@color/stat_label_background">
<TextView
android:gravity="right"
android:id="@+id/total_time_register"
android:layout_column="1"
android:layout_height="wrap_content"
android:layout_marginBottom="-9dip"
android:layout_marginTop="-9dip"
android:layout_span="2"
android:layout_width="fill_parent"
android:text="@string/unknown"
android:textColor="@color/stat_label_text"
android:textSize="40sp" />
style="@style/StatsRegister" />
<TextView
android:background="@color/black"
android:layout_column="3"
android:layout_marginBottom="-9dip"
android:layout_marginTop="-9dip"
android:layout_width="5dip"
android:textSize="40sp" />
style="@style/StatsSpacer"
android:layout_column="3"/>
<TextView
android:gravity="right"
android:id="@+id/average_speed_register"
android:layout_column="4"
android:layout_marginBottom="-9dip"
android:layout_marginTop="-9dip"
android:text="@string/unknown"
android:textColor="@color/stat_label_text"
android:textSize="40sp" />
style="@style/StatsRegister" />
<LinearLayout
android:gravity="bottom"
android:layout_column="5"
@@ -217,24 +156,12 @@ limitations under the License.
android:orientation="vertical">
<TextView
android:id="@+id/average_speed_unit_label_top"
android:gravity="bottom"
android:layout_height="wrap_content"
android:layout_marginBottom="-1dip"
android:layout_marginTop="-1dip"
android:layout_width="wrap_content"
android:textColor="@color/stat_label_text" />
style="@style/StatsUnit" />
<TextView
android:background="@color/black"
android:layout_height="3dip"
android:layout_width="fill_parent" />
style="@style/StatsDivider" />
<TextView
android:id="@+id/average_speed_unit_label_bottom"
android:gravity="bottom"
android:layout_height="wrap_content"
android:layout_marginBottom="-1dip"
android:layout_marginTop="-1dip"
android:layout_width="wrap_content"
android:textColor="@color/stat_label_text" />
style="@style/StatsUnit" />
</LinearLayout>
</TableRow>
@@ -254,33 +181,18 @@ limitations under the License.
<TableRow
android:background="@color/stat_label_background">
<TextView
android:gravity="right"
android:id="@+id/moving_time_register"
android:layout_column="1"
android:layout_height="wrap_content"
android:layout_marginBottom="-9dip"
android:layout_marginTop="-9dip"
android:layout_span="2"
android:layout_width="fill_parent"
android:text="@string/unknown"
android:textColor="@color/stat_label_text"
android:textSize="40sp" />
style="@style/StatsRegister" />
<TextView
android:background="@color/black"
android:layout_column="3"
android:layout_marginBottom="-9dip"
android:layout_marginTop="-9dip"
android:layout_width="5dip"
android:textSize="40sp" />
style="@style/StatsSpacer"
android:layout_column="3"/>
<TextView
android:gravity="right"
android:id="@+id/average_moving_speed_register"
android:layout_column="4"
android:layout_marginBottom="-9dip"
android:layout_marginTop="-9dip"
android:text="@string/unknown"
android:textColor="@color/stat_label_text"
android:textSize="40sp" />
style="@style/StatsRegister" />
<LinearLayout
android:gravity="bottom"
android:layout_column="5"
@@ -289,24 +201,14 @@ limitations under the License.
android:orientation="vertical">
<TextView
android:id="@+id/average_moving_speed_unit_label_top"
android:gravity="bottom"
android:layout_height="wrap_content"
android:layout_marginBottom="-1dip"
android:layout_marginTop="-1dip"
android:layout_width="wrap_content"
android:textColor="@color/stat_label_text" />
style="@style/StatsUnit" />
<TextView
android:background="@color/black"
style="@style/StatsDivider" />
android:layout_height="3dip"
android:layout_width="fill_parent" />
<TextView
android:id="@+id/average_moving_speed_unit_label_bottom"
android:gravity="bottom"
android:layout_height="wrap_content"
android:layout_marginBottom="-1dip"
android:layout_marginTop="-1dip"
android:layout_width="wrap_content"
android:textColor="@color/stat_label_text" />
style="@style/StatsUnit" />
</LinearLayout>
</TableRow>
@@ -326,46 +228,24 @@ limitations under the License.
<TableRow
android:background="@color/stat_label_background">
<TextView
android:gravity="right"
android:id="@+id/elevation_register"
android:layout_column="1"
android:layout_marginBottom="-9dip"
android:layout_marginTop="-9dip"
android:text="@string/unknown"
android:textColor="@color/stat_label_text"
android:textSize="40sp" />
style="@style/StatsRegister" />
<TextView
android:gravity="left"
android:id="@+id/elevation_unit_label"
android:layout_column="2"
android:layout_marginBottom="-9dip"
android:layout_marginTop="-9dip"
android:text="@string/feet"
android:textColor="@color/stat_label_text"/>
style="@style/StatsUnitSolo" />
<TextView
android:background="@color/black"
android:layout_column="3"
android:layout_marginBottom="-9dip"
android:layout_marginTop="-9dip"
android:layout_width="5dip"
android:textSize="40sp" />
style="@style/StatsSpacer"
android:layout_column="3"/>
<TextView
android:gravity="right"
android:id="@+id/elevation_gain_register"
android:layout_column="4"
android:layout_marginBottom="-9dip"
android:layout_marginTop="-9dip"
android:text="@string/unknown"
android:textColor="@color/stat_label_text"
android:textSize="40sp" />
style="@style/StatsRegister" />
<TextView
android:gravity="right"
android:id="@+id/elevation_gain_unit_label"
android:layout_column="5"
android:layout_marginBottom="-9dip"
android:layout_marginTop="-9dip"
android:text="@string/feet"
android:textColor="@color/stat_label_text" />
style="@style/StatsUnitSolo" />
</TableRow>
<!-- Min and max elevation -->
@@ -384,46 +264,24 @@ limitations under the License.
<TableRow
android:background="@color/stat_label_background">
<TextView
android:gravity="right"
android:id="@+id/min_elevation_register"
android:layout_column="1"
android:layout_marginBottom="-9dip"
android:layout_marginTop="-9dip"
android:text="@string/unknown"
android:textColor="@color/stat_label_text"
android:textSize="40sp" />
style="@style/StatsRegister" />
<TextView
android:gravity="left"
android:id="@+id/min_elevation_unit_label"
android:layout_column="2"
android:layout_marginBottom="-9dip"
android:layout_marginTop="-9dip"
android:text="@string/feet"
android:textColor="@color/stat_label_text" />
style="@style/StatsUnitSolo" />
<TextView
android:background="@color/black"
android:layout_column="3"
android:layout_marginBottom="-9dip"
android:layout_marginTop="-9dip"
android:layout_width="5dip"
android:textSize="40sp" />
style="@style/StatsSpacer"
android:layout_column="3"/>
<TextView
android:gravity="right"
android:id="@+id/max_elevation_register"
android:layout_column="4"
android:layout_marginBottom="-9dip"
android:layout_marginTop="-9dip"
android:text="@string/unknown"
android:textColor="@color/stat_label_text"
android:textSize="40sp" />
style="@style/StatsRegister" />
<TextView
android:gravity="right"
android:id="@+id/max_elevation_unit_label"
android:layout_column="5"
android:layout_marginBottom="-9dip"
android:layout_marginTop="-9dip"
android:text="@string/feet"
android:textColor="@color/stat_label_text" />
style="@style/StatsUnitSolo" />
</TableRow>
<!-- Min and max grade -->
@@ -442,36 +300,18 @@ limitations under the License.
<TableRow
android:background="@color/stat_label_background">
<TextView
android:gravity="right"
android:id="@+id/min_grade_register"
android:layout_column="1"
android:layout_height="wrap_content"
android:layout_marginBottom="-9dip"
android:layout_marginTop="-9dip"
android:layout_width="fill_parent"
android:layout_span="2"
android:text="@string/unknown"
android:textColor="@color/stat_label_text"
android:textSize="40sp" />
style="@style/StatsRegister" />
<TextView
android:background="@color/black"
android:layout_column="3"
android:layout_marginBottom="-9dip"
android:layout_marginTop="-9dip"
android:layout_width="5dip"
android:textSize="40sp" />
style="@style/StatsSpacer"
android:layout_column="3"/>
<TextView
android:gravity="right"
android:id="@+id/max_grade_register"
android:layout_column="4"
android:layout_height="wrap_content"
android:layout_marginBottom="-9dip"
android:layout_marginTop="-9dip"
android:layout_width="fill_parent"
android:layout_span="2"
android:text="@string/unknown"
android:textColor="@color/stat_label_text"
android:textSize="40sp" />
style="@style/StatsRegister" />
</TableRow>
<!-- Latitude and Longitude -->
@@ -488,36 +328,19 @@ limitations under the License.
<TableRow
android:background="@color/stat_label_background">
<TextView
android:gravity="right"
android:id="@+id/latitude_register"
android:layout_column="1"
android:layout_height="wrap_content"
android:layout_marginBottom="-5dip"
android:layout_marginTop="-5dip"
android:layout_width="fill_parent"
android:layout_span="2"
android:text="@string/unknown"
android:textColor="@color/stat_label_text"
android:textSize="25sp" />
style="@style/StatsRegister.Small" />
<TextView
android:background="@color/black"
style="@style/StatsSpacer"
android:layout_column="3"
android:layout_marginBottom="-9dip"
android:layout_marginTop="-9dip"
android:layout_width="5dip"
android:textSize="25sp" />
<TextView
android:gravity="right"
android:id="@+id/longitude_register"
android:layout_column="4"
android:layout_height="wrap_content"
android:layout_marginBottom="-5dip"
android:layout_marginTop="-5dip"
android:layout_width="fill_parent"
android:layout_span="2"
android:text="@string/unknown"
android:textColor="@color/stat_label_text"
android:textSize="25sp" />
style="@style/StatsRegister.Small" />
</TableRow>
</TableLayout>
+5 -1
View File
@@ -22,10 +22,14 @@
android:title="@string/settings"/>
<item android:id="@+id/menu_aggregated_stats"
android:icon="@android:drawable/ic_menu_info_details"
android:orderInCategory="1002"
android:title="@string/aggregated_stats"/>
<item android:id="@+id/menu_sensor_state"
android:icon="@drawable/menu_sensor_status"
android:orderInCategory="1000"
android:title="@string/sensor_state"/>
<item android:id="@+id/menu_help"
android:icon="@android:drawable/ic_menu_info_details"
android:orderInCategory="1003"
+12
View File
@@ -390,4 +390,16 @@ limitations under the License.
<item>CSV</item>
<item>TCX</item>
</array>
<array name="sensor_type_options">
<item>None</item>
<item>Zephyr Bluetooth</item>
</array>
<!-- DO NOT TRANSLATE -->
<array name="sensor_type_values">
<item>@string/none</item>
<item>@string/zephyr_sensor_type</item>
</array>
</resources>
+11 -6
View File
@@ -15,17 +15,22 @@ See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<color name="gold">#FFffd700</color>
<color name="white">#FFFFFFFF</color>
<color name="black">#FF000000</color>
<color name="lightblue">#FF00FFFF</color>
<color name="blue">#FF0000FF</color>
<color name="blue_transparent">#400000FF</color>
<color name="red">#FFFF0000</color>
<color name="green">#FF00AA00</color>
<color name="gray">#FFAAAAAA</color>
<color name="darkgray">#FF444444</color>
<color name="gold">#FFffd700</color>
<color name="gray">#FFAAAAAA</color>
<color name="green">#FF00AA00</color>
<color name="lightblue">#FF00FFFF</color>
<color name="opaque_shadow">#70000000</color>
<color name="purple">#FF401465</color>
<color name="purple_transparent">#40401465</color>
<color name="red">#FFFF0000</color>
<color name="red_transparent">#40FF0000</color>
<color name="stat_label_background">#FFF0F0F0</color>
<color name="stat_label_text">#FF0F0F0F</color>
<color name="white">#FFFFFFFF</color>
<color name="yellow">#FFF8FF00</color>
<color name="yellow_transparent">#40F8FF00</color>
</resources>
-1
View File
@@ -33,7 +33,6 @@ limitations under the License.
<string name="send_stats_and_points_key">sendStatsAndPoints</string>
<string name="send_to_docs_key">sendToDocs</string>
<string name="send_to_my_maps_key">sendToMyMaps</string>
<string name="sensor_id_key">sensorId</string>
<string name="sensor_type_key">sensorType</string>
<string name="share_url_only_key">shareUrlOnly</string>
<string name="split_frequency_key">splitFrequency</string>
+10 -3
View File
@@ -32,6 +32,7 @@ limitations under the License.
<string name="choose_track_message">Please select the track to view</string>
<string name="current_track">Current track</string>
<string name="current_segment">Last segment</string>
<string name="sensor_state">Sensor State</string>
<string name="aggregated_stats">Aggregated Statistics</string>
<!-- Error messages -->
@@ -454,9 +455,7 @@ limitations under the License.
<!-- Sensor Types => DO NOT TRANSLATE -->
<string name="none">NONE</string>
<string name="ant_sensor_type">ANT</string>
<string name="srm_sensor_type">SRM</string>
<string name="athlosoft_sensor_type">ATHLOSOFT</string>
<string name="zephyr_sensor_type">ZEPHYR</string>
<!-- Bluetooth Sensor -->
<string name="bluetooth_connected">Connected to %s</string>
@@ -467,6 +466,14 @@ limitations under the License.
<string name="sensor_data_time">Sensor Data Time</string>
<string name="power_meter_enabled">Power meter enabled</string>
<string name="heart_rate_monitor_enabled">Heart rate monitor enabled</string>
<string name="no_sensor_manager">No Sensor Manager</string>
<string name="enabled">Enabled</string>
<string name="connecting">Connecting</string>
<string name="connected">Connected</string>
<string name="disconnected">Disconnected</string>
<string name="no_sensor_service">No Sensor Service</string>
<string name="searching">Searching</string>
<string name="sending">Sending</string>
<!-- Manifest description (not seen inside the app) -->
<string name="notification_permission_label">receive track recording notifications</string>
+77
View File
@@ -0,0 +1,77 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<resources>
<!-- The style for the unit on the statistics activity. -->
<style name="StatsUnit" parent="@android:Widget.TextView">
<item name="android:gravity">bottom</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:layout_marginBottom">-2dip</item>
<item name="android:layout_marginTop">-2dip</item>
<item name="android:layout_width">wrap_content</item>
<item name="android:textColor">@color/stat_label_text</item>
</style>
<!-- The style for the unit on the statistics activity where there is only one string. -->
<style name="StatsUnitSolo" parent="@android:Widget.TextView">
<item name="android:gravity">right</item>
<item name="android:layout_marginBottom">-9dip</item>
<item name="android:layout_marginTop">-9dip</item>
<item name="android:textColor">@color/stat_label_text</item>
</style>
<!-- The style for a text view for displaying a value on the statistics activity. -->
<style name="StatsRegister" parent="@android:Widget.TextView">
<item name="android:gravity">right</item>
<item name="android:layout_marginBottom">-9dip</item>
<item name="android:layout_marginTop">-9dip</item>
<item name="android:text">@string/unknown</item>
<item name="android:textColor">@color/stat_label_text</item>
<item name="android:textSize">40sp</item>
</style>
<!-- The style for a text view for displaying a large value on the statistics activity. -->
<style name="StatsRegister.Big">
<item name="android:layout_marginBottom">-13dip</item>
<item name="android:layout_marginTop">-13dip</item>
<item name="android:textSize">50sp</item>
</style>
<!-- The style for a text view for displaying a small value on the statistics activity. -->
<style name="StatsRegister.Small">
<item name="android:layout_height">wrap_content</item>
<item name="android:layout_marginBottom">-5dip</item>
<item name="android:layout_marginTop">-5dip</item>
<item name="android:layout_width">fill_parent</item>
<item name="android:textSize">25sp</item>
</style>
<!-- The style for a view which divides two columns in the statistics activity. -->
<style name="StatsSpacer" parent="@android:Widget.TextView">
<item name="android:background">@color/black</item>
<item name="android:layout_marginBottom">-9dip</item>
<item name="android:layout_marginTop">-9dip</item>
<item name="android:layout_width">5dip</item>
<item name="android:textSize">40sp</item>
</style>
<!-- The style for a view which divides the numerator and denominator of the units on the statistics activity. -->
<style name="StatsDivider" parent="@android:Widget.TextView">
<item name="android:background">@color/black</item>
<item name="android:layout_height">3dip</item>
<item name="android:layout_width">fill_parent</item>
</style>
</resources>
+23
View File
@@ -101,6 +101,29 @@ limitations under the License.
</PreferenceScreen>
<!-- Sensors -->
<PreferenceScreen android:title="@string/sensor_options">
<ListPreference android:key="@string/sensor_type_key"
android:title="@string/settings_sensor_type"
android:summary="@string/settings_sensor_type_summary"
android:entries="@array/sensor_type_options"
android:entryValues="@array/sensor_type_values"
android:defaultValue="@string/none"/>
<!-- Bluetooth -->
<PreferenceCategory android:title="@string/bluetooth_options">
<ListPreference android:key="@string/bluetooth_sensor_key"
android:title="@string/bluetooth_sensor_title"
android:summary="@string/bluetooth_sensor_summary"
android:defaultValue="any" />
<Preference android:key="@string/bluetooth_pairing_key"
android:title="@string/bluetooth_pairing"
android:summary="@string/bluetooth_pairing_summary"
android:persistent="false" />
</PreferenceCategory>
</PreferenceScreen>
<!-- Sharing settings -->
<PreferenceScreen android:title="@string/settings_sharing">
<CheckBoxPreference
@@ -16,11 +16,14 @@
package com.google.android.apps.mytracks;
import com.google.android.apps.mytracks.ChartView.Mode;
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.TrackPointsColumns;
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.StatusAnnouncerFactory;
import com.google.android.apps.mytracks.stats.DoubleBuffer;
import com.google.android.apps.mytracks.stats.TripStatisticsBuilder;
@@ -178,12 +181,12 @@ public class ChartActivity extends Activity implements
cursor.getColumnIndexOrThrow(TrackPointsColumns._ID);
ArrayList<double[]> data = new ArrayList<double[]>();
// 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<double[]> theData = new ArrayList<double[]>();
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);
@@ -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();
}
@@ -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();
}
}
@@ -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.
@@ -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<? extends Activity> activityClass) {
activity.startActivity(new Intent(activity, activityClass));
return true;
}
}
@@ -1128,7 +1128,7 @@ public class MyTracks extends TabActivity implements OnTouchListener,
long getSelectedTrackId() {
return selectedTrackId;
}
/**
* Binds to track recording service if it is running.
*/
@@ -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<String> entries = new ArrayList<String>();
List<String> entryValues = new ArrayList<String>();
// 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);
}
}
@@ -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));
}
}
}
@@ -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);
@@ -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);
}
}
}
@@ -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("</AltitudeMeters>");
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(" <HeartRateBpm>");
pw.print("<Value>");
pw.print(sensorData.getHeartRate().getValue());
pw.print("</Value>");
pw.println("</HeartRateBpm>");
}
if (sensorData.hasPower()
&& sensorData.getPower().getState() == Sensor.SensorState.SENDING
&& sensorData.getPower().hasValue()) {
pw.print(" <Extensions>");
pw.print("<TPX xmlns=\"http://www.garmin.com/xmlschemas/ActivityExtension/v2\">");
pw.print("<Watts>");
pw.print(sensorData.getPower().getValue());
pw.print("</Watts>");
pw.println("</TPX></Extensions>");
}
if (sensorData.hasCadence()
&& sensorData.getCadence().getState() == Sensor.SensorState.SENDING
&& sensorData.getCadence().hasValue()) {
pw.print(" <Cadence>");
pw.print(sensorData.getCadence().getValue());
pw.println("</Cadence>");
}
}
}
pw.println(" </Trackpoint>");
}
@@ -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.
@@ -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<? extends BluetoothDevice> 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);
}
}
}
}
@@ -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;
}
}
};
}
@@ -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);
}
@@ -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;
}
}
@@ -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;
}
}
}
@@ -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 "";
}
}
}
@@ -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;
}
}
@@ -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());
}
}
@@ -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<String> deviceNames, List<String> 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<String> deviceNames, List<String> deviceAddresses) {
ensureNotDiscovering();
Set<BluetoothDevice> 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<BluetoothDevice> 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<String> deviceNames, List<String> 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;
}
}
+1
View File
@@ -2,6 +2,7 @@
<classpath>
<classpathentry kind="src" path="src"/>
<classpathentry kind="src" path="gen"/>
<classpathentry exported="true" kind="lib" path="libs/protobuf-java-2.3.0-lite.jar"/>
<classpathentry kind="con" path="com.android.ide.eclipse.adt.ANDROID_FRAMEWORK"/>
<classpathentry kind="output" path="bin"/>
</classpath>
Binary file not shown.
+53
View File
@@ -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;
}
@@ -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;
}
}
@@ -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
@@ -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<SensorState>
internalGetValueMap() {
return internalValueMap;
}
private static com.google.protobuf.Internal.EnumLiteMap<SensorState>
internalValueMap =
new com.google.protobuf.Internal.EnumLiteMap<SensorState>() {
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)
}
@@ -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";
}
@@ -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();
}
@@ -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() {
@@ -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 = "<gpx><trk><name><![CDATA["
+ TRACK_NAME + "]]></name><desc><![CDATA[" + TRACK_DESC
+ "]]></desc><trkseg>" + "<trkpt lat=\"" + TRACK_LAT_1 + "\" lon=\""
@@ -93,6 +93,12 @@ public class GpxImporterTest extends AndroidTestCase {
.replaceAll(TRACK_LAT_1, "1000.0");
private static final String INVALID_TIME_TEST_GPX = VALID_TEST_GPX
.replaceAll(TRACK_TIME_1, "invalid");
private static final String INVALID_ALTITUDE_TEST_GPX = VALID_TEST_GPX
.replaceAll(TRACK_ELE_1, "invalid");
private static final String INVALID_LATITUDE_TEST_GPX = VALID_TEST_GPX
.replaceAll(TRACK_LAT_1, "invalid");
private static final String INVALID_LONGITUDE_TEST_GPX = VALID_TEST_GPX
.replaceAll(TRACK_LON_1, "invalid");
private static final long TRACK_ID = 1;
private static final long TRACK_POINT_ID_1 = 1;
@@ -122,7 +128,7 @@ public class GpxImporterTest extends AndroidTestCase {
}
/**
* Test import success
* Test import success.
*/
public void testImportSuccess() throws Exception {
Capture<Track> trackParam = new Capture<Track>();
@@ -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()))
@@ -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<Element> tags, Location... locs) {
private void assertTagsMatchPoints(List<Element> 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<Element> bpm = getChildElements(tag, "HeartRateBpm", 1);
assertEquals(Integer.toString(sds.getHeartRate().getValue()),
getChildTextValue(bpm.get(0), "Value"));
List<Element> ext = getChildElements(tag, "Extensions", 1);
List<Element> tpx = getChildElements(ext.get(0), "TPX", 1);
assertEquals(Integer.toString(sds.getPower().getValue()),
getChildTextValue(tpx.get(0), "Watts"));
}
}
@@ -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
@@ -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));
@@ -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);
}
}
@@ -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));
}
}
+1
View File
@@ -4,5 +4,6 @@
<classpathentry kind="src" path="gen"/>
<classpathentry kind="con" path="com.android.ide.eclipse.adt.ANDROID_FRAMEWORK"/>
<classpathentry kind="src" path="MyTracksLib_src"/>
<classpathentry exported="true" kind="lib" path="/MyTracksLib/libs/protobuf-java-2.3.0-lite.jar"/>
<classpathentry kind="output" path="bin"/>
</classpath>