From 63677b145110a1babb3e2365ff58b2a8f3f608e4 Mon Sep 17 00:00:00 2001 From: Sandor Dornbush Date: Thu, 9 Jun 2011 20:57:48 -0700 Subject: [PATCH] Merged widget. --- MyTracks/AndroidManifest.xml | 4 +- MyTracks/res/values-pl/arrays.xml | 8 +- MyTracks/res/values-pl/strings.xml | 30 +- MyTracks/res/values/do_not_translate.xml | 2 +- MyTracks/src/com/dsi/ant/AntInterface.java | 10 +- .../android/apps/mytracks/ChartActivity.java | 42 +- .../apps/mytracks/ChartValueSeries.java | 20 +- .../android/apps/mytracks/ChartView.java | 325 ++++--- .../apps/mytracks/ExportAllTracks.java | 25 +- .../android/apps/mytracks/MapActivity.java | 52 +- .../android/apps/mytracks/MyTracks.java | 38 +- .../android/apps/mytracks/StatsActivity.java | 50 +- .../android/apps/mytracks/StatsUtilities.java | 4 +- .../android/apps/mytracks/WaypointsList.java | 33 +- .../mytracks/content/DataSourceManager.java | 264 ++++++ .../DataSourcesWrapper.java} | 6 +- .../DataSourcesWrapperImpl.java} | 7 +- .../mytracks/{ => content}/TrackDataHub.java | 716 ++++++-------- .../{ => content}/TrackDataListener.java | 4 +- .../mytracks/content/TrackDataListeners.java | 170 ++++ .../apps/mytracks/io/SendToFusionTables.java | 12 +- .../apps/mytracks/io/SendToMyMaps.java | 83 +- .../apps/mytracks/io/docs/DocsHelper.java | 4 +- .../apps/mytracks/io/file/GpxTrackWriter.java | 3 +- .../apps/mytracks/io/file/KmlTrackWriter.java | 6 +- .../apps/mytracks/io/file/TcxTrackWriter.java | 4 +- .../mytracks/io/file/TrackWriterFactory.java | 8 + .../services/sensors/SensorUtils.java | 56 +- .../services/sensors/ZephyrMessageParser.java | 37 +- .../mytracks/stats/TripStatisticsBuilder.java | 1 - .../gdata/docs/SpreadsheetsClient.java | 6 +- .../gdata/docs/XmlDocsGDataParserFactory.java | 2 +- .../content/MyTracksProviderUtilsImpl.java | 76 +- .../apps/mytracks/TrackDataHubTest.java | 478 ---------- .../mytracks/content/TrackDataHubTest.java | 870 ++++++++++++++++++ .../io/backup/DatabaseDumperTest.java | 3 - .../apps/mytracks/io/docs/DocsHelperTest.java | 5 +- .../sensors/ZephyrMessageParserTest.java | 25 +- .../{ => ant}/AntDirectSensorManagerTest.java | 2 +- .../{ => ant}/AntSensorManagerTest.java | 2 +- .../signalstrength/SignalStrengthService.java | 14 +- 41 files changed, 2230 insertions(+), 1277 deletions(-) create mode 100644 MyTracks/src/com/google/android/apps/mytracks/content/DataSourceManager.java rename MyTracks/src/com/google/android/apps/mytracks/{TrackDataSources.java => content/DataSourcesWrapper.java} (91%) rename MyTracks/src/com/google/android/apps/mytracks/{TrackDataSourcesImpl.java => content/DataSourcesWrapperImpl.java} (95%) rename MyTracks/src/com/google/android/apps/mytracks/{ => content}/TrackDataHub.java (60%) rename MyTracks/src/com/google/android/apps/mytracks/{ => content}/TrackDataListener.java (97%) create mode 100644 MyTracks/src/com/google/android/apps/mytracks/content/TrackDataListeners.java delete mode 100644 MyTracksTest/src/com/google/android/apps/mytracks/TrackDataHubTest.java create mode 100644 MyTracksTest/src/com/google/android/apps/mytracks/content/TrackDataHubTest.java rename MyTracksTest/src/com/google/android/apps/mytracks/services/sensors/{ => ant}/AntDirectSensorManagerTest.java (97%) rename MyTracksTest/src/com/google/android/apps/mytracks/services/sensors/{ => ant}/AntSensorManagerTest.java (97%) diff --git a/MyTracks/AndroidManifest.xml b/MyTracks/AndroidManifest.xml index 7ce7f26a2..5c17c6cf5 100755 --- a/MyTracks/AndroidManifest.xml +++ b/MyTracks/AndroidManifest.xml @@ -17,8 +17,8 @@ limitations under the License. + android:versionCode="29" + android:versionName="1.1.6"> diff --git a/MyTracks/res/values-pl/arrays.xml b/MyTracks/res/values-pl/arrays.xml index bf55da1a4..b349decd7 100644 --- a/MyTracks/res/values-pl/arrays.xml +++ b/MyTracks/res/values-pl/arrays.xml @@ -150,7 +150,7 @@ limitations under the License. typ mieszany inny - bieanie + bieganie bieganie uliczne bieganie na torze bieganie terenowe @@ -170,10 +170,10 @@ limitations under the License. łyżwiarstwo torowe deskorolka - wędrówka po sniegu + wędrówka po śniegu narciarstwo snowboard - saneczkastwo + saneczkarstwo żeglarstwo lodowe żeglowanie @@ -237,7 +237,7 @@ limitations under the License. kominek remiza strażacka baza wędkarska - stcja paliw + stacja paliw pole golfowe sklep spożywczy fryzjer diff --git a/MyTracks/res/values-pl/strings.xml b/MyTracks/res/values-pl/strings.xml index d019ce3b4..f05d38f95 100644 --- a/MyTracks/res/values-pl/strings.xml +++ b/MyTracks/res/values-pl/strings.xml @@ -42,7 +42,7 @@ limitations under the License. Plik nie jest poprawnym plikiem GPX. Wystąpił błąd. Spróbuj ponownie. Trasa nie istnieje. - Plik jest za duży (musi być mniejszy nić 500 KB). Przerywam. + Plik jest za duży (musi być mniejszy niż 500 KB). Przerywam. Nie można wstawić markera. Brak poprawnych danych o lokalizacji. @@ -334,7 +334,6 @@ limitations under the License. Zespół: Wersja: Wsparcie: - Bartłomiej Niechwiej Wyślij do... @@ -385,4 +384,31 @@ limitations under the License. Jednostki miary Domyślnie używane są jednostki metryczne. Chcesz to zmienić? + + + Bluetooth + Opcje Bluetooth + Parowanie urządzenia + Przejdź do ustawień Bluetooth Androida aby sparować urządzenie + bluetoothSensorsEnabled + Opce ANT + Opcje Czujników + + + Połączony z %s + Łączenie urządzenia Bluetooth w toku... + Urządzenie Bluetooth niepołączone + + + Czas Danych Czujnika + Miernik mocy aktywny + Pulsometr aktywny + Brak Ustawień Czujników + Aktywny + Łączenie + Podłączony + Rozłączony + Brak Serwisu Czujników + Wykrywanie + Przesyłanie diff --git a/MyTracks/res/values/do_not_translate.xml b/MyTracks/res/values/do_not_translate.xml index 116175d63..3d752908a 100644 --- a/MyTracks/res/values/do_not_translate.xml +++ b/MyTracks/res/values/do_not_translate.xml @@ -23,7 +23,7 @@ limitations under the License. mytracks.appspot.com m.google.com/privacy Copyright 2009 Google Inc. - Bartlomiej Niechwiej + Bartłomiej Niechwiej Dylan Casey Leif Hendrik Wilden Rodrigo Damazio Bovendorp diff --git a/MyTracks/src/com/dsi/ant/AntInterface.java b/MyTracks/src/com/dsi/ant/AntInterface.java index 05583d857..0e425e57d 100644 --- a/MyTracks/src/com/dsi/ant/AntInterface.java +++ b/MyTracks/src/com/dsi/ant/AntInterface.java @@ -15,22 +15,20 @@ */ package com.dsi.ant; -import java.util.Arrays; +import com.dsi.ant.exception.AntInterfaceException; +import com.dsi.ant.exception.AntRemoteException; +import com.dsi.ant.exception.AntServiceNotConnectedException; -import android.app.Activity; -import android.app.AlertDialog; import android.content.ComponentName; import android.content.Context; -import android.content.DialogInterface; import android.content.Intent; import android.content.ServiceConnection; -import android.content.pm.PackageManager; import android.net.Uri; import android.os.IBinder; import android.os.RemoteException; import android.util.Log; -import com.dsi.ant.exception.*; +import java.util.Arrays; /** * Public API for controlling the Ant Service. AntInterface is a proxy diff --git a/MyTracks/src/com/google/android/apps/mytracks/ChartActivity.java b/MyTracks/src/com/google/android/apps/mytracks/ChartActivity.java index 0e648d44a..87efb8d58 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/ChartActivity.java +++ b/MyTracks/src/com/google/android/apps/mytracks/ChartActivity.java @@ -18,10 +18,12 @@ package com.google.android.apps.mytracks; import static com.google.android.apps.mytracks.Constants.TAG; import com.google.android.apps.mytracks.ChartView.Mode; -import com.google.android.apps.mytracks.TrackDataHub.ListenerDataType; import com.google.android.apps.mytracks.content.MyTracksLocation; import com.google.android.apps.mytracks.content.Sensor; +import com.google.android.apps.mytracks.content.TrackDataHub; +import com.google.android.apps.mytracks.content.TrackDataListener; import com.google.android.apps.mytracks.content.Sensor.SensorDataSet; +import com.google.android.apps.mytracks.content.TrackDataHub.ListenerDataType; import com.google.android.apps.mytracks.content.Track; import com.google.android.apps.mytracks.content.Waypoint; import com.google.android.apps.mytracks.services.tasks.StatusAnnouncerFactory; @@ -137,11 +139,12 @@ public class ChartActivity extends Activity implements TrackDataListener { } @Override - protected void onStart() { - super.onStart(); + protected void onResume() { + super.onResume(); dataHub.registerTrackDataListener(this, EnumSet.of( ListenerDataType.SELECTED_TRACK_CHANGED, + ListenerDataType.TRACK_UPDATES, ListenerDataType.POINT_UPDATES, ListenerDataType.SAMPLED_OUT_POINT_UPDATES, ListenerDataType.WAYPOINT_UPDATES, @@ -149,10 +152,10 @@ public class ChartActivity extends Activity implements TrackDataListener { } @Override - protected void onStop() { + protected void onPause() { dataHub.unregisterTrackDataListener(this); - super.onStop(); + super.onPause(); } private void zoomIn() { @@ -257,13 +260,13 @@ public class ChartActivity extends Activity implements TrackDataListener { // TODO: Account for segment splits? switch (mode) { case BY_DISTANCE: - timeOrDistance = profileLength; + timeOrDistance = profileLength / 1000.0; if (lastLocation != null) { double d = lastLocation.distanceTo(location); if (metricUnits) { - profileLength += d / 1000.0; + profileLength += d; } else { - profileLength += d * UnitConversions.KM_TO_MI / 1000.0; + profileLength += d * UnitConversions.KM_TO_MI; } } break; @@ -336,7 +339,6 @@ public class ChartActivity extends Activity implements TrackDataListener { @Override public void onSelectedTrackChanged(Track track, boolean isRecording) { - Log.e(TAG, "Visible", new Throwable()); runOnUiThread(new Runnable() { @Override public void run() { @@ -347,6 +349,11 @@ public class ChartActivity extends Activity implements TrackDataListener { @Override public void onTrackUpdated(Track track) { + if (track == null || track.getStatistics() == null) { + trackMaxSpeed = 0.0; + return; + } + trackMaxSpeed = track.getStatistics().getMaxSpeed(); } @@ -356,9 +363,16 @@ public class ChartActivity extends Activity implements TrackDataListener { lastLocation = null; startTime = -1; elevationBuffer.reset(); - speedBuffer.reset(); chartView.reset(); + speedBuffer.reset(); pendingPoints.clear(); + + runOnUiThread(new Runnable() { + @Override + public void run() { + chartView.resetScroll(); + } + }); } @Override @@ -405,15 +419,21 @@ public class ChartActivity extends Activity implements TrackDataListener { @Override public boolean onUnitsChanged(boolean metric) { + boolean changed = metric != this.metricUnits; + if (!changed) return false; + this.metricUnits = metric; chartView.setMetricUnits(metric); - + return true; // Reload data } @Override public boolean onReportSpeedChanged(boolean reportSpeed) { + boolean changed = reportSpeed != this.reportSpeed; + if (!changed) return false; + this.reportSpeed = reportSpeed; chartView.setReportSpeed(reportSpeed, this); diff --git a/MyTracks/src/com/google/android/apps/mytracks/ChartValueSeries.java b/MyTracks/src/com/google/android/apps/mytracks/ChartValueSeries.java index 57b88125d..3248763c2 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/ChartValueSeries.java +++ b/MyTracks/src/com/google/android/apps/mytracks/ChartValueSeries.java @@ -1,12 +1,12 @@ /* * 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 @@ -48,7 +48,7 @@ public class ChartValueSeries { private double spread; private int interval; private boolean enabled = true; - + /** * This class controls how effective min/max values of a {@link ChartValueSeries} are calculated. */ @@ -70,10 +70,10 @@ public class ChartValueSeries { this.intervals = intervals; this.absoluteMin = absoluteMin; this.absoluteMax = absoluteMax; - this.zoomLevels = zoomLevels; + this.zoomLevels = zoomLevels; checkArgs(); } - + private void checkArgs() { if (intervals <= 0 || zoomLevels == null || zoomLevels.length == 0) { throw new IllegalArgumentException("Expecing positive intervals and non-empty zoom levels"); @@ -100,12 +100,12 @@ public class ChartValueSeries { public int[] getZoomLevels() { return zoomLevels; } - + /** * Calculates the interval between markings given the min and max values. * This function attempts to find the smallest zoom level that fits [min,max] after rounding * it to the current zoom level. - * + * * @param min the minimum value in the series * @param max the maximum value in the series * @return the calculated interval for the given range @@ -136,7 +136,7 @@ public class ChartValueSeries { * @param fill The paint for filling the chart * @param stroke The paint for stroking the outside the chart, optional * @param zoomSettings The settings related to zooming - * + * * TODO: Get rid of Context and inject appropriate values instead. */ public ChartValueSeries(Context context, String formatString, int fillColor, int strokeColor, @@ -207,7 +207,7 @@ public class ChartValueSeries { } min = Math.min(min, zoomSettings.getAbsoluteMin()); max = Math.max(max, zoomSettings.getAbsoluteMax()); - + this.interval = zoomSettings.calculateInterval(min, max); // Round it up. effectiveMax = ((int) (max / interval)) * interval + interval; diff --git a/MyTracks/src/com/google/android/apps/mytracks/ChartView.java b/MyTracks/src/com/google/android/apps/mytracks/ChartView.java index 5e50b2fa0..0a6e665ad 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/ChartView.java +++ b/MyTracks/src/com/google/android/apps/mytracks/ChartView.java @@ -1,12 +1,12 @@ /* * Copyright 2008 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 @@ -28,9 +28,9 @@ import android.graphics.Canvas; import android.graphics.Color; import android.graphics.DashPathEffect; import android.graphics.Paint; -import android.graphics.Path; import android.graphics.Paint.Align; import android.graphics.Paint.Style; +import android.graphics.Path; import android.graphics.drawable.Drawable; import android.view.MotionEvent; import android.view.VelocityTracker; @@ -66,36 +66,38 @@ public class ChartView extends View { private int maxZoomLevel = 10; private static final int MAX_INTERVALS = 5; - + /* * Borders, margins, dimensions (in pixels): */ private int leftBorder = -1; /** - * Unscaled top border of the chart. + * Unscaled top border of the chart. */ private static final int TOP_BORDER = 15; - + /** * Device scaled top border of the chart. */ private int topBorder; /** - * Unscaled bottom border of the chart. + * Unscaled bottom border of the chart. */ private static final float BOTTOM_BORDER = 40; - + /** * Device scaled bottom border of the chart. */ private int bottomBorder; - + private static final int RIGHT_BORDER = 17; + /** Space to leave for drawing the unit labels */ private static final int UNIT_BORDER = 15; private static final int FONT_HEIGHT = 10; + private int w = 0; private int h = 0; private int effectiveWidth = 0; @@ -118,11 +120,11 @@ public class ChartView extends View { private ChartValueSeries[] series; private final ExtremityMonitor xMonitor = new ExtremityMonitor(); - private final NumberFormat xFormat = new DecimalFormat("###,###"); - private final NumberFormat xShortFormat = new DecimalFormat("#.0"); + private static final NumberFormat X_FORMAT = new DecimalFormat("###,###"); + private static final NumberFormat X_SHORT_FORMAT = new DecimalFormat("#.0"); /* - * Paints etc. used when drawing the histogram: + * Paints etc. used when drawing the chart: */ private final Paint borderPaint = new Paint(); private final Paint labelPaint = new Paint(); @@ -174,7 +176,7 @@ public class ChartView extends View { gridBarPaint.set(gridPaint); gridBarPaint.setPathEffect(new DashPathEffect(new float[] {3, 2}, 0)); - + clearPaint.setStyle(Style.FILL); clearPaint.setColor(context.getResources().getColor(R.color.white)); clearPaint.setAntiAlias(false); @@ -271,49 +273,12 @@ public class ChartView extends View { : R.string.pace_label)); } - /** - * Gets the data that is displayed by the chart. - * - * @return an array list with data points - */ - public ArrayList getData() { - return data; - } - - /** - * Sets the data that is to be displayed by the chart. - * - * @param theData an array list of data points - */ - public synchronized void setDataPoints(ArrayList theData) { - scrollTo(0, 0); - zoomLevel = 1; - data.clear(); - xMonitor.reset(); - for (ChartValueSeries cvs : series) { - cvs.reset(); - } - addDataPoints(theData); - } - - /** - * Adds a new data point to the chart. - * - * @param theData a data point - */ - 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++) { - if (!Double.isNaN(theData[i + 1])) { - series[i].update(theData[i + 1]); + for (int i = 1; i <= min; i++) { + if (!Double.isNaN(theData[i])) { + series[i - 1].update(theData[i]); } } // Fill in the extra's if needed. @@ -329,25 +294,32 @@ public class ChartView extends View { * * @param theData an array list of data points to be added */ - public synchronized void addDataPoints(ArrayList theData) { - data.addAll(theData); - for (int i = 0; i < theData.size(); i++) { - double d[] = theData.get(i); - addDataPointInternal(d); + public void addDataPoints(ArrayList theData) { + synchronized (data) { + data.addAll(theData); + for (int i = 0; i < theData.size(); i++) { + double d[] = theData.get(i); + addDataPointInternal(d); + } + updateDimensions(); + setUpPath(); } - updateDimensions(); - setUpPath(); } /** * Clears all data. - * Call this only from the UI thread! */ - public synchronized void reset() { - data.clear(); - zoomLevel = 1; + public void reset() { + synchronized (data) { + data.clear(); + xMonitor.reset(); + zoomLevel = 1; + updateDimensions(); + } + } + + public void resetScroll() { scrollTo(0, 0); - updateDimensions(); } /** @@ -392,13 +364,6 @@ public class ChartView extends View { } } - /** - * @return the current zoom level (1 equals to showing all data points) - */ - public int getZoomLevel() { - return zoomLevel; - } - /** * Initiates flinging. * @@ -427,14 +392,6 @@ public class ChartView extends View { scrollTo(scrollX, 0); } - /** - * Sets the scroll position of the chart. This will trigger a redraw. - */ - @Override - public void scrollTo(int x, int y) { - super.scrollTo(x, y); - } - /** * @return the current display mode (by distance, by time) */ @@ -551,38 +508,82 @@ public class ChartView extends View { } @Override - protected synchronized void onDraw(Canvas c) { - if (w != c.getWidth() || h != c.getHeight()) { - // Dimensions have changed (for example due to orientation change). - w = c.getWidth(); - h = c.getHeight(); - effectiveWidth = Math.max(0, w - leftBorder - RIGHT_BORDER); - effectiveHeight = Math.max(0, h - topBorder - bottomBorder); - setUpPath(); - } - c.save(); - c.drawColor(Color.WHITE); - if (data.size() < 1) { - drawXAxis(c); - drawYAxis(c); - c.restore(); - return; - } - - c.save(); - c.clipRect(leftBorder + 1 + getScrollX(), topBorder + 1, - w - RIGHT_BORDER + getScrollX() - 1, h - bottomBorder - 1); + protected void onDraw(Canvas c) { + synchronized (data) { + updateEffectiveDimensionsIfChanged(c); - drawGrid(c); - - // Draw the data series. - for (ChartValueSeries cvs : series) { - if (cvs.isEnabled() && cvs.hasData()) { - cvs.drawPath(c); + // Keep original state. + c.save(); + + c.drawColor(Color.WHITE); + + if (data.isEmpty()) { + // No data, draw only axes + drawXAxis(c); + drawYAxis(c); + c.restore(); + return; + } + + // Clip to graph drawing space + c.save(); + clipToGraphSpace(c); + + // Draw the grid and the data on it. + drawGrid(c); + drawDataSeries(c); + drawWaypoints(c); + + // Go back to full canvas drawing. + c.restore(); + + // Draw the axes and their labels. + drawAxesAndLabels(c); + + // Go back to original state. + c.restore(); + + // Draw the pointer + if (showPointer) { + drawPointer(c); } } - - // Draw the waypoints. + } + + /** Clips the given canvas to the area where the graph lines should be drawn. */ + private void clipToGraphSpace(Canvas c) { + c.clipRect(leftBorder + 1 + getScrollX(), topBorder + 1, + w - RIGHT_BORDER + getScrollX() - 1, h - bottomBorder - 1); + } + + /** Draws the axes and their labels into th e given canvas. */ + private void drawAxesAndLabels(Canvas c) { + drawXLabels(c); + drawXAxis(c); + drawSeriesTitles(c); + + c.translate(getScrollX(), 0); + drawYAxis(c); + float density = getContext().getResources().getDisplayMetrics().density; + final int spacer = (int) (5 * density); + int x = leftBorder - spacer; + for (ChartValueSeries cvs : series) { + if (cvs.isEnabled() && cvs.hasData()) { + x -= drawYLabels(cvs, c, x) + spacer; + } + } + } + + /** Draws the current pointer into the given canvas. */ + private void drawPointer(Canvas c) { + c.translate(getX(maxX) - pointer.getIntrinsicWidth() / 2, + getY(series[0], data.get(data.size() - 1)[1]) + - pointer.getIntrinsicHeight() / 2 - 12); + pointer.draw(c); + } + + /** Draws the waypoints into the given canvas. */ + private void drawWaypoints(Canvas c) { for (int i = 1; i < waypoints.size(); i++) { final Waypoint waypoint = waypoints.get(i); if (waypoint.getLocation() == null) { @@ -600,33 +601,18 @@ public class ChartView extends View { } c.restore(); } + } - c.restore(); - - // Draw the axis and labels. - drawXLabels(c); - drawXAxis(c); - drawSeriesTitles(c); - - c.translate(getScrollX(), 0); - drawYAxis(c); - float density = getContext().getResources().getDisplayMetrics().density; - final int spacer = (int) (5 * density); - int x = leftBorder - spacer; + /** Draws the data series into the given canvas. */ + private void drawDataSeries(Canvas c) { for (ChartValueSeries cvs : series) { if (cvs.isEnabled() && cvs.hasData()) { - x -= drawYLabels(cvs, c, x) + spacer; + cvs.drawPath(c); } } - c.restore(); - if (showPointer && !data.isEmpty()) { - c.translate(getX(maxX) - pointer.getIntrinsicWidth() / 2, - getY(series[0], data.get(data.size() - 1)[1]) - - pointer.getIntrinsicHeight() / 2 - 12); - pointer.draw(c); - } } + /** Draws the colored titles for the data series. */ private void drawSeriesTitles(Canvas c) { int sections = 1; for (ChartValueSeries cvs : series) { @@ -644,17 +630,24 @@ public class ChartView extends View { } /** - * Sets up the path that is used to draw the histogram in onDraw(). The path + * Sets up the path that is used to draw the chart in onDraw(). The path * needs to be updated any time after the data or histogram dimensions change. */ - private synchronized void setUpPath() { - for (ChartValueSeries cvs : series) { - cvs.getPath().reset(); - } - if (data.isEmpty()) { - return; - } + private void setUpPath() { + synchronized (data) { + for (ChartValueSeries cvs : series) { + cvs.getPath().reset(); + } + if (!data.isEmpty()) { + drawPaths(); + closePaths(); + } + } + } + + /** Actually draws the data points as a path. */ + private void drawPaths() { // All of the data points to the respective series. // TODO: Come up with a better sampling than Math.max(1, (maxZoomLevel - zoomLevel + 1) / 2); int sampling = 1; @@ -673,7 +666,10 @@ public class ChartView extends View { } } } + } + /** Closes the drawn path so it looks like a solid graph. */ + private void closePaths() { // Close the path. int yCorner = topBorder + effectiveHeight; int xCorner = getX(data.get(0)[0]); @@ -692,12 +688,13 @@ public class ChartView extends View { } } } - + /** - * Find the index of the first point which has a series populated. - * @param seriesIndex The index of the value series to search for. + * Finds the index of the first point which has a series populated. + * + * @param seriesIndex The index of the value series to search for * @return The index in the first data for the point in the series that has series - * index value populated or -1 if none is found. + * index value populated or -1 if none is found */ private int getFirstPointPopulatedIndex(int seriesIndex) { for (int i = 0; i < data.size(); i++) { @@ -709,7 +706,7 @@ public class ChartView extends View { } /** - * Update the histogram dimensions. + * Updates the chart dimensions. */ private void updateDimensions() { maxX = xMonitor.getMax(); @@ -730,10 +727,29 @@ public class ChartView extends View { float density = getContext().getResources().getDisplayMetrics().density; maxLength = Math.max(maxLength, 1); leftBorder = (int) (density * (4 + 8 * maxLength)); - effectiveWidth = w - leftBorder - RIGHT_BORDER; bottomBorder = (int) (density * BOTTOM_BORDER); topBorder = (int) (density * TOP_BORDER); - effectiveHeight = h - topBorder - bottomBorder; + updateEffectiveDimensions(); + } + + /** Updates the effective dimensions where the graph will be drawn. */ + private void updateEffectiveDimensions() { + effectiveWidth = Math.max(0, w - leftBorder - RIGHT_BORDER); + effectiveHeight = Math.max(0, h - topBorder - bottomBorder); + } + + /** + * Updates the effective dimensions where the graph will be drawn, only if the + * dimensions of the given canvas have changed since the last call. + */ + private void updateEffectiveDimensionsIfChanged(Canvas c) { + if (w != c.getWidth() || h != c.getHeight()) { + // Dimensions have changed (for example due to orientation change). + w = c.getWidth(); + h = c.getHeight(); + updateEffectiveDimensions(); + setUpPath(); + } } private int getX(double distance) { @@ -746,6 +762,7 @@ public class ChartView extends View { - (int) ((y - cvs.getMin()) * effectiveHeight / effectiveSpread); } + /** Draws the labels on the X axis into the given canvas. */ private void drawXLabels(Canvas c) { double interval = (int) (maxX / zoomLevel / 4); boolean shortFormat = false; @@ -770,6 +787,7 @@ public class ChartView extends View { } } + /** Draws the labels on the Y axis into the given canvas. */ private float drawYLabels(ChartValueSeries cvs, Canvas c, int x) { int interval = cvs.getInterval(); float maxTextWidth = 0; @@ -779,13 +797,14 @@ public class ChartView extends View { return maxTextWidth; } + /** Draws a single label on the X axis. */ private void drawXLabel(Canvas c, double x, boolean shortFormat) { if (x < 0) { return; } String s = (mode == Mode.BY_DISTANCE) - ? (shortFormat ? xShortFormat.format(x) : xFormat.format(x)) + ? (shortFormat ? X_SHORT_FORMAT.format(x) : X_FORMAT.format(x)) : StringUtils.formatTime((long) x); c.drawText(s, getX(x), @@ -793,6 +812,7 @@ public class ChartView extends View { labelPaint); } + /** Draws a single label on the Y axis. */ private float drawYLabel(ChartValueSeries cvs, Canvas c, int x, int y) { int desiredY = (int) ((y - cvs.getMin()) * effectiveHeight / (cvs.getInterval() * MAX_INTERVALS)); @@ -804,6 +824,7 @@ public class ChartView extends View { return p.measureText(text); } + /** Draws the actual X axis line and its label. */ private void drawXAxis(Canvas canvas) { float rightEdge = getX(maxX); final int y = effectiveHeight + topBorder; @@ -815,6 +836,7 @@ public class ChartView extends View { canvas.drawText(s, rightEdge, effectiveHeight + .2f * UNIT_BORDER + topBorder, labelPaint); } + /** Draws the actual Y axis line and its label. */ private void drawYAxis(Canvas canvas) { canvas.drawRect(0, 0, leftBorder - 1, effectiveHeight + topBorder + UNIT_BORDER + 1, @@ -826,18 +848,15 @@ public class ChartView extends View { int y = i * effectiveHeight / MAX_INTERVALS + topBorder; canvas.drawLine(leftBorder - 5, y, leftBorder, y, gridPaint); } - + Context c = getContext(); // TODO: This should really show units for all series. String s = metricUnits ? c.getString(R.string.meter) : c.getString(R.string.feet); canvas.drawText(s, leftBorder - UNIT_BORDER * .2f, UNIT_BORDER * .8f + topBorder, labelPaint); } - - private synchronized void drawGrid(Canvas c) { - if (data.isEmpty()) { - return; - } - + + /** Draws the grid for the graph. */ + private void drawGrid(Canvas c) { float rightEdge = getX(maxX); for (int i = 1; i < MAX_INTERVALS; ++i) { int y = i * effectiveHeight / MAX_INTERVALS + topBorder; diff --git a/MyTracks/src/com/google/android/apps/mytracks/ExportAllTracks.java b/MyTracks/src/com/google/android/apps/mytracks/ExportAllTracks.java index 93f2e9aa5..8aa9387da 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/ExportAllTracks.java +++ b/MyTracks/src/com/google/android/apps/mytracks/ExportAllTracks.java @@ -1,12 +1,12 @@ /* * 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 @@ -133,7 +133,7 @@ public class ExportAllTracks { Log.i(Constants.TAG, "ExportAllTracks: Releasing wake lock."); } Log.i(Constants.TAG, "ExportAllTracks: Done"); - Toast.makeText(activity, R.string.export_done, Toast.LENGTH_SHORT).show(); + showToast(R.string.export_done, Toast.LENGTH_SHORT); } private void makeProgressDialog(final int trackCount) { @@ -189,12 +189,16 @@ public class ExportAllTracks { Log.i(Constants.TAG, "ExportAllTracks: exporting: " + id); TrackWriter writer = TrackWriterFactory.newWriter(activity, providerUtils, id, format); + if (writer == null) { + showToast(R.string.error_track_does_not_exist, Toast.LENGTH_LONG); + return; + } + writer.writeTrack(); if (!writer.wasSuccess()) { // Abort the whole export on the first error. - int error = writer.getErrorMessage(); - Toast.makeText(activity, error, Toast.LENGTH_LONG).show(); + showToast(writer.getErrorMessage(), Toast.LENGTH_LONG); return; } } @@ -210,4 +214,13 @@ public class ExportAllTracks { } } } + + private void showToast(final int messageId, final int length) { + activity.runOnUiThread(new Runnable() { + @Override + public void run() { + Toast.makeText(activity, messageId, length).show(); + } + }); + } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/MapActivity.java b/MyTracks/src/com/google/android/apps/mytracks/MapActivity.java index 772096d65..1ac9c1f05 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/MapActivity.java +++ b/MyTracks/src/com/google/android/apps/mytracks/MapActivity.java @@ -1,12 +1,12 @@ /* * Copyright 2008 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 @@ -17,9 +17,11 @@ package com.google.android.apps.mytracks; import static com.google.android.apps.mytracks.Constants.TAG; -import com.google.android.apps.mytracks.TrackDataHub.ListenerDataType; import com.google.android.apps.mytracks.content.MyTracksProviderUtils; import com.google.android.apps.mytracks.content.Track; +import com.google.android.apps.mytracks.content.TrackDataHub; +import com.google.android.apps.mytracks.content.TrackDataHub.ListenerDataType; +import com.google.android.apps.mytracks.content.TrackDataListener; import com.google.android.apps.mytracks.content.Waypoint; import com.google.android.apps.mytracks.services.tasks.StatusAnnouncerFactory; import com.google.android.apps.mytracks.stats.TripStatistics; @@ -33,7 +35,6 @@ import com.google.android.maps.mytracks.R; import android.content.Intent; import android.location.Location; -import android.location.LocationManager; import android.os.Bundle; import android.provider.Settings; import android.util.Log; @@ -50,6 +51,7 @@ import android.widget.ImageButton; import android.widget.LinearLayout; import android.widget.RelativeLayout; import android.widget.TextView; +import android.widget.Toast; import java.util.EnumSet; @@ -176,9 +178,9 @@ public class MapActivity extends com.google.android.maps.MapActivity } @Override - protected void onStart() { + protected void onResume() { Log.d(TAG, "MapActivity.onStart"); - super.onStart(); + super.onResume(); dataHub.registerTrackDataListener(this, EnumSet.of( ListenerDataType.SELECTED_TRACK_CHANGED, @@ -199,12 +201,12 @@ public class MapActivity extends com.google.android.maps.MapActivity } @Override - protected void onStop() { + protected void onPause() { Log.d(TAG, "MapActivity.onStop"); dataHub.unregisterTrackDataListener(this); - super.onStop(); + super.onPause(); } // Utility functions: @@ -253,17 +255,18 @@ public class MapActivity extends com.google.android.maps.MapActivity * the current location is outside the visible area. */ private void showCurrentLocation() { - if (currentLocation == null || mapOverlay == null || mapView == null) { + if (mapOverlay == null || mapView == null) { return; } + mapOverlay.setMyLocation(currentLocation); mapView.postInvalidate(); - if (keepMyLocationVisible && !locationIsVisible(currentLocation)) { + + if (currentLocation != null && keepMyLocationVisible && !locationIsVisible(currentLocation)) { GeoPoint geoPoint = LocationUtils.getGeoPoint(currentLocation); MapController controller = mapView.getController(); controller.animateTo(geoPoint); } - } @Override @@ -334,9 +337,9 @@ public class MapActivity extends com.google.android.maps.MapActivity if (trackSelected) { busyPane.setVisibility(View.VISIBLE); - + zoomMapToBoundaries(track); - + mapOverlay.setShowEndMarker(!isRecording); busyPane.setVisibility(View.GONE); } @@ -462,21 +465,21 @@ public class MapActivity extends com.google.android.maps.MapActivity @Override public void onProviderStateChange(ProviderState state) { final int messageId; - final boolean bindClick; + final boolean isGpsDisabled; switch (state) { case DISABLED: messageId = R.string.status_enable_gps; - bindClick = true; + isGpsDisabled = true; break; case NO_FIX: case BAD_FIX: messageId = R.string.wait_for_fix; - bindClick = false; + isGpsDisabled = false; break; case GOOD_FIX: // Nothing to show. messageId = -1; - bindClick = false; + isGpsDisabled = false; break; default: throw new IllegalArgumentException("Unexpected state: " + state); @@ -489,7 +492,13 @@ public class MapActivity extends com.google.android.maps.MapActivity messageText.setText(messageId); messagePane.setVisibility(View.VISIBLE); - if (bindClick) { + if (isGpsDisabled) { + // Give a warning about this state. + Toast.makeText(MapActivity.this, + R.string.error_no_gps_location_provider, + Toast.LENGTH_LONG).show(); + + // Make clicking take the user to the location settings. messagePane.setOnClickListener(MapActivity.this); } else { messagePane.setOnClickListener(null); @@ -505,11 +514,6 @@ public class MapActivity extends com.google.android.maps.MapActivity @Override public void onCurrentLocationChanged(Location location) { - if (!location.getProvider().equals(LocationManager.GPS_PROVIDER)) { - Log.d(TAG, - "MapActivity: Network location update received (provider '" + location.getProvider() + "'."); - } - currentLocation = location; showCurrentLocation(); } diff --git a/MyTracks/src/com/google/android/apps/mytracks/MyTracks.java b/MyTracks/src/com/google/android/apps/mytracks/MyTracks.java index a69eb6910..ad8209b2e 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/MyTracks.java +++ b/MyTracks/src/com/google/android/apps/mytracks/MyTracks.java @@ -23,6 +23,7 @@ import static com.google.android.apps.mytracks.DialogManager.DIALOG_SEND_TO_GOOG import com.google.android.accounts.Account; import com.google.android.apps.mytracks.content.MyTracksProviderUtils; import com.google.android.apps.mytracks.content.Track; +import com.google.android.apps.mytracks.content.TrackDataHub; import com.google.android.apps.mytracks.content.WaypointCreationRequest; import com.google.android.apps.mytracks.io.AuthManager; import com.google.android.apps.mytracks.io.AuthManagerFactory; @@ -904,7 +905,7 @@ public class MyTracks extends TabActivity implements OnTouchListener, * Initiates the process to send tracks to google. * This is called once the user has selected sending options via the * SendToGoogleDialog. - * + * * TODO: Change this whole flow to an actual state machine. */ public void sendToGoogle() { @@ -962,8 +963,12 @@ public class MyTracks extends TabActivity implements OnTouchListener, // Update the map id for this track: try { Track track = providerUtils.getTrack(trackId); - track.setMapId(mapId); - providerUtils.updateTrack(track); + if (track != null) { + track.setMapId(mapId); + providerUtils.updateTrack(track); + } else { + Log.w(TAG, "Updating map id failed."); + } } catch (RuntimeException e) { // If that fails whatever reasons we'll just log an error, but // continue. @@ -1016,15 +1021,19 @@ public class MyTracks extends TabActivity implements OnTouchListener, // Update the table id for this track: try { Track track = providerUtils.getTrack(trackId); - track.setTableId(tableId); - providerUtils.updateTrack(track); + if (track != null) { + track.setTableId(tableId); + providerUtils.updateTrack(track); + } else { + Log.w(TAG, "Updating table id failed."); + } } catch (RuntimeException e) { // If that fails whatever reasons we'll just log an error, but // continue. Log.w(TAG, "Updating table id failed.", e); } } - + onSendToFusionTablesDone(); } }; @@ -1071,7 +1080,7 @@ public class MyTracks extends TabActivity implements OnTouchListener, Log.d(TAG, "Sending to Docs...."); setProgressValue(50); setProgressMessage(R.string.progress_message_sending_docs); - final SendToDocs sender = new SendToDocs(this, + final SendToDocs sender = new SendToDocs(this, authMap.get(SendToDocs.GDATA_SERVICE_NAME_TRIX), authMap.get(SendToDocs.GDATA_SERVICE_NAME_DOCLIST), trackId); sendToTrackId = trackId; @@ -1227,6 +1236,11 @@ public class MyTracks extends TabActivity implements OnTouchListener, */ public void saveTrack(long trackId, TrackFileFormat format) { TrackWriter writer = TrackWriterFactory.newWriter(this, providerUtils, trackId, format); + if (writer == null) { + dialogManager.showMessageDialog(R.string.error_track_does_not_exist, false); + return; + } + WriteProgressController controller = new WriteProgressController(this, writer); controller.setOnCompletionListener(new WriteProgressController.OnCompletionListener() { @Override @@ -1246,6 +1260,10 @@ public class MyTracks extends TabActivity implements OnTouchListener, */ public void sendTrack(long trackId, final TrackFileFormat format) { TrackWriter writer = TrackWriterFactory.newWriter(this, providerUtils, trackId, format); + if (writer == null) { + dialogManager.showMessageDialog(R.string.error_track_does_not_exist, false); + return; + } FileUtils fileUtils = new FileUtils(); String extension = format.getExtension(); @@ -1312,17 +1330,17 @@ public class MyTracks extends TabActivity implements OnTouchListener, long getRecordingTrackId() { return sharedPreferences.getLong(getString(R.string.recording_track_key), -1); } - + // @VisibleForTesting SharedPreferences getSharedPreferences() { return sharedPreferences; } - + // @VisibleForTesting static void clearInstance() { instance = null; } - + // @VisibleForTesting ITrackRecordingService getTrackRecordingService() { return trackRecordingService; diff --git a/MyTracks/src/com/google/android/apps/mytracks/StatsActivity.java b/MyTracks/src/com/google/android/apps/mytracks/StatsActivity.java index 45ac98624..e73111291 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/StatsActivity.java +++ b/MyTracks/src/com/google/android/apps/mytracks/StatsActivity.java @@ -17,8 +17,10 @@ package com.google.android.apps.mytracks; import static com.google.android.apps.mytracks.Constants.TAG; -import com.google.android.apps.mytracks.TrackDataHub.ListenerDataType; import com.google.android.apps.mytracks.content.Track; +import com.google.android.apps.mytracks.content.TrackDataHub; +import com.google.android.apps.mytracks.content.TrackDataHub.ListenerDataType; +import com.google.android.apps.mytracks.content.TrackDataListener; import com.google.android.apps.mytracks.content.Waypoint; import com.google.android.apps.mytracks.services.tasks.StatusAnnouncerFactory; import com.google.android.apps.mytracks.util.ApiFeatures; @@ -129,18 +131,18 @@ public class StatsActivity extends Activity implements TrackDataListener { } @Override - protected void onStart() { + protected void onResume() { dataHub.registerTrackDataListener(this, EnumSet.of( ListenerDataType.SELECTED_TRACK_CHANGED, ListenerDataType.TRACK_UPDATES, ListenerDataType.LOCATION_UPDATES, ListenerDataType.DISPLAY_PREFERENCES)); - super.onStart(); + super.onResume(); } @Override - protected void onStop() { + protected void onPause() { dataHub.unregisterTrackDataListener(this); if (thread != null) { @@ -153,6 +155,9 @@ public class StatsActivity extends Activity implements TrackDataListener { @Override public boolean onUnitsChanged(boolean metric) { + // Ignore if unchanged. + if (metric == utils.isMetricUnits()) return false; + utils.setMetricUnits(metric); updateLabels(); @@ -161,6 +166,9 @@ public class StatsActivity extends Activity implements TrackDataListener { @Override public boolean onReportSpeedChanged(boolean displaySpeed) { + // Ignore if unchanged. + if (displaySpeed == utils.isReportSpeed()) return false; + utils.setReportSpeed(displaySpeed); updateLabels(); @@ -248,21 +256,21 @@ public class StatsActivity extends Activity implements TrackDataListener { thread.interrupt(); thread = null; } - - if (track == null || track.getStatistics() == null) { - runOnUiThread(new Runnable() { - @Override - public void run() { - utils.setAllToUnknown(); - } - }); - } } @Override - public void onCurrentLocationChanged(Location loc) { + public void onCurrentLocationChanged(final Location loc) { if (dataHub.isRecordingSelected()) { - showLocation(loc); + runOnUiThread(new Runnable() { + @Override + public void run() { + if (loc != null) { + showLocation(loc); + } else { + showUnknownLocation(); + } + } + }); } } @@ -276,7 +284,12 @@ public class StatsActivity extends Activity implements TrackDataListener { switch (state) { case DISABLED: case NO_FIX: - showUnknownLocation(); + runOnUiThread(new Runnable() { + @Override + public void run() { + showUnknownLocation(); + } + }); break; } } @@ -286,6 +299,11 @@ public class StatsActivity extends Activity implements TrackDataListener { runOnUiThread(new Runnable() { @Override public void run() { + if (track == null || track.getStatistics() == null) { + utils.setAllToUnknown(); + return; + } + startTime = track.getStatistics().getStartTime(); if (!dataHub.isRecordingSelected()) { utils.setTime(R.id.total_time_register, diff --git a/MyTracks/src/com/google/android/apps/mytracks/StatsUtilities.java b/MyTracks/src/com/google/android/apps/mytracks/StatsUtilities.java index f6327dace..674122f2c 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/StatsUtilities.java +++ b/MyTracks/src/com/google/android/apps/mytracks/StatsUtilities.java @@ -83,6 +83,8 @@ public class StatsUtilities { public void setText(int id, double d, NumberFormat format) { if (!Double.isNaN(d) && !Double.isInfinite(d)) { setText(id, format.format(d)); + } else { + setUnknown(id); } } @@ -218,7 +220,7 @@ public class StatsUtilities { setGrade(R.id.max_grade_register, maxGrade); } - public void setAllStats(TripStatistics stats) { + public void setAllStats(TripStatistics stats) { setTime(R.id.moving_time_register, stats.getMovingTime()); setDistance(R.id.total_distance_register, stats.getTotalDistance() / 1000); setSpeed(R.id.average_speed_register, stats.getAverageSpeed() * 3.6); diff --git a/MyTracks/src/com/google/android/apps/mytracks/WaypointsList.java b/MyTracks/src/com/google/android/apps/mytracks/WaypointsList.java index cbaad0481..9dc0c4295 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/WaypointsList.java +++ b/MyTracks/src/com/google/android/apps/mytracks/WaypointsList.java @@ -1,12 +1,12 @@ /* * 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 @@ -73,15 +73,18 @@ public class WaypointsList extends ListActivity contextPosition = info.position; waypointId = WaypointsList.this.listView.getAdapter() .getItemId(contextPosition); - int type = providerUtils.getWaypoint(info.id).getType(); - menu.add(0, Constants.MENU_SHOW, 0, - R.string.waypointslist_show_waypoint); - menu.add(0, Constants.MENU_EDIT, 0, - R.string.waypointslist_edit_waypoint); - menu.add(0, Constants.MENU_DELETE, 0, - R.string.waypointslist_delete_waypoint).setEnabled( - recordingTrackId < 0 || type == Waypoint.TYPE_WAYPOINT || - info.id != providerUtils.getLastWaypointId(recordingTrackId)); + Waypoint waypoint = providerUtils.getWaypoint(info.id); + if (waypoint != null) { + int type = waypoint.getType(); + menu.add(0, Constants.MENU_SHOW, 0, + R.string.waypointslist_show_waypoint); + menu.add(0, Constants.MENU_EDIT, 0, + R.string.waypointslist_edit_waypoint); + menu.add(0, Constants.MENU_DELETE, 0, + R.string.waypointslist_delete_waypoint).setEnabled( + recordingTrackId < 0 || type == Waypoint.TYPE_WAYPOINT || + info.id != providerUtils.getLastWaypointId(recordingTrackId)); + } } }; @@ -159,7 +162,7 @@ public class WaypointsList extends ListActivity trackId = -1; } - final long firstWaypointId = providerUtils.getFirstWaypointId(trackId); + final long firstWaypointId = providerUtils.getFirstWaypointId(trackId); waypointsCursor = getContentResolver().query( WaypointsColumns.CONTENT_URI, null, WaypointsColumns.TRACKID + "=" + trackId + " AND " @@ -209,7 +212,7 @@ public class WaypointsList extends ListActivity new String[] { WaypointsColumns.NAME, WaypointsColumns.TIME, WaypointsColumns.CATEGORY, WaypointsColumns.TYPE }, new int[] { R.id.waypointslist_item_name, - R.id.waypointslist_item_time, + R.id.waypointslist_item_time, R.id.waypointslist_item_category, R.id.waypointslist_item_icon }); @@ -219,7 +222,7 @@ public class WaypointsList extends ListActivity waypointsCursor.getColumnIndexOrThrow(WaypointsColumns.TYPE); adapter.setViewBinder(new SimpleCursorAdapter.ViewBinder() { @Override - public boolean setViewValue(View view, Cursor cursor, int columnIndex) { + public boolean setViewValue(View view, Cursor cursor, int columnIndex) { if (columnIndex == timeIdx) { long time = cursor.getLong(timeIdx); TextView textView = (TextView) view; diff --git a/MyTracks/src/com/google/android/apps/mytracks/content/DataSourceManager.java b/MyTracks/src/com/google/android/apps/mytracks/content/DataSourceManager.java new file mode 100644 index 000000000..2b532bfa4 --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/content/DataSourceManager.java @@ -0,0 +1,264 @@ +package com.google.android.apps.mytracks.content; + +import static com.google.android.apps.mytracks.Constants.TAG; + +import com.google.android.apps.mytracks.content.TrackDataHub.ListenerDataType; + +import android.content.SharedPreferences; +import android.content.SharedPreferences.OnSharedPreferenceChangeListener; +import android.database.ContentObserver; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; +import android.location.Location; +import android.location.LocationListener; +import android.location.LocationManager; +import android.location.LocationProvider; +import android.os.Bundle; +import android.os.Handler; +import android.util.Log; + +import java.util.EnumSet; +import java.util.Set; + +/** + * External data source manager, which converts system-level events into My Tracks data events. + * + * @author Rodrigo Damazio + */ +class DataSourceManager { + + /** Single interface for receiving system events that were registered for. */ + interface DataSourceListener { + void notifyTrackUpdated(); + void notifyWaypointUpdated(); + void notifyPointsUpdated(); + void notifyPreferenceChanged(String key); + void notifyLocationProviderEnabled(boolean enabled); + void notifyLocationChanged(Location loc); + void notifyHeadingChanged(float heading); + } + + private final DataSourceListener listener; + + /** Observer for when the tracks table is updated. */ + private class TrackObserver extends ContentObserver { + public TrackObserver() { + super(contentHandler); + } + + @Override + public void onChange(boolean selfChange) { + listener.notifyTrackUpdated(); + } + } + + /** Observer for when the waypoints table is updated. */ + private class WaypointObserver extends ContentObserver { + public WaypointObserver() { + super(contentHandler); + } + + @Override + public void onChange(boolean selfChange) { + listener.notifyWaypointUpdated(); + } + } + + /** Observer for when the points table is updated. */ + private class PointObserver extends ContentObserver { + public PointObserver() { + super(contentHandler); + } + + @Override + public void onChange(boolean selfChange) { + listener.notifyPointsUpdated(); + } + } + + /** Listener for when preferences change. */ + private class HubSharedPreferenceListener implements OnSharedPreferenceChangeListener { + @Override + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { + listener.notifyPreferenceChanged(key); + } + } + + /** Listener for the current location (independent from track data). */ + private class CurrentLocationListener implements + LocationListener { + @Override + public void onStatusChanged(String provider, int status, Bundle extras) { + if (!LocationManager.GPS_PROVIDER.equals(provider)) return; + + listener.notifyLocationProviderEnabled(status == LocationProvider.AVAILABLE); + } + + @Override + public void onProviderEnabled(String provider) { + if (!LocationManager.GPS_PROVIDER.equals(provider)) return; + + listener.notifyLocationProviderEnabled(true); + } + + @Override + public void onProviderDisabled(String provider) { + if (!LocationManager.GPS_PROVIDER.equals(provider)) return; + + listener.notifyLocationProviderEnabled(false); + } + + @Override + public void onLocationChanged(Location location) { + listener.notifyLocationChanged(location); + } + } + + /** Listener for compass readings. */ + private class CompassListener implements + SensorEventListener { + @Override + public void onSensorChanged(SensorEvent event) { + listener.notifyHeadingChanged(event.values[0]); + } + + @Override + public void onAccuracyChanged(Sensor sensor, int accuracy) { + // Do nothing + } + } + + /** Wrapper for registering internal listeners. */ + private final DataSourcesWrapper dataSources; + + // Internal listeners (to receive data from the system) + private final Set registeredListeners = + EnumSet.noneOf(ListenerDataType.class); + private final Handler contentHandler; + private final ContentObserver pointObserver; + private final ContentObserver waypointObserver; + private final ContentObserver trackObserver; + private final LocationListener locationListener; + private final OnSharedPreferenceChangeListener preferenceListener; + private final SensorEventListener compassListener; + + DataSourceManager(DataSourceListener listener, DataSourcesWrapper dataSources) { + this.listener = listener; + this.dataSources = dataSources; + + contentHandler = new Handler(); + pointObserver = new PointObserver(); + waypointObserver = new WaypointObserver(); + trackObserver = new TrackObserver(); + + compassListener = new CompassListener(); + locationListener = new CurrentLocationListener(); + preferenceListener = new HubSharedPreferenceListener(); + } + + /** Updates the internal (sensor, position, etc) listeners. */ + void updateAllListeners(EnumSet externallyNeededListeners) { + EnumSet neededListeners = EnumSet.copyOf(externallyNeededListeners); + + // Special case - map sampled-out points type to points type since they + // correspond to the same internal listener. + if (neededListeners.contains(ListenerDataType.SAMPLED_OUT_POINT_UPDATES)) { + neededListeners.remove(ListenerDataType.SAMPLED_OUT_POINT_UPDATES); + neededListeners.add(ListenerDataType.POINT_UPDATES); + } + + Log.d(TAG, "Updating internal listeners to types " + neededListeners); + + // Unnecessary = registered - needed + Set unnecessaryListeners = EnumSet.copyOf(registeredListeners); + unnecessaryListeners.removeAll(neededListeners); + + // Missing = needed - registered + Set missingListeners = EnumSet.copyOf(neededListeners); + missingListeners.removeAll(registeredListeners); + + // Remove all unnecessary listeners. + for (ListenerDataType type : unnecessaryListeners) { + unregisterListener(type); + } + + // Add all missing listeners. + for (ListenerDataType type : missingListeners) { + registerListener(type); + } + + // Now all needed types are registered. + registeredListeners.clear(); + registeredListeners.addAll(neededListeners); + } + + private void registerListener(ListenerDataType type) { + switch (type) { + case COMPASS_UPDATES: { + // Listen to compass + Sensor compass = dataSources.getSensor(Sensor.TYPE_ORIENTATION); + if (compass != null) { + Log.d(TAG, "TrackDataHub: Now registering sensor listener."); + dataSources.registerSensorListener(compassListener, compass, SensorManager.SENSOR_DELAY_UI); + } + break; + } + case LOCATION_UPDATES: + dataSources.requestLocationUpdates(locationListener); + break; + case POINT_UPDATES: + dataSources.registerContentObserver( + TrackPointsColumns.CONTENT_URI, false, pointObserver); + break; + case TRACK_UPDATES: + dataSources.registerContentObserver(TracksColumns.CONTENT_URI, false, trackObserver); + break; + case WAYPOINT_UPDATES: + dataSources.registerContentObserver( + WaypointsColumns.CONTENT_URI, false, waypointObserver); + break; + case DISPLAY_PREFERENCES: + dataSources.registerOnSharedPreferenceChangeListener(preferenceListener); + break; + case SAMPLED_OUT_POINT_UPDATES: + throw new IllegalArgumentException("Should have been mapped to point updates"); + } + } + + private void unregisterListener(ListenerDataType type) { + switch (type) { + case COMPASS_UPDATES: + dataSources.unregisterSensorListener(compassListener); + break; + case LOCATION_UPDATES: + dataSources.removeLocationUpdates(locationListener); + break; + case POINT_UPDATES: + dataSources.unregisterContentObserver(pointObserver); + break; + case TRACK_UPDATES: + dataSources.unregisterContentObserver(trackObserver); + break; + case WAYPOINT_UPDATES: + dataSources.unregisterContentObserver(waypointObserver); + break; + case DISPLAY_PREFERENCES: + dataSources.unregisterOnSharedPreferenceChangeListener(preferenceListener); + break; + case SAMPLED_OUT_POINT_UPDATES: + throw new IllegalArgumentException("Should have been mapped to point updates"); + } + } + + /** Unregisters all internal (sensor, position, etc.) listeners. */ + void unregisterAllListeners() { + dataSources.removeLocationUpdates(locationListener); + dataSources.unregisterSensorListener(compassListener); + dataSources.unregisterContentObserver(trackObserver); + dataSources.unregisterContentObserver(waypointObserver); + dataSources.unregisterContentObserver(pointObserver); + dataSources.unregisterOnSharedPreferenceChangeListener(preferenceListener); + } +} diff --git a/MyTracks/src/com/google/android/apps/mytracks/TrackDataSources.java b/MyTracks/src/com/google/android/apps/mytracks/content/DataSourcesWrapper.java similarity index 91% rename from MyTracks/src/com/google/android/apps/mytracks/TrackDataSources.java rename to MyTracks/src/com/google/android/apps/mytracks/content/DataSourcesWrapper.java index 8801532d8..a4dbf3544 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/TrackDataSources.java +++ b/MyTracks/src/com/google/android/apps/mytracks/content/DataSourcesWrapper.java @@ -13,7 +13,7 @@ * License for the specific language governing permissions and limitations under * the License. */ -package com.google.android.apps.mytracks; +package com.google.android.apps.mytracks.content; import android.content.SharedPreferences.OnSharedPreferenceChangeListener; import android.database.ContentObserver; @@ -24,11 +24,11 @@ import android.location.LocationListener; import android.net.Uri; /** - * Interface for abstracting external data sources. + * Interface for abstracting registration of external data source listeners. * * @author Rodrigo Damazio */ -interface TrackDataSources { +interface DataSourcesWrapper { // Preferences void registerOnSharedPreferenceChangeListener( OnSharedPreferenceChangeListener listener); diff --git a/MyTracks/src/com/google/android/apps/mytracks/TrackDataSourcesImpl.java b/MyTracks/src/com/google/android/apps/mytracks/content/DataSourcesWrapperImpl.java similarity index 95% rename from MyTracks/src/com/google/android/apps/mytracks/TrackDataSourcesImpl.java rename to MyTracks/src/com/google/android/apps/mytracks/content/DataSourcesWrapperImpl.java index 420440beb..c4a94c3a0 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/TrackDataSourcesImpl.java +++ b/MyTracks/src/com/google/android/apps/mytracks/content/DataSourcesWrapperImpl.java @@ -13,11 +13,12 @@ * License for the specific language governing permissions and limitations under * the License. */ -package com.google.android.apps.mytracks; +package com.google.android.apps.mytracks.content; import static com.google.android.apps.mytracks.Constants.MAX_LOCATION_AGE_MS; import static com.google.android.apps.mytracks.Constants.MAX_NETWORK_AGE_MS; +import com.google.android.apps.mytracks.Constants; import com.google.android.maps.mytracks.R; import android.content.ContentResolver; @@ -41,7 +42,7 @@ import android.widget.Toast; * * @author Rodrigo Damazio */ -class TrackDataSourcesImpl implements TrackDataSources { +class DataSourcesWrapperImpl implements DataSourcesWrapper { // System services private final SensorManager sensorManager; private final LocationManager locationManager; @@ -49,7 +50,7 @@ class TrackDataSourcesImpl implements TrackDataSources { private final SharedPreferences sharedPreferences; private final Context context; - TrackDataSourcesImpl(Context context, SharedPreferences sharedPreferences) { + DataSourcesWrapperImpl(Context context, SharedPreferences sharedPreferences) { this.context = context; this.sensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE); this.locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE); diff --git a/MyTracks/src/com/google/android/apps/mytracks/TrackDataHub.java b/MyTracks/src/com/google/android/apps/mytracks/content/TrackDataHub.java similarity index 60% rename from MyTracks/src/com/google/android/apps/mytracks/TrackDataHub.java rename to MyTracks/src/com/google/android/apps/mytracks/content/TrackDataHub.java index eac3f3df5..b5bf78e61 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/TrackDataHub.java +++ b/MyTracks/src/com/google/android/apps/mytracks/content/TrackDataHub.java @@ -1,68 +1,58 @@ /* * Copyright 2011 Google Inc. - * + * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. */ -package com.google.android.apps.mytracks; +package com.google.android.apps.mytracks.content; +import static com.google.android.apps.mytracks.Constants.DEFAULT_MIN_REQUIRED_ACCURACY; +import static com.google.android.apps.mytracks.Constants.MAX_DISPLAYED_WAYPOINTS_POINTS; import static com.google.android.apps.mytracks.Constants.MAX_LOCATION_AGE_MS; +import static com.google.android.apps.mytracks.Constants.MAX_NETWORK_AGE_MS; import static com.google.android.apps.mytracks.Constants.TAG; +import static com.google.android.apps.mytracks.Constants.TARGET_DISPLAYED_TRACK_POINTS; -import com.google.android.apps.mytracks.TrackDataListener.ProviderState; -import com.google.android.apps.mytracks.content.MyTracksProviderUtils; +import com.google.android.apps.mytracks.content.DataSourceManager.DataSourceListener; +import com.google.android.apps.mytracks.content.MyTracksProviderUtils.DoubleBufferedLocationFactory; import com.google.android.apps.mytracks.content.MyTracksProviderUtils.LocationIterator; -import com.google.android.apps.mytracks.content.Track; -import com.google.android.apps.mytracks.content.TrackPointsColumns; -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.TrackDataListener.ProviderState; +import com.google.android.apps.mytracks.content.TrackDataListeners.ListenerRegistration; import com.google.android.apps.mytracks.util.ApiFeatures; import com.google.android.apps.mytracks.util.LocationUtils; import com.google.android.maps.mytracks.R; import android.content.Context; import android.content.SharedPreferences; -import android.content.SharedPreferences.OnSharedPreferenceChangeListener; -import android.database.ContentObserver; import android.database.Cursor; import android.hardware.GeomagneticField; -import android.hardware.Sensor; -import android.hardware.SensorEvent; -import android.hardware.SensorEventListener; -import android.hardware.SensorManager; import android.location.Location; -import android.location.LocationListener; import android.location.LocationManager; -import android.location.LocationProvider; -import android.os.Bundle; import android.os.Handler; import android.os.HandlerThread; import android.util.Log; -import android.widget.Toast; import java.util.Collections; -import java.util.EnumMap; import java.util.EnumSet; -import java.util.HashMap; -import java.util.LinkedHashSet; -import java.util.Map; import java.util.Set; /** * Track data hub, which receives data (both live and recorded) from many * different sources and distributes it to those interested after some standard * processing. - * + * + * TODO: Simplify the threading model here, it's overly complex and it's not obvious why + * certain race conditions won't happen. + * * @author Rodrigo Damazio */ public class TrackDataHub { @@ -74,6 +64,9 @@ public class TrackDataHub { private final String METRIC_UNITS_KEY; private final String SPEED_REPORTING_KEY; + // Overridable constants + private final int targetNumPoints; + /** Types of data that we can expose. */ public static enum ListenerDataType { /** Listen to when the selected track changes. */ @@ -105,37 +98,72 @@ public class TrackDataHub { DISPLAY_PREFERENCES; } + /** Listener which receives events from the system. */ + private class HubDataSourceListener implements DataSourceListener { + @Override + public void notifyTrackUpdated() { + TrackDataHub.this.notifyTrackUpdated(getListenersFor(ListenerDataType.TRACK_UPDATES)); + } + + @Override + public void notifyWaypointUpdated() { + TrackDataHub.this.notifyWaypointUpdated(getListenersFor(ListenerDataType.WAYPOINT_UPDATES)); + } + + @Override + public void notifyPointsUpdated() { + TrackDataHub.this.notifyPointsUpdated(true, 0, 0, + getListenersFor(ListenerDataType.POINT_UPDATES), + getListenersFor(ListenerDataType.SAMPLED_OUT_POINT_UPDATES)); + } + + @Override + public void notifyPreferenceChanged(String key) { + TrackDataHub.this.notifyPreferenceChanged(key); + } + + @Override + public void notifyLocationProviderEnabled(boolean enabled) { + hasProviderEnabled = enabled; + TrackDataHub.this.notifyFixType(); + } + + @Override + public void notifyLocationChanged(Location loc) { + TrackDataHub.this.notifyLocationChanged(loc, + getListenersFor(ListenerDataType.LOCATION_UPDATES)); + } + + @Override + public void notifyHeadingChanged(float heading) { + lastSeenMagneticHeading = heading; + maybeUpdateDeclination(); + TrackDataHub.this.notifyHeadingChanged(getListenersFor(ListenerDataType.COMPASS_UPDATES)); + } + } + // Application services private final Context context; - private final TrackDataSources dataSources; private final MyTracksProviderUtils providerUtils; private final SharedPreferences preferences; - // Internal listeners (to receive data from the system) - private final ContentObserver pointObserver; - private final ContentObserver waypointObserver; - private final ContentObserver trackObserver; - private final LocationListener locationListener; - private final OnSharedPreferenceChangeListener preferenceListener; - private final SensorEventListener compassListener; - - /** Set of internal listeners which are already registered. */ - private final Set registeredInternalListeners = - EnumSet.noneOf(ListenerDataType.class); - - /** Map of external listener to its registration details. */ - private final Map registeredListeners = - new HashMap(); - - /** Map of data type to external listeners interested in it. */ - private final Map> listenerSetsPerType = - new EnumMap>(ListenerDataType.class); - // Get content notifications on the main thread, send listener callbacks in another. // This ensures listener calls are serialized. private final HandlerThread listenerHandlerThread; private final Handler listenerHandler; + /** Manager for external listeners (those from activities). */ + private final TrackDataListeners listeners; + + /** Wrapper for interacting with system data managers. */ + private final DataSourcesWrapper dataSources; + + /** Manager for system data listener registrations. */ + private final DataSourceManager dataSourceManager; + + /** Condensed listener for system data listener events. */ + private final DataSourceListener dataSourceListener = new HubDataSourceListener(); + /** Whether we've been started. */ private boolean started; @@ -151,169 +179,41 @@ public class TrackDataHub { // Cached GPS readings private Location lastSeenLocation; - private boolean hasProviderEnabled; + private boolean hasProviderEnabled = true; private boolean hasFix; private boolean hasGoodFix; // Transient state about the selected track private long selectedTrackId; - private long recordingTrackId; private long firstSeenLocationId; private long lastSeenLocationId; private int numLoadedPoints; - - /** Internal representation of a listener's registration. */ - private static class ListenerRegistration { - final TrackDataListener listener; - final EnumSet types; - // TODO: Add the last-notified point ID here, to allow pausing/resuming. - - public ListenerRegistration(TrackDataListener listener, - EnumSet types) { - this.listener = listener; - this.types = types; - } - - public boolean isInterestedIn(ListenerDataType type) { - return types.contains(type); - } - } - - /** Callback for when the tracks table is updated. */ - private class TrackObserverCallback implements Runnable { - @Override - public void run() { - notifyTrackUpdated(getListenersFor(ListenerDataType.TRACK_UPDATES)); - } - } - - /** Callback for when the waypoints table is updated. */ - private class WaypointObserverCallback implements Runnable { - @Override - public void run() { - notifyWaypointUpdated(getListenersFor(ListenerDataType.WAYPOINT_UPDATES)); - } - } - - /** Callback for when the points table is updated. */ - private class PointObserverCallback implements Runnable { - @Override - public void run() { - notifyPointsUpdated(true, - getListenersFor(ListenerDataType.POINT_UPDATES), - getListenersFor(ListenerDataType.SAMPLED_OUT_POINT_UPDATES)); - } - } - - /** Listener for when preferences change. */ - private class HubSharedPreferenceListener implements OnSharedPreferenceChangeListener { - @Override - public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { - notifyPreferenceChanged(key); - } - } - - /** - * Generic content observer which will call a given {@link Runnable} in the - * given handler if the content has changed and we're recording the selected - * track. - */ - private class TrackContentObserver extends ContentObserver { - private final Runnable callback; - - public TrackContentObserver(Handler contentHandler, Runnable callback) { - super(contentHandler); - - this.callback = callback; - } - - @Override - public void onChange(boolean selfChange) { - Log.v(TAG, "TrackContentObserver.onChange"); - - // We want to filter only updates from the selected track, but since - // we can't see what the update is, we'll let two cases pass: - // 1 - The point(s) was(ere) changed because it's a recording track - // (and thus we care about it if the recording is the selected one) - // 2 - The point(s) was(ere) changed because it's syncing a track - // (and thus there will be no new points for the selected one) - if (!isRecordingSelected()) { - return; - } - - // Update can potentially be lengthy, put it in its own thread: - runInListenerThread(callback); - } - } - - /** Listener for the current location (independent from track data). */ - private class CurrentLocationListener implements - LocationListener { - @Override - public void onStatusChanged(String provider, int status, Bundle extras) { - if (!LocationManager.GPS_PROVIDER.equals(provider)) return; - - hasProviderEnabled = (status == LocationProvider.AVAILABLE); - notifyFixType(); - } - - @Override - public void onProviderEnabled(String provider) { - if (!LocationManager.GPS_PROVIDER.equals(provider)) return; - - hasProviderEnabled = true; - notifyFixType(); - } - - @Override - public void onProviderDisabled(String provider) { - if (!LocationManager.GPS_PROVIDER.equals(provider)) return; - - hasProviderEnabled = false; - notifyFixType(); - } - - @Override - public void onLocationChanged(Location location) { - notifyLocationChanged(location, - getListenersFor(ListenerDataType.LOCATION_UPDATES)); - } - } - - /** Listener for compass readings. */ - private class CompassListener implements - SensorEventListener { - @Override - public void onSensorChanged(SensorEvent event) { - lastSeenMagneticHeading = event.values[0]; - maybeUpdateDeclination(); - notifyHeadingChanged(getListenersFor(ListenerDataType.COMPASS_UPDATES)); - } - - @Override - public void onAccuracyChanged(Sensor sensor, int accuracy) { - // Do nothing - } - } + private int lastSamplingFrequency; + private DoubleBufferedLocationFactory locationFactory; /** * Default constructor. */ public TrackDataHub(Context ctx, SharedPreferences preferences, MyTracksProviderUtils providerUtils) { - this(ctx, new TrackDataSourcesImpl(ctx, preferences), preferences, providerUtils); + this(ctx, new DataSourcesWrapperImpl(ctx, preferences), new TrackDataListeners(), + preferences, providerUtils, TARGET_DISPLAYED_TRACK_POINTS); } /** * Injection constructor. */ // @VisibleForTesting - TrackDataHub(Context ctx, TrackDataSources dataSources, SharedPreferences preferences, - MyTracksProviderUtils providerUtils) { + TrackDataHub(Context ctx, DataSourcesWrapper dataSources, TrackDataListeners listeners, + SharedPreferences preferences, MyTracksProviderUtils providerUtils, int targetNumPoints) { this.context = ctx; - this.dataSources = dataSources; + this.listeners = listeners; this.preferences = preferences; this.providerUtils = providerUtils; + this.dataSources = dataSources; + this.targetNumPoints = targetNumPoints; + this.dataSourceManager = new DataSourceManager(dataSourceListener, dataSources); + this.locationFactory = new DoubleBufferedLocationFactory(); SELECTED_TRACK_KEY = context.getString(R.string.selected_track_key); RECORDING_TRACK_KEY = context.getString(R.string.recording_track_key); @@ -321,24 +221,11 @@ public class TrackDataHub { METRIC_UNITS_KEY = context.getString(R.string.metric_units_key); SPEED_REPORTING_KEY = context.getString(R.string.report_speed_key); - // Create sets for all data type at startup. - for (ListenerDataType type : ListenerDataType.values()) { - listenerSetsPerType.put(type, new LinkedHashSet()); - } - listenerHandlerThread = new HandlerThread("trackDataContentThread"); listenerHandlerThread.start(); listenerHandler = new Handler(listenerHandlerThread.getLooper()); - preferenceListener = new HubSharedPreferenceListener(); - - Handler contentHandler = new Handler(); - pointObserver = new TrackContentObserver(contentHandler, new PointObserverCallback()); - waypointObserver = new TrackContentObserver(contentHandler, new WaypointObserverCallback()); - trackObserver = new TrackContentObserver(contentHandler, new TrackObserverCallback()); - - compassListener = new CompassListener(); - locationListener = new CurrentLocationListener(); + resetState(); } /** @@ -353,25 +240,21 @@ public class TrackDataHub { } started = true; - dataSources.registerOnSharedPreferenceChangeListener(preferenceListener); - loadSharedPreferences(); - // This may or may not register internal listeners, depending on whether // we already had external listeners. - updateInternalListeners(); + dataSourceManager.updateAllListeners(getNeededListenerTypes()); + loadSharedPreferences(); // If there were listeners already registered, make sure they become up-to-date. - // TODO: This should really only send new data (in a start-stop-start cycle). loadDataForAllListeners(); } private void loadSharedPreferences() { selectedTrackId = preferences.getLong(SELECTED_TRACK_KEY, -1); - recordingTrackId = preferences.getLong(RECORDING_TRACK_KEY, -1); useMetricUnits = preferences.getBoolean(METRIC_UNITS_KEY, true); reportSpeed = preferences.getBoolean(SPEED_REPORTING_KEY, true); minRequiredAccuracy = preferences.getInt(MIN_REQUIRED_ACCURACY_KEY, - Constants.DEFAULT_MIN_REQUIRED_ACCURACY); + DEFAULT_MIN_REQUIRED_ACCURACY); } /** @@ -385,10 +268,8 @@ public class TrackDataHub { return; } - dataSources.unregisterOnSharedPreferenceChangeListener(preferenceListener); - // Unregister internal listeners even if there are external listeners registered. - unregisterInternalListeners(); + dataSourceManager.unregisterAllListeners(); started = false; } @@ -470,7 +351,7 @@ public class TrackDataHub { if (!started) { loadSharedPreferences(); } - return recordingTrackId > 0; + return preferences.getLong(RECORDING_TRACK_KEY, -1) > 0; } /** Returns whether the selected track is still being recorded. */ @@ -478,6 +359,7 @@ public class TrackDataHub { if (!started) { loadSharedPreferences(); } + long recordingTrackId = preferences.getLong(RECORDING_TRACK_KEY, -1); return recordingTrackId > 0 && recordingTrackId == selectedTrackId; } @@ -500,11 +382,20 @@ public class TrackDataHub { selectedTrackId = trackId; // Force it to reload data from the beginning. + Log.d(TAG, "Loading track"); + resetState(); + + loadDataForAllListeners(); + } + + /** + * Resets the internal state of what data has already been loaded into listeners. + */ + private void resetState() { firstSeenLocationId = -1; lastSeenLocationId = -1; numLoadedPoints = 0; - - loadDataForAllListeners(); + lastSamplingFrequency = -1; } /** @@ -514,188 +405,82 @@ public class TrackDataHub { loadTrack(-1); } - /** - * Registers a listener to send data to. - * It is ok to call this method before {@link start}, and in that case - * the data will only be passed to listeners when {@link start} is called. - * - * @param listener the listener to register - * @param dataTypes the type of data that the listener is interested in - */ - public void registerTrackDataListener(final TrackDataListener listener, EnumSet dataTypes) { - Log.d(TAG, "Registered track data listener: " + listener); - ListenerRegistration registration = new ListenerRegistration(listener, dataTypes); - synchronized (registeredListeners) { - if (registeredListeners.get(listener) != null) { - throw new IllegalStateException("Listener already registered"); - } - registeredListeners.put(listener, registration); - - for (ListenerDataType type : dataTypes) { - // This is guaranteed not to be null. - Set typeSet = listenerSetsPerType.get(type); - typeSet.add(listener); - } + public void registerTrackDataListener( + TrackDataListener listener, EnumSet dataTypes) { + synchronized (listeners) { + ListenerRegistration registration = listeners.registerTrackDataListener(listener, dataTypes); // Don't load any data or start internal listeners if start() hasn't been // called. When it is called, we'll do both things. if (!started) return; reloadDataForListener(registration); - } - updateInternalListeners(); + dataSourceManager.updateAllListeners(getNeededListenerTypes()); + } } - /** - * Unregisters a listener to send data to. - * - * @param listener the listener to unregister - */ public void unregisterTrackDataListener(TrackDataListener listener) { - Log.d(TAG, "Unregistered track data listener: " + listener); - synchronized (registeredListeners) { - // Remove and keep the corresponding registration. - ListenerRegistration match = registeredListeners.remove(listener); - if (match == null) { - Log.w(TAG, "Tried to unregister listener which is not registered."); - return; - } - - // Remove it from the per-type sets - for (ListenerDataType type : match.types) { - listenerSetsPerType.get(type).remove(listener); - } + synchronized (listeners) { + listeners.unregisterTrackDataListener(listener); // Don't load any data or start internal listeners if start() hasn't been // called. When it is called, we'll do both things. if (!started) return; + + dataSourceManager.updateAllListeners(getNeededListenerTypes()); } - updateInternalListeners(); - } - - /** Updates the internal (sensor, position, etc) listeners. */ - private void updateInternalListeners() { - synchronized (registeredListeners) { - Set registeredListeners = registeredInternalListeners; - Set neededListeners = EnumSet.noneOf(ListenerDataType.class); - for (ListenerRegistration registration : this.registeredListeners.values()) { - neededListeners.addAll(registration.types); - } - - // Unnecessary = registered - needed - Set unnecessaryListeners = EnumSet.copyOf(registeredListeners); - unnecessaryListeners.removeAll(neededListeners); - - // Missing = needed - registered - Set missingListeners = EnumSet.copyOf(neededListeners); - missingListeners.removeAll(registeredListeners); - - // Remove all unnecessary listeners. - for (ListenerDataType type : unnecessaryListeners) { - switch (type) { - case COMPASS_UPDATES: - dataSources.unregisterSensorListener(compassListener); - break; - case LOCATION_UPDATES: - dataSources.removeLocationUpdates(locationListener); - break; - case POINT_UPDATES: - case SAMPLED_OUT_POINT_UPDATES: - // Special case - don't unregister if the other type is needed. - if (!neededListeners.contains(ListenerDataType.POINT_UPDATES) && - !neededListeners.contains(ListenerDataType.SAMPLED_OUT_POINT_UPDATES)) { - dataSources.unregisterContentObserver(pointObserver); - } - break; - case TRACK_UPDATES: - dataSources.unregisterContentObserver(trackObserver); - break; - case WAYPOINT_UPDATES: - dataSources.unregisterContentObserver(waypointObserver); - break; - } - } - - // Add all missing listeners. - for (ListenerDataType type : missingListeners) { - switch (type) { - case COMPASS_UPDATES: { - // Listen to compass - Sensor compass = dataSources.getSensor(Sensor.TYPE_ORIENTATION); - if (compass != null) { - Log.d(Constants.TAG, "TrackDataHub: Now registering sensor listener."); - dataSources.registerSensorListener(compassListener, compass, SensorManager.SENSOR_DELAY_UI); - } - break; - } - case LOCATION_UPDATES: - dataSources.requestLocationUpdates(locationListener); - break; - case POINT_UPDATES: - case SAMPLED_OUT_POINT_UPDATES: - // Special case - don't register if the other type was already registered. - if (!registeredListeners.contains(ListenerDataType.POINT_UPDATES) && - !registeredListeners.contains(ListenerDataType.SAMPLED_OUT_POINT_UPDATES)) { - dataSources.registerContentObserver( - TrackPointsColumns.CONTENT_URI, false, pointObserver); - } - break; - case TRACK_UPDATES: - dataSources.registerContentObserver(TracksColumns.CONTENT_URI, false, trackObserver); - break; - case WAYPOINT_UPDATES: - dataSources.registerContentObserver( - WaypointsColumns.CONTENT_URI, false, waypointObserver); - break; - } - } - - // Now all needed types are registered. - registeredInternalListeners.clear(); - registeredInternalListeners.addAll(neededListeners); - } // synchronized - } - - /** Unregisters all internal (sensor, position, etc.) listeners. */ - private void unregisterInternalListeners() { - dataSources.removeLocationUpdates(locationListener); - dataSources.unregisterSensorListener(compassListener); - dataSources.unregisterContentObserver(trackObserver); - dataSources.unregisterContentObserver(waypointObserver); - dataSources.unregisterContentObserver(pointObserver); } /** * Reloads all track data received so far into the specified listeners. */ public void reloadDataForListener(TrackDataListener listener) { - reloadDataForListener(registeredListeners.get(listener)); + ListenerRegistration registration; + synchronized (listeners) { + registration = listeners.getRegistration(listener); + reloadDataForListener(registration); + } } /** * Reloads all track data received so far into the specified listeners. + * + * Assumes it's called from a block that synchronizes on {@link #listeners}. */ private void reloadDataForListener(final ListenerRegistration registration) { if (!started) { Log.w(TAG, "Not started, not reloading"); return; } + if (registration == null) { + return; + } + + // If a listener happens to be added after this method but before the Runnable below is + // executed, it will have triggered a separate call to load data only up to the point this + // listener got to. This is ensured by being synchronized on listeners. + final boolean isOnlyListener = (listeners.getNumListeners() == 1); runInListenerThread(new Runnable() { @SuppressWarnings("unchecked") @Override public void run() { + // Reload everything if either it's a different track, or the track has been resampled + // (this also covers the case of a new registration). + boolean reloadAll = registration.lastTrackId != selectedTrackId || + registration.lastSamplingFrequency != lastSamplingFrequency; + Log.d(TAG, "Doing a " + (reloadAll ? "full" : "partial") + " reload for " + registration); + TrackDataListener listener = registration.listener; Set listenerSet = Collections.singleton(listener); if (registration.isInterestedIn(ListenerDataType.DISPLAY_PREFERENCES)) { - // Ignore the return values here, we're already sending the full data set anyway - listener.onUnitsChanged(useMetricUnits); - listener.onReportSpeedChanged(reportSpeed); + reloadAll |= listener.onUnitsChanged(useMetricUnits); + reloadAll |= listener.onReportSpeedChanged(reportSpeed); } - if (registration.isInterestedIn(ListenerDataType.SELECTED_TRACK_CHANGED)) { + if (reloadAll && registration.isInterestedIn(ListenerDataType.SELECTED_TRACK_CHANGED)) { notifySelectedTrackChanged(selectedTrackId, listenerSet); } @@ -708,8 +493,27 @@ public class TrackDataHub { boolean interestedInSampledOutPoints = registration.isInterestedIn(ListenerDataType.SAMPLED_OUT_POINT_UPDATES); if (interestedInPoints || interestedInSampledOutPoints) { - notifyPointsCleared(listenerSet); - notifyPointsUpdated(false, + long minPointId = 0; + int previousNumPoints = 0; + + if (reloadAll) { + // Clear existing points and send them all again + notifyPointsCleared(listenerSet); + } else { + // Send only new points + minPointId = registration.lastPointId + 1; + previousNumPoints = registration.numLoadedPoints; + } + + // If this is the only listener we have registered, keep the state that we serve to it as + // a reference for other future listeners. + if (isOnlyListener && reloadAll) { + resetState(); + } + + notifyPointsUpdated(isOnlyListener, + minPointId, + previousNumPoints, listenerSet, interestedInSampledOutPoints ? listenerSet : Collections.EMPTY_SET); } @@ -737,20 +541,23 @@ public class TrackDataHub { * Reloads all track data received so far into the specified listeners. */ private void loadDataForAllListeners() { - if (registeredListeners.isEmpty()) { - Log.d(TAG, "No listeners, not reloading"); - return; - } if (!started) { Log.w(TAG, "Not started, not reloading"); return; } + synchronized (listeners) { + if (!listeners.hasListeners()) { + Log.d(TAG, "No listeners, not reloading"); + return; + } + } runInListenerThread(new Runnable() { @Override public void run() { // Ignore the return values here, we're already sending the full data set anyway - for (TrackDataListener listener : getListenersFor(ListenerDataType.DISPLAY_PREFERENCES)) { + for (TrackDataListener listener : + getListenersFor(ListenerDataType.DISPLAY_PREFERENCES)) { listener.onUnitsChanged(useMetricUnits); listener.onReportSpeedChanged(reportSpeed); } @@ -765,10 +572,10 @@ public class TrackDataHub { Set sampledOutPointListeners = getListenersFor(ListenerDataType.SAMPLED_OUT_POINT_UPDATES); notifyPointsCleared(pointListeners); - notifyPointsUpdated(true, pointListeners, sampledOutPointListeners); + notifyPointsUpdated(true, 0, 0, pointListeners, sampledOutPointListeners); notifyWaypointUpdated(getListenersFor(ListenerDataType.WAYPOINT_UPDATES)); - + if (lastSeenLocation != null) { notifyLocationChanged(lastSeenLocation, true, getListenersFor(ListenerDataType.LOCATION_UPDATES)); @@ -787,11 +594,9 @@ public class TrackDataHub { * @param key the key to the preference that changed */ private void notifyPreferenceChanged(String key) { - if (RECORDING_TRACK_KEY.equals(key)) { - recordingTrackId = preferences.getLong(RECORDING_TRACK_KEY, -1); - } else if (MIN_REQUIRED_ACCURACY_KEY.equals(key)) { + if (MIN_REQUIRED_ACCURACY_KEY.equals(key)) { minRequiredAccuracy = preferences.getInt(MIN_REQUIRED_ACCURACY_KEY, - Constants.DEFAULT_MIN_REQUIRED_ACCURACY); + DEFAULT_MIN_REQUIRED_ACCURACY); } else if (METRIC_UNITS_KEY.equals(key)) { useMetricUnits = preferences.getBoolean(METRIC_UNITS_KEY, true); notifyUnitsChanged(); @@ -808,12 +613,15 @@ public class TrackDataHub { runInListenerThread(new Runnable() { @Override public void run() { - Set listeners = getListenersFor(ListenerDataType.DISPLAY_PREFERENCES); + Set displayListeners = + getListenersFor(ListenerDataType.DISPLAY_PREFERENCES); - for (TrackDataListener listener : listeners) { + for (TrackDataListener listener : displayListeners) { // TODO: Do the reloading just once for all interested listeners if (listener.onReportSpeedChanged(reportSpeed)) { - reloadDataForListener(registeredListeners.get(listener)); + synchronized (listeners) { + reloadDataForListener(listeners.getRegistration(listener)); + } } } } @@ -827,11 +635,13 @@ public class TrackDataHub { runInListenerThread(new Runnable() { @Override public void run() { - Set listeners = getListenersFor(ListenerDataType.DISPLAY_PREFERENCES); + Set displayListeners = getListenersFor(ListenerDataType.DISPLAY_PREFERENCES); - for (TrackDataListener listener : listeners) { + for (TrackDataListener listener : displayListeners) { if (listener.onUnitsChanged(useMetricUnits)) { - reloadDataForListener(registeredListeners.get(listener)); + synchronized (listeners) { + reloadDataForListener(listeners.getRegistration(listener)); + } } } } @@ -843,9 +653,6 @@ public class TrackDataHub { final TrackDataListener.ProviderState state; if (!hasProviderEnabled) { state = ProviderState.DISABLED; - - // Give a global warning about this state. - Toast.makeText(context, R.string.error_no_gps_location_provider, Toast.LENGTH_LONG).show(); } else if (!hasFix) { state = ProviderState.NO_FIX; } else if (!hasGoodFix) { @@ -859,7 +666,8 @@ public class TrackDataHub { public void run() { // Notify to everyone. Log.d(TAG, "Notifying fix type: " + state); - for (TrackDataListener listener : getListenersFor(ListenerDataType.LOCATION_UPDATES)) { + for (TrackDataListener listener : + getListenersFor(ListenerDataType.LOCATION_UPDATES)) { listener.onProviderStateChange(state); } } @@ -885,7 +693,7 @@ public class TrackDataHub { * @param forceUpdate whether to force the notifications to happen * @param listeners the listeners to notify */ - private void notifyLocationChanged(final Location location, boolean forceUpdate, + private void notifyLocationChanged(Location location, boolean forceUpdate, final Set listeners) { if (location == null) return; if (listeners.isEmpty()) return; @@ -895,26 +703,56 @@ public class TrackDataHub { boolean oldHasFix = hasFix; boolean oldHasGoodFix = hasGoodFix; - // We consider a good fix to be a recent one with reasonable accuracy. + long now = System.currentTimeMillis(); if (isGpsLocation) { - lastSeenLocation = location; - hasFix = (location != null && System.currentTimeMillis() - location.getTime() <= MAX_LOCATION_AGE_MS); + // We consider a good fix to be a recent one with reasonable accuracy. + hasFix = !isLocationOld(location, now, MAX_LOCATION_AGE_MS); hasGoodFix = (location != null && location.getAccuracy() <= minRequiredAccuracy); - if (hasFix != oldHasFix || hasGoodFix != oldHasGoodFix || forceUpdate) { - notifyFixType(); + } else { + if (!isLocationOld(lastSeenLocation, now, MAX_LOCATION_AGE_MS)) { + // This is a network location, but we have a recent/valid GPS location, just ignore this. + return; + } + + // We haven't gotten a GPS location in a while (or at all), assume we have no fix anymore. + hasFix = false; + hasGoodFix = false; + + // If the network location is recent, we'll use that. + if (isLocationOld(location, now, MAX_NETWORK_AGE_MS)) { + // Alas, we have no clue where we are. + location = null; } } + if (hasFix != oldHasFix || hasGoodFix != oldHasGoodFix || forceUpdate) { + notifyFixType(); + } + + lastSeenLocation = location; + final Location finalLoc = location; runInListenerThread(new Runnable() { @Override public void run() { for (TrackDataListener listener : listeners) { - listener.onCurrentLocationChanged(location); + listener.onCurrentLocationChanged(finalLoc); } } }); } + /** + * Returns true if the given location is either invalid or too old. + * + * @param location the location to test + * @param now the current timestamp in milliseconds + * @param maxAge the maximum age in milliseconds + * @return true if it's invalid or too old, false otherwise + */ + private static boolean isLocationOld(Location location, long now, long maxAge) { + return !LocationUtils.isValidLocation(location) || now - location.getTime() > maxAge; + } + /** * Notifies that the current heading has changed. * @@ -952,10 +790,6 @@ public class TrackDataHub { public void run() { for (TrackDataListener listener : listeners) { listener.onSelectedTrackChanged(track, isRecordingSelected()); - - if (track != null) { - listener.onTrackUpdated(track); - } } } }); @@ -992,7 +826,7 @@ public class TrackDataHub { // Always reload all the waypoints. final Cursor cursor = providerUtils.getWaypointsCursor( - selectedTrackId, 0L, Constants.MAX_DISPLAYED_WAYPOINTS_POINTS); + selectedTrackId, 0L, MAX_DISPLAYED_WAYPOINTS_POINTS); runInListenerThread(new Runnable() { @Override @@ -1009,7 +843,7 @@ public class TrackDataHub { if (!LocationUtils.isValidLocation(waypoint.getLocation())) { continue; } - + for (TrackDataListener listener : listeners) { listener.onNewWaypoint(waypoint); } @@ -1020,7 +854,7 @@ public class TrackDataHub { cursor.close(); } } - + for (TrackDataListener listener : listeners) { listener.onNewWaypointsDone(); } @@ -1049,15 +883,16 @@ public class TrackDataHub { /** * Notifies the given listeners about track points in the given ID range. * - * @param minPointId the first point ID to notify, inclusive - * @param maxPointId the last poind ID to notify, inclusive * @param keepState whether to load and save state about the already-notified points. * If true, only new points are reported. - * If false, then the whole track will be loaded, without affecting the store. - * @param listeners the listeners to notify - * @param trackDataListeners + * If false, then the whole track will be loaded, without affecting the state. + * @param minPointId the first point ID to notify, inclusive, or 0 to determine from + * internal state + * @param previousNumPoints the number of points to assume were previously loaded for + * these listeners, or 0 to assume it's the kept state */ private void notifyPointsUpdated(final boolean keepState, + final long minPointId, final int previousNumPoints, final Set sampledListeners, final Set sampledOutListeners) { if (sampledListeners.isEmpty() && sampledOutListeners.isEmpty()) return; @@ -1065,38 +900,42 @@ public class TrackDataHub { runInListenerThread(new Runnable() { @Override public void run() { - notifyPointsUpdatedSync(keepState, sampledListeners, sampledOutListeners); + notifyPointsUpdatedSync(keepState, minPointId, previousNumPoints, sampledListeners, sampledOutListeners); } }); } /** - * Asynchronous version of the above method. + * Synchronous version of the above method. */ private void notifyPointsUpdatedSync(boolean keepState, + long minPointId, int previousNumPoints, Set sampledListeners, Set sampledOutListeners) { // If we're loading state, start from after the last seen point up to the last recorded one // (all new points) // If we're not loading state, then notify about all the previously-seen points. - long minPointId = keepState ? lastSeenLocationId + 1 : 0; + if (minPointId <= 0) { + minPointId = keepState ? lastSeenLocationId + 1 : 0; + } long maxPointId = keepState ? -1 : lastSeenLocationId; // TODO: Move (re)sampling to a separate class. - if (numLoadedPoints >= Constants.MAX_DISPLAYED_TRACK_POINTS) { - // We're about to exceed the maximum allowed number of points, so reload + if (numLoadedPoints >= targetNumPoints) { + // We're about to exceed the maximum desired number of points, so reload // the whole track with fewer points (the sampling frequency will be // lower). We do this for every listener even if we were loading just for // a few of them (why miss the oportunity?). Log.i(TAG, "Resampling point set after " + numLoadedPoints + " points."); - firstSeenLocationId = -1; - lastSeenLocationId = -1; - numLoadedPoints = 0; - sampledListeners = getListenersFor(ListenerDataType.POINT_UPDATES); - sampledOutListeners = getListenersFor(ListenerDataType.SAMPLED_OUT_POINT_UPDATES); + resetState(); + synchronized (listeners) { + sampledListeners = getListenersFor(ListenerDataType.POINT_UPDATES); + sampledOutListeners = getListenersFor(ListenerDataType.SAMPLED_OUT_POINT_UPDATES); + } maxPointId = -1; minPointId = 0; + previousNumPoints = 0; keepState = true; for (TrackDataListener listener : sampledListeners) { @@ -1108,15 +947,15 @@ public class TrackDataHub { long currentSelectedTrackId = selectedTrackId; // If we're ignoring state, start from the beginning of the track - int localNumLoadedPoints = keepState ? numLoadedPoints : 0; - long localFirstSeenLocationId = keepState ? firstSeenLocationId : 0; + int localNumLoadedPoints = previousNumPoints; + if (previousNumPoints <= 0) { + localNumLoadedPoints = keepState ? numLoadedPoints : 0; + } + long localFirstSeenLocationId = keepState ? firstSeenLocationId : -1; long localLastSeenLocationId = minPointId; long lastStoredLocationId = providerUtils.getLastLocationId(currentSelectedTrackId); int pointSamplingFrequency = -1; - // Create a double-buffering location provider. - MyTracksProviderUtils.DoubleBufferedLocationFactory locationFactory = - new MyTracksProviderUtils.DoubleBufferedLocationFactory(); LocationIterator it = providerUtils.getLocationIterator( currentSelectedTrackId, minPointId, false, locationFactory); @@ -1146,9 +985,11 @@ public class TrackDataHub { if (pointSamplingFrequency == -1) { // Now we already have at least one point, calculate the sampling // frequency. + // It should be noted that a non-obvious consequence of this sampling is that + // no matter how many points we get in the newest batch, we'll never exceed + // MAX_DISPLAYED_TRACK_POINTS = 2 * TARGET_DISPLAYED_TRACK_POINTS before resampling. long numTotalPoints = lastStoredLocationId - localFirstSeenLocationId; - pointSamplingFrequency = - (int) (1 + numTotalPoints / Constants.TARGET_DISPLAYED_TRACK_POINTS); + pointSamplingFrequency = (int) (1 + numTotalPoints / targetNumPoints); } notifyNewPoint(location, locationId, lastStoredLocationId, @@ -1165,8 +1006,20 @@ public class TrackDataHub { lastSeenLocationId = localLastSeenLocationId; } + // Always keep the sampling frequency - if it changes we'll do a full reload above anyway. + lastSamplingFrequency = pointSamplingFrequency; + for (TrackDataListener listener : sampledListeners) { listener.onNewTrackPointsDone(); + + // Update the listener state + ListenerRegistration registration = listeners.getRegistration(listener); + if (registration != null) { + registration.lastTrackId = currentSelectedTrackId; + registration.lastPointId = localLastSeenLocationId; + registration.lastSamplingFrequency = pointSamplingFrequency; + registration.numLoadedPoints = localNumLoadedPoints; + } } } @@ -1190,27 +1043,21 @@ public class TrackDataHub { // Include a point if it fits one of the following criteria: // - Has the mod for the sampling frequency (includes first point). // - Is the last point and we are not recording this track. + boolean recordingSelected = isRecordingSelected(); boolean includeInSample = (numLoadedPoints % pointSamplingFrequency == 0 || - (!isRecordingSelected() && locationId == lastStoredLocationId)); + (!recordingSelected && locationId == lastStoredLocationId)); if (!includeInSample) { for (TrackDataListener listener : sampledOutListeners) { listener.onSampledOutTrackPoint(location); } - return; - } - - // Point is valid and included in sample. - for (TrackDataListener listener : sampledListeners) { - // No need to allocate a new location (we can safely reuse the existing). - listener.onNewTrackPoint(location); - } - } - - private Set getListenersFor(ListenerDataType type) { - synchronized (registeredListeners) { - return listenerSetsPerType.get(type); + } else { + // Point is valid and included in sample. + for (TrackDataListener listener : sampledListeners) { + // No need to allocate a new location (we can safely reuse the existing). + listener.onNewTrackPoint(location); + } } } @@ -1218,4 +1065,19 @@ public class TrackDataHub { protected void runInListenerThread(Runnable runnable) { listenerHandler.post(runnable); } + + private Set getListenersFor(ListenerDataType type) { + synchronized (listeners) { + return listeners.getListenersFor(type); + } + } + + private EnumSet getNeededListenerTypes() { + EnumSet neededTypes = listeners.getAllRegisteredTypes(); + + // We always want preference updates. + neededTypes.add(ListenerDataType.DISPLAY_PREFERENCES); + + return neededTypes; + } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/TrackDataListener.java b/MyTracks/src/com/google/android/apps/mytracks/content/TrackDataListener.java similarity index 97% rename from MyTracks/src/com/google/android/apps/mytracks/TrackDataListener.java rename to MyTracks/src/com/google/android/apps/mytracks/content/TrackDataListener.java index 4ba1539bc..be71e785e 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/TrackDataListener.java +++ b/MyTracks/src/com/google/android/apps/mytracks/content/TrackDataListener.java @@ -13,7 +13,7 @@ * License for the specific language governing permissions and limitations under * the License. */ -package com.google.android.apps.mytracks; +package com.google.android.apps.mytracks.content; import com.google.android.apps.mytracks.content.Track; import com.google.android.apps.mytracks.content.Waypoint; @@ -23,8 +23,6 @@ import android.location.Location; /** * Listener for track data, for both initial and incremental loading. * - * TODO: Allow listening to just part of the data received here. - * * @author Rodrigo Damazio */ public interface TrackDataListener { diff --git a/MyTracks/src/com/google/android/apps/mytracks/content/TrackDataListeners.java b/MyTracks/src/com/google/android/apps/mytracks/content/TrackDataListeners.java new file mode 100644 index 000000000..a1edf8796 --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/content/TrackDataListeners.java @@ -0,0 +1,170 @@ +/* + * Copyright 2011 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.google.android.apps.mytracks.content; + +import static com.google.android.apps.mytracks.Constants.TAG; + +import com.google.android.apps.mytracks.content.TrackDataHub.ListenerDataType; + +import android.util.Log; + +import java.util.EnumMap; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; +import java.util.WeakHashMap; + +/** + * Manager for the external data listeners and their listening types. + * + * @author Rodrigo Damazio + */ +class TrackDataListeners { + + /** Internal representation of a listener's registration. */ + static class ListenerRegistration { + final TrackDataListener listener; + final EnumSet types; + + // State that was last notified to the listener, for resuming after a pause. + long lastTrackId; + long lastPointId; + int lastSamplingFrequency; + int numLoadedPoints; + + public ListenerRegistration(TrackDataListener listener, + EnumSet types) { + this.listener = listener; + this.types = types; + } + + public boolean isInterestedIn(ListenerDataType type) { + return types.contains(type); + } + + @Override + public String toString() { + return "ListenerRegistration [listener=" + listener + ", types=" + types + + ", lastTrackId=" + lastTrackId + ", lastPointId=" + lastPointId + + ", lastSamplingFrequency=" + lastSamplingFrequency + + ", numLoadedPoints=" + numLoadedPoints + "]"; + } + } + + /** Map of external listener to its registration details. */ + private final Map registeredListeners = + new HashMap(); + + /** + * Map of external paused listener to its registration details. + * This will automatically discard listeners which are GCed. + */ + private final WeakHashMap oldListeners = + new WeakHashMap(); + + /** Map of data type to external listeners interested in it. */ + private final Map> listenerSetsPerType = + new EnumMap>(ListenerDataType.class); + + public TrackDataListeners() { + // Create sets for all data types at startup. + for (ListenerDataType type : ListenerDataType.values()) { + listenerSetsPerType.put(type, new LinkedHashSet()); + } + } + + /** + * Registers a listener to send data to. + * It is ok to call this method before {@link start}, and in that case + * the data will only be passed to listeners when {@link start} is called. + * + * @param listener the listener to register + * @param dataTypes the type of data that the listener is interested in + */ + public ListenerRegistration registerTrackDataListener(final TrackDataListener listener, EnumSet dataTypes) { + Log.d(TAG, "Registered track data listener: " + listener); + if (registeredListeners.containsKey(listener)) { + throw new IllegalStateException("Listener already registered"); + } + + ListenerRegistration registration = oldListeners.remove(listener); + if (registration == null) { + registration = new ListenerRegistration(listener, dataTypes); + } + registeredListeners.put(listener, registration); + + for (ListenerDataType type : dataTypes) { + // This is guaranteed not to be null. + Set typeSet = listenerSetsPerType.get(type); + typeSet.add(listener); + } + + return registration; + } + + /** + * Unregisters a listener to send data to. + * + * @param listener the listener to unregister + */ + public void unregisterTrackDataListener(TrackDataListener listener) { + Log.d(TAG, "Unregistered track data listener: " + listener); + // Remove and keep the corresponding registration. + ListenerRegistration match = registeredListeners.remove(listener); + if (match == null) { + Log.w(TAG, "Tried to unregister listener which is not registered."); + return; + } + + // Remove it from the per-type sets + for (ListenerDataType type : match.types) { + listenerSetsPerType.get(type).remove(listener); + } + + // Keep it around in case it's re-registered soon + oldListeners.put(listener, match); + } + + public ListenerRegistration getRegistration(TrackDataListener listener) { + ListenerRegistration registration = registeredListeners.get(listener); + if (registration == null) { + registration = oldListeners.get(listener); + } + return registration; + } + + public Set getListenersFor(ListenerDataType type) { + return listenerSetsPerType.get(type); + } + + public EnumSet getAllRegisteredTypes() { + EnumSet listeners = EnumSet.noneOf(ListenerDataType.class); + for (ListenerRegistration registration : this.registeredListeners.values()) { + listeners.addAll(registration.types); + } + return listeners; + } + + public boolean hasListeners() { + return !registeredListeners.isEmpty(); + } + + public int getNumListeners() { + return registeredListeners.size(); + } +} diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/SendToFusionTables.java b/MyTracks/src/com/google/android/apps/mytracks/io/SendToFusionTables.java index eeb769b88..fd2c517b7 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/SendToFusionTables.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/SendToFusionTables.java @@ -179,6 +179,11 @@ public class SendToFusionTables implements Runnable { // Get the track meta-data Track track = providerUtils.getTrack(trackId); + if (track == null) { + Log.w(Constants.TAG, "Cannot get track."); + return; + } + String originalDescription = track.getDescription(); // Create a new table: @@ -207,7 +212,6 @@ public class SendToFusionTables implements Runnable { Log.d(Constants.TAG, "SendToFusionTables: Done: " + success); progressIndicator.setProgressValue(PROGRESS_COMPLETE); } finally { - final boolean finalSuccess = success; final int finalStatusMessageId = statusMessageId; context.runOnUiThread(new Runnable() { @@ -304,7 +308,7 @@ public class SendToFusionTables implements Runnable { Cursor locationsCursor = providerUtils.getLocationsCursor(track.getId(), 0, -1, false); try { - if (!locationsCursor.moveToFirst()) { + if (locationsCursor == null || !locationsCursor.moveToFirst()) { Log.w(Constants.TAG, "Unable to get any points to upload"); return false; } @@ -391,7 +395,9 @@ public class SendToFusionTables implements Runnable { return true; } finally { - locationsCursor.close(); + if (locationsCursor != null) { + locationsCursor.close(); + } } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/SendToMyMaps.java b/MyTracks/src/com/google/android/apps/mytracks/io/SendToMyMaps.java index ffc895da6..f451078d3 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/SendToMyMaps.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/SendToMyMaps.java @@ -1,12 +1,12 @@ /* * Copyright 2008 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 @@ -76,7 +76,7 @@ public class SendToMyMaps implements Runnable { public interface OnSendCompletedListener { void onSendCompleted(String mapId, boolean success, int statusMessage); } - + public SendToMyMaps(Activity context, String mapId, AuthManager auth, long trackId, ProgressIndicator progressIndicator, OnSendCompletedListener onCompletion) { @@ -103,15 +103,20 @@ public class SendToMyMaps implements Runnable { progressIndicator.setProgressValue(1); progressIndicator.setProgressMessage( R.string.progress_message_reading_track); - + // Get the track meta-data Track track = providerUtils.getTrack(trackId); + if (track == null) { + Log.w(Constants.TAG, "Cannot get track."); + return; + } + String originalDescription = track.getDescription(); track.setDescription("

" + track.getDescription() + "

" + stringUtils.generateTrackDescription(track, null, null) + "

"); mapsClient = new MapsFacade(context, auth); - + // Create a new map if necessary: boolean isNewMap = mapId.equals(NEW_MAP_ID); if (isNewMap) { @@ -166,7 +171,7 @@ public class SendToMyMaps implements Runnable { Log.w(TAG, "SendToMyMaps: upload waypoints failed."); } } - + if (success) { statusMessageId = isNewMap ? R.string.status_new_mymap_has_been_created @@ -178,7 +183,7 @@ public class SendToMyMaps implements Runnable { if (mapsClient != null) { mapsClient.cleanUp(); } - + final boolean finalSuccess = success; final int finalStatusMessageId = statusMessageId; context.runOnUiThread(new Runnable() { @@ -206,17 +211,17 @@ public class SendToMyMaps implements Runnable { Cursor locationsCursor = providerUtils.getLocationsCursor(track.getId(), 0, -1, false); try { - if (!locationsCursor.moveToFirst()) { + if (locationsCursor == null || !locationsCursor.moveToFirst()) { Log.w(TAG, "Unable to get any points to upload"); return false; } - + totalLocationsRead = 0; totalLocationsPrepared = 0; totalLocationsUploaded = 0; totalLocations = locationsCursor.getCount(); totalSegmentsUploaded = 0; - + // Limit the number of elevation readings. Ideally we would want around 250. int elevationSamplingFrequency = Math.max(1, (int) (totalLocations / 250.0)); @@ -224,12 +229,12 @@ public class SendToMyMaps implements Runnable { "Using elevation sampling factor: " + elevationSamplingFrequency + " on " + totalLocations); double totalDistance = 0; - + Vector distances = new Vector(); Vector elevations = new Vector(); DoubleBuffer elevationBuffer = new DoubleBuffer(Constants.ELEVATION_SMOOTHING_FACTOR); - + List locations = new ArrayList(MAX_POINTS_PER_UPLOAD); progressIndicator.setProgressMessage( R.string.progress_message_reading_track); @@ -238,15 +243,15 @@ public class SendToMyMaps implements Runnable { if (totalLocationsRead % 100 == 0) { updateProgress(); } - + Location loc = providerUtils.createLocation(locationsCursor); locations.add(loc); - + if (totalLocationsRead == 0) { // Put a marker at the first point of the first valid segment: mapsClient.uploadMarker(mapId, track.getName(), track.getDescription(), loc, true); } - + // Add to the elevation profile. if (loc != null && LocationUtils.isValidLocation(loc)) { // All points go into the smoothing buffer... @@ -256,19 +261,19 @@ public class SendToMyMaps implements Runnable { double dist = lastLocation.distanceTo(loc); totalDistance += dist; } - + // ...but only a few points are really used to keep the url short. if (totalLocationsRead % elevationSamplingFrequency == 0) { distances.add(totalDistance); elevations.add(elevationBuffer.getAverage()); } } - + // If the location was not valid, it's a segment split, so make sure the // distance between the previous segment and the new one is not accounted // for in the next iteration. lastLocation = loc; - + // Every now and then, upload the accumulated points if (totalLocationsRead % MAX_POINTS_PER_UPLOAD == MAX_POINTS_PER_UPLOAD - 1) { @@ -276,28 +281,30 @@ public class SendToMyMaps implements Runnable { return false; } } - + totalLocationsRead++; } while (locationsCursor.moveToNext()); - + // Do a final upload with what's left if (!prepareAndUploadPoints(track, locations)) { return false; } - + // Put an end marker at the last point of the last valid segment: if (lastLocation != null) { track.setDescription("

" + originalDescription + "

" + stringUtils.generateTrackDescription( track, distances, elevations) + "

"); - return mapsClient.uploadMarker(mapId, track.getName(), track.getDescription(), + return mapsClient.uploadMarker(mapId, track.getName(), track.getDescription(), lastLocation, false); } - + return true; } finally { - locationsCursor.close(); + if (locationsCursor != null) { + locationsCursor.close(); + } } } @@ -306,17 +313,17 @@ public class SendToMyMaps implements Runnable { progressIndicator.setProgressMessage( R.string.progress_message_preparing_track); updateProgress(); - + int numLocations = locations.size(); if (numLocations < 2) { Log.d(TAG, "Not preparing/uploading too few points"); totalLocationsUploaded += numLocations; return true; } - + // Prepare/pre-process the points ArrayList splitTracks = prepareLocations(track, locations); - + // Start uploading them progressIndicator.setProgressMessage( R.string.progress_message_sending_mymaps); @@ -330,7 +337,7 @@ public class SendToMyMaps implements Runnable { Log.d(TAG, "SendToMyMaps: Prepared feature for upload w/ " + splitTrack.getLocations().size() + " points."); - + // Transmit tracks via GData feed: // ------------------------------- Log.d(TAG, @@ -340,7 +347,7 @@ public class SendToMyMaps implements Runnable { return false; } } - + locations.clear(); totalLocationsUploaded += numLocations; updateProgress(); @@ -358,7 +365,7 @@ public class SendToMyMaps implements Runnable { private ArrayList prepareLocations( Track track, Iterable locations) { ArrayList splitTracks = new ArrayList(); - + // Create segments from each full track: Track segment = new Track(); TripStatistics segmentStats = segment.getStatistics(); @@ -377,11 +384,11 @@ public class SendToMyMaps implements Runnable { if (loc.getLatitude() > 90) { startNewTrackSegment = true; } - + if (startNewTrackSegment) { // Close up the last segment. prepareTrackSegment(segment, splitTracks); - + Log.d(TAG, "MyTracksSendToMyMaps: Starting new track segment..."); startNewTrackSegment = false; @@ -391,7 +398,7 @@ public class SendToMyMaps implements Runnable { segment.setDescription(/* track.getDescription() */ ""); segment.setCategory(track.getCategory()); } - + if (loc.getLatitude() <= 90) { segment.addLocation(loc); if (segmentStats.getStartTime() < 0) { @@ -400,9 +407,9 @@ public class SendToMyMaps implements Runnable { } totalLocationsPrepared++; } - + prepareTrackSegment(segment, splitTracks); - + return splitTracks; } @@ -425,13 +432,13 @@ public class SendToMyMaps implements Runnable { && segment.getLocations().size() > 0) { segmentStats.setStopTime(segment.getLocations().size() - 1); } - + /* * Decimate to 2 meter precision. Mapshop doesn't like too many * points: */ LocationUtils.decimate(segment, 2.0); - + /* It the track still has > 500 points, split it in pieces: */ if (segment.getLocations().size() > 500) { splitTracks.addAll(LocationUtils.split(segment, 500)); diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/docs/DocsHelper.java b/MyTracks/src/com/google/android/apps/mytracks/io/docs/DocsHelper.java index ce9752f39..abb052dc8 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/docs/DocsHelper.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/docs/DocsHelper.java @@ -363,8 +363,8 @@ public class DocsHelper { wr.close(); rd.close(); } - - private static IOException newIOException(GDataWrapper wrapper, + + private static IOException newIOException(GDataWrapper wrapper, String message) { return new IOException(String.format("%s: %d: %s", message, wrapper.getErrorType(), wrapper.getErrorMessage())); diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/file/GpxTrackWriter.java b/MyTracks/src/com/google/android/apps/mytracks/io/file/GpxTrackWriter.java index 88fde6c3c..d51948658 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/file/GpxTrackWriter.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/file/GpxTrackWriter.java @@ -21,6 +21,7 @@ import com.google.android.apps.mytracks.io.file.TrackWriterFactory.TrackFileForm import com.google.android.apps.mytracks.util.StringUtils; import android.location.Location; +import android.os.Build; import java.io.OutputStream; import java.io.PrintWriter; @@ -85,7 +86,7 @@ public class GpxTrackWriter implements TrackFormatWriter { pw.println(""); pw.println(""); pw.println(""); - pw.println("My Tracks running on Android" - + ""); + pw.format("My Tracks running on %s" + + "\n", Build.MODEL); + pw.println("" + StringUtils.stringAsCData(track.getName()) + ""); pw.println("" diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/file/TcxTrackWriter.java b/MyTracks/src/com/google/android/apps/mytracks/io/file/TcxTrackWriter.java index eaf45a7cc..6ff9a4840 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/file/TcxTrackWriter.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/file/TcxTrackWriter.java @@ -187,7 +187,7 @@ public class TcxTrackWriter implements TrackFormatWriter { && sensorData.getCadence().getState() == Sensor.SensorState.SENDING && sensorData.getCadence().hasValue()) { pw.print(" "); - pw.print(sensorData.getCadence().getValue()); + pw.print(Math.min(255, sensorData.getCadence().getValue())); pw.println(""); } } @@ -209,7 +209,7 @@ public class TcxTrackWriter implements TrackFormatWriter { } pw.println(" "); pw.print(" "); - pw.format("%s\n", Build.MODEL); + pw.format("My Tracks running on %s\n", Build.MODEL); // The following code is correct. ID is inconsistently capitalized in the // TCX schema. diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/file/TrackWriterFactory.java b/MyTracks/src/com/google/android/apps/mytracks/io/file/TrackWriterFactory.java index 5aabf6133..49f1f9f01 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/file/TrackWriterFactory.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/file/TrackWriterFactory.java @@ -16,10 +16,13 @@ package com.google.android.apps.mytracks.io.file; +import static com.google.android.apps.mytracks.Constants.TAG; + import com.google.android.apps.mytracks.content.MyTracksProviderUtils; import com.google.android.apps.mytracks.content.Track; import android.content.Context; +import android.util.Log; /** * A factory to produce track writers for any format. @@ -90,6 +93,11 @@ public class TrackWriterFactory { MyTracksProviderUtils providerUtils, long trackId, TrackFileFormat format) { Track track = providerUtils.getTrack(trackId); + if (track == null) { + Log.w(TAG, "Trying to create a writer for an invalid track, id=" + trackId); + return null; + } + return newWriter(context, providerUtils, track, format); } 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 index 28f2f4fb8..e52103f23 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/services/sensors/SensorUtils.java +++ b/MyTracks/src/com/google/android/apps/mytracks/services/sensors/SensorUtils.java @@ -24,14 +24,16 @@ import com.google.android.maps.mytracks.R; * A collection of methods for message parsers. * * @author Sandor Dornbush + * @author Nico Laum */ 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 @@ -42,6 +44,58 @@ public class SensorUtils { return r; } + /** + * Extract one unsigned short from a little 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 unsignedShortToIntLittleEndian(byte[] buffer, int index) { + int r = buffer[index] & 0xFF; + r |= (buffer[index + 1] & 0xFF) << 8; + return r; + } + + /** + * Returns CRC8 (polynomial 0x8C) from byte array buffer[start] until + * (including) buffer[end] + * + * @param buffer the byte array of data (payload) + * @param start the position in the byte array where the payload begins + * @param end the position in the byte array where the payload ends + * @return CRC8 value + */ + public static byte getCrc8(byte[] buffer, int start, int length) { + byte crc = 0x0; + + for (int i = start; i < (start + length); i++) { + crc = crc8PushByte(crc, buffer[i]); + } + return crc; + } + + /** + * Updates a CRC8 value by using the next byte passed to this method + * + * @param crc int of crc value + * @param add the next byte to add to the CRC8 calculation + */ + private static byte crc8PushByte(byte crc, byte add) { + crc = (byte) (crc ^ add); + + for (int i = 0; i < 8; i++) { + if ((crc & 0x1) != 0x0) { + // Using a 0xFF bit assures that 0-bits are introduced during the shift operation. + // Otherwise, implicit casts to signed int could shift in 1-bits if the signed bit is 1. + crc = (byte) (((crc & 0xFF) >> 1) ^ 0x8C); + } else { + crc = (byte) ((crc & 0xFF) >> 1); + } + } + return crc; + } + public static String getStateAsString(Sensor.SensorState state, Context c) { switch (state) { case NONE: 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 index da7a3672e..48fd5daa0 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/services/sensors/ZephyrMessageParser.java +++ b/MyTracks/src/com/google/android/apps/mytracks/services/sensors/ZephyrMessageParser.java @@ -21,12 +21,16 @@ import com.google.android.apps.mytracks.Constants; import com.google.android.apps.mytracks.content.Sensor; /** - * An implementation of a SensorData parser for Zephyr HRM. + * An implementation of a Sensor MessageParser for Zephyr. * * @author Sandor Dornbush */ public class ZephyrMessageParser implements MessageParser { + public static final int ZEPHYR_HXM_BYTE_STX = 0; + public static final int ZEPHYR_HXM_BYTE_CRC = 58; + public static final int ZEPHYR_HXM_BYTE_ETX = 59; + @Override public Sensor.SensorDataSet parseBuffer(byte[] buffer) { StringBuilder sb = new StringBuilder(); @@ -34,20 +38,24 @@ public class ZephyrMessageParser implements MessageParser { sb.append(String.format("%02X", buffer[i])); } Log.w(Constants.TAG, "Got zephyr data: " + sb); - // The provided units are 1/16 strides per minute. - // TODO: Fix the cadence calculation. - // 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.SensorData.Builder heartrate = Sensor.SensorData.newBuilder() + .setValue(buffer[12] & 0xFF) + .setState(Sensor.SensorState.SENDING); + // Changes Nico Laum (Power and Cadence) + Sensor.SensorData.Builder power = Sensor.SensorData.newBuilder() + .setValue(buffer[11]) + .setState(Sensor.SensorState.SENDING); + Sensor.SensorData.Builder cadence = Sensor.SensorData.newBuilder() + .setValue(SensorUtils.unsignedShortToIntLittleEndian(buffer, 56) / 16) + .setState(Sensor.SensorState.SENDING); + Sensor.SensorDataSet sds = Sensor.SensorDataSet.newBuilder() .setCreationTime(System.currentTimeMillis()) - .setHeartRate(b) + .setPower(power) + .setHeartRate(heartrate) + .setCadence(cadence) .build(); return sds; @@ -55,8 +63,11 @@ public class ZephyrMessageParser implements MessageParser { @Override public boolean isValid(byte[] buffer) { - // TODO crc etc. - return buffer[0] == 0x02 && buffer[59] == 0x03; + // Check STX (Start of Text), ETX (End of Text) and CRC Checksum + return buffer.length > ZEPHYR_HXM_BYTE_ETX + && buffer[ZEPHYR_HXM_BYTE_STX] == 0x02 + && buffer[ZEPHYR_HXM_BYTE_ETX] == 0x03 + && SensorUtils.getCrc8(buffer, 3, 55) == buffer[ZEPHYR_HXM_BYTE_CRC]; } @Override diff --git a/MyTracks/src/com/google/android/apps/mytracks/stats/TripStatisticsBuilder.java b/MyTracks/src/com/google/android/apps/mytracks/stats/TripStatisticsBuilder.java index ddad2394a..c1dd59442 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/stats/TripStatisticsBuilder.java +++ b/MyTracks/src/com/google/android/apps/mytracks/stats/TripStatisticsBuilder.java @@ -19,7 +19,6 @@ package com.google.android.apps.mytracks.stats; import static com.google.android.apps.mytracks.Constants.TAG; import com.google.android.apps.mytracks.Constants; -import com.google.android.apps.mytracks.SettingsActivity; import android.location.Location; import android.util.Log; diff --git a/MyTracks/src/com/google/wireless/gdata/docs/SpreadsheetsClient.java b/MyTracks/src/com/google/wireless/gdata/docs/SpreadsheetsClient.java index 803c5bf70..59e675f24 100755 --- a/MyTracks/src/com/google/wireless/gdata/docs/SpreadsheetsClient.java +++ b/MyTracks/src/com/google/wireless/gdata/docs/SpreadsheetsClient.java @@ -153,7 +153,7 @@ public class SpreadsheetsClient extends GDataServiceClient { * @return a parser for the indicated feed * @throws ParseException if the response from the server could not be parsed */ - @SuppressWarnings("unchecked") + @SuppressWarnings("rawtypes") @Override public GDataParser getParserForFeed( Class feedEntryClass, String feedUri, String authToken) @@ -227,7 +227,7 @@ public class SpreadsheetsClient extends GDataServiceClient { * Converts an HTTP exception which happened while writing to the equivalent * local exception. */ - @SuppressWarnings("unchecked") + @SuppressWarnings("rawtypes") private void convertHttpExceptionForWrites( Class entryClass, String message, HttpException cause) throws ParseException, IOException { @@ -253,7 +253,7 @@ public class SpreadsheetsClient extends GDataServiceClient { /** * Parses one entry from the input stream. */ - @SuppressWarnings("unchecked") + @SuppressWarnings("rawtypes") private Entry parseEntry(Class entryClass, InputStream is) throws ParseException, IOException { GDataParser parser = null; diff --git a/MyTracks/src/com/google/wireless/gdata/docs/XmlDocsGDataParserFactory.java b/MyTracks/src/com/google/wireless/gdata/docs/XmlDocsGDataParserFactory.java index 09840754e..f350dc0dd 100644 --- a/MyTracks/src/com/google/wireless/gdata/docs/XmlDocsGDataParserFactory.java +++ b/MyTracks/src/com/google/wireless/gdata/docs/XmlDocsGDataParserFactory.java @@ -48,7 +48,7 @@ public class XmlDocsGDataParserFactory implements GDataParserFactory { } } - @SuppressWarnings("unchecked") + @SuppressWarnings({ "unchecked", "rawtypes" }) @Override public GDataParser createParser(Class cls, InputStream is) throws ParseException { diff --git a/MyTracksLib/src/com/google/android/apps/mytracks/content/MyTracksProviderUtilsImpl.java b/MyTracksLib/src/com/google/android/apps/mytracks/content/MyTracksProviderUtilsImpl.java index 64905da79..971bdd60f 100644 --- a/MyTracksLib/src/com/google/android/apps/mytracks/content/MyTracksProviderUtilsImpl.java +++ b/MyTracksLib/src/com/google/android/apps/mytracks/content/MyTracksProviderUtilsImpl.java @@ -1,12 +1,12 @@ /* * Copyright 2008 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 @@ -40,7 +40,7 @@ import java.util.NoSuchElementException; public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { private final ContentResolver contentResolver; - + private int defaultCursorBatchSize = 2000; public MyTracksProviderUtilsImpl(ContentResolver contentResolver) { @@ -199,12 +199,12 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { public final int idxLatitude; public final int idxLongitude; public final int idxAltitude; - public final int idxTime; - public final int idxBearing; + public final int idxTime; + public final int idxBearing; public final int idxAccuracy; public final int idxSpeed; - public final int idxSensor; - + public final int idxSensor; + public CachedTrackColumnIndices(Cursor cursor) { idxId = cursor.getColumnIndex(TrackPointsColumns._ID); idxLatitude = cursor.getColumnIndexOrThrow(TrackPointsColumns.LATITUDE); @@ -217,11 +217,11 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { idxSensor = cursor.getColumnIndexOrThrow(TrackPointsColumns.SENSOR); } } - + private void fillLocation(Cursor cursor, CachedTrackColumnIndices columnIndices, Location location) { location.reset(); - + if (!cursor.isNull(columnIndices.idxLatitude)) { location.setLatitude(1. * cursor.getInt(columnIndices.idxLatitude) / 1E6); } @@ -256,7 +256,7 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { } } } - + @Override public void fillLocation(Cursor cursor, Location location) { CachedTrackColumnIndices columnIndicies = new CachedTrackColumnIndices(cursor); @@ -660,6 +660,10 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { @Override public Waypoint getFirstWaypoint(long trackId) { + if (trackId < 0) { + return null; + } + Cursor cursor = contentResolver.query( WaypointsColumns.CONTENT_URI, null /*projection*/, @@ -682,6 +686,10 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { @Override public Waypoint getWaypoint(long waypointId) { + if (waypointId < 0) { + return null; + } + Cursor cursor = contentResolver.query( WaypointsColumns.CONTENT_URI, null /*projection*/, @@ -704,6 +712,10 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { @Override public long getLastLocationId(long trackId) { + if (trackId < 0) { + return -1; + } + final String[] projection = {"_id"}; Cursor cursor = contentResolver.query( TrackPointsColumns.CONTENT_URI, @@ -728,6 +740,10 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { @Override public long getFirstWaypointId(long trackId) { + if (trackId < 0) { + return -1; + } + final String[] projection = {"_id"}; Cursor cursor = contentResolver.query( WaypointsColumns.CONTENT_URI, @@ -752,6 +768,10 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { @Override public long getLastWaypointId(long trackId) { + if (trackId < 0) { + return -1; + } + final String[] projection = {"_id"}; Cursor cursor = contentResolver.query( WaypointsColumns.CONTENT_URI, @@ -815,6 +835,10 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { @Override public Location getLocation(long id) { + if (id < 0) { + return null; + } + String selection = TrackPointsColumns._ID + "=" + id; return findLocationBy(selection); } @@ -822,7 +846,11 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { @Override public Cursor getLocationsCursor(long trackId, long minTrackPointId, int maxLocations, boolean descending) { - String selection; + if (trackId < 0 || maxLocations < 1) { + return null; + } + + String selection; if (minTrackPointId >= 0) { selection = String.format("%s=%d AND %s%s%d", TrackPointsColumns.TRACKID, trackId, TrackPointsColumns._ID, @@ -842,6 +870,10 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { @Override public Cursor getWaypointsCursor(long trackId, long minWaypointId, int maxWaypoints) { + if (trackId < 0 || maxWaypoints < 1) { + return null; + } + String selection; if (minWaypointId > 0) { selection = String.format("%s=%d AND %s>=%d", @@ -863,6 +895,10 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { @Override public Track getTrack(long id) { + if (id < 0) { + return null; + } + String select = TracksColumns._ID + "=" + id; return findTrackBy(select); } @@ -929,6 +965,10 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { @Override public boolean trackExists(long id) { + if (id < 0) { + return false; + } + Cursor cursor = null; try { final String[] projection = { TracksColumns._ID }; @@ -967,7 +1007,7 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { private Cursor cursor = getCursor(startTrackPointId); private final CachedTrackColumnIndices columnIndices = cursor != null ? new CachedTrackColumnIndices(cursor) : null; - + private Cursor getCursor(long trackPointId) { return getLocationsCursor(trackId, trackPointId, defaultCursorBatchSize, descending); } @@ -979,7 +1019,7 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { cursor = getCursor(pointId); return cursor != null; } - + @Override public long getLocationId() { return lastTrackPointId; @@ -999,7 +1039,7 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { return cursor.getCount() == defaultCursorBatchSize && advanceCursorToNextBatch() && !cursor.isAfterLast(); } - + return true; } @@ -1009,11 +1049,11 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { !(cursor.moveToNext() || advanceCursorToNextBatch() || cursor.moveToNext())) { throw new NoSuchElementException(); } - + lastTrackPointId = cursor.getLong(columnIndices.idxId); Location location = locationFactory.createLocation(); fillLocation(cursor, columnIndices, location); - + return location; } @@ -1031,7 +1071,7 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { } }; } - + // @VisibleForTesting void setDefaultCursorBatchSize(int defaultCursorBatchSize) { this.defaultCursorBatchSize = defaultCursorBatchSize; diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/TrackDataHubTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/TrackDataHubTest.java deleted file mode 100644 index 43f97b80b..000000000 --- a/MyTracksTest/src/com/google/android/apps/mytracks/TrackDataHubTest.java +++ /dev/null @@ -1,478 +0,0 @@ -/* - * Copyright 2011 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ -package com.google.android.apps.mytracks; - -import static com.google.android.testing.mocking.AndroidMock.anyInt; -import static com.google.android.testing.mocking.AndroidMock.capture; -import static com.google.android.testing.mocking.AndroidMock.eq; -import static com.google.android.testing.mocking.AndroidMock.expect; -import static com.google.android.testing.mocking.AndroidMock.isA; -import static com.google.android.testing.mocking.AndroidMock.leq; -import static com.google.android.testing.mocking.AndroidMock.same; - -import com.google.android.apps.mytracks.TrackDataHub.ListenerDataType; -import com.google.android.apps.mytracks.TrackDataListener.ProviderState; -import com.google.android.apps.mytracks.content.MyTracksProviderUtils; -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.services.TrackRecordingServiceTest.MockContext; -import com.google.android.maps.mytracks.R; -import com.google.android.testing.mocking.AndroidMock; - -import android.content.SharedPreferences; -import android.content.SharedPreferences.OnSharedPreferenceChangeListener; -import android.database.ContentObserver; -import android.database.Cursor; -import android.database.MatrixCursor; -import android.hardware.Sensor; -import android.hardware.SensorEvent; -import android.hardware.SensorEventListener; -import android.location.Location; -import android.location.LocationListener; -import android.provider.BaseColumns; -import android.test.AndroidTestCase; -import android.test.RenamingDelegatingContext; -import android.test.mock.MockContentResolver; - -import java.lang.reflect.Constructor; -import java.util.EnumSet; - -import org.easymock.Capture; -import org.easymock.IAnswer; - -/** - * Tests for {@link TrackDataHub}. - * - * @author Rodrigo Damazio - */ -public class TrackDataHubTest extends AndroidTestCase { - - private static final long TRACK_ID = 42; - private MyTracksProviderUtils providerUtils; - private TrackDataHub hub; - private TrackDataSources dataSources; - private SharedPreferences prefs; - private TrackDataListener listener1; - private TrackDataListener listener2; - private Capture preferenceListenerCapture = - new Capture(); - private MockContext context; - private float declination; - - @Override - protected void setUp() throws Exception { - super.setUp(); - - MockContentResolver mockContentResolver = new MockContentResolver(); - RenamingDelegatingContext targetContext = new RenamingDelegatingContext( - getContext(), getContext(), "test."); - context = new MockContext(mockContentResolver, targetContext); - - prefs = context.getSharedPreferences(Constants.SETTINGS_NAME, 0); - providerUtils = AndroidMock.createMock("providerUtils", MyTracksProviderUtils.class); - dataSources = AndroidMock.createNiceMock("dataSources", TrackDataSources.class); - - hub = new TrackDataHub(context, dataSources, prefs, providerUtils) { - @Override - protected void runInListenerThread(Runnable runnable) { - // Run everything in the same thread. - runnable.run(); - } - - @Override - protected float getDeclinationFor(Location location, long timestamp) { - return declination; - } - }; - - listener1 = AndroidMock.createMock("listener1", TrackDataListener.class); - listener2 = AndroidMock.createMock("listener2", TrackDataListener.class); - } - - @Override - protected void tearDown() throws Exception { - AndroidMock.reset(dataSources); - - // Expect everything to be unregistered. - if (preferenceListenerCapture.hasCaptured()) { - dataSources.unregisterOnSharedPreferenceChangeListener(preferenceListenerCapture.getValue()); - } - dataSources.removeLocationUpdates(isA(LocationListener.class)); - dataSources.unregisterSensorListener(isA(SensorEventListener.class)); - dataSources.unregisterContentObserver(isA(ContentObserver.class)); - AndroidMock.expectLastCall().times(3); - - AndroidMock.replay(dataSources); - - hub.stop(); - hub.destroy(); - - super.tearDown(); - } - - public void testTrackListen() { - Capture observerCapture = new Capture(); - Track track = new Track(); - prefs.edit().putLong("recordingTrack", TRACK_ID) - .putLong("selectedTrack", TRACK_ID).commit(); - expect(providerUtils.getTrack(TRACK_ID)).andStubReturn(track); - expectStart(); - dataSources.registerContentObserver( - eq(TracksColumns.CONTENT_URI), eq(false), capture(observerCapture)); - - // Expect the initial loading. - // Both listeners (registered before and after start) should get the same data. - listener1.onTrackUpdated(track); - listener2.onTrackUpdated(track); - - AndroidMock.replay(dataSources, providerUtils, listener1, listener2); - - hub.registerTrackDataListener(listener1, EnumSet.of(ListenerDataType.TRACK_UPDATES)); - hub.start(); - hub.registerTrackDataListener(listener2, EnumSet.of(ListenerDataType.TRACK_UPDATES)); - - AndroidMock.verify(listener1, listener2, dataSources); - AndroidMock.reset(listener1, listener2, dataSources); - - ContentObserver observer = observerCapture.getValue(); - - // Now expect an update. - listener1.onTrackUpdated(track); - listener2.onTrackUpdated(track); - - AndroidMock.replay(listener1, listener2, dataSources); - - observer.onChange(false); - - AndroidMock.verify(listener1, listener2, dataSources); - AndroidMock.reset(listener1, listener2, dataSources); - - // Unregister one, get another update. - listener2.onTrackUpdated(track); - - AndroidMock.replay(listener1, listener2, dataSources); - - hub.unregisterTrackDataListener(listener1); - - observer.onChange(false); - - AndroidMock.verify(listener1, listener2, dataSources); - AndroidMock.reset(listener1, listener2, dataSources); - - // Unregister the other, expect internal unregistration - dataSources.unregisterContentObserver(observer); - - AndroidMock.replay(listener1, listener2, dataSources); - - hub.unregisterTrackDataListener(listener2); - observer.onChange(false); - - AndroidMock.verify(listener1, listener2, dataSources); - } - - private static class FixedSizeCursorAnswer implements IAnswer { - private final int size; - - public FixedSizeCursorAnswer(int size) { - this.size = size; - } - - @Override - public Cursor answer() throws Throwable { - MatrixCursor cursor = new MatrixCursor(new String[] { BaseColumns._ID }); - for (long i = 1; i <= size; i++) { - cursor.addRow(new Object[] { i }); - } - return cursor; - } - } - - public void testWaypointListen() { - Capture observerCapture = new Capture(); - prefs.edit().putLong("recordingTrack", TRACK_ID) - .putLong("selectedTrack", TRACK_ID).commit(); - - Waypoint wpt1 = new Waypoint(), - wpt2 = new Waypoint(), - wpt3 = new Waypoint(), - wpt4 = new Waypoint(); - Location loc = new Location("gps"); - loc.setLatitude(10.0); - loc.setLongitude(8.0); - wpt1.setLocation(loc); - wpt2.setLocation(loc); - wpt3.setLocation(loc); - wpt4.setLocation(loc); - - expect(providerUtils.getWaypointsCursor( - eq(TRACK_ID), leq(0L), eq(Constants.MAX_DISPLAYED_WAYPOINTS_POINTS))) - .andStubAnswer(new FixedSizeCursorAnswer(2)); - expect(providerUtils.createWaypoint(isA(Cursor.class))) - .andReturn(wpt1) - .andReturn(wpt2) - .andReturn(wpt1) - .andReturn(wpt2); - - expectStart(); - dataSources.registerContentObserver( - eq(WaypointsColumns.CONTENT_URI), eq(false), capture(observerCapture)); - - // Expect the initial loading. - // Both listeners (registered before and after start) should get the same data. - listener1.clearWaypoints(); - listener1.onNewWaypoint(wpt1); - listener1.onNewWaypoint(wpt2); - listener1.onNewWaypointsDone(); - listener2.clearWaypoints(); - listener2.onNewWaypoint(wpt1); - listener2.onNewWaypoint(wpt2); - listener2.onNewWaypointsDone(); - - AndroidMock.replay(dataSources, providerUtils, listener1, listener2); - - hub.registerTrackDataListener(listener1, EnumSet.of(ListenerDataType.WAYPOINT_UPDATES)); - hub.start(); - hub.registerTrackDataListener(listener2, EnumSet.of(ListenerDataType.WAYPOINT_UPDATES)); - - AndroidMock.verify(listener1, listener2, dataSources, providerUtils); - AndroidMock.reset(listener1, listener2, dataSources, providerUtils); - - ContentObserver observer = observerCapture.getValue(); - - expect(providerUtils.getWaypointsCursor( - eq(TRACK_ID), leq(0L), eq(Constants.MAX_DISPLAYED_WAYPOINTS_POINTS))) - .andStubAnswer(new FixedSizeCursorAnswer(3)); - expect(providerUtils.createWaypoint(isA(Cursor.class))) - .andReturn(wpt1) - .andReturn(wpt2) - .andReturn(wpt3); - - // Now expect an update. - listener1.clearWaypoints(); - listener2.clearWaypoints(); - listener1.onNewWaypoint(wpt1); - listener2.onNewWaypoint(wpt1); - listener1.onNewWaypoint(wpt2); - listener2.onNewWaypoint(wpt2); - listener1.onNewWaypoint(wpt3); - listener2.onNewWaypoint(wpt3); - listener1.onNewWaypointsDone(); - listener2.onNewWaypointsDone(); - - AndroidMock.replay(listener1, listener2, dataSources, providerUtils); - - observer.onChange(false); - - AndroidMock.verify(listener1, listener2, dataSources, providerUtils); - AndroidMock.reset(listener1, listener2, dataSources, providerUtils); - - // Unregister one, get another update. - expect(providerUtils.getWaypointsCursor( - eq(TRACK_ID), leq(0L), eq(Constants.MAX_DISPLAYED_WAYPOINTS_POINTS))) - .andStubAnswer(new FixedSizeCursorAnswer(4)); - expect(providerUtils.createWaypoint(isA(Cursor.class))) - .andReturn(wpt1) - .andReturn(wpt2) - .andReturn(wpt3) - .andReturn(wpt4); - - // Now expect an update. - listener2.clearWaypoints(); - listener2.onNewWaypoint(wpt1); - listener2.onNewWaypoint(wpt2); - listener2.onNewWaypoint(wpt3); - listener2.onNewWaypoint(wpt4); - listener2.onNewWaypointsDone(); - - AndroidMock.replay(listener1, listener2, dataSources, providerUtils); - - hub.unregisterTrackDataListener(listener1); - - observer.onChange(false); - - AndroidMock.verify(listener1, listener2, dataSources, providerUtils); - AndroidMock.reset(listener1, listener2, dataSources, providerUtils); - - // Unregister the other, expect internal unregistration - dataSources.unregisterContentObserver(observer); - - AndroidMock.replay(listener1, listener2, dataSources, providerUtils); - - hub.unregisterTrackDataListener(listener2); - observer.onChange(false); - - AndroidMock.verify(listener1, listener2, dataSources, providerUtils); - } - - public void testPointsListen() { - // TODO - } - - public void testPointsListen_resample() { - // TODO - } - - public void testLocationListen() { - // TODO - } - - public void testCompassListen() throws Exception { - Sensor compass = newSensor(); - expect(dataSources.getSensor(Sensor.TYPE_ORIENTATION)).andReturn(compass); - Capture listenerCapture = new Capture(); - dataSources.registerSensorListener(capture(listenerCapture), same(compass), anyInt()); - - Capture locationListenerCapture = new Capture(); - dataSources.requestLocationUpdates(capture(locationListenerCapture)); - - SensorEvent event = newSensorEvent(); - event.sensor = compass; - - // First, get a dummy heading update. - listener1.onCurrentHeadingChanged(0.0); - - // Then, get a heading update without a known location (thus can't calculate declination). - listener1.onCurrentHeadingChanged(42.0f); - - // Also expect location updates which are not relevant to us. - listener1.onProviderStateChange(isA(ProviderState.class)); - AndroidMock.expectLastCall().anyTimes(); - - AndroidMock.replay(dataSources, providerUtils, listener1); - - hub.registerTrackDataListener(listener1, - EnumSet.of(ListenerDataType.COMPASS_UPDATES, ListenerDataType.LOCATION_UPDATES)); - hub.start(); - - SensorEventListener sensorListener = listenerCapture.getValue(); - LocationListener locationListener = locationListenerCapture.getValue(); - event.values[0] = 42.0f; - sensorListener.onSensorChanged(event); - - AndroidMock.verify(dataSources, providerUtils, listener1); - AndroidMock.reset(dataSources, providerUtils, listener1); - - // Expect the heading update to include declination. - listener1.onCurrentHeadingChanged(52.0); - - // Also expect location updates which are not relevant to us. - listener1.onProviderStateChange(isA(ProviderState.class)); - AndroidMock.expectLastCall().anyTimes(); - listener1.onCurrentLocationChanged(isA(Location.class)); - AndroidMock.expectLastCall().anyTimes(); - - AndroidMock.replay(dataSources, providerUtils, listener1); - - // Now try injecting a location update, triggering a declination update. - Location location = new Location("gps"); - location.setLatitude(10.0); - location.setLongitude(20.0); - location.setAltitude(30.0); - declination = 10.0f; - locationListener.onLocationChanged(location); - sensorListener.onSensorChanged(event); - - AndroidMock.verify(dataSources, providerUtils, listener1); - AndroidMock.reset(dataSources, providerUtils, listener1); - - listener1.onCurrentHeadingChanged(52.0); - - AndroidMock.replay(dataSources, providerUtils, listener1); - - // Now try changing the known declination - it should still return the old declination, since - // updates only happen sparsely. - declination = 20.0f; - sensorListener.onSensorChanged(event); - - AndroidMock.verify(dataSources, providerUtils, listener1); - } - - private Sensor newSensor() throws Exception { - Constructor constructor = Sensor.class.getDeclaredConstructor(); - constructor.setAccessible(true); - return constructor.newInstance(); - } - - private SensorEvent newSensorEvent() throws Exception { - Constructor constructor = SensorEvent.class.getDeclaredConstructor(int.class); - constructor.setAccessible(true); - return constructor.newInstance(3); - } - - public void testDisplayPreferencesListen() throws Exception { - String metricUnitsKey = context.getString(R.string.metric_units_key); - String speedKey = context.getString(R.string.report_speed_key); - - prefs.edit() - .putBoolean(metricUnitsKey, true) - .putBoolean(speedKey, true) - .commit(); - - Capture listenerCapture = - new Capture(); - dataSources.registerOnSharedPreferenceChangeListener(capture(listenerCapture)); - - expect(listener1.onUnitsChanged(true)).andReturn(false); - expect(listener2.onUnitsChanged(true)).andReturn(false); - expect(listener1.onReportSpeedChanged(true)).andReturn(false); - expect(listener2.onReportSpeedChanged(true)).andReturn(false); - - AndroidMock.replay(dataSources, providerUtils, listener1, listener2); - - hub.registerTrackDataListener(listener1, EnumSet.of(ListenerDataType.DISPLAY_PREFERENCES)); - hub.start(); - hub.registerTrackDataListener(listener2, EnumSet.of(ListenerDataType.DISPLAY_PREFERENCES)); - - AndroidMock.verify(dataSources, providerUtils, listener1, listener2); - AndroidMock.reset(dataSources, providerUtils, listener1, listener2); - - expect(listener1.onReportSpeedChanged(false)).andReturn(false); - expect(listener2.onReportSpeedChanged(false)).andReturn(false); - - AndroidMock.replay(dataSources, providerUtils, listener1, listener2); - - prefs.edit() - .putBoolean(speedKey, false) - .commit(); - OnSharedPreferenceChangeListener listener = listenerCapture.getValue(); - listener.onSharedPreferenceChanged(prefs, speedKey); - - AndroidMock.verify(dataSources, providerUtils, listener1, listener2); - AndroidMock.reset(dataSources, providerUtils, listener1, listener2); - - expect(listener1.onUnitsChanged(false)).andReturn(false); - expect(listener2.onUnitsChanged(false)).andReturn(false); - - AndroidMock.replay(dataSources, providerUtils, listener1, listener2); - - prefs.edit() - .putBoolean(metricUnitsKey, false) - .commit(); - listener.onSharedPreferenceChanged(prefs, metricUnitsKey); - - AndroidMock.verify(dataSources, providerUtils, listener1, listener2); - } - - public void testFullListener() { - // TODO: test loading a track, getting updates, loading another, unloading - } - - private void expectStart() { - dataSources.registerOnSharedPreferenceChangeListener(capture(preferenceListenerCapture)); - } -} diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/content/TrackDataHubTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/content/TrackDataHubTest.java new file mode 100644 index 000000000..9f69c79f5 --- /dev/null +++ b/MyTracksTest/src/com/google/android/apps/mytracks/content/TrackDataHubTest.java @@ -0,0 +1,870 @@ +/* + * Copyright 2011 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.google.android.apps.mytracks.content; + +import static com.google.android.testing.mocking.AndroidMock.anyInt; +import static com.google.android.testing.mocking.AndroidMock.capture; +import static com.google.android.testing.mocking.AndroidMock.eq; +import static com.google.android.testing.mocking.AndroidMock.expect; +import static com.google.android.testing.mocking.AndroidMock.isA; +import static com.google.android.testing.mocking.AndroidMock.leq; +import static com.google.android.testing.mocking.AndroidMock.same; + +import com.google.android.apps.mytracks.Constants; +import com.google.android.apps.mytracks.content.MyTracksProviderUtils.LocationFactory; +import com.google.android.apps.mytracks.content.MyTracksProviderUtils.LocationIterator; +import com.google.android.apps.mytracks.content.TrackDataHub.ListenerDataType; +import com.google.android.apps.mytracks.content.TrackDataListener.ProviderState; +import com.google.android.apps.mytracks.services.TrackRecordingServiceTest.MockContext; +import com.google.android.maps.mytracks.R; +import com.google.android.testing.mocking.AndroidMock; + +import android.content.SharedPreferences; +import android.content.SharedPreferences.OnSharedPreferenceChangeListener; +import android.database.ContentObserver; +import android.database.Cursor; +import android.database.MatrixCursor; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; +import android.location.Location; +import android.location.LocationListener; +import android.provider.BaseColumns; +import android.test.AndroidTestCase; +import android.test.RenamingDelegatingContext; +import android.test.mock.MockContentResolver; + +import java.lang.reflect.Constructor; +import java.util.EnumSet; +import java.util.HashSet; +import java.util.Set; + +import org.easymock.Capture; +import org.easymock.IAnswer; + +/** + * Tests for {@link TrackDataHub}. + * + * @author Rodrigo Damazio + */ +public class TrackDataHubTest extends AndroidTestCase { + + private static final long TRACK_ID = 42L; + private static final int TARGET_POINTS = 50; + + private MyTracksProviderUtils providerUtils; + private TrackDataHub hub; + private TrackDataListeners listeners; + private DataSourcesWrapper dataSources; + private SharedPreferences prefs; + private TrackDataListener listener1; + private TrackDataListener listener2; + private Capture preferenceListenerCapture = + new Capture(); + private MockContext context; + private float declination; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + MockContentResolver mockContentResolver = new MockContentResolver(); + RenamingDelegatingContext targetContext = new RenamingDelegatingContext( + getContext(), getContext(), "test."); + context = new MockContext(mockContentResolver, targetContext); + + prefs = context.getSharedPreferences(Constants.SETTINGS_NAME, 0); + providerUtils = AndroidMock.createMock("providerUtils", MyTracksProviderUtils.class); + dataSources = AndroidMock.createNiceMock("dataSources", DataSourcesWrapper.class); + + listeners = new TrackDataListeners(); + hub = new TrackDataHub(context, dataSources, listeners, prefs, providerUtils, TARGET_POINTS) { + @Override + protected void runInListenerThread(Runnable runnable) { + // Run everything in the same thread. + runnable.run(); + } + + @Override + protected float getDeclinationFor(Location location, long timestamp) { + return declination; + } + }; + + listener1 = AndroidMock.createStrictMock("listener1", TrackDataListener.class); + listener2 = AndroidMock.createStrictMock("listener2", TrackDataListener.class); + } + + @Override + protected void tearDown() throws Exception { + AndroidMock.reset(dataSources); + + // Expect everything to be unregistered. + if (preferenceListenerCapture.hasCaptured()) { + dataSources.unregisterOnSharedPreferenceChangeListener(preferenceListenerCapture.getValue()); + } + dataSources.removeLocationUpdates(isA(LocationListener.class)); + dataSources.unregisterSensorListener(isA(SensorEventListener.class)); + dataSources.unregisterContentObserver(isA(ContentObserver.class)); + AndroidMock.expectLastCall().times(3); + + AndroidMock.replay(dataSources); + + hub.stop(); + hub.destroy(); + + super.tearDown(); + } + + public void testTrackListen() { + Capture observerCapture = new Capture(); + Track track = new Track(); + prefs.edit().putLong("recordingTrack", TRACK_ID) + .putLong("selectedTrack", TRACK_ID).commit(); + expect(providerUtils.getTrack(TRACK_ID)).andStubReturn(track); + expectStart(); + dataSources.registerContentObserver( + eq(TracksColumns.CONTENT_URI), eq(false), capture(observerCapture)); + + // Expect the initial loading. + // Both listeners (registered before and after start) should get the same data. + listener1.onTrackUpdated(track); + listener2.onTrackUpdated(track); + + replay(); + + hub.registerTrackDataListener(listener1, EnumSet.of(ListenerDataType.TRACK_UPDATES)); + hub.start(); + hub.registerTrackDataListener(listener2, EnumSet.of(ListenerDataType.TRACK_UPDATES)); + + verifyAndReset(); + + ContentObserver observer = observerCapture.getValue(); + expect(providerUtils.getTrack(TRACK_ID)).andStubReturn(track); + + // Now expect an update. + listener1.onTrackUpdated(track); + listener2.onTrackUpdated(track); + + replay(); + + observer.onChange(false); + + verifyAndReset(); + + // Unregister one, get another update. + expect(providerUtils.getTrack(TRACK_ID)).andStubReturn(track); + listener2.onTrackUpdated(track); + + replay(); + + hub.unregisterTrackDataListener(listener1); + + observer.onChange(false); + + verifyAndReset(); + + // Unregister the other, expect internal unregistration + dataSources.unregisterContentObserver(observer); + + replay(); + + hub.unregisterTrackDataListener(listener2); + observer.onChange(false); + + verifyAndReset(); + } + + private static class FixedSizeCursorAnswer implements IAnswer { + private final int size; + + public FixedSizeCursorAnswer(int size) { + this.size = size; + } + + @Override + public Cursor answer() throws Throwable { + MatrixCursor cursor = new MatrixCursor(new String[] { BaseColumns._ID }); + for (long i = 1; i <= size; i++) { + cursor.addRow(new Object[] { i }); + } + return cursor; + } + } + + private static class FixedSizeLocationIterator implements LocationIterator { + private final long startId; + private final Location[] locs; + private final Set splitIndexSet = new HashSet(); + private int currentIdx = -1; + + public FixedSizeLocationIterator(long startId, int size) { + this(startId, size, null); + } + + public FixedSizeLocationIterator(long startId, int size, int... splitIndices) { + this.startId = startId; + this.locs = new Location[size]; + + for (int i = 0; i < size; i++) { + Location loc = new Location("gps"); + loc.setLatitude(-15.0 + i / 1000.0); + loc.setLongitude(37 + i / 1000.0); + loc.setAltitude(i); + + locs[i] = loc; + } + + if (splitIndices != null) { + for (int splitIdx : splitIndices) { + splitIndexSet.add(splitIdx); + + Location splitLoc = locs[splitIdx]; + splitLoc.setLatitude(100.0); + splitLoc.setLongitude(200.0); + } + } + } + + public void expectLocationsDelivered(TrackDataListener listener) { + for (int i = 0; i < locs.length; i++) { + if (splitIndexSet.contains(i)) { + listener.onSegmentSplit(); + } else { + listener.onNewTrackPoint(locs[i]); + } + } + } + + public void expectSampledLocationsDelivered( + TrackDataListener listener, int sampleFrequency, boolean includeSampledOut) { + for (int i = 0; i < locs.length; i++) { + if (splitIndexSet.contains(i)) { + listener.onSegmentSplit(); + } else if (i % sampleFrequency == 0) { + listener.onNewTrackPoint(locs[i]); + } else if (includeSampledOut) { + listener.onSampledOutTrackPoint(locs[i]); + } + } + } + + @Override + public boolean hasNext() { + return currentIdx < (locs.length - 1); + } + + @Override + public Location next() { + currentIdx++; + return locs[currentIdx]; + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + + @Override + public long getLocationId() { + return startId + currentIdx; + } + + @Override + public void close() { + // Do nothing + } + } + + public void testWaypointListen() { + Capture observerCapture = new Capture(); + prefs.edit().putLong("recordingTrack", TRACK_ID) + .putLong("selectedTrack", TRACK_ID).commit(); + + Waypoint wpt1 = new Waypoint(), + wpt2 = new Waypoint(), + wpt3 = new Waypoint(), + wpt4 = new Waypoint(); + Location loc = new Location("gps"); + loc.setLatitude(10.0); + loc.setLongitude(8.0); + wpt1.setLocation(loc); + wpt2.setLocation(loc); + wpt3.setLocation(loc); + wpt4.setLocation(loc); + + expect(providerUtils.getWaypointsCursor( + eq(TRACK_ID), leq(0L), eq(Constants.MAX_DISPLAYED_WAYPOINTS_POINTS))) + .andStubAnswer(new FixedSizeCursorAnswer(2)); + expect(providerUtils.createWaypoint(isA(Cursor.class))) + .andReturn(wpt1) + .andReturn(wpt2) + .andReturn(wpt1) + .andReturn(wpt2); + + expectStart(); + dataSources.registerContentObserver( + eq(WaypointsColumns.CONTENT_URI), eq(false), capture(observerCapture)); + + // Expect the initial loading. + // Both listeners (registered before and after start) should get the same data. + listener1.clearWaypoints(); + listener1.onNewWaypoint(wpt1); + listener1.onNewWaypoint(wpt2); + listener1.onNewWaypointsDone(); + listener2.clearWaypoints(); + listener2.onNewWaypoint(wpt1); + listener2.onNewWaypoint(wpt2); + listener2.onNewWaypointsDone(); + + replay(); + + hub.registerTrackDataListener(listener1, EnumSet.of(ListenerDataType.WAYPOINT_UPDATES)); + hub.start(); + hub.registerTrackDataListener(listener2, EnumSet.of(ListenerDataType.WAYPOINT_UPDATES)); + + verifyAndReset(); + + ContentObserver observer = observerCapture.getValue(); + + expect(providerUtils.getWaypointsCursor( + eq(TRACK_ID), leq(0L), eq(Constants.MAX_DISPLAYED_WAYPOINTS_POINTS))) + .andStubAnswer(new FixedSizeCursorAnswer(3)); + expect(providerUtils.createWaypoint(isA(Cursor.class))) + .andReturn(wpt1) + .andReturn(wpt2) + .andReturn(wpt3); + + // Now expect an update. + listener1.clearWaypoints(); + listener2.clearWaypoints(); + listener1.onNewWaypoint(wpt1); + listener2.onNewWaypoint(wpt1); + listener1.onNewWaypoint(wpt2); + listener2.onNewWaypoint(wpt2); + listener1.onNewWaypoint(wpt3); + listener2.onNewWaypoint(wpt3); + listener1.onNewWaypointsDone(); + listener2.onNewWaypointsDone(); + + replay(); + + observer.onChange(false); + + verifyAndReset(); + + // Unregister one, get another update. + expect(providerUtils.getWaypointsCursor( + eq(TRACK_ID), leq(0L), eq(Constants.MAX_DISPLAYED_WAYPOINTS_POINTS))) + .andStubAnswer(new FixedSizeCursorAnswer(4)); + expect(providerUtils.createWaypoint(isA(Cursor.class))) + .andReturn(wpt1) + .andReturn(wpt2) + .andReturn(wpt3) + .andReturn(wpt4); + + // Now expect an update. + listener2.clearWaypoints(); + listener2.onNewWaypoint(wpt1); + listener2.onNewWaypoint(wpt2); + listener2.onNewWaypoint(wpt3); + listener2.onNewWaypoint(wpt4); + listener2.onNewWaypointsDone(); + + replay(); + + hub.unregisterTrackDataListener(listener1); + + observer.onChange(false); + + verifyAndReset(); + + // Unregister the other, expect internal unregistration + dataSources.unregisterContentObserver(observer); + + replay(); + + hub.unregisterTrackDataListener(listener2); + observer.onChange(false); + + verifyAndReset(); + } + + public void testPointsListen() { + Capture observerCapture = new Capture(); + prefs.edit().putLong("recordingTrack", TRACK_ID) + .putLong("selectedTrack", TRACK_ID).commit(); + + expectStart(); + dataSources.registerContentObserver( + eq(TrackPointsColumns.CONTENT_URI), eq(false), capture(observerCapture)); + + FixedSizeLocationIterator locationIterator = new FixedSizeLocationIterator(1, 10, 5); + expect(providerUtils.getLocationIterator( + eq(TRACK_ID), eq(0L), eq(false), isA(LocationFactory.class))) + .andReturn(locationIterator); + expect(providerUtils.getLastLocationId(TRACK_ID)).andReturn(10L); + + listener1.clearTrackPoints(); + locationIterator.expectLocationsDelivered(listener1); + listener1.onNewTrackPointsDone(); + + replay(); + + hub.start(); + hub.registerTrackDataListener(listener1, EnumSet.of(ListenerDataType.POINT_UPDATES)); + + verifyAndReset(); + + // Register a second listener - it will get the same points as the previous one + locationIterator = new FixedSizeLocationIterator(1, 10, 5); + expect(providerUtils.getLocationIterator( + eq(TRACK_ID), eq(0L), eq(false), isA(LocationFactory.class))) + .andReturn(locationIterator); + expect(providerUtils.getLastLocationId(TRACK_ID)).andReturn(10L); + + listener2.clearTrackPoints(); + locationIterator.expectLocationsDelivered(listener2); + listener2.onNewTrackPointsDone(); + + replay(); + + hub.start(); + hub.registerTrackDataListener(listener2, EnumSet.of(ListenerDataType.POINT_UPDATES)); + + verifyAndReset(); + + // Deliver more points - should go to both listeners, without clearing. + ContentObserver observer = observerCapture.getValue(); + + locationIterator = new FixedSizeLocationIterator(11, 10, 1); + expect(providerUtils.getLocationIterator( + eq(TRACK_ID), eq(11L), eq(false), isA(LocationFactory.class))) + .andReturn(locationIterator); + expect(providerUtils.getLastLocationId(TRACK_ID)).andReturn(20L); + + locationIterator.expectLocationsDelivered(listener1); + locationIterator.expectLocationsDelivered(listener2); + listener1.onNewTrackPointsDone(); + listener2.onNewTrackPointsDone(); + + replay(); + + observer.onChange(false); + + verifyAndReset(); + + // Unregister listener1, switch tracks to ensure data is cleared/reloaded. + locationIterator = new FixedSizeLocationIterator(101, 10); + expect(providerUtils.getLocationIterator( + eq(TRACK_ID + 1), eq(0L), eq(false), isA(LocationFactory.class))) + .andReturn(locationIterator); + expect(providerUtils.getLastLocationId(TRACK_ID + 1)).andReturn(110L); + + listener2.clearTrackPoints(); + locationIterator.expectLocationsDelivered(listener2); + listener2.onNewTrackPointsDone(); + + replay(); + + hub.unregisterTrackDataListener(listener1); + hub.loadTrack(TRACK_ID + 1); + + verifyAndReset(); + } + + public void testPointsListen_beforeStart() { + + } + + public void testPointsListen_reRegister() { + Capture observerCapture = new Capture(); + prefs.edit().putLong("recordingTrack", TRACK_ID) + .putLong("selectedTrack", TRACK_ID).commit(); + + expectStart(); + dataSources.registerContentObserver( + eq(TrackPointsColumns.CONTENT_URI), eq(false), capture(observerCapture)); + + FixedSizeLocationIterator locationIterator = new FixedSizeLocationIterator(1, 10, 5); + expect(providerUtils.getLocationIterator( + eq(TRACK_ID), eq(0L), eq(false), isA(LocationFactory.class))) + .andReturn(locationIterator); + expect(providerUtils.getLastLocationId(TRACK_ID)).andReturn(10L); + + listener1.clearTrackPoints(); + locationIterator.expectLocationsDelivered(listener1); + listener1.onNewTrackPointsDone(); + + replay(); + + hub.start(); + hub.registerTrackDataListener(listener1, EnumSet.of(ListenerDataType.POINT_UPDATES)); + + verifyAndReset(); + + // Unregister + ContentObserver observer = observerCapture.getValue(); + dataSources.unregisterContentObserver(observer); + + replay(); + + hub.unregisterTrackDataListener(listener1); + + verifyAndReset(); + + // Register again, except only points since unregistered. + dataSources.registerContentObserver( + eq(TrackPointsColumns.CONTENT_URI), eq(false), capture(observerCapture)); + + locationIterator = new FixedSizeLocationIterator(11, 10); + expect(providerUtils.getLocationIterator( + eq(TRACK_ID), eq(11L), eq(false), isA(LocationFactory.class))) + .andReturn(locationIterator); + expect(providerUtils.getLastLocationId(TRACK_ID)).andReturn(20L); + + locationIterator.expectLocationsDelivered(listener1); + listener1.onNewTrackPointsDone(); + + replay(); + + hub.registerTrackDataListener(listener1, EnumSet.of(ListenerDataType.POINT_UPDATES)); + + verifyAndReset(); + + // Deliver more points - should still be incremental. + locationIterator = new FixedSizeLocationIterator(21, 10, 1); + expect(providerUtils.getLocationIterator( + eq(TRACK_ID), eq(21L), eq(false), isA(LocationFactory.class))) + .andReturn(locationIterator); + expect(providerUtils.getLastLocationId(TRACK_ID)).andReturn(30L); + + locationIterator.expectLocationsDelivered(listener1); + listener1.onNewTrackPointsDone(); + + replay(); + + observer.onChange(false); + + verifyAndReset(); + } + + public void testPointsListen_reRegisterTrackChanged() { + Capture observerCapture = new Capture(); + prefs.edit().putLong("recordingTrack", TRACK_ID) + .putLong("selectedTrack", TRACK_ID).commit(); + + expectStart(); + dataSources.registerContentObserver( + eq(TrackPointsColumns.CONTENT_URI), eq(false), capture(observerCapture)); + + FixedSizeLocationIterator locationIterator = new FixedSizeLocationIterator(1, 10, 5); + expect(providerUtils.getLocationIterator( + eq(TRACK_ID), eq(0L), eq(false), isA(LocationFactory.class))) + .andReturn(locationIterator); + expect(providerUtils.getLastLocationId(TRACK_ID)).andReturn(10L); + + listener1.clearTrackPoints(); + locationIterator.expectLocationsDelivered(listener1); + listener1.onNewTrackPointsDone(); + + replay(); + + hub.start(); + hub.registerTrackDataListener(listener1, EnumSet.of(ListenerDataType.POINT_UPDATES)); + + verifyAndReset(); + + // Unregister + ContentObserver observer = observerCapture.getValue(); + dataSources.unregisterContentObserver(observer); + + replay(); + + hub.unregisterTrackDataListener(listener1); + + verifyAndReset(); + + // Register again after track changed, expect all points. + dataSources.registerContentObserver( + eq(TrackPointsColumns.CONTENT_URI), eq(false), capture(observerCapture)); + + locationIterator = new FixedSizeLocationIterator(1, 10); + expect(providerUtils.getLocationIterator( + eq(TRACK_ID + 1), eq(0L), eq(false), isA(LocationFactory.class))) + .andReturn(locationIterator); + expect(providerUtils.getLastLocationId(TRACK_ID + 1)).andReturn(10L); + + listener1.clearTrackPoints(); + locationIterator.expectLocationsDelivered(listener1); + listener1.onNewTrackPointsDone(); + + replay(); + + hub.loadTrack(TRACK_ID + 1); + hub.registerTrackDataListener(listener1, EnumSet.of(ListenerDataType.POINT_UPDATES)); + + verifyAndReset(); + } + + public void testPointsListen_largeTrackSampling() { + Capture observerCapture = new Capture(); + prefs.edit().putLong("recordingTrack", TRACK_ID) + .putLong("selectedTrack", TRACK_ID).commit(); + + expectStart(); + dataSources.registerContentObserver( + eq(TrackPointsColumns.CONTENT_URI), eq(false), capture(observerCapture)); + + FixedSizeLocationIterator locationIterator = new FixedSizeLocationIterator(1, 200, 4, 25, 71, 120); + expect(providerUtils.getLocationIterator( + eq(TRACK_ID), eq(0L), eq(false), isA(LocationFactory.class))) + .andReturn(locationIterator); + expect(providerUtils.getLastLocationId(TRACK_ID)).andReturn(200L); + + listener1.clearTrackPoints(); + listener2.clearTrackPoints(); + locationIterator.expectSampledLocationsDelivered(listener1, 4, false); + locationIterator.expectSampledLocationsDelivered(listener2, 4, true); + listener1.onNewTrackPointsDone(); + listener2.onNewTrackPointsDone(); + + replay(); + + hub.registerTrackDataListener(listener1, + EnumSet.of(ListenerDataType.POINT_UPDATES)); + hub.registerTrackDataListener(listener2, + EnumSet.of(ListenerDataType.POINT_UPDATES, ListenerDataType.SAMPLED_OUT_POINT_UPDATES)); + hub.start(); + + verifyAndReset(); + } + + public void testPointsListen_resampling() { + Capture observerCapture = new Capture(); + prefs.edit().putLong("recordingTrack", TRACK_ID) + .putLong("selectedTrack", TRACK_ID).commit(); + + expectStart(); + dataSources.registerContentObserver( + eq(TrackPointsColumns.CONTENT_URI), eq(false), capture(observerCapture)); + + // Deliver 30 points (no sampling happens) + FixedSizeLocationIterator locationIterator = new FixedSizeLocationIterator(1, 30, 5); + expect(providerUtils.getLocationIterator( + eq(TRACK_ID), eq(0L), eq(false), isA(LocationFactory.class))) + .andReturn(locationIterator); + expect(providerUtils.getLastLocationId(TRACK_ID)).andReturn(30L); + + listener1.clearTrackPoints(); + locationIterator.expectLocationsDelivered(listener1); + listener1.onNewTrackPointsDone(); + + replay(); + + hub.start(); + hub.registerTrackDataListener(listener1, EnumSet.of(ListenerDataType.POINT_UPDATES)); + + verifyAndReset(); + + // Now deliver 30 more (incrementally sampled) + ContentObserver observer = observerCapture.getValue(); + locationIterator = new FixedSizeLocationIterator(31, 30); + expect(providerUtils.getLocationIterator( + eq(TRACK_ID), eq(31L), eq(false), isA(LocationFactory.class))) + .andReturn(locationIterator); + expect(providerUtils.getLastLocationId(TRACK_ID)).andReturn(60L); + + locationIterator.expectSampledLocationsDelivered(listener1, 2, false); + listener1.onNewTrackPointsDone(); + + replay(); + + observer.onChange(false); + + verifyAndReset(); + + // Now another 30 (triggers resampling) + locationIterator = new FixedSizeLocationIterator(1, 90); + expect(providerUtils.getLocationIterator( + eq(TRACK_ID), eq(0L), eq(false), isA(LocationFactory.class))) + .andReturn(locationIterator); + expect(providerUtils.getLastLocationId(TRACK_ID)).andReturn(90L); + + listener1.clearTrackPoints(); + locationIterator.expectSampledLocationsDelivered(listener1, 2, false); + listener1.onNewTrackPointsDone(); + + replay(); + + observer.onChange(false); + + verifyAndReset(); + } + + public void testLocationListen() { + // TODO + } + + public void testCompassListen() throws Exception { + AndroidMock.resetToDefault(listener1); + + Sensor compass = newSensor(); + expect(dataSources.getSensor(Sensor.TYPE_ORIENTATION)).andReturn(compass); + Capture listenerCapture = new Capture(); + dataSources.registerSensorListener(capture(listenerCapture), same(compass), anyInt()); + + Capture locationListenerCapture = new Capture(); + dataSources.requestLocationUpdates(capture(locationListenerCapture)); + + SensorEvent event = newSensorEvent(); + event.sensor = compass; + + // First, get a dummy heading update. + listener1.onCurrentHeadingChanged(0.0); + + // Then, get a heading update without a known location (thus can't calculate declination). + listener1.onCurrentHeadingChanged(42.0f); + + // Also expect location updates which are not relevant to us. + listener1.onProviderStateChange(isA(ProviderState.class)); + AndroidMock.expectLastCall().anyTimes(); + + replay(); + + hub.registerTrackDataListener(listener1, + EnumSet.of(ListenerDataType.COMPASS_UPDATES, ListenerDataType.LOCATION_UPDATES)); + hub.start(); + + SensorEventListener sensorListener = listenerCapture.getValue(); + LocationListener locationListener = locationListenerCapture.getValue(); + event.values[0] = 42.0f; + sensorListener.onSensorChanged(event); + + verifyAndReset(); + + // Expect the heading update to include declination. + listener1.onCurrentHeadingChanged(52.0); + + // Also expect location updates which are not relevant to us. + listener1.onProviderStateChange(isA(ProviderState.class)); + AndroidMock.expectLastCall().anyTimes(); + listener1.onCurrentLocationChanged(isA(Location.class)); + AndroidMock.expectLastCall().anyTimes(); + + replay(); + + // Now try injecting a location update, triggering a declination update. + Location location = new Location("gps"); + location.setLatitude(10.0); + location.setLongitude(20.0); + location.setAltitude(30.0); + declination = 10.0f; + locationListener.onLocationChanged(location); + sensorListener.onSensorChanged(event); + + verifyAndReset(); + + listener1.onCurrentHeadingChanged(52.0); + + replay(); + + // Now try changing the known declination - it should still return the old declination, since + // updates only happen sparsely. + declination = 20.0f; + sensorListener.onSensorChanged(event); + + verifyAndReset(); + } + + private Sensor newSensor() throws Exception { + Constructor constructor = Sensor.class.getDeclaredConstructor(); + constructor.setAccessible(true); + return constructor.newInstance(); + } + + private SensorEvent newSensorEvent() throws Exception { + Constructor constructor = SensorEvent.class.getDeclaredConstructor(int.class); + constructor.setAccessible(true); + return constructor.newInstance(3); + } + + public void testDisplayPreferencesListen() throws Exception { + String metricUnitsKey = context.getString(R.string.metric_units_key); + String speedKey = context.getString(R.string.report_speed_key); + + prefs.edit() + .putBoolean(metricUnitsKey, true) + .putBoolean(speedKey, true) + .commit(); + + Capture listenerCapture = + new Capture(); + dataSources.registerOnSharedPreferenceChangeListener(capture(listenerCapture)); + + expect(listener1.onUnitsChanged(true)).andReturn(false); + expect(listener2.onUnitsChanged(true)).andReturn(false); + expect(listener1.onReportSpeedChanged(true)).andReturn(false); + expect(listener2.onReportSpeedChanged(true)).andReturn(false); + + replay(); + + hub.registerTrackDataListener(listener1, EnumSet.of(ListenerDataType.DISPLAY_PREFERENCES)); + hub.start(); + hub.registerTrackDataListener(listener2, EnumSet.of(ListenerDataType.DISPLAY_PREFERENCES)); + + verifyAndReset(); + + expect(listener1.onReportSpeedChanged(false)).andReturn(false); + expect(listener2.onReportSpeedChanged(false)).andReturn(false); + + replay(); + + prefs.edit() + .putBoolean(speedKey, false) + .commit(); + OnSharedPreferenceChangeListener listener = listenerCapture.getValue(); + listener.onSharedPreferenceChanged(prefs, speedKey); + + AndroidMock.verify(dataSources, providerUtils, listener1, listener2); + AndroidMock.reset(dataSources, providerUtils, listener1, listener2); + + expect(listener1.onUnitsChanged(false)).andReturn(false); + expect(listener2.onUnitsChanged(false)).andReturn(false); + + replay(); + + prefs.edit() + .putBoolean(metricUnitsKey, false) + .commit(); + listener.onSharedPreferenceChanged(prefs, metricUnitsKey); + + verifyAndReset(); + } + + private void expectStart() { + dataSources.registerOnSharedPreferenceChangeListener(capture(preferenceListenerCapture)); + } + + private void replay() { + AndroidMock.replay(dataSources, providerUtils, listener1, listener2); + } + + private void verifyAndReset() { + AndroidMock.verify(listener1, listener2, dataSources, providerUtils); + AndroidMock.reset(listener1, listener2, dataSources, providerUtils); + } +} diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/io/backup/DatabaseDumperTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/io/backup/DatabaseDumperTest.java index d1615b8cf..2f74afb5c 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/io/backup/DatabaseDumperTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/io/backup/DatabaseDumperTest.java @@ -110,9 +110,6 @@ public class DatabaseDumperTest extends TestCase { // Verify the number of rows assertEquals(FAKE_DATA.length, reader.readInt()); - // Verify the row contents - byte[] blob = new byte[4]; - // Row 0 -- everything populated assertEquals(EXPECTED_FIELD_SETS[0], reader.readLong()); assertEquals(42, reader.readInt()); diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/io/docs/DocsHelperTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/io/docs/DocsHelperTest.java index b1402c06f..93d1d3b03 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/io/docs/DocsHelperTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/io/docs/DocsHelperTest.java @@ -16,6 +16,7 @@ package com.google.android.apps.mytracks.io.docs; import com.google.android.apps.mytracks.io.gdata.GDataWrapper; +import com.google.wireless.gdata.client.GDataServiceClient; import android.content.Context; import android.test.mock.MockContext; @@ -43,7 +44,7 @@ public class DocsHelperTest extends TestCase { // touch AndroidMock at all to get this failure. // The bug is filed with Android Mock as // http://code.google.com/p/android-mock/issues/detail?id=3 - private class MockGDataWrapper extends GDataWrapper { + private class MockGDataWrapper extends GDataWrapper { private final boolean returnValue; MockGDataWrapper(boolean returnValue) { @@ -51,7 +52,7 @@ public class DocsHelperTest extends TestCase { } @Override - public boolean runQuery(QueryFunction queryFunction) { + public boolean runQuery(QueryFunction queryFunction) { return returnValue; } } 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 index 6cebf3692..910b1ca98 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/services/sensors/ZephyrMessageParserTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/services/sensors/ZephyrMessageParserTest.java @@ -9,21 +9,40 @@ public class ZephyrMessageParserTest extends TestCase { ZephyrMessageParser parser = new ZephyrMessageParser(); public void testIsValid() { - byte[] buf = new byte[60]; + byte[] smallBuf = new byte[59]; + assertFalse(parser.isValid(smallBuf)); + // A complete and valid Zephyr HxM packet + byte[] buf = { 2,38,55,26,0,49,101,80,0,49,98,100,42,113,120,-53,-24,-60,-123,-61,117,-69,42,-75,74,-78,51,-79,27,-83,28,-88,28,-93,29,-98,25,-103,26,-108,26,-113,59,-118,0,0,0,0,0,0,-22,3,125,1,48,0,96,4,30,0 }; + // Make buffer invalid + buf[0] = buf[58] = buf[59] = 0; assertFalse(parser.isValid(buf)); buf[0] = 0x02; assertFalse(parser.isValid(buf)); + buf[58] = 0x1E; + assertFalse(parser.isValid(buf)); buf[59] = 0x03; assertTrue(parser.isValid(buf)); } public void testParseBuffer() { byte[] buf = new byte[60]; - buf[12] = 50; + // Heartrate (-1 =^ 255 unsigned byte) + buf[12] = -1; + // Power + buf[11] = 51; + // Cadence (=^ 255*16 strides/min) + buf[56] = -1; + buf[57] = 15; Sensor.SensorDataSet sds = parser.parseBuffer(buf); assertTrue(sds.hasHeartRate()); assertTrue(sds.getHeartRate().getState() == Sensor.SensorState.SENDING); - assertEquals(50, sds.getHeartRate().getValue()); + assertEquals(255, sds.getHeartRate().getValue()); + assertTrue(sds.hasPower()); + assertTrue(sds.getPower().getState() == Sensor.SensorState.SENDING); + assertEquals(51, sds.getPower().getValue()); + assertTrue(sds.hasCadence()); + assertTrue(sds.getCadence().getState() == Sensor.SensorState.SENDING); + assertEquals(255, sds.getCadence().getValue()); } public void testFindNextAlignment() { diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/services/sensors/AntDirectSensorManagerTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/services/sensors/ant/AntDirectSensorManagerTest.java similarity index 97% rename from MyTracksTest/src/com/google/android/apps/mytracks/services/sensors/AntDirectSensorManagerTest.java rename to MyTracksTest/src/com/google/android/apps/mytracks/services/sensors/ant/AntDirectSensorManagerTest.java index c50ecb985..0b0cf33e7 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/services/sensors/AntDirectSensorManagerTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/services/sensors/ant/AntDirectSensorManagerTest.java @@ -13,7 +13,7 @@ * License for the specific language governing permissions and limitations under * the License. */ -package com.google.android.apps.mytracks.services.sensors; +package com.google.android.apps.mytracks.services.sensors.ant; import com.dsi.ant.AntMesg; import com.google.android.apps.mytracks.Constants; diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/services/sensors/AntSensorManagerTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/services/sensors/ant/AntSensorManagerTest.java similarity index 97% rename from MyTracksTest/src/com/google/android/apps/mytracks/services/sensors/AntSensorManagerTest.java rename to MyTracksTest/src/com/google/android/apps/mytracks/services/sensors/ant/AntSensorManagerTest.java index 0019bf0ae..7f57b9ba4 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/services/sensors/AntSensorManagerTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/services/sensors/ant/AntSensorManagerTest.java @@ -13,7 +13,7 @@ * License for the specific language governing permissions and limitations under * the License. */ -package com.google.android.apps.mytracks.services.sensors; +package com.google.android.apps.mytracks.services.sensors.ant; import com.google.android.apps.mytracks.services.sensors.ant.AntSensorManager; diff --git a/SignalStrengthSampler/src/com/google/android/apps/mytracks/signalstrength/SignalStrengthService.java b/SignalStrengthSampler/src/com/google/android/apps/mytracks/signalstrength/SignalStrengthService.java index 8a9cf4cd3..70d417368 100644 --- a/SignalStrengthSampler/src/com/google/android/apps/mytracks/signalstrength/SignalStrengthService.java +++ b/SignalStrengthSampler/src/com/google/android/apps/mytracks/signalstrength/SignalStrengthService.java @@ -1,12 +1,12 @@ /* * 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 @@ -15,10 +15,10 @@ */ package com.google.android.apps.mytracks.signalstrength; -import static com.google.android.apps.mytracks.signalstrength.SignalStrengthConstants.*; +import static com.google.android.apps.mytracks.signalstrength.SignalStrengthConstants.START_SAMPLING; +import static com.google.android.apps.mytracks.signalstrength.SignalStrengthConstants.STOP_SAMPLING; +import static com.google.android.apps.mytracks.signalstrength.SignalStrengthConstants.TAG; -import com.google.android.apps.mytracks.content.MyTracksProviderUtils; -import com.google.android.apps.mytracks.content.MyTracksProviderUtilsImpl; import com.google.android.apps.mytracks.content.WaypointCreationRequest; import com.google.android.apps.mytracks.services.ITrackRecordingService; import com.google.android.apps.mytracks.signalstrength.SignalStrengthListener.SignalStrengthCallback; @@ -206,7 +206,7 @@ public class SignalStrengthService extends Service // Tell the user we've stopped Toast.makeText(this, R.string.stopped_sampling, Toast.LENGTH_SHORT).show(); - + // Stop stopSelf(); }