diff --git a/MyTracks/.classpath b/MyTracks/.classpath index 7c5cb8f1e..3d5ddb422 100644 --- a/MyTracks/.classpath +++ b/MyTracks/.classpath @@ -6,5 +6,7 @@ + + diff --git a/MyTracks/.project b/MyTracks/.project index 03e694b1f..ef19912e8 100644 --- a/MyTracks/.project +++ b/MyTracks/.project @@ -30,4 +30,11 @@ com.android.ide.eclipse.adt.AndroidNature org.eclipse.jdt.core.javanature + + + MyTracksLib_src + 2 + _android_MyTracksLib_e2802071/src + + diff --git a/MyTracks/AndroidManifest.xml b/MyTracks/AndroidManifest.xml index 2b6362942..578bd1e48 100755 --- a/MyTracks/AndroidManifest.xml +++ b/MyTracks/AndroidManifest.xml @@ -1,9 +1,24 @@ + + android:versionCode="22" + android:versionName="1.0.21"> @@ -13,7 +28,10 @@ android:value="AEdPqrEAAAAIi-_QiwoRSc9_bAC9cmuNXTQyU8ajJmGtKdhskQ" /> + android:authorities="com.google.android.maps.mytracks" + android:exported="true" + android:readPermission="com.google.android.apps.mytracks.READ_TRACK_DATA" + android:writePermission="com.google.android.apps.mytracks.WRITE_TRACK_DATA" /> + android:label="MyTracks" + android:exported="false" /> + android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|orientation|fontScale" + android:exported="false" /> @@ -60,19 +80,26 @@ android:label="@string/aggregated_stats" /> + android:label="@string/statistics" + android:exported="false" /> + + + android:label="@string/elevation_label" + android:exported="false" /> + android:label="@string/welcome" + android:exported="false" /> + android:exported="true" + android:permission="com.google.android.apps.mytracks.WRITE_TRACK_DATA" /> @@ -89,6 +116,8 @@ + + @@ -102,7 +131,32 @@ - + + + + + + + + + + + + + + diff --git a/MyTracks/default.properties b/MyTracks/default.properties index 85cbfc4dd..fcf640319 100644 --- a/MyTracks/default.properties +++ b/MyTracks/default.properties @@ -11,3 +11,4 @@ split.density=false # Project target. target=Google Inc.:Google APIs:9 +android.library.reference.1=../MyTracksLib/ diff --git a/MyTracks/proguard.cfg b/MyTracks/proguard.cfg new file mode 100644 index 000000000..8ad7d3352 --- /dev/null +++ b/MyTracks/proguard.cfg @@ -0,0 +1,34 @@ +-optimizationpasses 5 +-dontusemixedcaseclassnames +-dontskipnonpubliclibraryclasses +-dontpreverify +-verbose +-optimizations !code/simplification/arithmetic,!field/*,!class/merging/* + +-keep public class * extends android.app.Activity +-keep public class * extends android.app.Application +-keep public class * extends android.app.Service +-keep public class * extends android.content.BroadcastReceiver +-keep public class * extends android.content.ContentProvider +-keep public class com.android.vending.licensing.ILicensingService + +-keepclasseswithmembernames class * { + native ; +} + +-keepclasseswithmembernames class * { + public (android.content.Context, android.util.AttributeSet); +} + +-keepclasseswithmembernames class * { + public (android.content.Context, android.util.AttributeSet, int); +} + +-keepclassmembers enum * { + public static **[] values(); + public static ** valueOf(java.lang.String); +} + +-keep class * implements android.os.Parcelable { + public static final android.os.Parcelable$Creator *; +} diff --git a/MyTracks/res/drawable/chart_settings.png b/MyTracks/res/drawable/chart_settings.png index d608eedf3..63147af45 100644 Binary files a/MyTracks/res/drawable/chart_settings.png and b/MyTracks/res/drawable/chart_settings.png differ diff --git a/MyTracks/res/drawable/menu_sensor_status.png b/MyTracks/res/drawable/menu_sensor_status.png new file mode 100644 index 000000000..80e68b847 Binary files /dev/null and b/MyTracks/res/drawable/menu_sensor_status.png differ diff --git a/MyTracks/res/layout/chart_settings.xml b/MyTracks/res/layout/chart_settings.xml index e2f13303d..6178579bc 100644 --- a/MyTracks/res/layout/chart_settings.xml +++ b/MyTracks/res/layout/chart_settings.xml @@ -103,6 +103,21 @@ limitations under the License. android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="@string/speed"/> + + + diff --git a/MyTracks/res/layout/sensor_state.xml b/MyTracks/res/layout/sensor_state.xml new file mode 100644 index 000000000..a6cd7f810 --- /dev/null +++ b/MyTracks/res/layout/sensor_state.xml @@ -0,0 +1,131 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + android:layout_column="5" + android:text="@string/watts" /> + + + + + + + + + + android:layout_column="2" + android:text="@string/revolutions_per_minute" /> + + + + + + + + + + + + + + + diff --git a/MyTracks/res/layout/stats.xml b/MyTracks/res/layout/stats.xml index 342e3b59f..af30d0875 100644 --- a/MyTracks/res/layout/stats.xml +++ b/MyTracks/res/layout/stats.xml @@ -42,15 +42,10 @@ limitations under the License. + android:layout_span="4"/> + style="@style/StatsUnit" /> + style="@style/StatsDivider" /> + style="@style/StatsUnit" /> @@ -100,43 +83,25 @@ limitations under the License. + android:layout_column="1" /> + style="@style/StatsUnitSolo" /> + style="@style/StatsSpacer" + android:layout_column="3"/> + style="@style/StatsRegister" + android:layout_column="4" /> + style="@style/StatsUnit" /> + style="@style/StatsDivider" /> + style="@style/StatsUnit" /> @@ -182,33 +135,19 @@ limitations under the License. + style="@style/StatsRegister" /> + style="@style/StatsSpacer" + android:layout_column="3"/> + style="@style/StatsRegister" /> + style="@style/StatsUnit" /> + style="@style/StatsDivider" /> + style="@style/StatsUnit" /> @@ -254,33 +181,18 @@ limitations under the License. + style="@style/StatsRegister" /> + style="@style/StatsSpacer" + android:layout_column="3"/> + style="@style/StatsRegister" /> + style="@style/StatsUnit" /> android:layout_height="3dip" android:layout_width="fill_parent" /> + style="@style/StatsUnit" /> @@ -326,46 +228,24 @@ limitations under the License. + style="@style/StatsRegister" /> + style="@style/StatsUnitSolo" /> + style="@style/StatsSpacer" + android:layout_column="3"/> + style="@style/StatsRegister" /> + style="@style/StatsUnitSolo" /> @@ -384,46 +264,24 @@ limitations under the License. + style="@style/StatsRegister" /> + style="@style/StatsUnitSolo" /> + style="@style/StatsSpacer" + android:layout_column="3"/> + style="@style/StatsRegister" /> + style="@style/StatsUnitSolo" /> @@ -442,36 +300,18 @@ limitations under the License. + style="@style/StatsRegister" /> + style="@style/StatsSpacer" + android:layout_column="3"/> + style="@style/StatsRegister" /> @@ -488,36 +328,19 @@ limitations under the License. + style="@style/StatsRegister.Small" /> + style="@style/StatsRegister.Small" /> diff --git a/MyTracks/res/menu/main.xml b/MyTracks/res/menu/main.xml index 9acffc9b3..8ff5a9e71 100644 --- a/MyTracks/res/menu/main.xml +++ b/MyTracks/res/menu/main.xml @@ -22,10 +22,14 @@ android:title="@string/settings"/> + + Beim teilen mit Freunden wird nur die Karten URL gesendet. Beim teilen mit Freunden wird der GPS Track selbst gesendet. - Empfangsstärke Aufzeichnungsfrequenz - Dauer zwischen zwei Aufzeichnungen der Telefonempfangsstärke. - Zerteilungs Frequenz Längste Dauer oder Entfernung nach der automatisch ein Marker eingefügt wird. Tempo diff --git a/MyTracks/res/values-es/strings.xml b/MyTracks/res/values-es/strings.xml index 5e6a29f8b..7a58e15e8 100644 --- a/MyTracks/res/values-es/strings.xml +++ b/MyTracks/res/values-es/strings.xml @@ -187,9 +187,6 @@ limitations under the License. Sólo el URL del mapa será enviado al compartir Los contenidos de la pista también serán incluídos al compartir - Frecuencia de medición de señal - Tiempo entre grabaciones de la intensidad de la señal del teléfono - Dividir frecuencia Tiempo o distancia máxima para insertar una nueva marca de división Paso diff --git a/MyTracks/res/values-fr/strings.xml b/MyTracks/res/values-fr/strings.xml index a67f90749..bbe44c007 100644 --- a/MyTracks/res/values-fr/strings.xml +++ b/MyTracks/res/values-fr/strings.xml @@ -187,9 +187,6 @@ limitations under the License. Seule l\'adresse URL de la carte sera envoyée lors du partage Le contenu du parcours sera également inclus lors du partage - Échantillonnage du signal - Intervalle de temps entre deux enregistrements de la force du signal - Fréquence de séparation Temps ou distance minimum avant l\'insertion d\'un marqueur de séparation Rythme diff --git a/MyTracks/res/values-it/strings.xml b/MyTracks/res/values-it/strings.xml index 2f9024552..2fe4ff4ba 100644 --- a/MyTracks/res/values-it/strings.xml +++ b/MyTracks/res/values-it/strings.xml @@ -193,9 +193,6 @@ limitations under the License. Solo l\'URL della mappa sarà inviata quando condividi un percorso Anche il contenuto del percorso sarà inviato quando condividi un persorso - Frequenza di campionamento - Tempo fra due registrazioni della qualità del segnale cellulare - Frequenza di divisione Tempo o distanza massima dopo di cui il percorso verrà diviso Andatura diff --git a/MyTracks/res/values-ja/strings.xml b/MyTracks/res/values-ja/strings.xml index b88304819..6a5745f77 100644 --- a/MyTracks/res/values-ja/strings.xml +++ b/MyTracks/res/values-ja/strings.xml @@ -194,9 +194,6 @@ limitations under the License. 共有時にマップのURLのみ送信します 共有時にトラックのコンテントが含まれます - 信号サンプリング周波数 - 端末の信号強度記録の時間間隔 - 分割頻度 分割マーカを挿入する時間または距離の間隔 ペース @@ -384,12 +381,4 @@ limitations under the License. 計測単位についてのご注意 デフォルトの単位はメートルです.設定を変更しますか? - - - 誤り率: %1$d - S/N比: %1$d - Ec/Io比: %1$.1f dB - GSM 強度: %1$d - CDMA 強度: %1$d dBm - EVDO 強度: %1$d dBm diff --git a/MyTracks/res/values-pl/strings.xml b/MyTracks/res/values-pl/strings.xml index 049209c7d..2d597a9e2 100644 --- a/MyTracks/res/values-pl/strings.xml +++ b/MyTracks/res/values-pl/strings.xml @@ -195,9 +195,6 @@ limitations under the License. Tylko odnośnik do mapy będzie wysyłany podczas dzielenia się Zawartość trasy będzie również przesyłana - Częstotliowść próbkowania sygnału - Czas między dwoma zapisami siły sygnału telefonu - Częstotliwość rozdzielania Maksymalny czas lub odległość po którym wstawiony zostanie znacznik rozdzielenia @@ -416,12 +413,4 @@ limitations under the License. Jednostki miary Domyślnie używane są jednostki metryczne. Chcesz to zmienić? - - - Poziom błędów: %1$d - Stosunek sygnału do szumu: %1$d - Ec/lo: %1$.1f dB - Siła GSM: %1$d - Siła CDMA: %1$d dBm - Siła EVDO: %1$d dBm diff --git a/MyTracks/res/values-sv/strings.xml b/MyTracks/res/values-sv/strings.xml index fa43e18e7..3b18a5f95 100644 --- a/MyTracks/res/values-sv/strings.xml +++ b/MyTracks/res/values-sv/strings.xml @@ -195,9 +195,6 @@ limitations under the License. Bara länken till kartan kommer att skickas vid delning Innehållet i spåret kommer också att inkluderas vid delning - Signalprovsfrekvens - Tid mellan två inspelningar av telefonens signalstyrka - Delningsfrekvens Maximala tiden eller distansen varefter en ny delningsmarkör kommer infogas Tempo @@ -383,12 +380,4 @@ limitations under the License. Mätenheter Som standard är enheterna metriska. Vill du ändra den inställningen? - - - Felfrekvens: %1$d - Signal / brus-förhållande: %1$d - Ec/lo: %1$.1f dB - GSM-styrka: %1$d - CDMA-styrka: %1$d dBm - EVDO-styrka: %1$d dBm diff --git a/MyTracks/res/values-zh-rCN/strings.xml b/MyTracks/res/values-zh-rCN/strings.xml index 648550a39..cacf8422c 100644 --- a/MyTracks/res/values-zh-rCN/strings.xml +++ b/MyTracks/res/values-zh-rCN/strings.xml @@ -201,9 +201,6 @@ limitations under the License. 在分享时只发送地图的 URL 在分享时也包括轨迹的内容 - 信号采样频率 - 记录电话信号强度的时间间隔 - 切割频度 插入新切割标记所需要达到的最大时间或距离 @@ -451,14 +448,6 @@ limitations under the License. 测量单位提示 默认单位均为公制. 你想要修改此设置吗? - - 错误率: %1$d - 信噪比: %1$d - Ec/lo: %1$.1f dB - GSM 强度 %1$d - CDMA 强度 %1$d dBm - EVDO 强度 %1$d dBm - 蓝牙 蓝牙设置 @@ -467,12 +456,6 @@ limitations under the License. 感应器选项 - - - - - - 连接到 %s 蓝牙感应器连接中 ... diff --git a/MyTracks/res/values/arrays.xml b/MyTracks/res/values/arrays.xml index ac7772419..3eb41e8fe 100644 --- a/MyTracks/res/values/arrays.xml +++ b/MyTracks/res/values/arrays.xml @@ -390,4 +390,16 @@ limitations under the License. CSV TCX + + + None + Zephyr Bluetooth + + + + + @string/none + @string/zephyr_sensor_type + + diff --git a/MyTracks/res/values/colors.xml b/MyTracks/res/values/colors.xml index 911148cc0..3ce80b119 100644 --- a/MyTracks/res/values/colors.xml +++ b/MyTracks/res/values/colors.xml @@ -15,17 +15,22 @@ See the License for the specific language governing permissions and limitations under the License. --> - #FFffd700 - #FFFFFFFF #FF000000 - #FF00FFFF #FF0000FF #400000FF - #FFFF0000 - #FF00AA00 - #FFAAAAAA #FF444444 + #FFffd700 + #FFAAAAAA + #FF00AA00 + #FF00FFFF #70000000 + #FF401465 + #40401465 + #FFFF0000 + #40FF0000 #FFF0F0F0 #FF0F0F0F + #FFFFFFFF + #FFF8FF00 + #40F8FF00 diff --git a/MyTracks/res/values/keys.xml b/MyTracks/res/values/keys.xml index 43a843ebc..ebf7cdf07 100644 --- a/MyTracks/res/values/keys.xml +++ b/MyTracks/res/values/keys.xml @@ -33,10 +33,8 @@ limitations under the License. sendStatsAndPoints sendToDocs sendToMyMaps - sensorId sensorType shareUrlOnly - signalSamplingFrequency splitFrequency timestampTrackName diff --git a/MyTracks/res/values/strings.xml b/MyTracks/res/values/strings.xml index 6212f9cf2..b837dcc82 100644 --- a/MyTracks/res/values/strings.xml +++ b/MyTracks/res/values/strings.xml @@ -32,6 +32,7 @@ limitations under the License. Please select the track to view Current track Last segment + Sensor State Aggregated Statistics @@ -201,9 +202,6 @@ limitations under the License. Only the URL of the map will be sent when sharing The contents of the track will also be included when sharing - Signal sampling frequency - Time between two recordings of the phone\'s signal strength - Split frequency Maximum time or distance after which a new split marker will be inserted @@ -447,14 +445,6 @@ limitations under the License. Units of Measure Notice By default units are metric. Would you like to change that setting? - - Error Rate: %1$d - Signal to Noise Ratio: %1$d - Ec/lo: %1$.1f dB - GSM Strength: %1$d - CDMA Strength: %1$d dBm - EVDO Strength: %1$d dBm - Bluetooth Bluetooth options @@ -465,9 +455,7 @@ limitations under the License. NONE - ANT - SRM - ATHLOSOFT + ZEPHYR Connected to %s @@ -478,5 +466,20 @@ limitations under the License. Sensor Data Time Power meter enabled Heart rate monitor enabled + No Sensor Manager + Enabled + Connecting + Connected + Disconnected + No Sensor Service + Searching + Sending + + receive track recording notifications + read recorded track data + write recorded track data, start/stop recording tracks + Be notified when a track starts or stops being recorded. + Be able to read track data, such as points and waypoints. + Be able to write track data, such as starting recording or adding waypoints. diff --git a/MyTracks/res/values/styles.xml b/MyTracks/res/values/styles.xml new file mode 100644 index 000000000..b9e0bb68f --- /dev/null +++ b/MyTracks/res/values/styles.xml @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/MyTracks/res/xml/preferences.xml b/MyTracks/res/xml/preferences.xml index 848bab5a2..c0b519386 100644 --- a/MyTracks/res/xml/preferences.xml +++ b/MyTracks/res/xml/preferences.xml @@ -84,14 +84,6 @@ limitations under the License. android:entryValues="@array/min_required_accuracy_values" android:defaultValue="200" /> - - + + + + + + + + + + + + data = new ArrayList(); // Need two locations so we can keep track of the last location. - Location location = new Location(""); + Location location = new MyTracksLocation(""); do { lastSeenLocationId = cursor.getLong(idColumnIdx); providerUtils.fillLocation(cursor, location); if (MyTracksUtils.isValidLocation(location)) { - double[] point = new double[3]; + double[] point = new double[6]; location = getDataPoint(location, track, point); data.add(point); } @@ -464,10 +467,14 @@ public class ChartActivity extends Activity implements /** * Given a location, creates a new data point for the chart. A data point is - * an array double[2], where: + * an array double[3 or 6], where: * data[0] = the time or distance * data[1] = the elevation * data[2] = the speed + * and possibly: + * data[3] = power + * data[4] = cadence + * data[5] = heart rate * * This must be called in order for each point. * @@ -477,6 +484,35 @@ public class ChartActivity extends Activity implements * @return the previous location, now available for reuse */ private Location getDataPoint(Location location, Track track, double[] result) { + if (location instanceof MyTracksLocation && + ((MyTracksLocation) location).getSensorDataSet() != null) { + SensorDataSet sensorData = ((MyTracksLocation) location).getSensorDataSet(); + if (sensorData.hasPower() + && sensorData.getPower().getState() == Sensor.SensorState.SENDING + && sensorData.getPower().hasValue()) { + result[3] = sensorData.getPower().getValue(); + } else { + result[3] = Double.NaN; + } + if (sensorData.hasCadence() + && sensorData.getCadence().getState() == Sensor.SensorState.SENDING + && sensorData.getCadence().hasValue()) { + result[4] = sensorData.getCadence().getValue(); + } else { + result[4] = Double.NaN; + } + if (sensorData.hasHeartRate() + && sensorData.getHeartRate().getState() == Sensor.SensorState.SENDING + && sensorData.getHeartRate().hasValue()) { + result[5] = sensorData.getHeartRate().getValue(); + } else { + result[5] = Double.NaN; + } + } else { + result[3] = Double.NaN; + result[4] = Double.NaN; + result[5] = Double.NaN; + } switch (mode) { case BY_DISTANCE: result[0] = profileLength; @@ -529,7 +565,7 @@ public class ChartActivity extends Activity implements if (oldLastLocation == null) { // No previous location, but return a blank one for reuse - return new Location(""); + return new MyTracksLocation(""); } return oldLastLocation; @@ -576,7 +612,7 @@ public class ChartActivity extends Activity implements final ArrayList theData = new ArrayList(); int points = 0; // Need two locations so we can keep track of the last location. - Location location = new Location(""); + Location location = new MyTracksLocation(""); while (lastLocationRead < track.getStopId()) { cursor = providerUtils.getLocationsCursor( selectedTrackId, lastLocationRead, bufferSize, false); @@ -592,7 +628,8 @@ public class ChartActivity extends Activity implements if (MyTracksUtils.isValidLocation(location)) { lastLocationRead = lastSeenLocationId = cursor.getLong(idColumnIdx); - double[] point = new double[3]; + // TODO Can we be smarter about choosing 3 or 6 entries? + double[] point = new double[6]; location = getDataPoint(location, track, point); if (points % chartSamplingFrequency == 0) { theData.add(point); diff --git a/MyTracks/src/com/google/android/apps/mytracks/ChartSettingsDialog.java b/MyTracks/src/com/google/android/apps/mytracks/ChartSettingsDialog.java index 0610e0671..34d64e82f 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/ChartSettingsDialog.java +++ b/MyTracks/src/com/google/android/apps/mytracks/ChartSettingsDialog.java @@ -35,8 +35,7 @@ import android.widget.RadioGroup; */ public class ChartSettingsDialog extends Dialog { private RadioButton distance; - private CheckBox elevation; - private CheckBox speed; + private CheckBox[] series; public ChartSettingsDialog(Context context) { super(context); @@ -61,8 +60,18 @@ public class ChartSettingsDialog extends Dialog { }); distance = (RadioButton) findViewById(R.id.chart_settings_by_distance); - elevation = (CheckBox) findViewById(R.id.chart_settings_elevation); - speed = (CheckBox) findViewById(R.id.chart_settings_speed); + + series = new CheckBox[ChartView.NUM_SERIES]; + series[ChartView.ELEVATION_SERIES] = + (CheckBox) findViewById(R.id.chart_settings_elevation); + series[ChartView.SPEED_SERIES] = + (CheckBox) findViewById(R.id.chart_settings_speed); + series[ChartView.POWER_SERIES] = + (CheckBox) findViewById(R.id.chart_settings_power); + series[ChartView.CADENCE_SERIES] = + (CheckBox) findViewById(R.id.chart_settings_cadence); + series[ChartView.HEART_RATE_SERIES] = + (CheckBox) findViewById(R.id.chart_settings_heart_rate); } public void setup(ChartActivity chart) { @@ -74,8 +83,9 @@ public class ChartSettingsDialog extends Dialog { rd.check(chart.getMode() == Mode.BY_DISTANCE ? R.id.chart_settings_by_distance : R.id.chart_settings_by_time); - elevation.setChecked(chart.isSeriesEnabled(ChartView.ELEVATION_SERIES)); - speed.setChecked(chart.isSeriesEnabled(ChartView.SPEED_SERIES)); + for (int i = 0; i < ChartView.NUM_SERIES; i++) { + series[i].setChecked(chart.isSeriesEnabled(i)); + } } private void handleOk() { @@ -83,8 +93,9 @@ public class ChartSettingsDialog extends Dialog { chart.setMode(distance.isChecked() ? Mode.BY_DISTANCE : Mode.BY_TIME); // TODO: check that something is visible. - chart.setSeriesEnabled(ChartView.ELEVATION_SERIES, elevation.isChecked()); - chart.setSeriesEnabled(ChartView.SPEED_SERIES, speed.isChecked()); + for (int i = 0; i < ChartView.NUM_SERIES; i++) { + chart.setSeriesEnabled(i, series[i].isChecked()); + } dismiss(); } diff --git a/MyTracks/src/com/google/android/apps/mytracks/ChartValueSeries.java b/MyTracks/src/com/google/android/apps/mytracks/ChartValueSeries.java index d5ff24834..acf61ab16 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/ChartValueSeries.java +++ b/MyTracks/src/com/google/android/apps/mytracks/ChartValueSeries.java @@ -42,6 +42,7 @@ public class ChartValueSeries { private double min = 0; private double max = 1; private int effectiveMax = 0; + private int absoluteMax = Integer.MIN_VALUE; private int effectiveMin = 0; private double spread = 0; private boolean enabled = true; @@ -131,7 +132,7 @@ public class ChartValueSeries { if (min < 0) { effectiveMin -= rounding; } - spread = effectiveMax - effectiveMin; + spread = getMax() - effectiveMin; } /** @@ -139,7 +140,7 @@ public class ChartValueSeries { */ public int getMaxLabelLength() { String minS = format.format(effectiveMin); - String maxS = format.format(effectiveMax); + String maxS = format.format(getMax()); return Math.max(minS.length(), maxS.length()); } @@ -151,10 +152,16 @@ public class ChartValueSeries { } /** - * @return The rounded up maximum value + * @return The minimum of the rounded up max value and the effective max. */ public int getMax() { - return effectiveMax; + // Return the effective max if no absolute is set + // or the effective max is less than the absolute. + if ((absoluteMax == Integer.MIN_VALUE) || (effectiveMax < absoluteMax)) { + return effectiveMax; + } else { + return absoluteMax; + } } /** @@ -211,4 +218,16 @@ public class ChartValueSeries { public void setEnabled(boolean enabled) { this.enabled = enabled; } + + public int getAbsoluteMax() { + return absoluteMax; + } + + public void setAbsoluteMax(int absoluteMax) { + this.absoluteMax = absoluteMax; + } + + public boolean hasData() { + return monitor.hasData(); + } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/ChartView.java b/MyTracks/src/com/google/android/apps/mytracks/ChartView.java index 4ff195a76..06cc848bf 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/ChartView.java +++ b/MyTracks/src/com/google/android/apps/mytracks/ChartView.java @@ -87,7 +87,10 @@ public class ChartView extends View { */ public static final int ELEVATION_SERIES = 0; public static final int SPEED_SERIES = 1; - public static final int NUM_SERIES = 2; + public static final int POWER_SERIES = 2; + public static final int CADENCE_SERIES = 3; + public static final int HEART_RATE_SERIES = 4; + public static final int NUM_SERIES = 5; private ChartValueSeries[] series; private final ExtremityMonitor xMonitor = new ExtremityMonitor(); @@ -172,38 +175,44 @@ public class ChartView extends View { series = new ChartValueSeries[NUM_SERIES]; // Create the paints - Paint fillPaint1 = new Paint(); - fillPaint1.setStyle(Style.FILL); - fillPaint1.setColor(context.getResources().getColor(R.color.green)); - fillPaint1.setAntiAlias(true); - - Paint fillPaint2 = new Paint(); - fillPaint2.setStyle(Style.FILL); - fillPaint2.setColor( - context.getResources().getColor(R.color.blue_transparent)); - fillPaint2.setAntiAlias(true); - - Paint strokePaint = new Paint(); - strokePaint.setStyle(Style.STROKE); - strokePaint.setColor(context.getResources().getColor(R.color.blue)); - strokePaint.setAntiAlias(true); + Paint fillPaint = new Paint(); + fillPaint.setStyle(Style.FILL); + fillPaint.setColor(context.getResources().getColor(R.color.green)); + fillPaint.setAntiAlias(true); // Create the value series series[ELEVATION_SERIES] = new ChartValueSeries(context, "###,###", - fillPaint1, + fillPaint, null, 100, R.string.elevation); - series[SPEED_SERIES] = - new ChartValueSeries(context, - "###,###.0", - fillPaint2, - strokePaint, - 5, - R.string.speed); + series[SPEED_SERIES] = getSeries(context, R.color.blue_transparent, + R.color.blue, "###,###.0", R.string.speed); + series[POWER_SERIES] = getSeries(context, R.color.red_transparent, + R.color.red, "###,###", R.string.power); + series[POWER_SERIES].setAbsoluteMax(1500); + series[CADENCE_SERIES] = getSeries(context, R.color.purple_transparent, + R.color.purple, "###,###", R.string.cadence); + series[HEART_RATE_SERIES] = getSeries(context, R.color.yellow_transparent, + R.color.yellow, "###,###", R.string.heart_rate); + } + + private ChartValueSeries getSeries(Context context, + int fillColor, int strokeColor, String format, int seriesName) { + Paint fillPaint = new Paint(); + fillPaint.setStyle(Style.FILL); + fillPaint.setColor(context.getResources().getColor(fillColor)); + fillPaint.setAntiAlias(true); + + Paint strokePaint = new Paint(); + strokePaint.setStyle(Style.STROKE); + strokePaint.setColor(context.getResources().getColor(strokeColor)); + strokePaint.setAntiAlias(true); + return new ChartValueSeries(context, format, fillPaint, strokePaint, 5, + seriesName); } public void clearWaypoints() { @@ -266,13 +275,25 @@ public class ChartView extends View { */ public synchronized void addDataPoint(double[] theData) { data.add(theData); + addDataPointInternal(theData); + updateDimensions(); + setupPath(); + } + + private void addDataPointInternal(double[] theData) { xMonitor.update(theData[0]); int min = Math.min(series.length, theData.length - 1); for (int i = 0; i < min; i++) { - series[i].update(theData[i + 1]); + if (!Double.isNaN(theData[i + 1])) { + series[i].update(theData[i + 1]); + } + } + // Fill in the extra's if needed. + for (int i = theData.length; i < series.length; i++) { + if (series[i].hasData()) { + series[i].update(0); + } } - updateDimensions(); - setupPath(); } /** @@ -284,11 +305,7 @@ public class ChartView extends View { data.addAll(theData); for (int i = 0; i < theData.size(); i++) { double d[] = theData.get(i); - xMonitor.update(d[0]); - int min = Math.min(series.length, d.length - 1); - for (int j = 0; j < min; j++) { - series[j].update(d[j + 1]); - } + addDataPointInternal(d); } updateDimensions(); setupPath(); @@ -526,7 +543,7 @@ public class ChartView extends View { // Draw the data series for (ChartValueSeries cvs : series) { - if (cvs.isEnabled()) { + if (cvs.isEnabled() && cvs.hasData()) { cvs.drawPath(c); } } @@ -560,7 +577,7 @@ public class ChartView extends View { c.translate(getScrollX(), 0); drawYAxis(c); for (ChartValueSeries cvs : series) { - if (cvs.isEnabled()) { + if (cvs.isEnabled() && cvs.hasData()) { drawYLabels(cvs, c); } } @@ -576,13 +593,13 @@ public class ChartView extends View { private void drawSeriesTitles(Canvas c) { int sections = 1; for (ChartValueSeries cvs : series) { - if (cvs.isEnabled()) { + if (cvs.isEnabled() && cvs.hasData()) { sections++; } } int j = 0; for (ChartValueSeries cvs : series) { - if (cvs.isEnabled()) { + if (cvs.isEnabled() && cvs.hasData()) { int y = (int) (w * zoomLevel * ((double) (++j) / sections)); c.drawText(cvs.getTitle(), y, TOP_BORDER, cvs.getPaint()); } @@ -601,11 +618,11 @@ public class ChartView extends View { return; } - int min = Math.min(series.length, data.get(0).length - 1); - // All of the data points to the respective series. for (int i = 0; i < data.size(); i++) { double[] d = data.get(i); + int min = Math.min(series.length, d.length - 1); + for (int j = 0; j < min; j++) { ChartValueSeries cvs = series[j]; Path path = cvs.getPath(); @@ -616,17 +633,36 @@ public class ChartView extends View { // Close the path. int yCorner = TOP_BORDER + effectiveHeight; int xCorner = getX(data.get(0)[0]); + int min = series.length; for (int j = 0; j < min; j++) { ChartValueSeries cvs = series[j]; Path path = cvs.getPath(); - // Bottom right corner - path.lineTo(getX(data.get(data.size() - 1)[0]), yCorner); - // Bottom left corner - path.lineTo(xCorner, yCorner); - // Top right corner - path.lineTo(xCorner, getY(cvs, data.get(0)[j + 1])); + int first = getFirstPointPopulatedIndex(j + 1); + if (first != -1) { + // Bottom right corner + path.lineTo(getX(data.get(data.size() - 1)[0]), yCorner); + // Bottom left corner + path.lineTo(xCorner, yCorner); + // Top right corner + path.lineTo(xCorner, getY(cvs, data.get(first)[j + 1])); + } } } + + /** + * Find the index of the first point which has a series populated. + * @param seriesIndex The index of the value series to search for. + * @return The index in the first data for the point in the series that has series + * index value populated or -1 if none is found. + */ + private int getFirstPointPopulatedIndex(int seriesIndex) { + for (int i = 0; i < data.size(); i++) { + if (data.get(i).length > seriesIndex) { + return i; + } + } + return -1; + } /** * Update the histogram dimensions. diff --git a/MyTracks/src/com/google/android/apps/mytracks/MenuManager.java b/MyTracks/src/com/google/android/apps/mytracks/MenuManager.java index 374e4d945..aecda4b3d 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/MenuManager.java +++ b/MyTracks/src/com/google/android/apps/mytracks/MenuManager.java @@ -17,6 +17,7 @@ package com.google.android.apps.mytracks; import com.google.android.maps.mytracks.R; +import android.app.Activity; import android.content.Intent; import android.view.Menu; import android.view.MenuItem; @@ -63,8 +64,8 @@ class MenuManager { return true; } case R.id.menu_list_tracks: { - Intent startIntent = new Intent(activity, MyTracksList.class); - activity.startActivityForResult(startIntent, MyTracksConstants.SHOW_TRACK); + activity.startActivityForResult(new Intent(activity, MyTracksList.class), + MyTracksConstants.SHOW_TRACK); return true; } case R.id.menu_list_markers: { @@ -73,20 +74,17 @@ class MenuManager { activity.startActivityForResult(startIntent, MyTracksConstants.SHOW_WAYPOINT); return true; } + case R.id.menu_sensor_state: { + return startActivity(SensorStateActivity.class); + } case R.id.menu_settings: { - Intent startIntent = new Intent(activity, MyTracksSettings.class); - activity.startActivity(startIntent); - return true; + return startActivity(MyTracksSettings.class); } case R.id.menu_aggregated_stats: { - Intent startIntent = new Intent(activity, AggregatedStatsActivity.class); - activity.startActivity(startIntent); - return true; + return startActivity(AggregatedStatsActivity.class); } case R.id.menu_help: { - Intent startIntent = new Intent(activity, WelcomeActivity.class); - activity.startActivity(startIntent); - return true; + return startActivity(WelcomeActivity.class); } case MyTracksConstants.MENU_CLEAR_MAP: { activity.setSelectedTrackId(-1); @@ -95,4 +93,9 @@ class MenuManager { } return false; } + + private boolean startActivity(Class activityClass) { + activity.startActivity(new Intent(activity, activityClass)); + return true; + } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/MyTracks.java b/MyTracks/src/com/google/android/apps/mytracks/MyTracks.java index 87bf63159..60ff3a840 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/MyTracks.java +++ b/MyTracks/src/com/google/android/apps/mytracks/MyTracks.java @@ -63,7 +63,6 @@ import android.os.Bundle; import android.os.Handler; import android.os.HandlerThread; import android.os.IBinder; -import android.os.RemoteException; import android.util.Log; import android.view.Menu; import android.view.MenuItem; @@ -229,7 +228,9 @@ public class MyTracks extends TabActivity implements OnTouchListener, } try { return trackRecordingService.isRecording(); - } catch (RemoteException e) { + // TODO: We catch Exception, because after eliminating the service process + // all exceptions it may throw are no longer wrapped in a RemoteException. + } catch (Exception e) { Log.e(MyTracksConstants.TAG, "MyTracks: Remote exception.", e); // Fall back to alternative check method. @@ -411,6 +412,11 @@ public class MyTracks extends TabActivity implements OnTouchListener, return dialogManager.onCreateDialog(id, args); } + @Override + protected Dialog onCreateDialog(int id) { + return dialogManager.onCreateDialog(id, null); + } + @Override protected void onPrepareDialog(int id, Dialog dialog) { super.onPrepareDialog(id, dialog); @@ -914,7 +920,9 @@ public class MyTracks extends TabActivity implements OnTouchListener, Log.e(MyTracksConstants.TAG, "Cannot insert waypoint marker?"); return -4; } - } catch (RemoteException e) { + // TODO: We catch Exception, because after eliminating the service process + // all exceptions it may throw are no longer wrapped in a RemoteException. + } catch (Exception e) { Toast.makeText(this, R.string.error_unable_to_insert_marker, Toast.LENGTH_LONG).show(); Log.e(MyTracksConstants.TAG, "Cannot insert waypoint marker.", e); @@ -954,7 +962,9 @@ public class MyTracks extends TabActivity implements OnTouchListener, Log.e(MyTracksConstants.TAG, "Cannot insert statistics marker?"); return -4; } - } catch (RemoteException e) { + // TODO: We catch Exception, because after eliminating the service process + // all exceptions it may throw are no longer wrapped in a RemoteException. + } catch (Exception e) { Toast.makeText(this, R.string.error_unable_to_insert_marker, Toast.LENGTH_LONG).show(); Log.e(MyTracksConstants.TAG, "Cannot insert statistics marker?", e); @@ -1022,7 +1032,9 @@ public class MyTracks extends TabActivity implements OnTouchListener, setSelectedTrackId(recordingTrackId); Toast.makeText(this, getString(R.string.status_now_recording), Toast.LENGTH_SHORT).show(); - } catch (RemoteException e) { + // TODO: We catch Exception, because after eliminating the service process + // all exceptions it may throw are no longer wrapped in a RemoteException. + } catch (Exception e) { Toast.makeText(this, getString(R.string.error_unable_to_start_recording), Toast.LENGTH_SHORT).show(); @@ -1055,7 +1067,9 @@ public class MyTracks extends TabActivity implements OnTouchListener, long currentTrackId = recordingTrackId; try { trackRecordingService.endCurrentTrack(); - } catch (RemoteException e) { + // TODO: We catch Exception, because after eliminating the service process + // all exceptions it may throw are no longer wrapped in a RemoteException. + } catch (Exception e) { Log.e(MyTracksConstants.TAG, "Unable to stop recording.", e); } Intent intent = new Intent(MyTracks.this, MyTracksDetails.class); @@ -1120,7 +1134,7 @@ public class MyTracks extends TabActivity implements OnTouchListener, long getSelectedTrackId() { return selectedTrackId; } - + /** * Binds to track recording service if it is running. */ diff --git a/MyTracks/src/com/google/android/apps/mytracks/MyTracksSettings.java b/MyTracks/src/com/google/android/apps/mytracks/MyTracksSettings.java index e58c99388..c47365ae1 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/MyTracksSettings.java +++ b/MyTracks/src/com/google/android/apps/mytracks/MyTracksSettings.java @@ -15,12 +15,17 @@ */ package com.google.android.apps.mytracks; +import java.util.ArrayList; +import java.util.List; + import com.google.android.apps.mytracks.io.backup.BackupActivityHelper; import com.google.android.apps.mytracks.io.backup.BackupPreferencesListener; import com.google.android.apps.mytracks.services.StatusAnnouncerFactory; import com.google.android.apps.mytracks.util.ApiFeatures; +import com.google.android.apps.mytracks.util.BluetoothDeviceUtils; import com.google.android.maps.mytracks.R; +import android.content.Intent; import android.content.SharedPreferences; import android.os.Bundle; import android.preference.CheckBoxPreference; @@ -30,6 +35,7 @@ import android.preference.PreferenceActivity; import android.preference.PreferenceManager; import android.preference.Preference.OnPreferenceChangeListener; import android.preference.Preference.OnPreferenceClickListener; +import android.provider.Settings; /** * An activity that let's the user see and edit the settings. @@ -53,6 +59,7 @@ public class MyTracksSettings extends PreferenceActivity { public static final int DEFAULT_SPLIT_FREQUENCY = 0; private BackupPreferencesListener backupListener; + private SharedPreferences preferences; /** Called when the activity is first created. */ @@ -96,6 +103,19 @@ public class MyTracksSettings extends PreferenceActivity { }); updatePreferenceUnits(metricUnitsPreference.isChecked()); + ListPreference sensorTypePreference = + (ListPreference) findPreference(getString(R.string.sensor_type_key)); + sensorTypePreference.setOnPreferenceChangeListener( + new OnPreferenceChangeListener() { + @Override + public boolean onPreferenceChange(Preference preference, + Object newValue) { + updateSensorSettings((String) newValue); + return true; + } + }); + updateSensorSettings(sensorTypePreference.getValue()); + // Disable TTS announcement preference if not available if (!apiFeatures.hasTextToSpeech()) { IntegerListPreference announcementFrequency = @@ -112,6 +132,7 @@ public class MyTracksSettings extends PreferenceActivity { protected void onResume() { super.onResume(); + configureBluetoothPreferences(); Preference backupNowPreference = findPreference(getString(R.string.backup_to_sd_key)); Preference restoreNowPreference = @@ -158,7 +179,15 @@ public class MyTracksSettings extends PreferenceActivity { getPreferenceManager().getSharedPreferences() .unregisterOnSharedPreferenceChangeListener(backupListener); - super.onDestroy(); + super.onPause(); + } + private void updateSensorSettings(String sensorType) { + boolean usesBluetooth = + getString(R.string.zephyr_sensor_type).equals(sensorType); + findPreference( + getString(R.string.bluetooth_sensor_key)).setEnabled(usesBluetooth); + findPreference( + getString(R.string.bluetooth_pairing_key)).setEnabled(usesBluetooth); } /** @@ -194,4 +223,44 @@ public class MyTracksSettings extends PreferenceActivity { ? R.array.split_frequency_options : R.array.split_frequency_options_ft); } + + + /** + * Configures preference actions related to bluetooth. + */ + private void configureBluetoothPreferences() { + if (BluetoothDeviceUtils.isBluetoothMethodSupported()) { + // Populate the list of bluetooth devices + populateBluetoothDeviceList(); + + // Make the pair devices preference go to the system preferences + findPreference(getString(R.string.bluetooth_pairing_key)) + .setOnPreferenceClickListener(new OnPreferenceClickListener() { + public boolean onPreferenceClick(Preference preference) { + Intent settingsIntent = new Intent(Settings.ACTION_BLUETOOTH_SETTINGS); + startActivity(settingsIntent); + return false; + } + }); + } + } + + /** + * Populates the list preference with all available bluetooth devices. + */ + private void populateBluetoothDeviceList() { + // Build the list of entries and their values + List entries = new ArrayList(); + List entryValues = new ArrayList(); + + // The actual devices + BluetoothDeviceUtils.getInstance().populateDeviceLists(entries, entryValues); + + CharSequence[] entriesArray = entries.toArray(new CharSequence[entries.size()]); + CharSequence[] entryValuesArray = entryValues.toArray(new CharSequence[entryValues.size()]); + ListPreference devicesPreference = + (ListPreference) findPreference(getString(R.string.bluetooth_sensor_key)); + devicesPreference.setEntryValues(entryValuesArray); + devicesPreference.setEntries(entriesArray); + } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/SensorStateActivity.java b/MyTracks/src/com/google/android/apps/mytracks/SensorStateActivity.java new file mode 100644 index 000000000..fb9559d26 --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/SensorStateActivity.java @@ -0,0 +1,193 @@ +/* + * Copyright 2010 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.google.android.apps.mytracks; + +import static com.google.android.apps.mytracks.MyTracksConstants.TAG; +import com.google.android.apps.mytracks.content.Sensor; +import com.google.android.apps.mytracks.services.ITrackRecordingService; +import com.google.android.apps.mytracks.services.sensors.SensorUtils; +import com.google.android.maps.mytracks.R; +import com.google.protobuf.InvalidProtocolBufferException; + +import android.app.Activity; +import android.os.Bundle; +import android.os.RemoteException; +import android.util.Log; +import android.widget.TextView; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Timer; +import java.util.TimerTask; + +/** + * An activity that displays information about sensors. + * + * @author Sandor Dornbush + */ +public class SensorStateActivity extends Activity { + + private static final SimpleDateFormat TIMESTAMP_FORMAT = + new SimpleDateFormat("HH:mm:ss"); + private static final long REFRESH_PERIOD_MS = 250; + + private final StatsUtilities utils; + + /** + * This timer periodically invokes the refresh timer task. + */ + private Timer timer; + + private final Runnable stateUpdater = new Runnable() { + public void run() { + updateState(); + } + }; + + /** + * A task which will update the U/I. + */ + private class RefreshTask extends TimerTask { + @Override + public void run() { + runOnUiThread(stateUpdater); + } + }; + + public SensorStateActivity() { + utils = new StatsUtilities(this); + Log.w(TAG, "SensorStateActivity()"); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + Log.w(TAG, "SensorStateActivity.onCreate"); + + setContentView(R.layout.sensor_state); + + updateState(); + } + + @Override + protected void onResume() { + super.onResume(); + timer = new Timer(); + timer.schedule(new RefreshTask(), REFRESH_PERIOD_MS, REFRESH_PERIOD_MS); + } + + @Override + protected void onPause() { + super.onPause(); + timer.cancel(); + timer.purge(); + timer = null; + } + + protected void updateState() { + MyTracks mt = MyTracks.getInstance(); + + ITrackRecordingService service = + mt == null ? null : mt.getTrackRecordingService(); + if (service == null) { + Log.d(MyTracksConstants.TAG, "Could not get track recording service."); + updateSensorState(Sensor.SensorState.NONE); + updateSensorData(null); + return; + } + Sensor.SensorDataSet sds = null; + try { + byte[] buff = service.getSensorData(); + if (buff != null) { + sds = Sensor.SensorDataSet.parseFrom(buff); + updateSensorData(sds); + } + } catch (RemoteException e) { + Log.e(MyTracksConstants.TAG, "Could not read sensor data.", e); + } catch (InvalidProtocolBufferException e) { + Log.e(MyTracksConstants.TAG, "Could not read sensor data.", e); + } + updateSensorData(sds); + + try { + int i = service.getSensorState(); + updateSensorState(Sensor.SensorState.valueOf(i)); + } catch (RemoteException e) { + Log.e(MyTracksConstants.TAG, "Could not read sensor state.", e); + updateSensorState(Sensor.SensorState.NONE); + } + } + + private void updateSensorState(Sensor.SensorState state) { + TextView sensorTime = + ((TextView) findViewById(R.id.sensor_state_register)); + sensorTime.setText(SensorUtils.getStateAsString(state, this)); + } + + protected void updateSensorData(Sensor.SensorDataSet sds) { + if (sds == null) { + utils.setUnknown(R.id.sensor_data_time_register); + utils.setUnknown(R.id.cadence_state_register); + utils.setUnknown(R.id.power_state_register); + utils.setUnknown(R.id.heart_rate_register); + return; + } + + TextView sensorTime = + ((TextView) findViewById(R.id.sensor_data_time_register)); + sensorTime.setText( + TIMESTAMP_FORMAT.format(new Date(sds.getCreationTime()))); + + if (sds.hasPower() && sds.getPower().hasValue() + && sds.getPower().getState() == Sensor.SensorState.SENDING) { + utils.setText(R.id.power_state_register, + Integer.toString(sds.getPower().getValue())); + } else { + utils.setText(R.id.power_state_register, + SensorUtils.getStateAsString( + sds.hasPower() + ? sds.getPower().getState() + : Sensor.SensorState.NONE, + this)); + } + + if (sds.hasCadence() && sds.getCadence().hasValue() + && sds.getCadence().getState() == Sensor.SensorState.SENDING) { + utils.setText(R.id.cadence_state_register, + Integer.toString(sds.getCadence().getValue())); + } else { + utils.setText(R.id.cadence_state_register, + SensorUtils.getStateAsString( + sds.hasCadence() + ? sds.getCadence().getState() + : Sensor.SensorState.NONE, + this)); + } + + if (sds.hasHeartRate() && sds.getHeartRate().hasValue() + && sds.getHeartRate().getState() == Sensor.SensorState.SENDING) { + utils.setText(R.id.heart_rate_register, + Integer.toString(sds.getHeartRate().getValue())); + } else { + utils.setText(R.id.heart_rate_register, + SensorUtils.getStateAsString( + sds.hasHeartRate() + ? sds.getHeartRate().getState() + : Sensor.SensorState.NONE, + this)); + } + } +} diff --git a/MyTracks/src/com/google/android/apps/mytracks/content/MyTracksProvider.java b/MyTracks/src/com/google/android/apps/mytracks/content/MyTracksProvider.java index 010e553f8..89604e4b2 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/content/MyTracksProvider.java +++ b/MyTracks/src/com/google/android/apps/mytracks/content/MyTracksProvider.java @@ -15,6 +15,8 @@ */ package com.google.android.apps.mytracks.content; +import com.google.android.apps.mytracks.MyTracksConstants; + import android.content.ContentProvider; import android.content.ContentUris; import android.content.ContentValues; @@ -38,7 +40,7 @@ import android.util.Log; public class MyTracksProvider extends ContentProvider { private static final String DATABASE_NAME = "mytracks.db"; - private static final int DATABASE_VERSION = 17; + private static final int DATABASE_VERSION = 18; private static final int TRACKPOINTS = 1; private static final int TRACKPOINTS_ID = 2; private static final int TRACKS = 3; @@ -70,7 +72,8 @@ public class MyTracksProvider extends ContentProvider { + TrackPointsColumns.ALTITUDE + " FLOAT, " + TrackPointsColumns.ACCURACY + " FLOAT, " + TrackPointsColumns.SPEED + " FLOAT, " - + TrackPointsColumns.BEARING + " FLOAT);"); + + TrackPointsColumns.BEARING + " FLOAT, " + + TrackPointsColumns.SENSOR + " BLOB);"); db.execSQL("CREATE TABLE " + TRACKS_TABLE + " (" + TracksColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " + TracksColumns.NAME + " STRING, " @@ -132,12 +135,19 @@ public class MyTracksProvider extends ContentProvider { @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { - Log.w(TAG, "Upgrading database from version " + oldVersion + " to " - + newVersion + ", which will destroy all old data"); - db.execSQL("DROP TABLE IF EXISTS " + TRACKPOINTS_TABLE); - db.execSQL("DROP TABLE IF EXISTS " + TRACKS_TABLE); - db.execSQL("DROP TABLE IF EXISTS " + WAYPOINTS_TABLE); - onCreate(db); + if (oldVersion < 17) { + Log.w(TAG, "Upgrading database from version " + oldVersion + " to " + + newVersion + ", which will destroy all old data"); + db.execSQL("DROP TABLE IF EXISTS " + TRACKPOINTS_TABLE); + db.execSQL("DROP TABLE IF EXISTS " + TRACKS_TABLE); + db.execSQL("DROP TABLE IF EXISTS " + WAYPOINTS_TABLE); + onCreate(db); + } else if (oldVersion >= 17) { + Log.w(TAG, "Upgrading database from version " + oldVersion + " to " + + newVersion + ", adding sensor column."); + db.execSQL("ALTER TABLE " + TRACKPOINTS_TABLE + + " ADD " + TrackPointsColumns.SENSOR + " BLOB"); + } } } @@ -235,6 +245,7 @@ public class MyTracksProvider extends ContentProvider { } } + @Override public int bulkInsert(Uri url, ContentValues[] valuesBulk) { Log.d(MyTracksProvider.TAG, "MyTracksProvider.bulkInsert"); @@ -346,6 +357,8 @@ public class MyTracksProvider extends ContentProvider { throw new IllegalArgumentException("Unknown URL " + url); } + Log.i(MyTracksConstants.TAG, + "Build query: " + qb.buildQuery(projection, selection, selectionArgs, null, null, sortOrder, null)); Cursor c = qb.query(db, projection, selection, selectionArgs, null, null, sortOrder); c.setNotificationUri(getContext().getContentResolver(), url); diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/GpxImporter.java b/MyTracks/src/com/google/android/apps/mytracks/io/GpxImporter.java index 571859cc9..e1d58b354 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/GpxImporter.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/GpxImporter.java @@ -342,8 +342,14 @@ public class GpxImporter extends DefaultHandler { // create new location and set attributes Location loc = new Location(LocationManager.GPS_PROVIDER); - loc.setLatitude(Double.parseDouble(latitude)); - loc.setLongitude(Double.parseDouble(longitude)); + try { + loc.setLatitude(Double.parseDouble(latitude)); + loc.setLongitude(Double.parseDouble(longitude)); + } catch (NumberFormatException e) { + String msg = createErrorMessage( + "Unable to parse lat/long: " + latitude + "/" + longitude); + throw new SAXException(msg, e); + } return loc; } @@ -472,9 +478,14 @@ public class GpxImporter extends DefaultHandler { } } - private void onAltitudeElementEnd() { + private void onAltitudeElementEnd() throws SAXException { if (location != null) { - location.setAltitude(Double.parseDouble(content)); + try { + location.setAltitude(Double.parseDouble(content)); + } catch (NumberFormatException e) { + String msg = createErrorMessage("Unable to parse altitude: " + content); + throw new SAXException(msg, e); + } } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/TcxTrackWriter.java b/MyTracks/src/com/google/android/apps/mytracks/io/TcxTrackWriter.java index 7ad325f41..a09e2fd60 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/TcxTrackWriter.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/TcxTrackWriter.java @@ -15,8 +15,11 @@ */ package com.google.android.apps.mytracks.io; +import com.google.android.apps.mytracks.content.MyTracksLocation; +import com.google.android.apps.mytracks.content.Sensor; import com.google.android.apps.mytracks.content.Track; import com.google.android.apps.mytracks.content.Waypoint; +import com.google.android.apps.mytracks.content.Sensor.SensorDataSet; import com.google.android.apps.mytracks.io.TrackWriterFactory.TrackFileFormat; import android.location.Location; @@ -141,6 +144,37 @@ public class TcxTrackWriter implements TrackFormatWriter { pw.print(location.getAltitude()); pw.println(""); + if (location instanceof MyTracksLocation) { + SensorDataSet sensorData = ((MyTracksLocation) location).getSensorDataSet(); + if (sensorData != null) { + if (sensorData.hasHeartRate() + && sensorData.getHeartRate().getState() == Sensor.SensorState.SENDING + && sensorData.getHeartRate().hasValue()) { + pw.print(" "); + pw.print(""); + pw.print(sensorData.getHeartRate().getValue()); + pw.print(""); + pw.println(""); + } + if (sensorData.hasPower() + && sensorData.getPower().getState() == Sensor.SensorState.SENDING + && sensorData.getPower().hasValue()) { + pw.print(" "); + pw.print(""); + pw.print(""); + pw.print(sensorData.getPower().getValue()); + pw.print(""); + pw.println(""); + } + if (sensorData.hasCadence() + && sensorData.getCadence().getState() == Sensor.SensorState.SENDING + && sensorData.getCadence().hasValue()) { + pw.print(" "); + pw.print(sensorData.getCadence().getValue()); + pw.println(""); + } + } + } pw.println(" "); } diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/backup/BackupColumns.java b/MyTracks/src/com/google/android/apps/mytracks/io/backup/BackupColumns.java new file mode 100644 index 000000000..9fb3f739a --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/io/backup/BackupColumns.java @@ -0,0 +1,67 @@ +/* + * 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.io.backup; + +import static com.google.android.apps.mytracks.content.ContentTypeIds.*; +import com.google.android.apps.mytracks.content.TrackPointsColumns; +import com.google.android.apps.mytracks.content.TracksColumns; +import com.google.android.apps.mytracks.content.WaypointsColumns; + +public class BackupColumns { + /** Columns that go into the backup. */ + public static final String[] POINTS_BACKUP_COLUMNS = + { TrackPointsColumns._ID, TrackPointsColumns.TRACKID, TrackPointsColumns.LATITUDE, + TrackPointsColumns.LONGITUDE, TrackPointsColumns.ALTITUDE, TrackPointsColumns.BEARING, + TrackPointsColumns.TIME, TrackPointsColumns.ACCURACY, TrackPointsColumns.SPEED }; + public static final byte[] POINTS_BACKUP_COLUMN_TYPES = + { LONG_TYPE_ID, LONG_TYPE_ID, INT_TYPE_ID, INT_TYPE_ID, FLOAT_TYPE_ID, + FLOAT_TYPE_ID, LONG_TYPE_ID, FLOAT_TYPE_ID, FLOAT_TYPE_ID }; + + public static final String[] TRACKS_BACKUP_COLUMNS = { + TracksColumns._ID, TracksColumns.NAME, TracksColumns.DESCRIPTION, TracksColumns.CATEGORY, + TracksColumns.STARTID, TracksColumns.STOPID, TracksColumns.STARTTIME, TracksColumns.STOPTIME, + TracksColumns.NUMPOINTS, TracksColumns.TOTALDISTANCE, TracksColumns.TOTALTIME, + TracksColumns.MOVINGTIME, TracksColumns.AVGSPEED, TracksColumns.AVGMOVINGSPEED, + TracksColumns.MAXSPEED, TracksColumns.MINELEVATION, TracksColumns.MAXELEVATION, + TracksColumns.ELEVATIONGAIN, TracksColumns.MINGRADE, TracksColumns.MAXGRADE, + TracksColumns.MINLAT, TracksColumns.MAXLAT, TracksColumns.MINLON, TracksColumns.MAXLON }; + public static final byte[] TRACKS_BACKUP_COLUMN_TYPES = { + LONG_TYPE_ID, STRING_TYPE_ID, STRING_TYPE_ID, STRING_TYPE_ID, + LONG_TYPE_ID, LONG_TYPE_ID, LONG_TYPE_ID, LONG_TYPE_ID, INT_TYPE_ID, + FLOAT_TYPE_ID, LONG_TYPE_ID, LONG_TYPE_ID, FLOAT_TYPE_ID, FLOAT_TYPE_ID, + FLOAT_TYPE_ID, FLOAT_TYPE_ID, FLOAT_TYPE_ID, FLOAT_TYPE_ID, FLOAT_TYPE_ID, + FLOAT_TYPE_ID, INT_TYPE_ID, INT_TYPE_ID, INT_TYPE_ID, INT_TYPE_ID }; + + public static final String[] WAYPOINTS_BACKUP_COLUMNS = { + WaypointsColumns._ID, WaypointsColumns.TRACKID, WaypointsColumns.NAME, + WaypointsColumns.DESCRIPTION, WaypointsColumns.CATEGORY, WaypointsColumns.ICON, + WaypointsColumns.TYPE, WaypointsColumns.LENGTH, WaypointsColumns.DURATION, + WaypointsColumns.STARTTIME, WaypointsColumns.STARTID, WaypointsColumns.STOPID, + WaypointsColumns.LATITUDE, WaypointsColumns.LONGITUDE, WaypointsColumns.ALTITUDE, + WaypointsColumns.BEARING, WaypointsColumns.TIME, WaypointsColumns.ACCURACY, + WaypointsColumns.SPEED, WaypointsColumns.TOTALDISTANCE, WaypointsColumns.TOTALTIME, + WaypointsColumns.MOVINGTIME, WaypointsColumns.AVGSPEED, WaypointsColumns.AVGMOVINGSPEED, + WaypointsColumns.MAXSPEED, WaypointsColumns.MINELEVATION, WaypointsColumns.MAXELEVATION, + WaypointsColumns.ELEVATIONGAIN, WaypointsColumns.MINGRADE, WaypointsColumns.MAXGRADE }; + public static final byte[] WAYPOINTS_BACKUP_COLUMN_TYPES = { + LONG_TYPE_ID, LONG_TYPE_ID, STRING_TYPE_ID, STRING_TYPE_ID, + STRING_TYPE_ID, STRING_TYPE_ID, INT_TYPE_ID, FLOAT_TYPE_ID, LONG_TYPE_ID, + LONG_TYPE_ID, LONG_TYPE_ID, LONG_TYPE_ID, INT_TYPE_ID, INT_TYPE_ID, + FLOAT_TYPE_ID, FLOAT_TYPE_ID, LONG_TYPE_ID, FLOAT_TYPE_ID, FLOAT_TYPE_ID, + FLOAT_TYPE_ID, LONG_TYPE_ID, LONG_TYPE_ID, FLOAT_TYPE_ID, FLOAT_TYPE_ID, + FLOAT_TYPE_ID, FLOAT_TYPE_ID, FLOAT_TYPE_ID, FLOAT_TYPE_ID, FLOAT_TYPE_ID, + FLOAT_TYPE_ID }; +} diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/backup/ExternalFileBackup.java b/MyTracks/src/com/google/android/apps/mytracks/io/backup/ExternalFileBackup.java index 48155fe78..cc346e2f7 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/backup/ExternalFileBackup.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/backup/ExternalFileBackup.java @@ -153,16 +153,16 @@ class ExternalFileBackup { // Create all the auxiliary classes that will do the writing PreferenceBackupHelper preferencesHelper = new PreferenceBackupHelper(); DatabaseDumper trackDumper = new DatabaseDumper( - TracksColumns.BACKUP_COLUMNS, - TracksColumns.BACKUP_COLUMN_TYPES, + BackupColumns.TRACKS_BACKUP_COLUMNS, + BackupColumns.TRACKS_BACKUP_COLUMN_TYPES, false); DatabaseDumper waypointDumper = new DatabaseDumper( - WaypointsColumns.BACKUP_COLUMNS, - WaypointsColumns.BACKUP_COLUMN_TYPES, + BackupColumns.WAYPOINTS_BACKUP_COLUMNS, + BackupColumns.WAYPOINTS_BACKUP_COLUMN_TYPES, false); DatabaseDumper pointDumper = new DatabaseDumper( - TrackPointsColumns.BACKUP_COLUMNS, - TrackPointsColumns.BACKUP_COLUMN_TYPES, + BackupColumns.POINTS_BACKUP_COLUMNS, + BackupColumns.POINTS_BACKUP_COLUMN_TYPES, false); // Open the target for writing diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/PreferenceManager.java b/MyTracks/src/com/google/android/apps/mytracks/services/PreferenceManager.java index 413ddb364..fee208d54 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/services/PreferenceManager.java +++ b/MyTracks/src/com/google/android/apps/mytracks/services/PreferenceManager.java @@ -40,7 +40,6 @@ public class PreferenceManager implements OnSharedPreferenceChangeListener { private final String minRecordingIntervalKey; private final String minRequiredAccuracyKey; private final String recordingTrackKey; - private final String signalSamplingFrequencyKey; private final String splitFrequencyKey; public PreferenceManager(TrackRecordingService service) { @@ -72,8 +71,6 @@ public class PreferenceManager implements OnSharedPreferenceChangeListener { service.getString(R.string.min_required_accuracy_key); recordingTrackKey = service.getString(R.string.recording_track_key); - signalSamplingFrequencyKey = - service.getString(R.string.signal_sampling_frequency_key); splitFrequencyKey = service.getString(R.string.split_frequency_key); @@ -163,10 +160,6 @@ public class PreferenceManager implements OnSharedPreferenceChangeListener { service.getSplitManager().setSplitFrequency( sharedPreferences.getInt(splitFrequencyKey, 0)); } - if (key == null || key.equals(signalSamplingFrequencyKey)) { - service.getSignalManager().setFrequency( - sharedPreferences.getInt(signalSamplingFrequencyKey, -1), service); - } if (key == null || key.equals(metricUnitsKey)) { service.getSplitManager().setMetricUnits( sharedPreferences.getBoolean(metricUnitsKey, true)); diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/SignalStrengthTaskFactory.java b/MyTracks/src/com/google/android/apps/mytracks/services/SignalStrengthTaskFactory.java deleted file mode 100644 index 844b48b95..000000000 --- a/MyTracks/src/com/google/android/apps/mytracks/services/SignalStrengthTaskFactory.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright 2010 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ -package com.google.android.apps.mytracks.services; - -import com.google.android.apps.mytracks.MyTracksConstants; -import com.google.android.apps.mytracks.util.ApiFeatures; - -import android.content.Context; -import android.util.Log; - -/** - * Factory for producing a proper {@link SignalStrengthTask} according to the - * current API level. - * - * @author Rodrigo Damazio - */ -public class SignalStrengthTaskFactory { - private final boolean hasModernSignalStrength; - - SignalStrengthTaskFactory(ApiFeatures apiFeatures) { - this.hasModernSignalStrength = apiFeatures.hasModernSignalStrength(); - } - - public PeriodicTask create(Context context) { - if (hasModernSignalStrength) { - Log.d(MyTracksConstants.TAG, - "TrackRecordingService using modern signal strength api."); - return new SignalStrengthTaskModern(context); - } else { - Log.w(MyTracksConstants.TAG, - "TrackRecordingService using legacy signal strength api."); - return new SignalStrengthTask(context); - } - } -} diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/TrackRecordingService.java b/MyTracks/src/com/google/android/apps/mytracks/services/TrackRecordingService.java index 56d6b571d..b3610bd44 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/services/TrackRecordingService.java +++ b/MyTracks/src/com/google/android/apps/mytracks/services/TrackRecordingService.java @@ -20,11 +20,16 @@ import static com.google.android.apps.mytracks.MyTracksConstants.RESUME_TRACK_EX import com.google.android.apps.mytracks.MyTracks; import com.google.android.apps.mytracks.MyTracksConstants; import com.google.android.apps.mytracks.MyTracksSettings; +import com.google.android.apps.mytracks.content.MyTracksLocation; import com.google.android.apps.mytracks.content.MyTracksProviderUtils; +import com.google.android.apps.mytracks.content.Sensor; import com.google.android.apps.mytracks.content.Track; import com.google.android.apps.mytracks.content.TracksColumns; import com.google.android.apps.mytracks.content.Waypoint; import com.google.android.apps.mytracks.content.WaypointsColumns; +import com.google.android.apps.mytracks.content.Sensor.SensorDataSet; +import com.google.android.apps.mytracks.services.sensors.SensorManager; +import com.google.android.apps.mytracks.services.sensors.SensorManagerFactory; import com.google.android.apps.mytracks.stats.TripStatistics; import com.google.android.apps.mytracks.stats.TripStatisticsBuilder; import com.google.android.apps.mytracks.util.ApiFeatures; @@ -69,7 +74,7 @@ public class TrackRecordingService extends Service implements LocationListener { "http://maps.google.com/mapfiles/ms/micons/ylw-pushpin.png"; static final int MAX_AUTO_RESUME_TRACK_RETRY_ATTEMPTS = 3; - + private NotificationManager notificationManager; private LocationManager locationManager; private WakeLock wakeLock; @@ -109,9 +114,10 @@ public class TrackRecordingService extends Service implements LocationListener { * Status announcer executer. */ private PeriodicTaskExecuter announcementExecuter; - private TaskExecuterManager signalManager; private SplitManager splitManager; + private SensorManager sensorManager; + private PreferenceManager prefManager; /** @@ -215,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: @@ -411,7 +424,6 @@ public class TrackRecordingService extends Service implements LocationListener { statsBuilder = new TripStatisticsBuilder(stats.getStartTime()); setUpAnnouncer(); - signalManager.restore(); splitManager.restore(); length = 0; lastValidLocation = null; @@ -525,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; @@ -549,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. @@ -629,14 +646,11 @@ public class TrackRecordingService extends Service implements LocationListener { locationManager = (LocationManager) getSystemService(LOCATION_SERVICE); splitManager = new SplitManager(this); - SignalStrengthTaskFactory strengthTaskFactory = - new SignalStrengthTaskFactory(ApiFeatures.getInstance()); - signalManager = - new TaskExecuterManager(-1, strengthTaskFactory.create(this), 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: */ @@ -710,17 +724,27 @@ public class TrackRecordingService extends Service implements LocationListener { showNotification(); prefManager.shutdown(); prefManager = null; - binder = null; checkLocationListener.cancel(); checkLocationListener = null; timer.cancel(); timer.purge(); unregisterLocationListener(); shutdownAnnouncer(); - signalManager.shutdown(); - signalManager = null; splitManager.shutdown(); splitManager = null; + if (sensorManager != null) { + sensorManager.onDestroy(); + sensorManager = null; + } + + // Make sure we have no indirect references to this service. + locationManager = null; + notificationManager = null; + providerUtils = null; + binder.detachFromService(); + binder = null; + + // This should be the last operation. releaseWakeLock(); super.onDestroy(); @@ -841,15 +865,25 @@ public class TrackRecordingService extends Service implements LocationListener { throw new IllegalStateException( "Unable to insert waypoint marker while not recording!"); } - - if (waypoint.getLocation() != null) { - waypoint.setLength(length); - waypoint.setDuration(waypoint.getLocation().getTime() - - statsBuilder.getStatistics().getStartTime()); - Uri uri = providerUtils.insertWaypoint(waypoint); - return Long.parseLong(uri.getLastPathSegment()); + + if (waypoint.getLocation() == null) { + if (lastValidLocation == null) { + Log.w(MyTracksConstants.TAG, "Cannot insert waypoint with no location"); + return -1; + } + + waypoint.setLocation(lastValidLocation); } - return -1; + + if (waypoint.getTrackId() < 0) { + waypoint.setTrackId(recordingTrackId); + } + + waypoint.setLength(length); + waypoint.setDuration(waypoint.getLocation().getTime() + - statsBuilder.getStatistics().getStartTime()); + Uri uri = providerUtils.insertWaypoint(waypoint); + return Long.parseLong(uri.getLastPathSegment()); } /** @@ -896,82 +930,153 @@ public class TrackRecordingService extends Service implements LocationListener { updateCurrentWaypoint(); return Long.parseLong(uri.getLastPathSegment()); } - + + private ServiceBinder binder = new ServiceBinder(this); + /** - * The ITrackRecordingService is defined through IDL. + * TODO: There is a bug in Android that leaks Binder instances. This bug is + * especially visible if we have a non-static class, as there is no way to + * nullify reference to the outer class (the service). + * A workaround is to use a static class and explicitly clear service + * and detach it from the underlying Binder. With this approach, we minimize + * the leak to 24 bytes per each service instance. + * + * For more details, see the following bug: + * http://code.google.com/p/android/issues/detail?id=6426. */ - private ITrackRecordingService.Stub binder = - new ITrackRecordingService.Stub() { - @Override - public boolean isRecording() { - return TrackRecordingService.this.isRecording(); - } + private static class ServiceBinder extends ITrackRecordingService.Stub { + private TrackRecordingService service; + + public ServiceBinder(TrackRecordingService service) { + this.service = service; + } + + /** + * Clears the reference to the outer class to minimize the leak. + */ + public void detachFromService() { + this.service = null; + attachInterface(null, null); + } + + @Override + public boolean isRecording() { + if (service == null) { + throw new IllegalStateException("The service has been already detached!"); + } + return service.isRecording(); + } - @Override - public long getRecordingTrackId() { - return recordingTrackId; - } + @Override + public long getRecordingTrackId() { + if (service == null) { + throw new IllegalStateException("The service has been already detached!"); + } + return service.recordingTrackId; + } - @Override - public boolean hasRecorded() { - return providerUtils.getLastTrackId() >= 0; - } + @Override + public boolean hasRecorded() { + if (service == null) { + throw new IllegalStateException("The service has been already detached!"); + } + return service.providerUtils.getLastTrackId() >= 0; + } - @Override - public long startNewTrack() { - return TrackRecordingService.this.startNewTrack(); - } + @Override + public long startNewTrack() { + if (service == null) { + throw new IllegalStateException("The service has been already detached!"); + } + return service.startNewTrack(); + } - /** - * Insert the given waypoint marker. Users can insert waypoint markers - * to tag locations with a name, description, category etc. - * - * @param waypoint a waypoint - * @return the unique id of the inserted marker - */ - @Override - public long insertWaypointMarker(Waypoint waypoint) { - return TrackRecordingService.this.insertWaypointMarker(waypoint); - } + /** + * Insert the given waypoint marker. Users can insert waypoint markers + * to tag locations with a name, description, category etc. + * + * @param waypoint a waypoint + * @return the unique id of the inserted marker + */ + @Override + public long insertWaypointMarker(Waypoint waypoint) { + if (service == null) { + throw new IllegalStateException("The service has been already detached!"); + } + return service.insertWaypointMarker(waypoint); + } - /** - * Insert a statistics marker. A statistics marker holds the stats for - * the last segment up to this marker. - * - * @param location the location where to insert - * @return the unique id of the inserted marker - */ - @Override - public long insertStatisticsMarker(Location location) { - return TrackRecordingService.this.insertStatisticsMarker(location); - } + /** + * Insert a statistics marker. A statistics marker holds the stats for + * the last segment up to this marker. + * + * @param location the location where to insert + * @return the unique id of the inserted marker + */ + @Override + public long insertStatisticsMarker(Location location) { + if (service == null) { + throw new IllegalStateException("The service has been already detached!"); + } + return service.insertStatisticsMarker(location); + } - @Override - public void endCurrentTrack() { - TrackRecordingService.this.endCurrentTrack(); - } + @Override + public void endCurrentTrack() { + if (service == null) { + throw new IllegalStateException("The service has been already detached!"); + } + service.endCurrentTrack(); + } - @Override - public void deleteAllTracks() { - if (isRecording()) { - throw new IllegalStateException( - "Cannot delete all tracks while recording!"); - } - providerUtils.deleteAllTracks(); - } + @Override + public void deleteAllTracks() { + if (service == null) { + throw new IllegalStateException("The service has been already detached!"); + } + if (isRecording()) { + throw new IllegalStateException("Cannot delete all tracks while recording!"); + } + service.providerUtils.deleteAllTracks(); + } - @Override - public void recordLocation(Location loc) { - onLocationChanged(loc); - } - }; + @Override + public void recordLocation(Location loc) { + if (service == null) { + throw new IllegalStateException("The service has been already detached!"); + } + 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() { Log.d(MyTracksConstants.TAG, "TrackRecordingService.startNewTrack"); if (recordingTrackId != -1 || isRecording) { throw new IllegalStateException("A track is already in progress!"); } - + long startTime = System.currentTimeMillis(); acquireWakeLock(); @@ -996,14 +1101,20 @@ public class TrackRecordingService extends Service implements LocationListener { showNotification(); registerLocationListener(); splitManager.restore(); - signalManager.restore(); + if (sensorManager != null) { + sensorManager.onStartTrack(); + } // Reset the number of auto-resume retries. setAutoResumeTrackRetries( getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0), 0); // Persist the current recording track. prefManager.setRecordingTrack(recordingTrackId); - + + // Notify the world that we're now recording. + sendTrackBroadcast( + R.string.track_started_broadcast_action, recordingTrackId); + return recordingTrackId; } @@ -1033,8 +1144,27 @@ public class TrackRecordingService extends Service implements LocationListener { "_id=" + recordingTrack.getId(), null); } showNotification(); + long recordedTrackId = recordingTrackId; prefManager.setRecordingTrack(recordingTrackId = -1); + + if (sensorManager != null) { + sensorManager.shutdown(); + } + releaseWakeLock(); + + // Notify the world that we're no longer recording. + sendTrackBroadcast( + R.string.track_stopped_broadcast_action, recordedTrackId); + } + + private void sendTrackBroadcast(int actionResId, long trackId) { + Intent broadcastIntent = + new Intent() + .setAction(getString(actionResId)) + .putExtra(getString(R.string.track_id_broadcast_extra), trackId); + sendBroadcast(broadcastIntent, + getString(R.string.broadcast_notifications_permission)); } public TripStatistics getTripStatistics() { @@ -1110,8 +1240,4 @@ public class TrackRecordingService extends Service implements LocationListener { public SplitManager getSplitManager() { return splitManager; } - - public TaskExecuterManager getSignalManager() { - return signalManager; - } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/sensors/BluetoothConnectionManager.java b/MyTracks/src/com/google/android/apps/mytracks/services/sensors/BluetoothConnectionManager.java new file mode 100644 index 000000000..986ae773b --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/services/sensors/BluetoothConnectionManager.java @@ -0,0 +1,432 @@ +/* + * Copyright (C) 2010 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.android.apps.mytracks.services.sensors; + +import com.google.android.apps.mytracks.MyTracksConstants; +import com.google.android.apps.mytracks.content.Sensor; + +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothSocket; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.util.Log; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.UUID; + +/** + * This class does all the work for setting up and managing Bluetooth + * connections with other devices. It has a thread that listens for incoming + * connections, a thread for connecting with a device, and a thread for + * performing data transmissions when connected. + * + * @author Sandor Dornbush + */ +public class BluetoothConnectionManager { + // Unique UUID for this application + private static final UUID SPP_UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB"); + + private MessageParser parser; + + // Member fields + private final BluetoothAdapter adapter; + private final Handler handler; + private ConnectThread connectThread; + private ConnectedThread connectedThread; + + private Sensor.SensorState state; + + // Message types sent from the BluetoothSenorService Handler + public static final int MESSAGE_STATE_CHANGE = 1; + public static final int MESSAGE_READ = 2; + public static final int MESSAGE_WRITE = 3; + public static final int MESSAGE_DEVICE_NAME = 4; + + // Key names received from the BluetoothSenorService Handler + public static final String DEVICE_NAME = "device_name"; + + /** + * Constructor. Prepares a new BluetoothSensor session. + * + * @param context The UI Activity Context + * @param handler A Handler to send messages back to the UI Activity + */ + public BluetoothConnectionManager(Handler handler, MessageParser parser) { + this.adapter = BluetoothAdapter.getDefaultAdapter(); + this.state = Sensor.SensorState.NONE; + this.handler = handler; + this.parser = parser; + } + + /** + * Set the current state of the sensor connection + * + * @param state An integer defining the current connection state + */ + private synchronized void setState(Sensor.SensorState state) { + // TODO pretty print this. + Log.d(MyTracksConstants.TAG, "setState(" + state + ")"); + this.state = state; + + // Give the new state to the Handler so the UI Activity can update + handler.obtainMessage(MESSAGE_STATE_CHANGE, state.getNumber(), -1).sendToTarget(); + } + + /** + * Return the current connection state. + */ + public synchronized Sensor.SensorState getState() { + return state; + } + + /** + * Start the sensor service. Specifically start AcceptThread to begin a session + * in listening (server) mode. Called by the Activity onResume() + */ + public synchronized void start() { + Log.d(MyTracksConstants.TAG, "BluetoothConnectionManager.start()"); + + // Cancel any thread attempting to make a connection + if (connectThread != null) { + connectThread.cancel(); + connectThread = null; + } + + // Cancel any thread currently running a connection + if (connectedThread != null) { + connectedThread.cancel(); + connectedThread = null; + } + + setState(Sensor.SensorState.NONE); + } + + /** + * Start the ConnectThread to initiate a connection to a remote device. + * + * @param device The BluetoothDevice to connect + */ + public synchronized void connect(BluetoothDevice device) { + Log.d(MyTracksConstants.TAG, "connect to: " + device); + + // Cancel any thread attempting to make a connection + if (state == Sensor.SensorState.CONNECTING) { + if (connectThread != null) { + connectThread.cancel(); + connectThread = null; + } + } + + // Cancel any thread currently running a connection + if (connectedThread != null) { + connectedThread.cancel(); + connectedThread = null; + } + + // Start the thread to connect with the given device + connectThread = new ConnectThread(device); + connectThread.start(); + setState(Sensor.SensorState.CONNECTING); + } + + /** + * Start the ConnectedThread to begin managing a Bluetooth connection + * + * @param socket The BluetoothSocket on which the connection was made + * @param device The BluetoothDevice that has been connected + */ + public synchronized void connected(BluetoothSocket socket, + BluetoothDevice device) { + Log.d(MyTracksConstants.TAG, "connected"); + + // Cancel the thread that completed the connection + if (connectThread != null) { + connectThread.cancel(); + connectThread = null; + } + + // Cancel any thread currently running a connection + if (connectedThread != null) { + connectedThread.cancel(); + connectedThread = null; + } + + // Start the thread to manage the connection and perform transmissions + connectedThread = new ConnectedThread(socket); + connectedThread.start(); + + // Send the name of the connected device back to the UI Activity + Message msg = handler.obtainMessage(MESSAGE_DEVICE_NAME); + Bundle bundle = new Bundle(); + bundle.putString(DEVICE_NAME, device.getName()); + msg.setData(bundle); + handler.sendMessage(msg); + + setState(Sensor.SensorState.CONNECTED); + } + + /** + * Stop all threads + */ + public synchronized void stop() { + Log.d(MyTracksConstants.TAG, "stop()"); + if (connectThread != null) { + connectThread.cancel(); + connectThread = null; + } + if (connectedThread != null) { + connectedThread.cancel(); + connectedThread = null; + } + setState(Sensor.SensorState.NONE); + } + + /** + * Write to the ConnectedThread in an unsynchronized manner + * + * @param out The bytes to write + * @see ConnectedThread#write(byte[]) + */ + public void write(byte[] out) { + // Create temporary object + ConnectedThread r; + // Synchronize a copy of the ConnectedThread + synchronized (this) { + if (state != Sensor.SensorState.CONNECTED) { + return; + } + r = connectedThread; + } + // Perform the write unsynchronized + r.write(out); + } + + /** + * Indicate that the connection attempt failed and notify the UI Activity. + */ + private void connectionFailed() { + setState(Sensor.SensorState.DISCONNECTED); + Log.i(MyTracksConstants.TAG, "Bluetooth connection failed."); + } + + /** + * Indicate that the connection was lost and notify the UI Activity. + */ + private void connectionLost() { + setState(Sensor.SensorState.DISCONNECTED); + Log.i(MyTracksConstants.TAG, "Bluetooth connection lost."); + } + + /** + * This thread runs while attempting to make an outgoing connection with a + * device. It runs straight through; the connection either succeeds or fails. + */ + private class ConnectThread extends Thread { + private final BluetoothSocket socket; + private final BluetoothDevice device; + + public ConnectThread(BluetoothDevice device) { + setName("ConnectThread-" + device.getName()); + this.device = device; + BluetoothSocket tmp = null; + + // Get a BluetoothSocket for a connection with the + // given BluetoothDevice + try { + tmp = getSocket(); + } catch (IOException e) { + Log.e(MyTracksConstants.TAG, "create() failed", e); + } + socket = tmp; + } + + private BluetoothSocket getSocket() throws IOException { + try { + Class c = device.getClass(); + Method insecure = c.getMethod("createInsecureRfcommSocket", + Integer.class); + insecure.setAccessible(true); + return (BluetoothSocket) insecure.invoke(device, 1); + } catch (SecurityException e) { + Log.e(MyTracksConstants.TAG, "Unable to get insecure connect.", e); + } catch (NoSuchMethodException e) { + Log.e(MyTracksConstants.TAG, "Unable to get insecure connect.", e); + } catch (IllegalArgumentException e) { + Log.e(MyTracksConstants.TAG, "Unable to get insecure connect.", e); + } catch (IllegalAccessException e) { + Log.e(MyTracksConstants.TAG, "Unable to get insecure connect.", e); + } catch (InvocationTargetException e) { + Log.e(MyTracksConstants.TAG, "Unable to get insecure connect.", e); + } + return device.createRfcommSocketToServiceRecord(SPP_UUID); + } + + @Override + public void run() { + Log.d(MyTracksConstants.TAG, "BEGIN mConnectThread"); + + // Always cancel discovery because it will slow down a connection + adapter.cancelDiscovery(); + + // Make a connection to the BluetoothSocket + try { + // This is a blocking call and will only return on a + // successful connection or an exception + socket.connect(); + } catch (IOException e) { + connectionFailed(); + // Close the socket + try { + socket.close(); + } catch (IOException e2) { + Log.e(MyTracksConstants.TAG, + "unable to close() socket during connection failure", e2); + } + // Start the service over to restart listening mode + BluetoothConnectionManager.this.start(); + return; + } + + // Reset the ConnectThread because we're done + synchronized (BluetoothConnectionManager.this) { + connectThread = null; + } + + // Start the connected thread + connected(socket, device); + } + + public void cancel() { + try { + socket.close(); + } catch (IOException e) { + Log.e(MyTracksConstants.TAG, "close() of connect socket failed", e); + } + } + } + + /** + * This thread runs during a connection with a remote device. It handles all + * incoming and outgoing transmissions. + */ + private class ConnectedThread extends Thread { + private final BluetoothSocket btSocket; + private final InputStream mmInStream; + private final OutputStream mmOutStream; + + public ConnectedThread(BluetoothSocket socket) { + Log.d(MyTracksConstants.TAG, "create ConnectedThread"); + btSocket = socket; + InputStream tmpIn = null; + OutputStream tmpOut = null; + + // Get the BluetoothSocket input and output streams + try { + tmpIn = socket.getInputStream(); + tmpOut = socket.getOutputStream(); + } catch (IOException e) { + Log.e(MyTracksConstants.TAG, "temp sockets not created", e); + } + + mmInStream = tmpIn; + mmOutStream = tmpOut; + } + + @Override + public void run() { + Log.i(MyTracksConstants.TAG, "BEGIN mConnectedThread"); + byte[] buffer = new byte[parser.getFrameSize()]; + int bytes; + int offset = 0; + + // Keep listening to the InputStream while connected + while (true) { + try { + // Read from the InputStream + bytes = mmInStream.read(buffer, offset, parser.getFrameSize() - offset); + + if (bytes < 0) { + throw new IOException("EOF reached"); + } + + offset += bytes; + + if (offset != parser.getFrameSize()) { + // partial frame received, call read() again to receive the rest + continue; + } + + // check if its a valid frame + if (!parser.isValid(buffer)) { + int index = parser.findNextAlignment(buffer); + if (index > 0) { + // re-align + offset = parser.getFrameSize() - index; + System.arraycopy(buffer, index, buffer, 0, offset); + Log.w(MyTracksConstants.TAG, "Misaligned data, found new message at " + + index + " recovering..."); + continue; + } + Log.w(MyTracksConstants.TAG, "Could not find valid data, dropping data"); + offset = 0; + continue; + } + + offset = 0; + + // Send the obtained bytes to the UI Activity + handler.obtainMessage(MESSAGE_READ, bytes, -1, buffer) + .sendToTarget(); + } catch (IOException e) { + Log.e(MyTracksConstants.TAG, "disconnected", e); + connectionLost(); + break; + } + } + } + + /** + * Write to the connected OutStream. + * + * @param buffer The bytes to write + */ + public void write(byte[] buffer) { + try { + mmOutStream.write(buffer); + + // Share the sent message back to the UI Activity + handler.obtainMessage(MESSAGE_WRITE, -1, -1, buffer).sendToTarget(); + } catch (IOException e) { + Log.e(MyTracksConstants.TAG, "Exception during write", e); + } + } + + public void cancel() { + try { + btSocket.close(); + } catch (IOException e) { + Log.e(MyTracksConstants.TAG, "close() of connect socket failed", e); + } + } + } +} diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/sensors/BluetoothSensorManager.java b/MyTracks/src/com/google/android/apps/mytracks/services/sensors/BluetoothSensorManager.java new file mode 100644 index 000000000..32b16d1e4 --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/services/sensors/BluetoothSensorManager.java @@ -0,0 +1,166 @@ +/* + * Copyright 2010 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.google.android.apps.mytracks.services.sensors; + +import com.google.android.apps.mytracks.MyTracksConstants; +import com.google.android.apps.mytracks.MyTracksSettings; +import com.google.android.apps.mytracks.content.Sensor; +import com.google.android.maps.mytracks.R; + +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.content.Context; +import android.content.SharedPreferences; +import android.os.Handler; +import android.os.Message; +import android.util.Log; +import android.widget.Toast; + +/** + * Manage the connection to a bluetooth sensor. + * + * @author Sandor Dornbush + */ +public class BluetoothSensorManager extends SensorManager { + + // Local Bluetooth adapter + private BluetoothAdapter bluetoothAdapter = null; + + // Member object for the sensor threads and connections. + private BluetoothConnectionManager connectionManager = null; + + // Name of the connected device + private String connectedDeviceName = null; + + private Context context = null; + + private Sensor.SensorDataSet sensorDataSet = null; + + private MessageParser parser; + + public BluetoothSensorManager( + Context context, MessageParser parser) { + this.context = context; + this.parser = parser; + // Get local Bluetooth adapter + bluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); + // If BT is not available or not enabled quit. + if (!isEnabled()) { + return; + } + + setupSensor(); + } + + private void setupSensor() { + Log.d(MyTracksConstants.TAG, "setupSensor()"); + + // Initialize the BluetoothSensorAdapter to perform bluetooth connections. + connectionManager = new BluetoothConnectionManager(messageHandler, parser); + } + + public boolean isEnabled() { + return bluetoothAdapter != null && bluetoothAdapter.isEnabled(); + } + + public void setupChannel() { + if (!isEnabled() || connectionManager == null) { + Log.w(MyTracksConstants.TAG, "Disabled manager onStartTrack"); + return; + } + SharedPreferences prefs = + context.getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0); + String address = + prefs.getString(context.getString(R.string.bluetooth_sensor_key), null); + if (address == null) { + return; + } + Log.w(MyTracksConstants.TAG, "Connecting to bluetooth sensor: " + address); + // Get the BluetoothDevice object + BluetoothDevice device = bluetoothAdapter.getRemoteDevice(address); + // Attempt to connect to the device + connectionManager.connect(device); + + // Performing this check in onResume() covers the case in which BT was + // not enabled during onStart(), so we were paused to enable it... + // onResume() will be called when ACTION_REQUEST_ENABLE activity returns. + if (connectionManager != null) { + // Only if the state is STATE_NONE, do we know that we haven't started + // already + if (connectionManager.getState() == Sensor.SensorState.NONE) { + // Start the Bluetooth sensor services + Log.w(MyTracksConstants.TAG, "Disabled manager onStartTrack"); + connectionManager.start(); + } + } + } + + public void onDestroy() { + // Stop the Bluetooth sensor services + if (connectionManager != null) { + connectionManager.stop(); + } + } + + public Sensor.SensorDataSet getSensorDataSet() { + return sensorDataSet; + } + + public Sensor.SensorState getSensorState() { + return (connectionManager == null) + ? Sensor.SensorState.NONE + : connectionManager.getState(); + } + + // The Handler that gets information back from the BluetoothSensorService + private final Handler messageHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case BluetoothConnectionManager.MESSAGE_STATE_CHANGE: + // TODO should we update the SensorManager state var? + Log.i(MyTracksConstants.TAG, "MESSAGE_STATE_CHANGE: " + msg.arg1); + break; + case BluetoothConnectionManager.MESSAGE_WRITE: + break; + case BluetoothConnectionManager.MESSAGE_READ: + byte[] readBuf = null; + try { + readBuf = (byte[]) msg.obj; + sensorDataSet = parser.parseBuffer(readBuf); + Log.d(MyTracksConstants.TAG, "MESSAGE_READ: " + sensorDataSet.toString()); + } catch (IllegalArgumentException iae) { + sensorDataSet = null; + Log.i(MyTracksConstants.TAG, + "Got bad sensor data: " + new String(readBuf, 0, readBuf.length), + iae); + } catch (RuntimeException re) { + sensorDataSet = null; + Log.i(MyTracksConstants.TAG, "Unexpected exception on read.", re); + } + break; + case BluetoothConnectionManager.MESSAGE_DEVICE_NAME: + // save the connected device's name + connectedDeviceName = + msg.getData().getString(BluetoothConnectionManager.DEVICE_NAME); + Toast.makeText(context.getApplicationContext(), + "Connected to " + connectedDeviceName, Toast.LENGTH_SHORT) + .show(); + break; + } + } + }; +} diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/sensors/MessageParser.java b/MyTracks/src/com/google/android/apps/mytracks/services/sensors/MessageParser.java new file mode 100644 index 000000000..f490df048 --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/services/sensors/MessageParser.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2010 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.android.apps.mytracks.services.sensors; + +import com.google.android.apps.mytracks.content.Sensor; + +/** + * An interface for parsing a byte array to a SensorData object. + * + * @author Sandor Dornbush + */ +public interface MessageParser { + + public int getFrameSize(); + + public Sensor.SensorDataSet parseBuffer(byte[] readBuff); + + public boolean isValid(byte[] buffer); + + public int findNextAlignment(byte[] buffer); +} diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/sensors/SensorManager.java b/MyTracks/src/com/google/android/apps/mytracks/services/sensors/SensorManager.java new file mode 100644 index 000000000..49b421b50 --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/services/sensors/SensorManager.java @@ -0,0 +1,134 @@ +/* + * Copyright 2010 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.google.android.apps.mytracks.services.sensors; + +import java.util.Timer; +import java.util.TimerTask; + +import android.util.Log; + +import com.google.android.apps.mytracks.MyTracksConstants; +import com.google.android.apps.mytracks.content.Sensor; + +/** + * Manage the connection to a sensor. + * + * @author Sandor Dornbush + */ +public abstract class SensorManager { + + /** + * The maximum age where the data is considered valid. + */ + public static final long MAX_AGE = 5000; + + /** + * Time to wait after a time out to retry. + */ + public static final int RETRY_PERIOD = 30000; + + private Sensor.SensorState sensorState = Sensor.SensorState.NONE; + + private long sensorStateTimestamp = 0; + + /** + * A task to run periodically to check to see if connection was lost. + */ + private TimerTask checkSensorManager = new TimerTask() { + @Override + public void run() { + Log.i(MyTracksConstants.TAG, + "SensorManager state: " + getSensorState()); + switch (getSensorState()) { + case CONNECTING: + long age = System.currentTimeMillis() - getSensorStateTimestamp(); + if (age > 2 * RETRY_PERIOD) { + Log.i(MyTracksConstants.TAG, "Retrying connecting SensorManager."); + setupChannel(); + } + break; + case DISCONNECTED: + Log.i(MyTracksConstants.TAG, + "Re-registering disconnected SensoManager."); + setupChannel(); + break; + } + } + }; + + /** + * This timer invokes periodically the checkLocationListener timer task. + */ + private final Timer timer = new Timer(); + + /** + * Is the sensor that this manages enabled. + * @return true if the sensor is enabled + */ + public abstract boolean isEnabled(); + + /** + * This is called when my tracks starts recording a new track. + * This is the place to open connections to the sensor. + */ + public void onStartTrack() { + setupChannel(); + timer.schedule(checkSensorManager, RETRY_PERIOD, RETRY_PERIOD); + } + + public abstract void setupChannel(); + + public void shutdown() { + timer.cancel(); + onDestroy(); + } + + /** + * This is called when my tracks stops recording. + * This is the place to shutdown any open connections. + */ + public abstract void onDestroy(); + + /** + * Return the last sensor reading. + * @return The last reading from the sensor. + */ + public abstract Sensor.SensorDataSet getSensorDataSet(); + + public void setSensorState(Sensor.SensorState sensorState) { + this.sensorState = sensorState; + } + + /** + * Return the current sensor state. + * @return The current sensor state. + */ + public Sensor.SensorState getSensorState() { + return sensorState; + } + + public long getSensorStateTimestamp() { + return sensorStateTimestamp; + } + + /** + * @return True if the data is recent enough to be considered valid. + */ + public boolean isDataValid() { + return (System.currentTimeMillis() - getSensorDataSet().getCreationTime()) < MAX_AGE; + } + +} diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/sensors/SensorManagerFactory.java b/MyTracks/src/com/google/android/apps/mytracks/services/sensors/SensorManagerFactory.java new file mode 100644 index 000000000..01a8d8bf5 --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/services/sensors/SensorManagerFactory.java @@ -0,0 +1,61 @@ +/* + * Copyright 2009 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.android.apps.mytracks.services.sensors; + +import com.google.android.apps.mytracks.MyTracksConstants; +import com.google.android.apps.mytracks.MyTracksSettings; +import com.google.android.maps.mytracks.R; + +import android.content.Context; +import android.content.SharedPreferences; +import android.util.Log; + +/** + * A factory of SensorManagers. + * + * @author Sandor Dornbush + */ +public class SensorManagerFactory { + + private SensorManagerFactory() { + } + + /** + * Get a new sensor manager. + * @param context Context to fetch system preferences. + * @return The sensor manager that corresponds to the sensor type setting. + */ + public static SensorManager getSensorManager(Context context) { + SharedPreferences prefs = + context.getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0); + if (prefs == null) { + return null; + } + + String sensor = prefs.getString(context.getString(R.string.sensor_type_key), + null); + Log.i(MyTracksConstants.TAG, "Creating sensor of type: " + sensor); + + if (sensor == null) { + return null; + } else if (sensor.equals(context.getString(R.string.zephyr_sensor_type))) { + return new ZephyrSensorManager(context); + } else { + return null; + } + } +} diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/sensors/SensorUtils.java b/MyTracks/src/com/google/android/apps/mytracks/services/sensors/SensorUtils.java new file mode 100644 index 000000000..28f2f4fb8 --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/services/sensors/SensorUtils.java @@ -0,0 +1,61 @@ +/* + * Copyright 2010 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.google.android.apps.mytracks.services.sensors; + +import android.content.Context; + +import com.google.android.apps.mytracks.content.Sensor; +import com.google.android.maps.mytracks.R; + +/** + * A collection of methods for message parsers. + * + * @author Sandor Dornbush + */ +public class SensorUtils { + + private SensorUtils() { + } + + /** + * Extract one unsigned short from a big endian byte array. + * @param buffer the buffer to extract the short from + * @param index the first byte to be interpreted as part of the short + * @return The unsigned short at the given index in the buffer + */ + public static int unsignedShortToInt(byte[] buffer, int index) { + int r = (buffer[index] & 0xFF) << 8; + r |= buffer[index + 1] & 0xFF; + return r; + } + + public static String getStateAsString(Sensor.SensorState state, Context c) { + switch (state) { + case NONE: + return c.getString(R.string.none); + case CONNECTING: + return c.getString(R.string.connecting); + case CONNECTED: + return c.getString(R.string.connected); + case DISCONNECTED: + return c.getString(R.string.disconnected); + case SENDING: + return c.getString(R.string.sending); + default: + return ""; + } + } +} diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/sensors/ZephyrMessageParser.java b/MyTracks/src/com/google/android/apps/mytracks/services/sensors/ZephyrMessageParser.java new file mode 100644 index 000000000..8cf8f951f --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/services/sensors/ZephyrMessageParser.java @@ -0,0 +1,76 @@ +/* + * Copyright 2010 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.google.android.apps.mytracks.services.sensors; + +import android.util.Log; + +import com.google.android.apps.mytracks.MyTracksConstants; +import com.google.android.apps.mytracks.content.Sensor; + +/** + * An implementation of a SensorData parser for Zephyr HRM. + * + * @author Sandor Dornbush + */ +public class ZephyrMessageParser implements MessageParser { + + @Override + public Sensor.SensorDataSet parseBuffer(byte[] buffer) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < buffer.length; i++) { + sb.append(String.format("%02X", buffer[i])); + } + Log.w(MyTracksConstants.TAG, "Got zephyr data: " + sb); + // The provided units are 1/16 strides per minute. + int cadence = SensorUtils.unsignedShortToInt(buffer, 56); + // Heart Rate + Sensor.SensorData.Builder b = Sensor.SensorData.newBuilder() + .setValue(buffer[12] & 0xFF) + .setState(Sensor.SensorState.SENDING); + // Cadence + //.setCadence(cadence / 16) + //.build(); + Sensor.SensorDataSet sds = + Sensor.SensorDataSet.newBuilder() + .setCreationTime(System.currentTimeMillis()) + .setHeartRate(b) + .build(); + + return sds; + } + + @Override + public boolean isValid(byte[] buffer) { + // TODO crc etc. + return buffer[0] == 0x02 && buffer[59] == 0x03; + } + + @Override + public int getFrameSize() { + return 60; + } + + @Override + public int findNextAlignment(byte[] buffer) { + // TODO test or understand this code. + for (int i = 0; i < buffer.length - 1; i++) { + if (buffer[i] == 0x03 && buffer[i+1] == 0x02) { + return i; + } + } + return -1; + } +} diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/sensors/ZephyrSensorManager.java b/MyTracks/src/com/google/android/apps/mytracks/services/sensors/ZephyrSensorManager.java new file mode 100644 index 000000000..c5ff64839 --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/services/sensors/ZephyrSensorManager.java @@ -0,0 +1,26 @@ +/* + * Copyright 2010 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.google.android.apps.mytracks.services.sensors; + +import android.content.Context; + +public class ZephyrSensorManager extends BluetoothSensorManager { + + public ZephyrSensorManager(Context context) { + super(context, new ZephyrMessageParser()); + } + +} diff --git a/MyTracks/src/com/google/android/apps/mytracks/util/ApiFeatures.java b/MyTracks/src/com/google/android/apps/mytracks/util/ApiFeatures.java index 7526d6094..7f1016856 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/util/ApiFeatures.java +++ b/MyTracks/src/com/google/android/apps/mytracks/util/ApiFeatures.java @@ -102,10 +102,6 @@ public class ApiFeatures { return true; } - public boolean hasModernSignalStrength() { - return getApiLevel() >= 7; - } - public boolean hasStrictMode() { return getApiLevel() >= 9; } diff --git a/MyTracks/src/com/google/android/apps/mytracks/util/BluetoothDeviceUtils.java b/MyTracks/src/com/google/android/apps/mytracks/util/BluetoothDeviceUtils.java new file mode 100644 index 000000000..a819a8473 --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/util/BluetoothDeviceUtils.java @@ -0,0 +1,200 @@ +/* + * Copyright 2010 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.google.android.apps.mytracks.util; + +import com.google.android.apps.mytracks.MyTracksConstants; + +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothClass; +import android.bluetooth.BluetoothDevice; +import android.os.Build; +import android.util.Log; + +import java.util.List; +import java.util.Set; + +/** + * Utilities for dealing with bluetooth devices. + * This can be used safely even in systems that don't support bluetooth, + * in which case a dummy implementation will be used. + * + * @author Rodrigo Damazio + */ +public abstract class BluetoothDeviceUtils { + public static final String ANY_DEVICE = "any"; + private static BluetoothDeviceUtils instance; + + /** + * Dummy implementation, for systems that don't support bluetooth. + */ + private static class DummyImpl extends BluetoothDeviceUtils { + @Override + public void populateDeviceLists(List deviceNames, List deviceAddresses) { + // Do nothing - no devices to add + } + + @Override + public BluetoothDevice findDeviceMatching(String targetDeviceAddress) { + return null; + } + } + + /** + * Real implementation, for systems that DO support bluetooth. + */ + private static class RealImpl extends BluetoothDeviceUtils { + private final BluetoothAdapter bluetoothAdapter; + + public RealImpl() { + bluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); + + if (bluetoothAdapter == null) { + throw new IllegalStateException("Unable to get bluetooth adapter"); + } + } + + @Override + public void populateDeviceLists(List deviceNames, List deviceAddresses) { + ensureNotDiscovering(); + + Set pairedDevices = bluetoothAdapter.getBondedDevices(); + for (BluetoothDevice device : pairedDevices) { + BluetoothClass bluetoothClass = device.getBluetoothClass(); + if (bluetoothClass != null) { + // Not really sure what we want, but I know what we don't want. + switch(bluetoothClass.getMajorDeviceClass()) { + case BluetoothClass.Device.Major.COMPUTER: + case BluetoothClass.Device.Major.PHONE: + break; + default: + deviceAddresses.add(device.getAddress()); + deviceNames.add(device.getName()); + } + } + } + } + + @Override + public BluetoothDevice findDeviceMatching(String targetDeviceAddress) { + if (targetDeviceAddress.equals(ANY_DEVICE)) { + return findAnyDevice(); + } else { + return findDeviceByAddress(targetDeviceAddress); + } + } + + /** + * Finds and returns the first suitable bluetooth sensor. + */ + private BluetoothDevice findAnyDevice() { + ensureNotDiscovering(); + + Set pairedDevices = bluetoothAdapter.getBondedDevices(); + for (BluetoothDevice device : pairedDevices) { + // Look for the first paired computer device + if (isSuitableDevice(device)) { + return device; + } + } + + return null; + } + + /** + * Finds and returns a device with the given address, or null if it's not + * a suitable sensor. + */ + private BluetoothDevice findDeviceByAddress(String targetDeviceAddress) { + ensureNotDiscovering(); + + BluetoothDevice device = bluetoothAdapter.getRemoteDevice(targetDeviceAddress); + if (isSuitableDevice(device)) { + return device; + } + + return null; + } + + /** + * Ensures the bluetooth adapter is not in discovery mode. + */ + private void ensureNotDiscovering() { + // If it's in discovery mode, cancel that for now. + bluetoothAdapter.cancelDiscovery(); + } + + /** + * Checks whether the given device is a suitable sensor. + * + * @param device the device to check + * @return true if it's suitable, false otherwise + */ + private boolean isSuitableDevice(BluetoothDevice device) { + // Check that the device is bonded + if (device.getBondState() != BluetoothDevice.BOND_BONDED) { + return false; + } + + return true; + } + } + + /** + * Returns the proper (singleton) instance of this class. + */ + public static BluetoothDeviceUtils getInstance() { + if (instance == null) { + if (!isBluetoothMethodSupported()) { + Log.d(MyTracksConstants.TAG, "Using dummy bluetooth utils"); + instance = new DummyImpl(); + } else { + Log.d(MyTracksConstants.TAG, "Using real bluetooth utils"); + try { + instance = new RealImpl(); + } catch (IllegalStateException ise) { + Log.w(MyTracksConstants.TAG, "Oops, I mean, using dummy bluetooth utils", ise); + instance = new DummyImpl(); + } + } + } + return instance; + } + + /** + * Populates the given lists with the names and addresses of all suitable + * bluetooth devices. + * + * @param deviceNames the list to populate with user-visible names + * @param deviceAddresses the list to populate with device addresses + */ + public abstract void populateDeviceLists(List deviceNames, List deviceAddresses); + + /** + * Finds the bluetooth device with the given address. + * + * @param targetDeviceAddress the address of the device, or + * {@link #ANY_DEVICE} for using the first suitable device + * @return the device's descriptor, or null if not found + */ + public abstract BluetoothDevice findDeviceMatching(String targetDeviceAddress); + + /** + * @return whether the bluetooth method is supported on this device + */ + public static boolean isBluetoothMethodSupported() { + return Integer.parseInt(Build.VERSION.SDK) >= 5; + } +} diff --git a/MyTracksLib/.classpath b/MyTracksLib/.classpath new file mode 100644 index 000000000..67679b6b2 --- /dev/null +++ b/MyTracksLib/.classpath @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/MyTracksLib/.project b/MyTracksLib/.project new file mode 100644 index 000000000..1fefe8898 --- /dev/null +++ b/MyTracksLib/.project @@ -0,0 +1,33 @@ + + + MyTracksLib + + + + + + com.android.ide.eclipse.adt.ResourceManagerBuilder + + + + + com.android.ide.eclipse.adt.PreCompilerBuilder + + + + + org.eclipse.jdt.core.javabuilder + + + + + com.android.ide.eclipse.adt.ApkBuilder + + + + + + com.android.ide.eclipse.adt.AndroidNature + org.eclipse.jdt.core.javanature + + diff --git a/MyTracksLib/.settings/org.eclipse.jdt.core.prefs b/MyTracksLib/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 000000000..a46c35996 --- /dev/null +++ b/MyTracksLib/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,12 @@ +#Sun Nov 28 01:17:12 PST 2010 +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 +org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve +org.eclipse.jdt.core.compiler.compliance=1.6 +org.eclipse.jdt.core.compiler.debug.lineNumber=generate +org.eclipse.jdt.core.compiler.debug.localVariable=generate +org.eclipse.jdt.core.compiler.debug.sourceFile=generate +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.source=1.6 diff --git a/MyTracksLib/AndroidManifest.xml b/MyTracksLib/AndroidManifest.xml new file mode 100644 index 000000000..ce3f0ea4b --- /dev/null +++ b/MyTracksLib/AndroidManifest.xml @@ -0,0 +1,30 @@ + + + + + + + + \ No newline at end of file diff --git a/MyTracksLib/default.properties b/MyTracksLib/default.properties new file mode 100644 index 000000000..948be06ed --- /dev/null +++ b/MyTracksLib/default.properties @@ -0,0 +1,12 @@ +# This file is automatically generated by Android Tools. +# Do not modify this file -- YOUR CHANGES WILL BE ERASED! +# +# This file must be checked in Version Control Systems. +# +# To customize properties used by the Ant build system use, +# "build.properties", and override values to adapt the script to your +# project structure. + +# Project target. +target=android-8 +android.library=true diff --git a/MyTracksLib/libs/protobuf-java-2.3.0-lite.jar b/MyTracksLib/libs/protobuf-java-2.3.0-lite.jar new file mode 100644 index 000000000..9b6605da7 Binary files /dev/null and b/MyTracksLib/libs/protobuf-java-2.3.0-lite.jar differ diff --git a/MyTracksLib/proto/sensor.proto b/MyTracksLib/proto/sensor.proto new file mode 100644 index 000000000..b0ea1ac11 --- /dev/null +++ b/MyTracksLib/proto/sensor.proto @@ -0,0 +1,53 @@ +// Copyright 2010 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. + +package com.google.android.apps.mytracks.content; + +option optimize_for = LITE_RUNTIME; + +// An enumeration of the state of a single sensor. +enum SensorState { + // There is no sensor. + NONE = 1; + + // Now initiating an outgoing connection. + CONNECTING = 2; + + // Now connected to a remote device. + CONNECTED = 3; + + // A state where the sensor is not connected. + DISCONNECTED = 4; + + // This is the only state where the sensor data can be used. + SENDING = 5; +}; + +// The state and possible reading for one integer sensor. +message SensorData { + required SensorState state = 1 [default = NONE]; + + // Value is only defined if state = SENDING. + optional int32 value = 2; +}; + +// A message to encapsulate sensor readings associated with a track point. +message SensorDataSet { + // This is a required field. + optional uint64 creation_time = 1 [default = 0]; + + optional SensorData heart_rate = 2; + optional SensorData cadence = 3; + optional SensorData power = 4; +} \ No newline at end of file diff --git a/MyTracks/res/drawable/arrow_icon.png b/MyTracksLib/res/drawable/arrow_icon.png similarity index 100% rename from MyTracks/res/drawable/arrow_icon.png rename to MyTracksLib/res/drawable/arrow_icon.png diff --git a/MyTracksLib/res/values/strings.xml b/MyTracksLib/res/values/strings.xml new file mode 100644 index 000000000..a3c7af5b8 --- /dev/null +++ b/MyTracksLib/res/values/strings.xml @@ -0,0 +1,19 @@ + + + + My Tracks Base Library + diff --git a/MyTracksLib/res/values/third_party.xml b/MyTracksLib/res/values/third_party.xml new file mode 100644 index 000000000..e85f8ff1b --- /dev/null +++ b/MyTracksLib/res/values/third_party.xml @@ -0,0 +1,35 @@ + + + + + + com.google.android.apps.mytracks.TRACK_NOTIFICATIONS + com.google.android.apps.mytracks.READ_TRACK_DATA + com.google.android.apps.mytracks.WRITE_TRACK_DATA + + + com.google.android.apps.mytracks.TRACK_STARTED + com.google.android.apps.mytracks.TRACK_PAUSED + com.google.android.apps.mytracks.TRACK_RESUMED + com.google.android.apps.mytracks.TRACK_STOPPED + com.google.android.apps.mytracks.TRACK_ID + + + com.google.android.apps.mytracks.services.TrackRecordingService + com.google.android.maps.mytracks + + diff --git a/MyTracks/src/com/google/android/apps/mytracks/content/DescriptionGenerator.java b/MyTracksLib/src/com/google/android/apps/mytracks/content/DescriptionGenerator.java similarity index 100% rename from MyTracks/src/com/google/android/apps/mytracks/content/DescriptionGenerator.java rename to MyTracksLib/src/com/google/android/apps/mytracks/content/DescriptionGenerator.java diff --git a/MyTracksLib/src/com/google/android/apps/mytracks/content/MyTracksLocation.java b/MyTracksLib/src/com/google/android/apps/mytracks/content/MyTracksLocation.java new file mode 100644 index 000000000..4566225d5 --- /dev/null +++ b/MyTracksLib/src/com/google/android/apps/mytracks/content/MyTracksLocation.java @@ -0,0 +1,64 @@ +/* + * Copyright 2010 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.google.android.apps.mytracks.content; + +import android.location.Location; + +/** + * This class extends the standard Android location with extra information. + * + * @author Sandor Dornbush + */ +public class MyTracksLocation extends Location { + + private Sensor.SensorDataSet sensorDataSet = null; + + /** + * The id of this location from the provider. + */ + private int id = -1; + + public MyTracksLocation(Location location, Sensor.SensorDataSet sd) { + super(location); + this.sensorDataSet = sd; + } + + public MyTracksLocation(String string) { + super(string); + } + + public Sensor.SensorDataSet getSensorDataSet() { + return sensorDataSet; + } + + public void setSensorData(Sensor.SensorDataSet sensorData) { + this.sensorDataSet = sensorData; + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public void reset() { + super.reset(); + sensorDataSet = null; + id = -1; + } +} diff --git a/MyTracks/src/com/google/android/apps/mytracks/content/MyTracksProviderUtils.java b/MyTracksLib/src/com/google/android/apps/mytracks/content/MyTracksProviderUtils.java similarity index 100% rename from MyTracks/src/com/google/android/apps/mytracks/content/MyTracksProviderUtils.java rename to MyTracksLib/src/com/google/android/apps/mytracks/content/MyTracksProviderUtils.java diff --git a/MyTracks/src/com/google/android/apps/mytracks/content/MyTracksProviderUtilsImpl.java b/MyTracksLib/src/com/google/android/apps/mytracks/content/MyTracksProviderUtilsImpl.java similarity index 92% rename from MyTracks/src/com/google/android/apps/mytracks/content/MyTracksProviderUtilsImpl.java rename to MyTracksLib/src/com/google/android/apps/mytracks/content/MyTracksProviderUtilsImpl.java index 1e814f102..f9e466dce 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/content/MyTracksProviderUtilsImpl.java +++ b/MyTracksLib/src/com/google/android/apps/mytracks/content/MyTracksProviderUtilsImpl.java @@ -15,7 +15,10 @@ */ 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; @@ -74,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; } @@ -171,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; } @@ -188,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); @@ -210,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 @@ -449,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); } @@ -463,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 @@ -475,17 +499,16 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { final Waypoint nextWaypoint = getNextStatisticsWaypointAfter(deletedWaypoint); if (nextWaypoint != null) { - Log.d(MyTracksProvider.TAG, "Correcting marker " + nextWaypoint.getId() + Log.d(TAG, "Correcting marker " + nextWaypoint.getId() + " after deleted marker " + deletedWaypoint.getId()); nextWaypoint.getStatistics().merge(deletedWaypoint.getStatistics()); nextWaypoint.setDescription( descriptionGenerator.generateWaypointDescription(nextWaypoint)); if (!updateWaypoint(nextWaypoint)) { - Log.w(MyTracksProvider.TAG, "Update of marker was unsuccessful."); + Log.w(TAG, "Update of marker was unsuccessful."); } } else { - Log.d(MyTracksProvider.TAG, - "No statistics marker after the deleted one was found."); + Log.d(TAG, "No statistics marker after the deleted one was found."); } } contentResolver.delete( @@ -510,7 +533,7 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { return createWaypoint(cursor); } } catch (RuntimeException e) { - Log.w(MyTracksProvider.TAG, "Caught unexpected exception.", e); + Log.w(TAG, "Caught unexpected exception.", e); } finally { if (cursor != null) { cursor.close(); @@ -529,7 +552,7 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { null /*selectionArgs*/); return rows == 1; } catch (RuntimeException e) { - Log.e(MyTracksProvider.TAG, "Caught unexpected exception.", e); + Log.e(TAG, "Caught unexpected exception.", e); } return false; } @@ -549,7 +572,7 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { return createLocation(cursor); } } catch (RuntimeException e) { - Log.w(MyTracksProvider.TAG, "Caught an unexpeceted exception.", e); + Log.w(TAG, "Caught an unexpeceted exception.", e); } finally { if (cursor != null) { cursor.close(); @@ -573,7 +596,7 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { return createTrack(cursor); } } catch (RuntimeException e) { - Log.w(MyTracksProvider.TAG, "Caught unexpected exception.", e); + Log.w(TAG, "Caught unexpected exception.", e); } finally { if (cursor != null) { cursor.close(); @@ -601,7 +624,7 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { return createWaypoint(cursor); } } catch (RuntimeException e) { - Log.w(MyTracksProvider.TAG, "Caught an unexpected exception.", e); + Log.w(TAG, "Caught an unexpected exception.", e); } finally { cursor.close(); } @@ -623,7 +646,7 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { return createWaypoint(cursor); } } catch (RuntimeException e) { - Log.w(MyTracksProvider.TAG, "Caught an unexpected exception.", e); + Log.w(TAG, "Caught an unexpected exception.", e); } finally { cursor.close(); } @@ -647,7 +670,7 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { cursor.getColumnIndexOrThrow(TrackPointsColumns._ID)); } } catch (RuntimeException e) { - Log.w(MyTracksProvider.TAG, "Caught an unexpected exception.", e); + Log.w(TAG, "Caught an unexpected exception.", e); } finally { cursor.close(); } @@ -671,7 +694,7 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { cursor.getColumnIndexOrThrow(WaypointsColumns._ID)); } } catch (RuntimeException e) { - Log.w(MyTracksProvider.TAG, "Caught an unexpected exception.", e); + Log.w(TAG, "Caught an unexpected exception.", e); } finally { cursor.close(); } @@ -695,7 +718,7 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { cursor.getColumnIndexOrThrow(WaypointsColumns._ID)); } } catch (RuntimeException e) { - Log.w(MyTracksProvider.TAG, "Caught an unexpected exception.", e); + Log.w(TAG, "Caught an unexpected exception.", e); } finally { cursor.close(); } @@ -714,7 +737,7 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { return createTrack(cursor); } } catch (RuntimeException e) { - Log.w(MyTracksProvider.TAG, "Caught an unexpected exception.", e); + Log.w(TAG, "Caught an unexpected exception.", e); } finally { if (cursor != null) { cursor.close(); @@ -818,7 +841,7 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { long lastId = -1; Cursor cursor = getLocationsCursor(track.getId(), -1, maxPoints, true); if (cursor == null) { - Log.w(MyTracksProvider.TAG, "Cannot get a locations cursor!"); + Log.w(TAG, "Cannot get a locations cursor!"); return lastId; } try { @@ -835,7 +858,7 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { } while (cursor.moveToPrevious()); } } catch (RuntimeException e) { - Log.w(MyTracksProvider.TAG, "Caught unexpected exception.", e); + Log.w(TAG, "Caught unexpected exception.", e); } finally { cursor.close(); } @@ -851,14 +874,14 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { @Override public Uri insertTrack(Track track) { - Log.d(MyTracksProvider.TAG, "MyTracksProviderUtilsImpl.insertTrack"); + Log.d(TAG, "MyTracksProviderUtilsImpl.insertTrack"); return contentResolver.insert(TracksColumns.CONTENT_URI, createContentValues(track)); } @Override public Uri insertTrackPoint(Location location, long trackId) { - Log.d(MyTracksProvider.TAG, "MyTracksProviderUtilsImpl.insertTrackPoint"); + Log.d(TAG, "MyTracksProviderUtilsImpl.insertTrackPoint"); return contentResolver.insert(TrackPointsColumns.CONTENT_URI, createContentValues(location, trackId)); } @@ -877,7 +900,7 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { @Override public Uri insertWaypoint(Waypoint waypoint) { - Log.d(MyTracksProvider.TAG, "MyTracksProviderUtilsImpl.insertWaypoint"); + Log.d(TAG, "MyTracksProviderUtilsImpl.insertWaypoint"); waypoint.setId(-1); return contentResolver.insert(WaypointsColumns.CONTENT_URI, createContentValues(waypoint)); @@ -907,7 +930,7 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { @Override public void updateTrack(Track track) { - Log.d(MyTracksProvider.TAG, "MyTracksProviderUtilsImpl.updateTrack"); + Log.d(TAG, "MyTracksProviderUtilsImpl.updateTrack"); contentResolver.update(TracksColumns.CONTENT_URI, createContentValues(track), "_id=" + track.getId(), null); } diff --git a/MyTracksLib/src/com/google/android/apps/mytracks/content/Sensor.java b/MyTracksLib/src/com/google/android/apps/mytracks/content/Sensor.java new file mode 100644 index 000000000..54a9df31d --- /dev/null +++ b/MyTracksLib/src/com/google/android/apps/mytracks/content/Sensor.java @@ -0,0 +1,816 @@ +// Generated by the protocol buffer compiler. DO NOT EDIT! +// source: proto/sensor.proto + +package com.google.android.apps.mytracks.content; + +public final class Sensor { + private Sensor() {} + public static void registerAllExtensions( + com.google.protobuf.ExtensionRegistryLite registry) { + } + public enum SensorState + implements com.google.protobuf.Internal.EnumLite { + NONE(0, 1), + CONNECTING(1, 2), + CONNECTED(2, 3), + DISCONNECTED(3, 4), + SENDING(4, 5), + ; + + + public final int getNumber() { return value; } + + public static SensorState valueOf(int value) { + switch (value) { + case 1: return NONE; + case 2: return CONNECTING; + case 3: return CONNECTED; + case 4: return DISCONNECTED; + case 5: return SENDING; + default: return null; + } + } + + public static com.google.protobuf.Internal.EnumLiteMap + internalGetValueMap() { + return internalValueMap; + } + private static com.google.protobuf.Internal.EnumLiteMap + internalValueMap = + new com.google.protobuf.Internal.EnumLiteMap() { + public SensorState findValueByNumber(int number) { + return SensorState.valueOf(number) + ; } + }; + + private final int index; + private final int value; + private SensorState(int index, int value) { + this.index = index; + this.value = value; + } + + // @@protoc_insertion_point(enum_scope:com.google.android.apps.mytracks.content.SensorState) + } + + public static final class SensorData extends + com.google.protobuf.GeneratedMessageLite { + // Use SensorData.newBuilder() to construct. + private SensorData() { + initFields(); + } + private SensorData(boolean noInit) {} + + private static final SensorData defaultInstance; + public static SensorData getDefaultInstance() { + return defaultInstance; + } + + public SensorData getDefaultInstanceForType() { + return defaultInstance; + } + + // required .com.google.android.apps.mytracks.content.SensorState state = 1 [default = NONE]; + public static final int STATE_FIELD_NUMBER = 1; + private boolean hasState; + private com.google.android.apps.mytracks.content.Sensor.SensorState state_; + public boolean hasState() { return hasState; } + public com.google.android.apps.mytracks.content.Sensor.SensorState getState() { return state_; } + + // optional int32 value = 2; + public static final int VALUE_FIELD_NUMBER = 2; + private boolean hasValue; + private int value_ = 0; + public boolean hasValue() { return hasValue; } + public int getValue() { return value_; } + + private void initFields() { + state_ = com.google.android.apps.mytracks.content.Sensor.SensorState.NONE; + } + public final boolean isInitialized() { + if (!hasState) return false; + return true; + } + + public void writeTo(com.google.protobuf.CodedOutputStream output) + throws java.io.IOException { + getSerializedSize(); + if (hasState()) { + output.writeEnum(1, getState().getNumber()); + } + if (hasValue()) { + output.writeInt32(2, getValue()); + } + } + + private int memoizedSerializedSize = -1; + public int getSerializedSize() { + int size = memoizedSerializedSize; + if (size != -1) return size; + + size = 0; + if (hasState()) { + size += com.google.protobuf.CodedOutputStream + .computeEnumSize(1, getState().getNumber()); + } + if (hasValue()) { + size += com.google.protobuf.CodedOutputStream + .computeInt32Size(2, getValue()); + } + memoizedSerializedSize = size; + return size; + } + + public static com.google.android.apps.mytracks.content.Sensor.SensorData parseFrom( + com.google.protobuf.ByteString data) + throws com.google.protobuf.InvalidProtocolBufferException { + return newBuilder().mergeFrom(data).buildParsed(); + } + public static com.google.android.apps.mytracks.content.Sensor.SensorData parseFrom( + com.google.protobuf.ByteString data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return newBuilder().mergeFrom(data, extensionRegistry) + .buildParsed(); + } + public static com.google.android.apps.mytracks.content.Sensor.SensorData parseFrom(byte[] data) + throws com.google.protobuf.InvalidProtocolBufferException { + return newBuilder().mergeFrom(data).buildParsed(); + } + public static com.google.android.apps.mytracks.content.Sensor.SensorData parseFrom( + byte[] data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return newBuilder().mergeFrom(data, extensionRegistry) + .buildParsed(); + } + public static com.google.android.apps.mytracks.content.Sensor.SensorData parseFrom(java.io.InputStream input) + throws java.io.IOException { + return newBuilder().mergeFrom(input).buildParsed(); + } + public static com.google.android.apps.mytracks.content.Sensor.SensorData parseFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return newBuilder().mergeFrom(input, extensionRegistry) + .buildParsed(); + } + public static com.google.android.apps.mytracks.content.Sensor.SensorData parseDelimitedFrom(java.io.InputStream input) + throws java.io.IOException { + Builder builder = newBuilder(); + if (builder.mergeDelimitedFrom(input)) { + return builder.buildParsed(); + } else { + return null; + } + } + public static com.google.android.apps.mytracks.content.Sensor.SensorData parseDelimitedFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + Builder builder = newBuilder(); + if (builder.mergeDelimitedFrom(input, extensionRegistry)) { + return builder.buildParsed(); + } else { + return null; + } + } + public static com.google.android.apps.mytracks.content.Sensor.SensorData parseFrom( + com.google.protobuf.CodedInputStream input) + throws java.io.IOException { + return newBuilder().mergeFrom(input).buildParsed(); + } + public static com.google.android.apps.mytracks.content.Sensor.SensorData parseFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return newBuilder().mergeFrom(input, extensionRegistry) + .buildParsed(); + } + + public static Builder newBuilder() { return Builder.create(); } + public Builder newBuilderForType() { return newBuilder(); } + public static Builder newBuilder(com.google.android.apps.mytracks.content.Sensor.SensorData prototype) { + return newBuilder().mergeFrom(prototype); + } + public Builder toBuilder() { return newBuilder(this); } + + public static final class Builder extends + com.google.protobuf.GeneratedMessageLite.Builder< + com.google.android.apps.mytracks.content.Sensor.SensorData, Builder> { + private com.google.android.apps.mytracks.content.Sensor.SensorData result; + + // Construct using com.google.android.apps.mytracks.content.Sensor.SensorData.newBuilder() + private Builder() {} + + private static Builder create() { + Builder builder = new Builder(); + builder.result = new com.google.android.apps.mytracks.content.Sensor.SensorData(); + return builder; + } + + protected com.google.android.apps.mytracks.content.Sensor.SensorData internalGetResult() { + return result; + } + + public Builder clear() { + if (result == null) { + throw new IllegalStateException( + "Cannot call clear() after build()."); + } + result = new com.google.android.apps.mytracks.content.Sensor.SensorData(); + return this; + } + + public Builder clone() { + return create().mergeFrom(result); + } + + public com.google.android.apps.mytracks.content.Sensor.SensorData getDefaultInstanceForType() { + return com.google.android.apps.mytracks.content.Sensor.SensorData.getDefaultInstance(); + } + + public boolean isInitialized() { + return result.isInitialized(); + } + public com.google.android.apps.mytracks.content.Sensor.SensorData build() { + if (result != null && !isInitialized()) { + throw newUninitializedMessageException(result); + } + return buildPartial(); + } + + private com.google.android.apps.mytracks.content.Sensor.SensorData buildParsed() + throws com.google.protobuf.InvalidProtocolBufferException { + if (!isInitialized()) { + throw newUninitializedMessageException( + result).asInvalidProtocolBufferException(); + } + return buildPartial(); + } + + public com.google.android.apps.mytracks.content.Sensor.SensorData buildPartial() { + if (result == null) { + throw new IllegalStateException( + "build() has already been called on this Builder."); + } + com.google.android.apps.mytracks.content.Sensor.SensorData returnMe = result; + result = null; + return returnMe; + } + + public Builder mergeFrom(com.google.android.apps.mytracks.content.Sensor.SensorData other) { + if (other == com.google.android.apps.mytracks.content.Sensor.SensorData.getDefaultInstance()) return this; + if (other.hasState()) { + setState(other.getState()); + } + if (other.hasValue()) { + setValue(other.getValue()); + } + return this; + } + + public Builder mergeFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + while (true) { + int tag = input.readTag(); + switch (tag) { + case 0: + return this; + default: { + if (!parseUnknownField(input, extensionRegistry, tag)) { + return this; + } + break; + } + case 8: { + int rawValue = input.readEnum(); + com.google.android.apps.mytracks.content.Sensor.SensorState value = com.google.android.apps.mytracks.content.Sensor.SensorState.valueOf(rawValue); + if (value != null) { + setState(value); + } + break; + } + case 16: { + setValue(input.readInt32()); + break; + } + } + } + } + + + // required .com.google.android.apps.mytracks.content.SensorState state = 1 [default = NONE]; + public boolean hasState() { + return result.hasState(); + } + public com.google.android.apps.mytracks.content.Sensor.SensorState getState() { + return result.getState(); + } + public Builder setState(com.google.android.apps.mytracks.content.Sensor.SensorState value) { + if (value == null) { + throw new NullPointerException(); + } + result.hasState = true; + result.state_ = value; + return this; + } + public Builder clearState() { + result.hasState = false; + result.state_ = com.google.android.apps.mytracks.content.Sensor.SensorState.NONE; + return this; + } + + // optional int32 value = 2; + public boolean hasValue() { + return result.hasValue(); + } + public int getValue() { + return result.getValue(); + } + public Builder setValue(int value) { + result.hasValue = true; + result.value_ = value; + return this; + } + public Builder clearValue() { + result.hasValue = false; + result.value_ = 0; + return this; + } + + // @@protoc_insertion_point(builder_scope:com.google.android.apps.mytracks.content.SensorData) + } + + static { + defaultInstance = new SensorData(true); + com.google.android.apps.mytracks.content.Sensor.internalForceInit(); + defaultInstance.initFields(); + } + + // @@protoc_insertion_point(class_scope:com.google.android.apps.mytracks.content.SensorData) + } + + public static final class SensorDataSet extends + com.google.protobuf.GeneratedMessageLite { + // Use SensorDataSet.newBuilder() to construct. + private SensorDataSet() { + initFields(); + } + private SensorDataSet(boolean noInit) {} + + private static final SensorDataSet defaultInstance; + public static SensorDataSet getDefaultInstance() { + return defaultInstance; + } + + public SensorDataSet getDefaultInstanceForType() { + return defaultInstance; + } + + // optional uint64 creation_time = 1 [default = 0]; + public static final int CREATION_TIME_FIELD_NUMBER = 1; + private boolean hasCreationTime; + private long creationTime_ = 0L; + public boolean hasCreationTime() { return hasCreationTime; } + public long getCreationTime() { return creationTime_; } + + // optional .com.google.android.apps.mytracks.content.SensorData heart_rate = 2; + public static final int HEART_RATE_FIELD_NUMBER = 2; + private boolean hasHeartRate; + private com.google.android.apps.mytracks.content.Sensor.SensorData heartRate_; + public boolean hasHeartRate() { return hasHeartRate; } + public com.google.android.apps.mytracks.content.Sensor.SensorData getHeartRate() { return heartRate_; } + + // optional .com.google.android.apps.mytracks.content.SensorData cadence = 3; + public static final int CADENCE_FIELD_NUMBER = 3; + private boolean hasCadence; + private com.google.android.apps.mytracks.content.Sensor.SensorData cadence_; + public boolean hasCadence() { return hasCadence; } + public com.google.android.apps.mytracks.content.Sensor.SensorData getCadence() { return cadence_; } + + // optional .com.google.android.apps.mytracks.content.SensorData power = 4; + public static final int POWER_FIELD_NUMBER = 4; + private boolean hasPower; + private com.google.android.apps.mytracks.content.Sensor.SensorData power_; + public boolean hasPower() { return hasPower; } + public com.google.android.apps.mytracks.content.Sensor.SensorData getPower() { return power_; } + + private void initFields() { + heartRate_ = com.google.android.apps.mytracks.content.Sensor.SensorData.getDefaultInstance(); + cadence_ = com.google.android.apps.mytracks.content.Sensor.SensorData.getDefaultInstance(); + power_ = com.google.android.apps.mytracks.content.Sensor.SensorData.getDefaultInstance(); + } + public final boolean isInitialized() { + if (hasHeartRate()) { + if (!getHeartRate().isInitialized()) return false; + } + if (hasCadence()) { + if (!getCadence().isInitialized()) return false; + } + if (hasPower()) { + if (!getPower().isInitialized()) return false; + } + return true; + } + + public void writeTo(com.google.protobuf.CodedOutputStream output) + throws java.io.IOException { + getSerializedSize(); + if (hasCreationTime()) { + output.writeUInt64(1, getCreationTime()); + } + if (hasHeartRate()) { + output.writeMessage(2, getHeartRate()); + } + if (hasCadence()) { + output.writeMessage(3, getCadence()); + } + if (hasPower()) { + output.writeMessage(4, getPower()); + } + } + + private int memoizedSerializedSize = -1; + public int getSerializedSize() { + int size = memoizedSerializedSize; + if (size != -1) return size; + + size = 0; + if (hasCreationTime()) { + size += com.google.protobuf.CodedOutputStream + .computeUInt64Size(1, getCreationTime()); + } + if (hasHeartRate()) { + size += com.google.protobuf.CodedOutputStream + .computeMessageSize(2, getHeartRate()); + } + if (hasCadence()) { + size += com.google.protobuf.CodedOutputStream + .computeMessageSize(3, getCadence()); + } + if (hasPower()) { + size += com.google.protobuf.CodedOutputStream + .computeMessageSize(4, getPower()); + } + memoizedSerializedSize = size; + return size; + } + + public static com.google.android.apps.mytracks.content.Sensor.SensorDataSet parseFrom( + com.google.protobuf.ByteString data) + throws com.google.protobuf.InvalidProtocolBufferException { + return newBuilder().mergeFrom(data).buildParsed(); + } + public static com.google.android.apps.mytracks.content.Sensor.SensorDataSet parseFrom( + com.google.protobuf.ByteString data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return newBuilder().mergeFrom(data, extensionRegistry) + .buildParsed(); + } + public static com.google.android.apps.mytracks.content.Sensor.SensorDataSet parseFrom(byte[] data) + throws com.google.protobuf.InvalidProtocolBufferException { + return newBuilder().mergeFrom(data).buildParsed(); + } + public static com.google.android.apps.mytracks.content.Sensor.SensorDataSet parseFrom( + byte[] data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return newBuilder().mergeFrom(data, extensionRegistry) + .buildParsed(); + } + public static com.google.android.apps.mytracks.content.Sensor.SensorDataSet parseFrom(java.io.InputStream input) + throws java.io.IOException { + return newBuilder().mergeFrom(input).buildParsed(); + } + public static com.google.android.apps.mytracks.content.Sensor.SensorDataSet parseFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return newBuilder().mergeFrom(input, extensionRegistry) + .buildParsed(); + } + public static com.google.android.apps.mytracks.content.Sensor.SensorDataSet parseDelimitedFrom(java.io.InputStream input) + throws java.io.IOException { + Builder builder = newBuilder(); + if (builder.mergeDelimitedFrom(input)) { + return builder.buildParsed(); + } else { + return null; + } + } + public static com.google.android.apps.mytracks.content.Sensor.SensorDataSet parseDelimitedFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + Builder builder = newBuilder(); + if (builder.mergeDelimitedFrom(input, extensionRegistry)) { + return builder.buildParsed(); + } else { + return null; + } + } + public static com.google.android.apps.mytracks.content.Sensor.SensorDataSet parseFrom( + com.google.protobuf.CodedInputStream input) + throws java.io.IOException { + return newBuilder().mergeFrom(input).buildParsed(); + } + public static com.google.android.apps.mytracks.content.Sensor.SensorDataSet parseFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return newBuilder().mergeFrom(input, extensionRegistry) + .buildParsed(); + } + + public static Builder newBuilder() { return Builder.create(); } + public Builder newBuilderForType() { return newBuilder(); } + public static Builder newBuilder(com.google.android.apps.mytracks.content.Sensor.SensorDataSet prototype) { + return newBuilder().mergeFrom(prototype); + } + public Builder toBuilder() { return newBuilder(this); } + + public static final class Builder extends + com.google.protobuf.GeneratedMessageLite.Builder< + com.google.android.apps.mytracks.content.Sensor.SensorDataSet, Builder> { + private com.google.android.apps.mytracks.content.Sensor.SensorDataSet result; + + // Construct using com.google.android.apps.mytracks.content.Sensor.SensorDataSet.newBuilder() + private Builder() {} + + private static Builder create() { + Builder builder = new Builder(); + builder.result = new com.google.android.apps.mytracks.content.Sensor.SensorDataSet(); + return builder; + } + + protected com.google.android.apps.mytracks.content.Sensor.SensorDataSet internalGetResult() { + return result; + } + + public Builder clear() { + if (result == null) { + throw new IllegalStateException( + "Cannot call clear() after build()."); + } + result = new com.google.android.apps.mytracks.content.Sensor.SensorDataSet(); + return this; + } + + public Builder clone() { + return create().mergeFrom(result); + } + + public com.google.android.apps.mytracks.content.Sensor.SensorDataSet getDefaultInstanceForType() { + return com.google.android.apps.mytracks.content.Sensor.SensorDataSet.getDefaultInstance(); + } + + public boolean isInitialized() { + return result.isInitialized(); + } + public com.google.android.apps.mytracks.content.Sensor.SensorDataSet build() { + if (result != null && !isInitialized()) { + throw newUninitializedMessageException(result); + } + return buildPartial(); + } + + private com.google.android.apps.mytracks.content.Sensor.SensorDataSet buildParsed() + throws com.google.protobuf.InvalidProtocolBufferException { + if (!isInitialized()) { + throw newUninitializedMessageException( + result).asInvalidProtocolBufferException(); + } + return buildPartial(); + } + + public com.google.android.apps.mytracks.content.Sensor.SensorDataSet buildPartial() { + if (result == null) { + throw new IllegalStateException( + "build() has already been called on this Builder."); + } + com.google.android.apps.mytracks.content.Sensor.SensorDataSet returnMe = result; + result = null; + return returnMe; + } + + public Builder mergeFrom(com.google.android.apps.mytracks.content.Sensor.SensorDataSet other) { + if (other == com.google.android.apps.mytracks.content.Sensor.SensorDataSet.getDefaultInstance()) return this; + if (other.hasCreationTime()) { + setCreationTime(other.getCreationTime()); + } + if (other.hasHeartRate()) { + mergeHeartRate(other.getHeartRate()); + } + if (other.hasCadence()) { + mergeCadence(other.getCadence()); + } + if (other.hasPower()) { + mergePower(other.getPower()); + } + return this; + } + + public Builder mergeFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + while (true) { + int tag = input.readTag(); + switch (tag) { + case 0: + return this; + default: { + if (!parseUnknownField(input, extensionRegistry, tag)) { + return this; + } + break; + } + case 8: { + setCreationTime(input.readUInt64()); + break; + } + case 18: { + com.google.android.apps.mytracks.content.Sensor.SensorData.Builder subBuilder = com.google.android.apps.mytracks.content.Sensor.SensorData.newBuilder(); + if (hasHeartRate()) { + subBuilder.mergeFrom(getHeartRate()); + } + input.readMessage(subBuilder, extensionRegistry); + setHeartRate(subBuilder.buildPartial()); + break; + } + case 26: { + com.google.android.apps.mytracks.content.Sensor.SensorData.Builder subBuilder = com.google.android.apps.mytracks.content.Sensor.SensorData.newBuilder(); + if (hasCadence()) { + subBuilder.mergeFrom(getCadence()); + } + input.readMessage(subBuilder, extensionRegistry); + setCadence(subBuilder.buildPartial()); + break; + } + case 34: { + com.google.android.apps.mytracks.content.Sensor.SensorData.Builder subBuilder = com.google.android.apps.mytracks.content.Sensor.SensorData.newBuilder(); + if (hasPower()) { + subBuilder.mergeFrom(getPower()); + } + input.readMessage(subBuilder, extensionRegistry); + setPower(subBuilder.buildPartial()); + break; + } + } + } + } + + + // optional uint64 creation_time = 1 [default = 0]; + public boolean hasCreationTime() { + return result.hasCreationTime(); + } + public long getCreationTime() { + return result.getCreationTime(); + } + public Builder setCreationTime(long value) { + result.hasCreationTime = true; + result.creationTime_ = value; + return this; + } + public Builder clearCreationTime() { + result.hasCreationTime = false; + result.creationTime_ = 0L; + return this; + } + + // optional .com.google.android.apps.mytracks.content.SensorData heart_rate = 2; + public boolean hasHeartRate() { + return result.hasHeartRate(); + } + public com.google.android.apps.mytracks.content.Sensor.SensorData getHeartRate() { + return result.getHeartRate(); + } + public Builder setHeartRate(com.google.android.apps.mytracks.content.Sensor.SensorData value) { + if (value == null) { + throw new NullPointerException(); + } + result.hasHeartRate = true; + result.heartRate_ = value; + return this; + } + public Builder setHeartRate(com.google.android.apps.mytracks.content.Sensor.SensorData.Builder builderForValue) { + result.hasHeartRate = true; + result.heartRate_ = builderForValue.build(); + return this; + } + public Builder mergeHeartRate(com.google.android.apps.mytracks.content.Sensor.SensorData value) { + if (result.hasHeartRate() && + result.heartRate_ != com.google.android.apps.mytracks.content.Sensor.SensorData.getDefaultInstance()) { + result.heartRate_ = + com.google.android.apps.mytracks.content.Sensor.SensorData.newBuilder(result.heartRate_).mergeFrom(value).buildPartial(); + } else { + result.heartRate_ = value; + } + result.hasHeartRate = true; + return this; + } + public Builder clearHeartRate() { + result.hasHeartRate = false; + result.heartRate_ = com.google.android.apps.mytracks.content.Sensor.SensorData.getDefaultInstance(); + return this; + } + + // optional .com.google.android.apps.mytracks.content.SensorData cadence = 3; + public boolean hasCadence() { + return result.hasCadence(); + } + public com.google.android.apps.mytracks.content.Sensor.SensorData getCadence() { + return result.getCadence(); + } + public Builder setCadence(com.google.android.apps.mytracks.content.Sensor.SensorData value) { + if (value == null) { + throw new NullPointerException(); + } + result.hasCadence = true; + result.cadence_ = value; + return this; + } + public Builder setCadence(com.google.android.apps.mytracks.content.Sensor.SensorData.Builder builderForValue) { + result.hasCadence = true; + result.cadence_ = builderForValue.build(); + return this; + } + public Builder mergeCadence(com.google.android.apps.mytracks.content.Sensor.SensorData value) { + if (result.hasCadence() && + result.cadence_ != com.google.android.apps.mytracks.content.Sensor.SensorData.getDefaultInstance()) { + result.cadence_ = + com.google.android.apps.mytracks.content.Sensor.SensorData.newBuilder(result.cadence_).mergeFrom(value).buildPartial(); + } else { + result.cadence_ = value; + } + result.hasCadence = true; + return this; + } + public Builder clearCadence() { + result.hasCadence = false; + result.cadence_ = com.google.android.apps.mytracks.content.Sensor.SensorData.getDefaultInstance(); + return this; + } + + // optional .com.google.android.apps.mytracks.content.SensorData power = 4; + public boolean hasPower() { + return result.hasPower(); + } + public com.google.android.apps.mytracks.content.Sensor.SensorData getPower() { + return result.getPower(); + } + public Builder setPower(com.google.android.apps.mytracks.content.Sensor.SensorData value) { + if (value == null) { + throw new NullPointerException(); + } + result.hasPower = true; + result.power_ = value; + return this; + } + public Builder setPower(com.google.android.apps.mytracks.content.Sensor.SensorData.Builder builderForValue) { + result.hasPower = true; + result.power_ = builderForValue.build(); + return this; + } + public Builder mergePower(com.google.android.apps.mytracks.content.Sensor.SensorData value) { + if (result.hasPower() && + result.power_ != com.google.android.apps.mytracks.content.Sensor.SensorData.getDefaultInstance()) { + result.power_ = + com.google.android.apps.mytracks.content.Sensor.SensorData.newBuilder(result.power_).mergeFrom(value).buildPartial(); + } else { + result.power_ = value; + } + result.hasPower = true; + return this; + } + public Builder clearPower() { + result.hasPower = false; + result.power_ = com.google.android.apps.mytracks.content.Sensor.SensorData.getDefaultInstance(); + return this; + } + + // @@protoc_insertion_point(builder_scope:com.google.android.apps.mytracks.content.SensorDataSet) + } + + static { + defaultInstance = new SensorDataSet(true); + com.google.android.apps.mytracks.content.Sensor.internalForceInit(); + defaultInstance.initFields(); + } + + // @@protoc_insertion_point(class_scope:com.google.android.apps.mytracks.content.SensorDataSet) + } + + + static { + } + + public static void internalForceInit() {} + + // @@protoc_insertion_point(outer_class_scope) +} diff --git a/MyTracks/src/com/google/android/apps/mytracks/content/Track.java b/MyTracksLib/src/com/google/android/apps/mytracks/content/Track.java similarity index 99% rename from MyTracks/src/com/google/android/apps/mytracks/content/Track.java rename to MyTracksLib/src/com/google/android/apps/mytracks/content/Track.java index f881c966b..fe4c19453 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/content/Track.java +++ b/MyTracksLib/src/com/google/android/apps/mytracks/content/Track.java @@ -34,7 +34,7 @@ import java.util.ArrayList; public class Track implements Parcelable { /** - * Creator for a Track object + * Creator for a Track object. */ public static class Creator implements Parcelable.Creator { diff --git a/MyTracks/src/com/google/android/apps/mytracks/content/TrackPointsColumns.java b/MyTracksLib/src/com/google/android/apps/mytracks/content/TrackPointsColumns.java similarity index 76% rename from MyTracks/src/com/google/android/apps/mytracks/content/TrackPointsColumns.java rename to MyTracksLib/src/com/google/android/apps/mytracks/content/TrackPointsColumns.java index 36701db40..eba090517 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/content/TrackPointsColumns.java +++ b/MyTracksLib/src/com/google/android/apps/mytracks/content/TrackPointsColumns.java @@ -15,8 +15,6 @@ */ package com.google.android.apps.mytracks.content; -import static com.google.android.apps.mytracks.content.ContentTypeIds.*; - import android.net.Uri; import android.provider.BaseColumns; @@ -44,14 +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"; - - /** Columns that go into the backup. */ - public static final String[] BACKUP_COLUMNS = - { _ID, TRACKID, LATITUDE, LONGITUDE, ALTITUDE, BEARING, TIME, - ACCURACY, SPEED }; - - /** Types of the columns that go into the backup. */ - public static final byte[] BACKUP_COLUMN_TYPES = - { LONG_TYPE_ID, LONG_TYPE_ID, INT_TYPE_ID, INT_TYPE_ID, FLOAT_TYPE_ID, - FLOAT_TYPE_ID, LONG_TYPE_ID, FLOAT_TYPE_ID, FLOAT_TYPE_ID }; + public static final String SENSOR = "sensor"; } diff --git a/MyTracks/src/com/google/android/apps/mytracks/content/TracksColumns.java b/MyTracksLib/src/com/google/android/apps/mytracks/content/TracksColumns.java similarity index 75% rename from MyTracks/src/com/google/android/apps/mytracks/content/TracksColumns.java rename to MyTracksLib/src/com/google/android/apps/mytracks/content/TracksColumns.java index 5e22926c9..6fb122b89 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/content/TracksColumns.java +++ b/MyTracksLib/src/com/google/android/apps/mytracks/content/TracksColumns.java @@ -15,8 +15,6 @@ */ package com.google.android.apps.mytracks.content; -import static com.google.android.apps.mytracks.content.ContentTypeIds.*; - import android.net.Uri; import android.provider.BaseColumns; @@ -61,16 +59,4 @@ public interface TracksColumns extends BaseColumns { public static final String MINLON = "minlon"; public static final String MAXLON = "maxlon"; public static final String MAPID = "mapid"; - - public static final String[] BACKUP_COLUMNS = { - _ID, NAME, DESCRIPTION, CATEGORY, STARTID, STOPID, STARTTIME, STOPTIME, - NUMPOINTS, TOTALDISTANCE, TOTALTIME, MOVINGTIME, AVGSPEED, AVGMOVINGSPEED, - MAXSPEED, MINELEVATION, MAXELEVATION, ELEVATIONGAIN, MINGRADE, MAXGRADE, - MINLAT, MAXLAT, MINLON, MAXLON }; - public static final byte[] BACKUP_COLUMN_TYPES = { - LONG_TYPE_ID, STRING_TYPE_ID, STRING_TYPE_ID, STRING_TYPE_ID, - LONG_TYPE_ID, LONG_TYPE_ID, LONG_TYPE_ID, LONG_TYPE_ID, INT_TYPE_ID, - FLOAT_TYPE_ID, LONG_TYPE_ID, LONG_TYPE_ID, FLOAT_TYPE_ID, FLOAT_TYPE_ID, - FLOAT_TYPE_ID, FLOAT_TYPE_ID, FLOAT_TYPE_ID, FLOAT_TYPE_ID, FLOAT_TYPE_ID, - FLOAT_TYPE_ID, INT_TYPE_ID, INT_TYPE_ID, INT_TYPE_ID, INT_TYPE_ID }; } diff --git a/MyTracks/src/com/google/android/apps/mytracks/content/Waypoint.aidl b/MyTracksLib/src/com/google/android/apps/mytracks/content/Waypoint.aidl similarity index 100% rename from MyTracks/src/com/google/android/apps/mytracks/content/Waypoint.aidl rename to MyTracksLib/src/com/google/android/apps/mytracks/content/Waypoint.aidl diff --git a/MyTracks/src/com/google/android/apps/mytracks/content/Waypoint.java b/MyTracksLib/src/com/google/android/apps/mytracks/content/Waypoint.java similarity index 95% rename from MyTracks/src/com/google/android/apps/mytracks/content/Waypoint.java rename to MyTracksLib/src/com/google/android/apps/mytracks/content/Waypoint.java index 4507a4276..a2649a501 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/content/Waypoint.java +++ b/MyTracksLib/src/com/google/android/apps/mytracks/content/Waypoint.java @@ -49,7 +49,10 @@ public final class Waypoint implements Parcelable { waypoint.type = source.readInt(); waypoint.startId = source.readLong(); waypoint.stopId = source.readLong(); - waypoint.stats = source.readParcelable(classLoader); + byte hasStats = source.readByte(); + if (hasStats > 0) { + waypoint.stats = source.readParcelable(classLoader); + } byte hasLocation = source.readByte(); if (hasLocation > 0) { waypoint.location = source.readParcelable(classLoader); @@ -100,7 +103,10 @@ public final class Waypoint implements Parcelable { dest.writeInt(type); dest.writeLong(startId); dest.writeLong(stopId); - dest.writeParcelable(stats, 0); + dest.writeByte(stats == null ? (byte) 0 : (byte) 1); + if (stats != null) { + dest.writeParcelable(stats, 0); + } dest.writeByte(location == null ? (byte) 0 : (byte) 1); if (location != null) { dest.writeParcelable(location, 0); diff --git a/MyTracks/src/com/google/android/apps/mytracks/content/WaypointsColumns.java b/MyTracksLib/src/com/google/android/apps/mytracks/content/WaypointsColumns.java similarity index 73% rename from MyTracks/src/com/google/android/apps/mytracks/content/WaypointsColumns.java rename to MyTracksLib/src/com/google/android/apps/mytracks/content/WaypointsColumns.java index 08c57c179..8184bd583 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/content/WaypointsColumns.java +++ b/MyTracksLib/src/com/google/android/apps/mytracks/content/WaypointsColumns.java @@ -15,8 +15,6 @@ */ package com.google.android.apps.mytracks.content; -import static com.google.android.apps.mytracks.content.ContentTypeIds.*; - import android.net.Uri; import android.provider.BaseColumns; @@ -68,19 +66,4 @@ public interface WaypointsColumns extends BaseColumns { public static final String ELEVATIONGAIN = "elevationgain"; public static final String MINGRADE = "mingrade"; public static final String MAXGRADE = "maxgrade"; - - public static final String[] BACKUP_COLUMNS = { - _ID, TRACKID, NAME, DESCRIPTION, CATEGORY, ICON, TYPE, LENGTH, DURATION, - STARTTIME, STARTID, STOPID, LATITUDE, LONGITUDE, ALTITUDE, BEARING, TIME, - ACCURACY, SPEED, TOTALDISTANCE, TOTALTIME, MOVINGTIME, AVGSPEED, - AVGMOVINGSPEED, MAXSPEED, MINELEVATION, MAXELEVATION, ELEVATIONGAIN, - MINGRADE, MAXGRADE }; - public static final byte[] BACKUP_COLUMN_TYPES = { - LONG_TYPE_ID, LONG_TYPE_ID, STRING_TYPE_ID, STRING_TYPE_ID, - STRING_TYPE_ID, STRING_TYPE_ID, INT_TYPE_ID, FLOAT_TYPE_ID, LONG_TYPE_ID, - LONG_TYPE_ID, LONG_TYPE_ID, LONG_TYPE_ID, INT_TYPE_ID, INT_TYPE_ID, - FLOAT_TYPE_ID, FLOAT_TYPE_ID, LONG_TYPE_ID, FLOAT_TYPE_ID, FLOAT_TYPE_ID, - FLOAT_TYPE_ID, LONG_TYPE_ID, LONG_TYPE_ID, FLOAT_TYPE_ID, FLOAT_TYPE_ID, - FLOAT_TYPE_ID, FLOAT_TYPE_ID, FLOAT_TYPE_ID, FLOAT_TYPE_ID, FLOAT_TYPE_ID, - FLOAT_TYPE_ID }; } diff --git a/MyTracksLib/src/com/google/android/apps/mytracks/lib/MyTracksLibConstants.java b/MyTracksLib/src/com/google/android/apps/mytracks/lib/MyTracksLibConstants.java new file mode 100644 index 000000000..739cf79f1 --- /dev/null +++ b/MyTracksLib/src/com/google/android/apps/mytracks/lib/MyTracksLibConstants.java @@ -0,0 +1,28 @@ +/* + * 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.lib; + +/** + * Constants for the My Tracks common library. + * These constants should ideally not be used by third-party applications. + * + * @author Rodrigo Damazio + */ +public class MyTracksLibConstants { + public static final String TAG = "MyTracksLib"; + + private MyTracksLibConstants() {} +} diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/ITrackRecordingService.aidl b/MyTracksLib/src/com/google/android/apps/mytracks/services/ITrackRecordingService.aidl similarity index 86% rename from MyTracks/src/com/google/android/apps/mytracks/services/ITrackRecordingService.aidl rename to MyTracksLib/src/com/google/android/apps/mytracks/services/ITrackRecordingService.aidl index d0e492563..39484aef0 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/services/ITrackRecordingService.aidl +++ b/MyTracksLib/src/com/google/android/apps/mytracks/services/ITrackRecordingService.aidl @@ -84,4 +84,18 @@ interface ITrackRecordingService { * Deletes all the stored tracks. */ void deleteAllTracks(); + + /** + * The current sensor data. + * The data is returned as a byte array which is a binary version of a + * Sensor.SensorDataSet object. + * @return the current sensor data or null if there is none. + */ + byte[] getSensorData(); + + /** + * The current state of the sensor manager. + * The value is the value of a Sensor.SensorState enum. + */ + int getSensorState(); } diff --git a/MyTracks/src/com/google/android/apps/mytracks/stats/ExtremityMonitor.java b/MyTracksLib/src/com/google/android/apps/mytracks/stats/ExtremityMonitor.java similarity index 95% rename from MyTracks/src/com/google/android/apps/mytracks/stats/ExtremityMonitor.java rename to MyTracksLib/src/com/google/android/apps/mytracks/stats/ExtremityMonitor.java index fafcf9ae2..bb65ee76d 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/stats/ExtremityMonitor.java +++ b/MyTracksLib/src/com/google/android/apps/mytracks/stats/ExtremityMonitor.java @@ -103,6 +103,11 @@ public class ExtremityMonitor { public void setMax(double max) { this.max = max; } + + public boolean hasData() { + return min != Double.POSITIVE_INFINITY + && max != Double.NEGATIVE_INFINITY; + } @Override public String toString() { diff --git a/MyTracks/src/com/google/android/apps/mytracks/stats/TripStatistics.aidl b/MyTracksLib/src/com/google/android/apps/mytracks/stats/TripStatistics.aidl similarity index 100% rename from MyTracks/src/com/google/android/apps/mytracks/stats/TripStatistics.aidl rename to MyTracksLib/src/com/google/android/apps/mytracks/stats/TripStatistics.aidl diff --git a/MyTracks/src/com/google/android/apps/mytracks/stats/TripStatistics.java b/MyTracksLib/src/com/google/android/apps/mytracks/stats/TripStatistics.java similarity index 99% rename from MyTracks/src/com/google/android/apps/mytracks/stats/TripStatistics.java rename to MyTracksLib/src/com/google/android/apps/mytracks/stats/TripStatistics.java index 286158ead..e0a7dac76 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/stats/TripStatistics.java +++ b/MyTracksLib/src/com/google/android/apps/mytracks/stats/TripStatistics.java @@ -26,7 +26,7 @@ import android.os.Parcelable; * * @author Rodrigo Damazio */ -public class TripStatistics implements Parcelable { +public class TripStatistics implements Parcelable { /** * The start time for the trip. This is system time which might not match gps diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/io/GpxImporterTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/io/GpxImporterTest.java index 0c293b7d0..b819f5c2a 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/io/GpxImporterTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/io/GpxImporterTest.java @@ -19,9 +19,9 @@ import static com.google.android.testing.mocking.AndroidMock.eq; import static com.google.android.testing.mocking.AndroidMock.expect; import com.google.android.apps.mytracks.content.MyTracksProviderUtils; -import com.google.android.apps.mytracks.content.MyTracksProviderUtils.Factory; import com.google.android.apps.mytracks.content.Track; import com.google.android.apps.mytracks.content.TracksColumns; +import com.google.android.apps.mytracks.content.MyTracksProviderUtils.Factory; import com.google.android.apps.mytracks.testing.TestingProviderUtilsFactory; import com.google.android.testing.mocking.AndroidMock; import com.google.android.testing.mocking.UsesMocks; @@ -77,7 +77,7 @@ public class GpxImporterTest extends AndroidTestCase { DATE_FORMAT2.setTimeZone(utc); } - // TODO use real files from different sources with more track points + // TODO: use real files from different sources with more track points. private static final String VALID_TEST_GPX = "" + " trackParam = new Capture(); @@ -171,7 +177,7 @@ public class GpxImporterTest extends AndroidTestCase { } /** - * Test with invalid location - track should be deleted + * Test with invalid location - track should be deleted. */ public void testImportLocationFailure() throws ParserConfigurationException, SAXException, IOException { @@ -179,7 +185,7 @@ public class GpxImporterTest extends AndroidTestCase { } /** - * Test with invalid time - track should be deleted + * Test with invalid time - track should be deleted. */ public void testImportTimeFailure() throws ParserConfigurationException, SAXException, IOException { @@ -187,13 +193,37 @@ public class GpxImporterTest extends AndroidTestCase { } /** - * Test with invalid xml - track should be deleted + * Test with invalid xml - track should be deleted. */ public void testImportXMLFailure() throws ParserConfigurationException, SAXException, IOException { testInvalidXML(INVALID_XML_TEST_GPX); } + + /** + * Test with invalid altitude - track should be deleted. + */ + public void testImportInvalidAltitude() throws ParserConfigurationException, + SAXException, IOException { + testInvalidXML(INVALID_ALTITUDE_TEST_GPX); + } + /** + * Test with invalid latitude - track should be deleted. + */ + public void testImportInvalidLatitude() throws ParserConfigurationException, + SAXException, IOException { + testInvalidXML(INVALID_LATITUDE_TEST_GPX); + } + + /** + * Test with invalid longitude - track should be deleted. + */ + public void testImportInvalidLongitude() throws ParserConfigurationException, + SAXException, IOException { + testInvalidXML(INVALID_LONGITUDE_TEST_GPX); + } + private void testInvalidXML(String xml) throws ParserConfigurationException, IOException { expect(providerUtils.insertTrack((Track) AndroidMock.anyObject())) diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/io/TcxTrackWriterTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/io/TcxTrackWriterTest.java index ec132e0db..897df2792 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/io/TcxTrackWriterTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/io/TcxTrackWriterTest.java @@ -1,7 +1,8 @@ // Copyright 2010 Google Inc. All Rights Reserved. package com.google.android.apps.mytracks.io; -import android.location.Location; +import com.google.android.apps.mytracks.content.MyTracksLocation; +import com.google.android.apps.mytracks.content.Sensor; import org.w3c.dom.Document; import org.w3c.dom.Element; @@ -38,11 +39,11 @@ public class TcxTrackWriterTest extends TrackFormatWriterTest { /** * Asserts that the given tags describe the given points, in the same order. */ - private void assertTagsMatchPoints(List tags, Location... locs) { + private void assertTagsMatchPoints(List tags, MyTracksLocation... locs) { assertEquals(locs.length, tags.size()); for (int i = 0; i < locs.length; i++) { Element tag = tags.get(i); - Location loc = locs[i]; + MyTracksLocation loc = locs[i]; assertTagMatchesLocation(tag, loc); } @@ -51,7 +52,7 @@ public class TcxTrackWriterTest extends TrackFormatWriterTest { /** * Asserts that the given tag describes the given location. */ - private void assertTagMatchesLocation(Element tag, Location loc) { + private void assertTagMatchesLocation(Element tag, MyTracksLocation loc) { Element posTag = getChildElement(tag, "Position"); assertEquals(Double.toString(loc.getLatitude()), getChildTextValue(posTag, "LatitudeDegrees")); @@ -62,5 +63,16 @@ public class TcxTrackWriterTest extends TrackFormatWriterTest { getChildTextValue(tag, "Time")); assertEquals(Double.toString(loc.getAltitude()), getChildTextValue(tag, "AltitudeMeters")); + assertTrue(loc.getSensorDataSet() != null); + Sensor.SensorDataSet sds = loc.getSensorDataSet(); + + List bpm = getChildElements(tag, "HeartRateBpm", 1); + assertEquals(Integer.toString(sds.getHeartRate().getValue()), + getChildTextValue(bpm.get(0), "Value")); + + List ext = getChildElements(tag, "Extensions", 1); + List tpx = getChildElements(ext.get(0), "TPX", 1); + assertEquals(Integer.toString(sds.getPower().getValue()), + getChildTextValue(tpx.get(0), "Watts")); } } diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/io/TrackFormatWriterTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/io/TrackFormatWriterTest.java index 8e4bd0e44..89d38ad55 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/io/TrackFormatWriterTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/io/TrackFormatWriterTest.java @@ -1,10 +1,11 @@ // Copyright 2010 Google Inc. All Rights Reserved. package com.google.android.apps.mytracks.io; +import com.google.android.apps.mytracks.content.MyTracksLocation; +import com.google.android.apps.mytracks.content.Sensor; import com.google.android.apps.mytracks.content.Track; import com.google.android.apps.mytracks.content.Waypoint; -import android.location.Location; import android.test.AndroidTestCase; import org.w3c.dom.Document; @@ -46,7 +47,7 @@ public abstract class TrackFormatWriterTest extends AndroidTestCase { private static final int BUFFER_SIZE = 10240; protected static final long TRACK_ID = 12345L; protected Track track; - protected Location location1, location2, location3, location4; + protected MyTracksLocation location1, location2, location3, location4; protected Waypoint wp1, wp2; @Override @@ -58,10 +59,10 @@ public abstract class TrackFormatWriterTest extends AndroidTestCase { track.setName(TRACK_NAME); track.setDescription(TRACK_DESCRIPTION); - location1 = new Location("mock"); - location2 = new Location("mock"); - location3 = new Location("mock"); - location4 = new Location("mock"); + location1 = new MyTracksLocation("mock"); + location2 = new MyTracksLocation("mock"); + location3 = new MyTracksLocation("mock"); + location4 = new MyTracksLocation("mock"); populateLocations(location1, location2, location3, location4); wp1 = new Waypoint(); @@ -77,13 +78,24 @@ public abstract class TrackFormatWriterTest extends AndroidTestCase { /** * Populates the given locations with coordinates and time. */ - protected void populateLocations(Location... locs) { + protected void populateLocations(MyTracksLocation... locs) { for (int i = 0; i < locs.length; i++) { - Location loc = locs[i]; + MyTracksLocation loc = locs[i]; loc.setAltitude(i * 1000); loc.setLatitude(i); loc.setLongitude(-i); loc.setTime(10000000 + i * 1000); + Sensor.SensorData.Builder hr = Sensor.SensorData.newBuilder() + .setValue(100 + i) + .setState(Sensor.SensorState.SENDING); + Sensor.SensorData.Builder power = Sensor.SensorData.newBuilder() + .setValue(400 + i) + .setState(Sensor.SensorState.SENDING); + Sensor.SensorDataSet sds = + Sensor.SensorDataSet.newBuilder().setHeartRate(hr.build()) + .setPower(power) + .build(); + loc.setSensorData(sds); } } @@ -139,7 +151,7 @@ public abstract class TrackFormatWriterTest extends AndroidTestCase { } /** - * Returns all child elements of a given parent which gave the given name. + * Returns all child elements of a given parent which have the given name. * * @param parent the parent to get children from * @param elementName the element name to look for diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/services/SignalStrengthTaskFactoryTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/services/SignalStrengthTaskFactoryTest.java deleted file mode 100644 index e37684f64..000000000 --- a/MyTracksTest/src/com/google/android/apps/mytracks/services/SignalStrengthTaskFactoryTest.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright 2010 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ -package com.google.android.apps.mytracks.services; - -import com.google.android.apps.mytracks.util.ApiFeatures; - -import android.test.AndroidTestCase; - -/** - * Tests for {@link SignalStrengthTaskFactoryTest}. - * These tests require Eclair+ (API level 7) to run. - * - * @author Rodrigo Damazio - */ -public class SignalStrengthTaskFactoryTest extends AndroidTestCase { - /** - * Mock version of the {@link ApiFeatures} class. - */ - private static class MockApiFeatures extends ApiFeatures { - private boolean hasModernStrength; - - public void setHasModernSignalStrength(boolean hasModernStrength) { - this.hasModernStrength = hasModernStrength; - } - - @Override - public boolean hasModernSignalStrength() { - return hasModernStrength; - } - } - - private MockApiFeatures apiFeatures; - - @Override - protected void setUp() throws Exception { - super.setUp(); - - apiFeatures = new MockApiFeatures(); - } - - public void testCreate() { - apiFeatures.setHasModernSignalStrength(true); - SignalStrengthTaskFactory factory = - new SignalStrengthTaskFactory(apiFeatures); - - PeriodicTask task = factory.create(getContext()); - assertTrue(task.getClass().getName(), - task instanceof SignalStrengthTaskModern); - } - - public void testCreate_legacy() { - apiFeatures.setHasModernSignalStrength(false); - SignalStrengthTaskFactory factory = - new SignalStrengthTaskFactory(apiFeatures); - - PeriodicTask task = factory.create(getContext()); - assertTrue(task.getClass().getName(), task instanceof SignalStrengthTask); - } -} diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/services/TrackRecordingServiceTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/services/TrackRecordingServiceTest.java index 8b8e06669..f54f6d6c1 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/services/TrackRecordingServiceTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/services/TrackRecordingServiceTest.java @@ -26,10 +26,12 @@ import com.google.android.apps.mytracks.stats.TripStatistics; import com.google.android.apps.mytracks.util.ApiFeatures; import com.google.android.maps.mytracks.R; +import android.content.BroadcastReceiver; import android.content.ContentResolver; import android.content.Context; import android.content.ContextWrapper; import android.content.Intent; +import android.content.IntentFilter; import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; import android.location.Location; @@ -39,7 +41,9 @@ import android.test.ServiceTestCase; import android.test.mock.MockContentResolver; import android.test.suitebuilder.annotation.MediumTest; import android.test.suitebuilder.annotation.SmallTest; +import android.util.Log; +import java.util.ArrayList; import java.util.List; /** @@ -130,9 +134,9 @@ public class TrackRecordingServiceTest @Override protected void setUp() throws Exception { super.setUp(); - + ApiFeatures.injectInstance(new MockApiFeatures()); - + MockContentResolver mockContentResolver = new MockContentResolver(); RenamingDelegatingContext targetContext = new RenamingDelegatingContext( getContext(), getContext(), "test."); @@ -141,14 +145,14 @@ public class TrackRecordingServiceTest provider.attachInfo(context, null); mockContentResolver.addProvider(MyTracksProviderUtils.AUTHORITY, provider); setContext(context); - + providerUtils = MyTracksProviderUtils.Factory.get(context); - + sharedPreferences = context.getSharedPreferences( MyTracksSettings.SETTINGS_NAME, 0); // Let's use default values. sharedPreferences.edit().clear().commit(); - + // Disable auto resume by default. updateAutoResumePrefs(0, -1); // No recording track. @@ -168,7 +172,7 @@ public class TrackRecordingServiceTest IBinder service = bindService(createStartIntent()); assertNotNull(service); } - + @MediumTest public void testResumeAfterReboot_shouldResume() throws Exception { // Insert a dummy track and mark it as recording track. @@ -214,7 +218,7 @@ public class TrackRecordingServiceTest startIntent.putExtra(RESUME_TRACK_EXTRA_NAME, true); startService(startIntent); assertNotNull(getService()); - + assertTrue(getService().isRecording()); } @@ -237,7 +241,7 @@ public class TrackRecordingServiceTest ITrackRecordingService service = bindAndGetService(createStartIntent()); assertEquals(-1, service.getRecordingTrackId()); } - + @MediumTest public void testResumeAfterReboot_expiredTrack() throws Exception { // Insert a dummy track last updated 20 min ago. @@ -251,7 +255,7 @@ public class TrackRecordingServiceTest startIntent.putExtra(RESUME_TRACK_EXTRA_NAME, true); startService(startIntent); assertNotNull(getService()); - + // We don't expect to resume the previous track, because it has expired. assertFalse(getService().isRecording()); ITrackRecordingService service = bindAndGetService(createStartIntent()); @@ -284,17 +288,17 @@ public class TrackRecordingServiceTest public void testRecording_noTracks() throws Exception { List tracks = providerUtils.getAllTracks(); assertTrue(tracks.isEmpty()); - + ITrackRecordingService service = bindAndGetService(createStartIntent()); // Test if we start in no-recording mode by default. assertFalse(service.isRecording()); assertEquals(-1, service.getRecordingTrackId()); } - + @MediumTest public void testRecording_oldTracks() throws Exception { createDummyTrack(123, -1, false); - + ITrackRecordingService service = bindAndGetService(createStartIntent()); assertFalse(service.isRecording()); assertEquals(-1, service.getRecordingTrackId()); @@ -304,22 +308,71 @@ public class TrackRecordingServiceTest public void testRecording_orphanedRecordingTrack() throws Exception { // Just set recording track to a bogus value. setRecordingTrack(256); - + // Make sure that the service will not start recording and will clear // the bogus track. ITrackRecordingService service = bindAndGetService(createStartIntent()); assertFalse(service.isRecording()); assertEquals(-1, service.getRecordingTrackId()); } - + + /** + * Synchronous/waitable broadcast receiver to be used in testing. + */ + private class BlockingBroadcastReceiver extends BroadcastReceiver { + private static final long MAX_WAIT_TIME_MS = 10000; + private List receivedIntents = new ArrayList(); + + public List getReceivedIntents() { + return receivedIntents; + } + + @Override + public void onReceive(Context context, Intent intent) { + Log.d("MyTracksTest", "Got broadcast: " + intent); + synchronized (receivedIntents) { + receivedIntents.add(intent); + receivedIntents.notifyAll(); + } + } + + public boolean waitUntilReceived(int receiveCount) { + long deadline = System.currentTimeMillis() + MAX_WAIT_TIME_MS; + synchronized (receivedIntents) { + while (receivedIntents.size() < receiveCount) { + try { + // Wait releases synchronized lock until it returns + receivedIntents.wait(500); + } catch (InterruptedException e) { + // Do nothing + } + + if (System.currentTimeMillis() > deadline) { + return false; + } + } + } + + return true; + } + } + @MediumTest public void testStartNewTrack_noRecording() throws Exception { + // NOTICE: due to the way Android permissions work, if this fails, + // uninstall the test apk then retry - the test must be installed *after* + // My Tracks (go figure). + // Reference: http://code.google.com/p/android/issues/detail?id=5521 + BlockingBroadcastReceiver startReceiver = new BlockingBroadcastReceiver(); + String startAction = context.getString(R.string.track_started_broadcast_action); + context.registerReceiver(startReceiver, new IntentFilter(startAction)); + List tracks = providerUtils.getAllTracks(); assertTrue(tracks.isEmpty()); - + ITrackRecordingService service = bindAndGetService(createStartIntent()); assertFalse(service.isRecording()); - + long id = service.startNewTrack(); assertTrue(id >= 0); assertTrue(service.isRecording()); @@ -329,15 +382,26 @@ public class TrackRecordingServiceTest assertEquals(id, sharedPreferences.getLong( context.getString(R.string.recording_track_key), -1)); assertEquals(id, service.getRecordingTrackId()); + + // Verify that the start broadcast was received. + assertTrue(startReceiver.waitUntilReceived(1)); + List receivedIntents = startReceiver.getReceivedIntents(); + assertEquals(1, receivedIntents.size()); + Intent broadcastIntent = receivedIntents.get(0); + assertEquals(startAction, broadcastIntent.getAction()); + assertEquals(id, broadcastIntent.getLongExtra( + context.getString(R.string.track_id_broadcast_extra), -1)); + + context.unregisterReceiver(startReceiver); } - + @MediumTest public void testStartNewTrack_alreadyRecording() throws Exception { createDummyTrack(123, -1, true); - + ITrackRecordingService service = bindAndGetService(createStartIntent()); assertTrue(service.isRecording()); - + try { service.startNewTrack(); fail("Expecting IllegalStateException"); @@ -347,12 +411,17 @@ public class TrackRecordingServiceTest assertEquals(123, sharedPreferences.getLong( context.getString(R.string.recording_track_key), 0)); assertEquals(123, service.getRecordingTrackId()); - } - + } + @MediumTest public void testEndCurrentTrack_alreadyRecording() throws Exception { + // See comment above if this fails randomly. + BlockingBroadcastReceiver stopReceiver = new BlockingBroadcastReceiver(); + String stopAction = context.getString(R.string.track_stopped_broadcast_action); + context.registerReceiver(stopReceiver, new IntentFilter(stopAction)); + createDummyTrack(123, -1, true); - + ITrackRecordingService service = bindAndGetService(createStartIntent()); assertTrue(service.isRecording()); @@ -362,8 +431,19 @@ public class TrackRecordingServiceTest assertEquals(-1, sharedPreferences.getLong( context.getString(R.string.recording_track_key), 0)); assertEquals(-1, service.getRecordingTrackId()); + + // Verify that the stop broadcast was received. + assertTrue(stopReceiver.waitUntilReceived(1)); + List receivedIntents = stopReceiver.getReceivedIntents(); + assertEquals(1, receivedIntents.size()); + Intent broadcastIntent = receivedIntents.get(0); + assertEquals(stopAction, broadcastIntent.getAction()); + assertEquals(123, broadcastIntent.getLongExtra( + context.getString(R.string.track_id_broadcast_extra), -1)); + + context.unregisterReceiver(stopReceiver); } - + @MediumTest public void testEndCurrentTrack_noRecording() throws Exception { ITrackRecordingService service = bindAndGetService(createStartIntent()); @@ -561,16 +641,6 @@ public class TrackRecordingServiceTest functionalTest(R.string.min_recording_distance_key, 2); } - @MediumTest - public void testWithProperties_noSignalSamplingFreq() throws Exception { - functionalTest(R.string.signal_sampling_frequency_key, (Object) null); - } - - @MediumTest - public void testWithProperties_defaultSignalSamplingFreq() throws Exception { - functionalTest(R.string.signal_sampling_frequency_key, 1); - } - @MediumTest public void testWithProperties_noSplitFreq() throws Exception { functionalTest(R.string.split_frequency_key, (Object) null); @@ -622,6 +692,17 @@ public class TrackRecordingServiceTest functionalTest(R.string.min_required_accuracy_key, 500); } + @MediumTest + public void testWithProperties_noSensorType() throws Exception { + functionalTest(R.string.sensor_type_key, (Object) null); + } + + @MediumTest + public void testWithProperties_zephyrSensorType() throws Exception { + functionalTest(R.string.sensor_type_key, + context.getString(R.string.zephyr_sensor_type)); + } + private ITrackRecordingService bindAndGetService(Intent intent) { ITrackRecordingService service = ITrackRecordingService.Stub.asInterface( bindService(intent)); diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/services/sensors/SensorManagerFactoryTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/services/sensors/SensorManagerFactoryTest.java new file mode 100644 index 000000000..4dcc0b9d9 --- /dev/null +++ b/MyTracksTest/src/com/google/android/apps/mytracks/services/sensors/SensorManagerFactoryTest.java @@ -0,0 +1,39 @@ +package com.google.android.apps.mytracks.services.sensors; + +import com.google.android.apps.mytracks.MyTracksSettings; +import com.google.android.maps.mytracks.R; + +import android.content.SharedPreferences; +import android.test.AndroidTestCase; +import android.test.suitebuilder.annotation.SmallTest; + +public class SensorManagerFactoryTest extends AndroidTestCase { + + private SharedPreferences sharedPreferences; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + sharedPreferences = getContext().getSharedPreferences( + MyTracksSettings.SETTINGS_NAME, 0); + // Let's use default values. + sharedPreferences.edit().clear().commit(); + } + + @SmallTest + public void testDefaultSettings() throws Exception { + assertNull(SensorManagerFactory.getSensorManager(getContext())); + } + + @SmallTest + public void testCreateZephyr() throws Exception { + sharedPreferences.edit() + .putString(getContext().getString(R.string.sensor_type_key), + getContext().getString(R.string.zephyr_sensor_type)) + .commit(); + SensorManager sm = SensorManagerFactory.getSensorManager(getContext()); + assertNotNull(sm); + assertTrue(sm instanceof ZephyrSensorManager); + } +} diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/services/sensors/ZephyrMessageParserTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/services/sensors/ZephyrMessageParserTest.java new file mode 100644 index 000000000..6cebf3692 --- /dev/null +++ b/MyTracksTest/src/com/google/android/apps/mytracks/services/sensors/ZephyrMessageParserTest.java @@ -0,0 +1,36 @@ +package com.google.android.apps.mytracks.services.sensors; + +import com.google.android.apps.mytracks.content.Sensor; + +import junit.framework.TestCase; + +public class ZephyrMessageParserTest extends TestCase { + + ZephyrMessageParser parser = new ZephyrMessageParser(); + + public void testIsValid() { + byte[] buf = new byte[60]; + assertFalse(parser.isValid(buf)); + buf[0] = 0x02; + assertFalse(parser.isValid(buf)); + buf[59] = 0x03; + assertTrue(parser.isValid(buf)); + } + + public void testParseBuffer() { + byte[] buf = new byte[60]; + buf[12] = 50; + Sensor.SensorDataSet sds = parser.parseBuffer(buf); + assertTrue(sds.hasHeartRate()); + assertTrue(sds.getHeartRate().getState() == Sensor.SensorState.SENDING); + assertEquals(50, sds.getHeartRate().getValue()); + } + + public void testFindNextAlignment() { + byte[] buf = new byte[60]; + assertEquals(-1, parser.findNextAlignment(buf)); + buf[10] = 0x03; + buf[11] = 0x02; + assertEquals(10, parser.findNextAlignment(buf)); + } +} diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/util/ApiFeaturesTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/util/ApiFeaturesTest.java index f47ab6dbb..a0db9b8da 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/util/ApiFeaturesTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/util/ApiFeaturesTest.java @@ -65,17 +65,6 @@ public class ApiFeaturesTest extends TestCase { } } - public void testHasModernSignalStrength() { - for (int i = 3; i <= 6; i++) { - features.setApiLevel(i); - assertFalse(features.hasModernSignalStrength()); - } - features.setApiLevel(7); - assertTrue(features.hasModernSignalStrength()); - features.setApiLevel(8); - assertTrue(features.hasModernSignalStrength()); - } - public void testGetApiPlatformAdapter() { assertNotNull(features.getApiPlatformAdapter()); } diff --git a/SignalStrengthSampler/.classpath b/SignalStrengthSampler/.classpath new file mode 100644 index 000000000..57972d993 --- /dev/null +++ b/SignalStrengthSampler/.classpath @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/SignalStrengthSampler/.project b/SignalStrengthSampler/.project new file mode 100644 index 000000000..80fb1802d --- /dev/null +++ b/SignalStrengthSampler/.project @@ -0,0 +1,40 @@ + + + SignalStrengthSampler + + + + + + com.android.ide.eclipse.adt.ResourceManagerBuilder + + + + + com.android.ide.eclipse.adt.PreCompilerBuilder + + + + + org.eclipse.jdt.core.javabuilder + + + + + com.android.ide.eclipse.adt.ApkBuilder + + + + + + com.android.ide.eclipse.adt.AndroidNature + org.eclipse.jdt.core.javanature + + + + MyTracksLib_src + 2 + _android_MyTracksLib_e2802071/src + + + diff --git a/SignalStrengthSampler/.settings/org.eclipse.jdt.core.prefs b/SignalStrengthSampler/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 000000000..77576ec3e --- /dev/null +++ b/SignalStrengthSampler/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,12 @@ +#Tue Nov 23 23:09:24 PST 2010 +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 +org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve +org.eclipse.jdt.core.compiler.compliance=1.6 +org.eclipse.jdt.core.compiler.debug.lineNumber=generate +org.eclipse.jdt.core.compiler.debug.localVariable=generate +org.eclipse.jdt.core.compiler.debug.sourceFile=generate +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.source=1.6 diff --git a/SignalStrengthSampler/AndroidManifest.xml b/SignalStrengthSampler/AndroidManifest.xml new file mode 100644 index 000000000..03d216a34 --- /dev/null +++ b/SignalStrengthSampler/AndroidManifest.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/SignalStrengthSampler/default.properties b/SignalStrengthSampler/default.properties new file mode 100644 index 000000000..45aae789b --- /dev/null +++ b/SignalStrengthSampler/default.properties @@ -0,0 +1,12 @@ +# This file is automatically generated by Android Tools. +# Do not modify this file -- YOUR CHANGES WILL BE ERASED! +# +# This file must be checked in Version Control Systems. +# +# To customize properties used by the Ant build system use, +# "build.properties", and override values to adapt the script to your +# project structure. + +# Project target. +target=Google Inc.:Google APIs:8 +android.library.reference.1=../MyTracksLib/ diff --git a/SignalStrengthSampler/proguard.cfg b/SignalStrengthSampler/proguard.cfg new file mode 100644 index 000000000..8ad7d3352 --- /dev/null +++ b/SignalStrengthSampler/proguard.cfg @@ -0,0 +1,34 @@ +-optimizationpasses 5 +-dontusemixedcaseclassnames +-dontskipnonpubliclibraryclasses +-dontpreverify +-verbose +-optimizations !code/simplification/arithmetic,!field/*,!class/merging/* + +-keep public class * extends android.app.Activity +-keep public class * extends android.app.Application +-keep public class * extends android.app.Service +-keep public class * extends android.content.BroadcastReceiver +-keep public class * extends android.content.ContentProvider +-keep public class com.android.vending.licensing.ILicensingService + +-keepclasseswithmembernames class * { + native ; +} + +-keepclasseswithmembernames class * { + public (android.content.Context, android.util.AttributeSet); +} + +-keepclasseswithmembernames class * { + public (android.content.Context, android.util.AttributeSet, int); +} + +-keepclassmembers enum * { + public static **[] values(); + public static ** valueOf(java.lang.String); +} + +-keep class * implements android.os.Parcelable { + public static final android.os.Parcelable$Creator *; +} diff --git a/SignalStrengthSampler/res/drawable/icon.png b/SignalStrengthSampler/res/drawable/icon.png new file mode 100644 index 000000000..a07c69fa5 Binary files /dev/null and b/SignalStrengthSampler/res/drawable/icon.png differ diff --git a/SignalStrengthSampler/res/values/arrays.xml b/SignalStrengthSampler/res/values/arrays.xml new file mode 100644 index 000000000..a37bc949d --- /dev/null +++ b/SignalStrengthSampler/res/values/arrays.xml @@ -0,0 +1,41 @@ + + + + + + As often as possible + 1 min + 2 min + 5 min + 10 min + 15 min + 30 min + 60 min + + + + + -1 + 1 + 2 + 5 + 10 + 15 + 30 + 60 + + diff --git a/SignalStrengthSampler/res/values/strings.xml b/SignalStrengthSampler/res/values/strings.xml new file mode 100644 index 000000000..1549968fc --- /dev/null +++ b/SignalStrengthSampler/res/values/strings.xml @@ -0,0 +1,54 @@ + + + + Signal strength sampler for My Tracks + + Sampling settings + Automatic start + Automatically start inserting signal strength waypoints whenever My Tracks starts recording a track. + You will need to manually start signal sampling using the controls below. + Automatic stop + Automatically stop inserting signal strength waypoints whenever My Tracks stops recording a track. + You will need to manually stop signal sampling using the controls below. + Min. sampling period + Minimum time between two recordings of the phone\'s signal strength + + Sampling control + Start sampling + Starts sampling signal strength immediately, if My Tracks is recording. Sampling will stop once My Tracks stops recording. + Stop sampling + Stops sampling signal strength if sampling is currently active. + + Started sampling cell signal strength. + Stopped sampling cell signal strength. + - + + + Error Rate: %1$d + Signal to Noise Ratio: %1$d + Ec/lo: %1$.1f dB + GSM Strength: %1$d + CDMA Strength: %1$d dBm + EVDO Strength: %1$d dBm + + + autoStartSampling + autoStopSampling + signalSamplingFrequency + startSamplingControl + stopSamplingControl + diff --git a/SignalStrengthSampler/res/xml/preferences.xml b/SignalStrengthSampler/res/xml/preferences.xml new file mode 100644 index 000000000..ba4de39c2 --- /dev/null +++ b/SignalStrengthSampler/res/xml/preferences.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + diff --git a/SignalStrengthSampler/src/com/google/android/apps/mytracks/signalstrength/RecordingStateReceiver.java b/SignalStrengthSampler/src/com/google/android/apps/mytracks/signalstrength/RecordingStateReceiver.java new file mode 100644 index 000000000..1b57700ac --- /dev/null +++ b/SignalStrengthSampler/src/com/google/android/apps/mytracks/signalstrength/RecordingStateReceiver.java @@ -0,0 +1,71 @@ +/* + * 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.signalstrength; + +import static com.google.android.apps.mytracks.signalstrength.SignalStrengthConstants.*; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; +import android.util.Log; + +/** + * Broadcast listener which gets notified when we start or stop recording a track. + * + * @author Rodrigo Damazio + */ +public class RecordingStateReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context ctx, Intent intent) { + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(ctx); + + String action = intent.getAction(); + if (ctx.getString(R.string.track_started_broadcast_action).equals(action) || + ctx.getString(R.string.track_resumed_broadcast_action).equals(action)) { + boolean autoStart = preferences.getBoolean( + ctx.getString(R.string.settings_auto_start_key), false); + if (!autoStart) { + Log.d(TAG, "Not auto-starting signal sampling"); + return; + } + + startService(ctx); + } else if (ctx.getString(R.string.track_stopped_broadcast_action).equals(action) || + ctx.getString(R.string.track_paused_broadcast_action).equals(action)) { + boolean autoStop = preferences.getBoolean( + ctx.getString(R.string.settings_auto_stop_key), true); + if (!autoStop) { + Log.d(TAG, "Not auto-stopping signal sampling"); + return; + } + + stopService(ctx); + } else { + Log.e(TAG, "Unknown action received: " + action); + } + } + + // @VisibleForTesting + protected void stopService(Context ctx) { + SignalStrengthService.stopService(ctx); + } + + // @VisibleForTesting + protected void startService(Context ctx) { + SignalStrengthService.startService(ctx); + } +} diff --git a/SignalStrengthSampler/src/com/google/android/apps/mytracks/signalstrength/SignalStrengthConstants.java b/SignalStrengthSampler/src/com/google/android/apps/mytracks/signalstrength/SignalStrengthConstants.java new file mode 100644 index 000000000..f65918451 --- /dev/null +++ b/SignalStrengthSampler/src/com/google/android/apps/mytracks/signalstrength/SignalStrengthConstants.java @@ -0,0 +1,37 @@ +/* + * 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.signalstrength; + +import android.os.Build; + +/** + * Constants for the signal sampler. + * + * @author Rodrigo Damazio + */ +public class SignalStrengthConstants { + public static final String START_SAMPLING = + "com.google.android.apps.mytracks.signalstrength.START"; + public static final String STOP_SAMPLING = + "com.google.android.apps.mytracks.signalstrength.STOP"; + + public static final int ANDROID_API_LEVEL = Integer.parseInt( + Build.VERSION.SDK); + + public static final String TAG = "SignalStrengthSampler"; + + private SignalStrengthConstants() {} +} diff --git a/SignalStrengthSampler/src/com/google/android/apps/mytracks/signalstrength/SignalStrengthListener.java b/SignalStrengthSampler/src/com/google/android/apps/mytracks/signalstrength/SignalStrengthListener.java new file mode 100644 index 000000000..e65da532d --- /dev/null +++ b/SignalStrengthSampler/src/com/google/android/apps/mytracks/signalstrength/SignalStrengthListener.java @@ -0,0 +1,42 @@ +/* + * 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.signalstrength; + +/** + * Interface for the service that reads signal strength. + * + * @author Rodrigo Damazio + */ +public interface SignalStrengthListener { + /** + * Interface for getting notified about a new sampled signal strength. + */ + public interface SignalStrengthCallback { + void onSignalStrengthSampled( + String description, + String icon); + } + + /** + * Starts listening to signal strength. + */ + void register(); + + /** + * Stops listening to signal strength. + */ + void unregister(); +} diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/SignalStrengthTask.java b/SignalStrengthSampler/src/com/google/android/apps/mytracks/signalstrength/SignalStrengthListenerCupcake.java similarity index 68% rename from MyTracks/src/com/google/android/apps/mytracks/services/SignalStrengthTask.java rename to SignalStrengthSampler/src/com/google/android/apps/mytracks/signalstrength/SignalStrengthListenerCupcake.java index 3d6017f74..4fdb68a80 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/services/SignalStrengthTask.java +++ b/SignalStrengthSampler/src/com/google/android/apps/mytracks/signalstrength/SignalStrengthListenerCupcake.java @@ -13,14 +13,11 @@ * License for the specific language governing permissions and limitations under * the License. */ +package com.google.android.apps.mytracks.signalstrength; -package com.google.android.apps.mytracks.services; - -import com.google.android.apps.mytracks.MyTracksConstants; -import com.google.android.apps.mytracks.content.Waypoint; +import static com.google.android.apps.mytracks.signalstrength.SignalStrengthConstants.*; import android.content.Context; -import android.location.Location; import android.telephony.CellLocation; import android.telephony.NeighboringCellInfo; import android.telephony.PhoneStateListener; @@ -31,67 +28,49 @@ import java.util.List; /** * A class to monitor the network signal strength. + * + * TODO: i18n * * @author Sandor Dornbush */ -public class SignalStrengthTask extends PhoneStateListener - implements PeriodicTask { - +public class SignalStrengthListenerCupcake extends PhoneStateListener + implements SignalStrengthListener { private final Context context; + private final SignalStrengthCallback callback; + private TelephonyManager manager; private int signalStrength = -1; - public SignalStrengthTask(Context c) { - context = c; + public SignalStrengthListenerCupcake(Context context, SignalStrengthCallback callback) { + this.context = context; + this.callback = callback; } @Override - public void start() { - manager = - (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); + public void register() { + manager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); if (manager == null) { - Log.e(MyTracksConstants.TAG, "Cannot get telephony manager."); + Log.e(TAG, "Cannot get telephony manager."); } else { manager.listen(this, getListenEvents()); } } protected int getListenEvents() { - return PhoneStateListener.LISTEN_SIGNAL_STRENGTHS; + return PhoneStateListener.LISTEN_SIGNAL_STRENGTH; } @Override - public void onSignalStrengthChanged(int asu) { - this.signalStrength = asu; - Log.d(MyTracksConstants.TAG, "Signal Strength: " + signalStrength); + public void onSignalStrengthChanged(int signalStrength) { + Log.d(TAG, "Signal Strength: " + signalStrength); + this.signalStrength = signalStrength; + + notifySignalSampled(); } - @Override - public void run(TrackRecordingService service) { - Log.d(MyTracksConstants.TAG, "Adding signal marker"); - Location location = service.getLastLocation(); - if (manager == null) { - Log.d(MyTracksConstants.TAG, "Adding signal marker: marker null"); - return; - } - if (location == null) { - Log.d(MyTracksConstants.TAG, "Adding signal marker: location null"); - return; - } - Waypoint wpt = new Waypoint(); - wpt.setName("Signal Strength"); - wpt.setType(Waypoint.TYPE_WAYPOINT); - wpt.setTrackId(service.getRecordingTrackId()); + protected void notifySignalSampled() { int networkType = manager.getNetworkType(); - wpt.setIcon(getIcon(networkType)); - wpt.setLocation(location); - wpt.setDescription(getDescription()); - long waypointId = service.insertWaypointMarker(wpt); - if (waypointId >= 0) { - Log.d(MyTracksConstants.TAG, "Added signal marker"); - } else { - Log.e(MyTracksConstants.TAG, "Cannot insert waypoint marker?"); - } + callback.onSignalStrengthSampled(getDescription(), getIcon(networkType)); } /** @@ -138,7 +117,7 @@ public class SignalStrengthTask extends PhoneStateListener sb.append('\n'); List infos = manager.getNeighboringCellInfo(); - Log.i(MyTracksConstants.TAG, "Found " + infos.size() + " cells."); + Log.i(TAG, "Found " + infos.size() + " cells."); if (infos.size() > 0) { sb.append("Neighbors: "); for (NeighboringCellInfo info : infos) { @@ -178,7 +157,7 @@ public class SignalStrengthTask extends PhoneStateListener } @Override - public void shutdown() { + public void unregister() { if (manager != null) { manager.listen(this, PhoneStateListener.LISTEN_NONE); manager = null; @@ -192,4 +171,8 @@ public class SignalStrengthTask extends PhoneStateListener protected Context getContext() { return context; } + + public SignalStrengthCallback getSignalStrengthCallback() { + return callback; + } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/SignalStrengthTaskModern.java b/SignalStrengthSampler/src/com/google/android/apps/mytracks/signalstrength/SignalStrengthListenerEclair.java similarity index 91% rename from MyTracks/src/com/google/android/apps/mytracks/services/SignalStrengthTaskModern.java rename to SignalStrengthSampler/src/com/google/android/apps/mytracks/signalstrength/SignalStrengthListenerEclair.java index 12c6f74ee..f5f0e2d00 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/services/SignalStrengthTaskModern.java +++ b/SignalStrengthSampler/src/com/google/android/apps/mytracks/signalstrength/SignalStrengthListenerEclair.java @@ -14,10 +14,9 @@ * the License. */ -package com.google.android.apps.mytracks.services; +package com.google.android.apps.mytracks.signalstrength; -import com.google.android.apps.mytracks.MyTracksConstants; -import com.google.android.maps.mytracks.R; +import static com.google.android.apps.mytracks.signalstrength.SignalStrengthConstants.*; import android.content.Context; import android.telephony.PhoneStateListener; @@ -27,15 +26,17 @@ import android.util.Log; /** * A class to monitor the network signal strength. + * + * TODO: i18n * * @author Sandor Dornbush */ -public class SignalStrengthTaskModern extends SignalStrengthTask { +public class SignalStrengthListenerEclair extends SignalStrengthListenerCupcake { private SignalStrength signalStrength = null; - public SignalStrengthTaskModern(Context c) { - super(c); + public SignalStrengthListenerEclair(Context ctx, SignalStrengthCallback callback) { + super(ctx, callback); } @Override @@ -43,6 +44,13 @@ public class SignalStrengthTaskModern extends SignalStrengthTask { return PhoneStateListener.LISTEN_SIGNAL_STRENGTHS; } + @Override + public void onSignalStrengthsChanged(SignalStrength signalStrength) { + Log.d(TAG, "Signal Strength Modern: " + signalStrength); + this.signalStrength = signalStrength; + notifySignalSampled(); + } + /** * Gets a human readable description for the network type. * @@ -146,10 +154,4 @@ public class SignalStrengthTaskModern extends SignalStrengthTask { sb.append(getContext().getString(signalFormat, signal)); sb.append("\n"); } - - @Override - public void onSignalStrengthsChanged(SignalStrength signalStrength) { - Log.d(MyTracksConstants.TAG, "Signal Strength Modern: " + signalStrength); - this.signalStrength = signalStrength; - } } diff --git a/SignalStrengthSampler/src/com/google/android/apps/mytracks/signalstrength/SignalStrengthListenerFactory.java b/SignalStrengthSampler/src/com/google/android/apps/mytracks/signalstrength/SignalStrengthListenerFactory.java new file mode 100644 index 000000000..1272b321a --- /dev/null +++ b/SignalStrengthSampler/src/com/google/android/apps/mytracks/signalstrength/SignalStrengthListenerFactory.java @@ -0,0 +1,47 @@ +/* + * 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.signalstrength; + +import static com.google.android.apps.mytracks.signalstrength.SignalStrengthConstants.*; + +import com.google.android.apps.mytracks.signalstrength.SignalStrengthListener.SignalStrengthCallback; + +import android.content.Context; +import android.util.Log; + +/** + * Factory for producing a proper {@link SignalStrengthListenerCupcake} according to the + * current API level. + * + * @author Rodrigo Damazio + */ +public class SignalStrengthListenerFactory { + + public SignalStrengthListener create(Context context, SignalStrengthCallback callback) { + if (hasModernSignalStrength()) { + Log.d(TAG, "TrackRecordingService using modern signal strength api."); + return new SignalStrengthListenerEclair(context, callback); + } else { + Log.w(TAG, "TrackRecordingService using legacy signal strength api."); + return new SignalStrengthListenerCupcake(context, callback); + } + } + + // @VisibleForTesting + protected boolean hasModernSignalStrength() { + return ANDROID_API_LEVEL >= 7; + } +} diff --git a/SignalStrengthSampler/src/com/google/android/apps/mytracks/signalstrength/SignalStrengthPreferences.java b/SignalStrengthSampler/src/com/google/android/apps/mytracks/signalstrength/SignalStrengthPreferences.java new file mode 100644 index 000000000..84c66ce7a --- /dev/null +++ b/SignalStrengthSampler/src/com/google/android/apps/mytracks/signalstrength/SignalStrengthPreferences.java @@ -0,0 +1,56 @@ +/* + * 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.signalstrength; + +import android.os.Bundle; +import android.preference.Preference; +import android.preference.Preference.OnPreferenceClickListener; +import android.preference.PreferenceActivity; + +/** + * Main signal strength sampler activity, which displays preferences. + * + * @author Rodrigo Damazio + */ +public class SignalStrengthPreferences extends PreferenceActivity { + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + addPreferencesFromResource(R.xml.preferences); + + // Attach service control funciontality + findPreference(getString(R.string.settings_control_start_key)) + .setOnPreferenceClickListener(new OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + SignalStrengthService.startService(SignalStrengthPreferences.this); + return true; + } + }); + findPreference(getString(R.string.settings_control_stop_key)) + .setOnPreferenceClickListener(new OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + SignalStrengthService.stopService(SignalStrengthPreferences.this); + return true; + } + }); + + // TODO: Check that my tracks is installed - if not, give a warning and + // offer to go to the android market. + } +} \ No newline at end of file diff --git a/SignalStrengthSampler/src/com/google/android/apps/mytracks/signalstrength/SignalStrengthService.java b/SignalStrengthSampler/src/com/google/android/apps/mytracks/signalstrength/SignalStrengthService.java new file mode 100644 index 000000000..7f80c7433 --- /dev/null +++ b/SignalStrengthSampler/src/com/google/android/apps/mytracks/signalstrength/SignalStrengthService.java @@ -0,0 +1,257 @@ +/* + * 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.signalstrength; + +import static com.google.android.apps.mytracks.signalstrength.SignalStrengthConstants.*; + +import com.google.android.apps.mytracks.content.Waypoint; +import com.google.android.apps.mytracks.services.ITrackRecordingService; +import com.google.android.apps.mytracks.signalstrength.SignalStrengthListener.SignalStrengthCallback; + +import android.app.ActivityManager; +import android.app.ActivityManager.RunningServiceInfo; +import android.app.Service; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.content.SharedPreferences; +import android.content.SharedPreferences.OnSharedPreferenceChangeListener; +import android.os.IBinder; +import android.os.RemoteException; +import android.preference.PreferenceManager; +import android.util.Log; +import android.widget.Toast; + +import java.util.List; + +/** + * Serivce which actually reads signal strength and sends it to My Tracks. + * + * @author Rodrigo Damazio + */ +public class SignalStrengthService extends Service + implements ServiceConnection, SignalStrengthCallback, OnSharedPreferenceChangeListener { + + private ComponentName mytracksServiceComponent; + private SharedPreferences preferences; + private SignalStrengthListenerFactory signalListenerFactory; + private SignalStrengthListener signalListener; + private ITrackRecordingService mytracksService; + private long lastSamplingTime; + private long samplingPeriod; + + @Override + public void onCreate() { + super.onCreate(); + + mytracksServiceComponent = new ComponentName( + getString(R.string.mytracks_service_package), + getString(R.string.mytracks_service_class)); + preferences = PreferenceManager.getDefaultSharedPreferences(this); + signalListenerFactory = new SignalStrengthListenerFactory(); + } + + @Override + public void onStart(Intent intent, int startId) { + handleCommand(intent, startId); + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + handleCommand(intent, startId); + return START_STICKY; + } + + private void handleCommand(Intent intent, int startId) { + String action = intent.getAction(); + if (START_SAMPLING.equals(action)) { + startSampling(); + } else { + stopSampling(); + } + } + + private void startSampling() { + // TODO: Start foreground + + if (!isMytracksRunning()) { + Log.w(TAG, "My Tracks not running!"); + return; + } + + Log.d(TAG, "Starting sampling"); + Intent intent = new Intent(); + intent.setComponent(mytracksServiceComponent); + if (!bindService(intent, SignalStrengthService.this, 0)) { + Log.e(TAG, "Couldn't bind to service."); + return; + } + } + + private boolean isMytracksRunning() { + ActivityManager activityManager = (ActivityManager) getSystemService(ACTIVITY_SERVICE); + List services = activityManager.getRunningServices(Integer.MAX_VALUE); + for (RunningServiceInfo serviceInfo : services) { + if (serviceInfo.pid != 0 && + serviceInfo.service.equals(mytracksServiceComponent)) { + return true; + } + } + return false; + } + + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + synchronized (this) { + mytracksService = ITrackRecordingService.Stub.asInterface(service); + + Log.d(TAG, "Bound to My Tracks"); + + boolean recording = false; + try { + recording = mytracksService.isRecording(); + } catch (RemoteException e) { + Log.e(TAG, "Failed to talk to my tracks", e); + } + if (!recording) { + Log.w(TAG, "My Tracks is not recording, bailing"); + stopSampling(); + return; + } + + // We're ready to send waypoints, so register for signal sampling + signalListener = signalListenerFactory.create(this, this); + signalListener.register(); + + // Register for preference changes + samplingPeriod = Long.parseLong(preferences.getString( + getString(R.string.settings_min_signal_sampling_period_key), "-1")); + preferences.registerOnSharedPreferenceChangeListener(this); + + // Tell the user we've started. + Toast.makeText(this, R.string.started_sampling, Toast.LENGTH_SHORT).show(); + } + } + + @Override + public void onSignalStrengthSampled(String description, String icon) { + long now = System.currentTimeMillis(); + if (now - lastSamplingTime < samplingPeriod * 60 * 1000) { + return; + } + + // We don't set location or track id - My Tracks can fill those + Waypoint wpt = new Waypoint(); + wpt.setName("Signal Strength"); + wpt.setType(Waypoint.TYPE_WAYPOINT); + wpt.setIcon(icon); + wpt.setDescription(description); + + try { + long waypointId; + synchronized (this) { + if (mytracksService == null) { + Log.d(TAG, "No my tracks service to send to"); + return; + } + + waypointId = mytracksService.insertWaypointMarker(wpt); + } + + if (waypointId >= 0) { + Log.d(TAG, "Added signal marker"); + lastSamplingTime = now; + } else { + Log.e(TAG, "Cannot insert waypoint marker?"); + } + } catch (RemoteException e) { + Log.e(TAG, "Cannot talk to my tracks service", e); + } + } + + private void stopSampling() { + Log.d(TAG, "Stopping sampling"); + + synchronized (this) { + // Unregister from preference change updates + preferences.unregisterOnSharedPreferenceChangeListener(this); + + // Unregister from receiving signal updates + if (signalListener != null) { + signalListener.unregister(); + signalListener = null; + } + + // Unbind from My Tracks + if (mytracksService != null) { + unbindService(this); + mytracksService = null; + } + + // Reset the last sampling time + lastSamplingTime = 0; + + // Tell the user we've stopped + Toast.makeText(this, R.string.stopped_sampling, Toast.LENGTH_SHORT).show(); + + // Stop + stopSelf(); + } + } + + @Override + public void onServiceDisconnected(ComponentName name) { + Log.i(TAG, "Disconnected from My Tracks"); + synchronized (this) { + mytracksService = null; + } + } + + @Override + public void onDestroy() { + stopSampling(); + + super.onDestroy(); + } + + @Override + public void onSharedPreferenceChanged( + SharedPreferences sharedPreferences, String key) { + if (getString(R.string.settings_min_signal_sampling_period_key).equals(key)) { + samplingPeriod = Long.parseLong(sharedPreferences.getString(key, "-1")); + } + } + + @Override + public IBinder onBind(Intent intent) { + return null; + } + + public static void startService(Context context) { + Intent intent = new Intent(); + intent.setClass(context, SignalStrengthService.class); + intent.setAction(START_SAMPLING); + context.startService(intent); + } + + public static void stopService(Context context) { + Intent intent = new Intent(); + intent.setClass(context, SignalStrengthService.class); + intent.setAction(STOP_SAMPLING); + context.startService(intent); + } +} diff --git a/SignalStrengthSamplerTest/.classpath b/SignalStrengthSamplerTest/.classpath new file mode 100644 index 000000000..bee7810d7 --- /dev/null +++ b/SignalStrengthSamplerTest/.classpath @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/SignalStrengthSamplerTest/.project b/SignalStrengthSamplerTest/.project new file mode 100644 index 000000000..2b5c9679d --- /dev/null +++ b/SignalStrengthSamplerTest/.project @@ -0,0 +1,34 @@ + + + SignalStrengthSamplerTest + + + SignalStrengthSampler + + + + com.android.ide.eclipse.adt.ResourceManagerBuilder + + + + + com.android.ide.eclipse.adt.PreCompilerBuilder + + + + + org.eclipse.jdt.core.javabuilder + + + + + com.android.ide.eclipse.adt.ApkBuilder + + + + + + com.android.ide.eclipse.adt.AndroidNature + org.eclipse.jdt.core.javanature + + diff --git a/SignalStrengthSamplerTest/.settings/org.eclipse.jdt.core.prefs b/SignalStrengthSamplerTest/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 000000000..0906c3632 --- /dev/null +++ b/SignalStrengthSamplerTest/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,12 @@ +#Sun Nov 28 01:19:10 PST 2010 +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 +org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve +org.eclipse.jdt.core.compiler.compliance=1.6 +org.eclipse.jdt.core.compiler.debug.lineNumber=generate +org.eclipse.jdt.core.compiler.debug.localVariable=generate +org.eclipse.jdt.core.compiler.debug.sourceFile=generate +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.source=1.6 diff --git a/SignalStrengthSamplerTest/AndroidManifest.xml b/SignalStrengthSamplerTest/AndroidManifest.xml new file mode 100644 index 000000000..b9dbff3a8 --- /dev/null +++ b/SignalStrengthSamplerTest/AndroidManifest.xml @@ -0,0 +1,12 @@ + + + + + + + + \ No newline at end of file diff --git a/SignalStrengthSamplerTest/default.properties b/SignalStrengthSamplerTest/default.properties new file mode 100644 index 000000000..420db56e3 --- /dev/null +++ b/SignalStrengthSamplerTest/default.properties @@ -0,0 +1,11 @@ +# This file is automatically generated by Android Tools. +# Do not modify this file -- YOUR CHANGES WILL BE ERASED! +# +# This file must be checked in Version Control Systems. +# +# To customize properties used by the Ant build system use, +# "build.properties", and override values to adapt the script to your +# project structure. + +# Project target. +target=Google Inc.:Google APIs:8 diff --git a/SignalStrengthSamplerTest/libs/AndroidMockRuntime.jar b/SignalStrengthSamplerTest/libs/AndroidMockRuntime.jar new file mode 100644 index 000000000..40fa9e173 Binary files /dev/null and b/SignalStrengthSamplerTest/libs/AndroidMockRuntime.jar differ diff --git a/SignalStrengthSamplerTest/src/com/google/android/apps/mytracks/signalstrength/test/RecordingStateReceiverTest.java b/SignalStrengthSamplerTest/src/com/google/android/apps/mytracks/signalstrength/test/RecordingStateReceiverTest.java new file mode 100644 index 000000000..8f5b6c9cc --- /dev/null +++ b/SignalStrengthSamplerTest/src/com/google/android/apps/mytracks/signalstrength/test/RecordingStateReceiverTest.java @@ -0,0 +1,121 @@ +/* + * 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.signalstrength.test; + +import com.google.android.apps.mytracks.signalstrength.R; +import com.google.android.apps.mytracks.signalstrength.RecordingStateReceiver; + +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; +import android.test.AndroidTestCase; +import android.test.RenamingDelegatingContext; + +/** + * Tests for {@link RecordingStateReceiver}. + * + * @author Rodrigo Damazio + */ +public class RecordingStateReceiverTest extends AndroidTestCase { + + private RecordingStateReceiver receiver; + private RenamingDelegatingContext context; + private SharedPreferences preferences; + private int serviceStopCalls; + private int serviceStartCalls; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + context = new RenamingDelegatingContext(getContext(), "test."); + setContext(context); + + receiver = new RecordingStateReceiver() { + @Override + protected void stopService(Context ctx) { + serviceStopCalls++; + } + + @Override + protected void startService(Context ctx) { + serviceStartCalls++; + } + }; + + preferences = PreferenceManager.getDefaultSharedPreferences(context); + preferences + .edit() + .putBoolean(context.getString(R.string.settings_auto_start_key), true) + .putBoolean(context.getString(R.string.settings_auto_stop_key), true) + .commit(); + } + + public void testOnReceive_start() { + receiver.onReceive(context, + new Intent(context.getString(R.string.track_started_broadcast_action))); + assertServiceCalls(1, 0); + } + + public void testOnReceive_resume() { + receiver.onReceive(context, + new Intent(context.getString(R.string.track_resumed_broadcast_action))); + assertServiceCalls(1, 0); + } + + public void testOnReceive_stop() { + receiver.onReceive(context, + new Intent(context.getString(R.string.track_stopped_broadcast_action))); + assertServiceCalls(0, 1); + } + + public void testOnReceive_pause() { + receiver.onReceive(context, + new Intent(context.getString(R.string.track_paused_broadcast_action))); + assertServiceCalls(0, 1); + } + + public void testOnReceive_noAutoStart() { + preferences + .edit() + .putBoolean(context.getString(R.string.settings_auto_start_key), false) + .commit(); + receiver.onReceive(context, + new Intent(context.getString(R.string.track_started_broadcast_action))); + assertServiceCalls(0, 0); + } + + public void testOnReceive_noAutoStop() { + preferences + .edit() + .putBoolean(context.getString(R.string.settings_auto_stop_key), false) + .commit(); + receiver.onReceive(context, + new Intent(context.getString(R.string.track_stopped_broadcast_action))); + assertServiceCalls(0, 0); + } + + public void testOnReceive_other() { + receiver.onReceive(context, new Intent("bla")); + assertServiceCalls(0, 0); + } + + private void assertServiceCalls(int startCalls, int stopCalls) { + assertEquals(startCalls, serviceStartCalls); + assertEquals(stopCalls, serviceStopCalls); + } +} diff --git a/SignalStrengthSamplerTest/src/com/google/android/apps/mytracks/signalstrength/test/SignalStrengthListenerFactoryTest.java b/SignalStrengthSamplerTest/src/com/google/android/apps/mytracks/signalstrength/test/SignalStrengthListenerFactoryTest.java new file mode 100644 index 000000000..2e26be26a --- /dev/null +++ b/SignalStrengthSamplerTest/src/com/google/android/apps/mytracks/signalstrength/test/SignalStrengthListenerFactoryTest.java @@ -0,0 +1,68 @@ +/* + * 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.signalstrength.test; + +import com.google.android.apps.mytracks.signalstrength.SignalStrengthListener; +import com.google.android.apps.mytracks.signalstrength.SignalStrengthListener.SignalStrengthCallback; +import com.google.android.apps.mytracks.signalstrength.SignalStrengthListenerCupcake; +import com.google.android.apps.mytracks.signalstrength.SignalStrengthListenerEclair; +import com.google.android.apps.mytracks.signalstrength.SignalStrengthListenerFactory; +import com.google.android.testing.mocking.AndroidMock; + +import android.test.AndroidTestCase; + +/** + * Tests for {@link SignalStrengthListenerFactory}. + * These tests require Eclair+ (API level 7) to run. + * + * @author Rodrigo Damazio + */ +public class SignalStrengthListenerFactoryTest extends AndroidTestCase { + private boolean hasModernSignalStrength; + private TestableSignalStrengthListenerFactory factory; + private SignalStrengthCallback callback; + + private class TestableSignalStrengthListenerFactory extends SignalStrengthListenerFactory { + @Override + protected boolean hasModernSignalStrength() { + return hasModernSignalStrength; + } + } + + @Override + protected void setUp() throws Exception { + super.setUp(); + + factory = new TestableSignalStrengthListenerFactory(); + callback = AndroidMock.createMock(SignalStrengthCallback.class); + } + + public void testCreate_eclair() { + hasModernSignalStrength = true; + + SignalStrengthListener listener = factory.create(getContext(), callback); + assertTrue(listener.getClass().getName(), + listener instanceof SignalStrengthListenerEclair); + } + + public void testCreate_legacy() { + hasModernSignalStrength = false; + + SignalStrengthListener listener = factory.create(getContext(), callback); + assertTrue(listener.getClass().getName(), + listener instanceof SignalStrengthListenerCupcake); + } +}