Merged widget.

This commit is contained in:
Sandor Dornbush
2011-06-09 20:57:48 -07:00
parent 7020b78faa
commit 63677b1451
41 changed files with 2230 additions and 1277 deletions
+2 -2
View File
@@ -17,8 +17,8 @@ limitations under the License.
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.google.android.maps.mytracks"
android:installLocation="auto"
android:versionCode="27"
android:versionName="1.1.4">
android:versionCode="29"
android:versionName="1.1.6">
<application android:icon="@drawable/arrow_icon"
android:label="@string/app_name"
android:backupAgent="com.google.android.apps.mytracks.io.backup.MyTracksBackupAgent">
+4 -4
View File
@@ -150,7 +150,7 @@ limitations under the License.
<item>typ mieszany</item>
<item>inny</item>
<item>bieanie</item>
<item>bieganie</item>
<item>bieganie uliczne</item>
<item>bieganie na torze</item>
<item>bieganie terenowe</item>
@@ -170,10 +170,10 @@ limitations under the License.
<item>łyżwiarstwo torowe</item>
<item>deskorolka</item>
<item>wędrówka po sniegu</item>
<item>wędrówka po śniegu</item>
<item>narciarstwo</item>
<item>snowboard</item>
<item>saneczkastwo</item>
<item>saneczkarstwo</item>
<item>żeglarstwo lodowe</item>
<item>żeglowanie</item>
@@ -237,7 +237,7 @@ limitations under the License.
<item>kominek</item>
<item>remiza strażacka</item>
<item>baza wędkarska</item>
<item>stcja paliw</item>
<item>stacja paliw</item>
<item>pole golfowe</item>
<item>sklep spożywczy</item>
<item>fryzjer</item>
+28 -2
View File
@@ -42,7 +42,7 @@ limitations under the License.
<string name="error_invalid_gpx_format">Plik nie jest poprawnym plikiem GPX.</string>
<string name="error_generic">Wystąpił błąd. Spróbuj ponownie.</string>
<string name="error_track_does_not_exist">Trasa nie istnieje.</string>
<string name="error_out_of_memory">Plik jest za duży (musi być mniejszy nić 500 KB). Przerywam.</string>
<string name="error_out_of_memory">Plik jest za duży (musi być mniejszy niż 500 KB). Przerywam.</string>
<string name="error_unable_to_insert_marker">Nie można wstawić markera. Brak poprawnych danych o lokalizacji.</string>
<!-- Status messages -->
@@ -334,7 +334,6 @@ limitations under the License.
<string name="about_team">Zespół:</string>
<string name="about_version">Wersja:</string>
<string name="about_support">Wsparcie:</string>
<string name="about_bart">Bartłomiej Niechwiej</string>
<!-- Send to Google -->
<string name="sendtogoogle_title">Wyślij do...</string>
@@ -385,4 +384,31 @@ limitations under the License.
<!-- Check Units -->
<string name="check_units_title">Jednostki miary</string>
<string name="check_units_message">Domyślnie używane są jednostki metryczne. Chcesz to zmienić?</string>
<!-- Fake preference keys - never saved, but show options in the preferences screen -->
<string name="method_bluetooth">Bluetooth</string>
<string name="bluetooth_options">Opcje Bluetooth</string>
<string name="bluetooth_pairing">Parowanie urządzenia</string>
<string name="bluetooth_pairing_summary">Przejdź do ustawień Bluetooth Androida aby sparować urządzenie</string>
<string name="sensor_bluetooth_options_key">bluetoothSensorsEnabled</string>
<string name="ant_options">Opce ANT</string>
<string name="sensor_options">Opcje Czujników</string>
<!-- Bluetooth Sensor -->
<string name="bluetooth_connected">Połączony z %s</string>
<string name="bluetooth_connecting">Łączenie urządzenia Bluetooth w toku...</string>
<string name="bluetooth_not_connected">Urządzenie Bluetooth niepołączone</string>
<!-- Sensor State -->
<string name="sensor_data_time">Czas Danych Czujnika</string>
<string name="power_meter_enabled">Miernik mocy aktywny</string>
<string name="heart_rate_monitor_enabled">Pulsometr aktywny</string>
<string name="no_sensor_manager">Brak Ustawień Czujników</string>
<string name="enabled">Aktywny</string>
<string name="connecting">Łączenie</string>
<string name="connected">Podłączony</string>
<string name="disconnected">Rozłączony</string>
<string name="no_sensor_service">Brak Serwisu Czujników</string>
<string name="searching">Wykrywanie</string>
<string name="sending">Przesyłanie</string>
</resources>
+1 -1
View File
@@ -23,7 +23,7 @@ limitations under the License.
<string name="about_weburl">mytracks.appspot.com</string>
<string name="about_privacy_weburl">m.google.com/privacy</string>
<string name="about_copyright_message">Copyright 2009 Google Inc.</string>
<string name="about_bart">Bartlomiej Niechwiej</string>
<string name="about_bart">Bartłomiej Niechwiej</string>
<string name="about_dylan">Dylan Casey</string>
<string name="about_leif">Leif Hendrik Wilden</string>
<string name="about_rdamazio">Rodrigo Damazio Bovendorp</string>
+4 -6
View File
@@ -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
@@ -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);
@@ -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;
@@ -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<double[]> 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<double[]> 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<double[]> theData) {
data.addAll(theData);
for (int i = 0; i < theData.size(); i++) {
double d[] = theData.get(i);
addDataPointInternal(d);
public void addDataPoints(ArrayList<double[]> 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;
@@ -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();
}
});
}
}
@@ -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();
}
@@ -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;
@@ -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,
@@ -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);
@@ -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;
@@ -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<ListenerDataType> 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<ListenerDataType> externallyNeededListeners) {
EnumSet<ListenerDataType> 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<ListenerDataType> unnecessaryListeners = EnumSet.copyOf(registeredListeners);
unnecessaryListeners.removeAll(neededListeners);
// Missing = needed - registered
Set<ListenerDataType> 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);
}
}
@@ -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);
@@ -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);
@@ -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 {
@@ -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<ListenerDataType> 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<ListenerDataType> 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<TrackDataListener, ListenerRegistration> registeredListeners =
new HashMap<TrackDataListener, ListenerRegistration>();
/**
* Map of external paused listener to its registration details.
* This will automatically discard listeners which are GCed.
*/
private final WeakHashMap<TrackDataListener, ListenerRegistration> oldListeners =
new WeakHashMap<TrackDataListener, ListenerRegistration>();
/** Map of data type to external listeners interested in it. */
private final Map<ListenerDataType, Set<TrackDataListener>> listenerSetsPerType =
new EnumMap<ListenerDataType, Set<TrackDataListener>>(ListenerDataType.class);
public TrackDataListeners() {
// Create sets for all data types at startup.
for (ListenerDataType type : ListenerDataType.values()) {
listenerSetsPerType.put(type, new LinkedHashSet<TrackDataListener>());
}
}
/**
* 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<ListenerDataType> 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<TrackDataListener> 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<TrackDataListener> getListenersFor(ListenerDataType type) {
return listenerSetsPerType.get(type);
}
public EnumSet<ListenerDataType> getAllRegisteredTypes() {
EnumSet<ListenerDataType> 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();
}
}
@@ -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();
}
}
}
@@ -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("<p>" + track.getDescription() + "</p><p>"
+ stringUtils.generateTrackDescription(track, null, null) + "</p>");
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<Double> distances = new Vector<Double>();
Vector<Double> elevations = new Vector<Double>();
DoubleBuffer elevationBuffer =
new DoubleBuffer(Constants.ELEVATION_SMOOTHING_FACTOR);
List<Location> locations = new ArrayList<Location>(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("<p>" + originalDescription + "</p><p>"
+ stringUtils.generateTrackDescription(
track, distances, elevations)
+ "</p>");
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<Track> 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<Track> prepareLocations(
Track track, Iterable<Location> locations) {
ArrayList<Track> splitTracks = new ArrayList<Track>();
// 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));
@@ -363,8 +363,8 @@ public class DocsHelper {
wr.close();
rd.close();
}
private static IOException newIOException(GDataWrapper wrapper,
private static IOException newIOException(GDataWrapper<GDataServiceClient> wrapper,
String message) {
return new IOException(String.format("%s: %d: %s", message,
wrapper.getErrorType(), wrapper.getErrorMessage()));
@@ -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("<?xml-stylesheet type=\"text/xsl\" href=\"details.xsl\"?>");
pw.println("<gpx");
pw.println(" version=\"1.1\"");
pw.println(" creator=\"My Tracks for the G1 running Android\"");
pw.format(" creator=\"My Tracks running on %s\"\n", Build.MODEL);
pw.println(" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"");
pw.println(" xmlns=\"http://www.topografix.com/GPX/1/1\"");
pw.print(" xmlns:topografix=\"http://www.topografix.com/GPX/Private/"
@@ -22,6 +22,7 @@ import com.google.android.apps.mytracks.util.StringUtils;
import android.content.Context;
import android.location.Location;
import android.os.Build;
import java.io.OutputStream;
import java.io.PrintWriter;
@@ -70,8 +71,9 @@ public class KmlTrackWriter implements TrackFormatWriter {
pw.print(" xmlns=\"http://earth.google.com/kml/2.0\"");
pw.println(" xmlns:atom=\"http://www.w3.org/2005/Atom\">");
pw.println("<Document>");
pw.println("<atom:author><atom:name>My Tracks running on Android"
+ "</atom:name></atom:author>");
pw.format("<atom:author><atom:name>My Tracks running on %s"
+ "</atom:name></atom:author>\n", Build.MODEL);
pw.println("<name>" + StringUtils.stringAsCData(track.getName())
+ "</name>");
pw.println("<description>"
@@ -187,7 +187,7 @@ public class TcxTrackWriter implements TrackFormatWriter {
&& sensorData.getCadence().getState() == Sensor.SensorState.SENDING
&& sensorData.getCadence().hasValue()) {
pw.print(" <Cadence>");
pw.print(sensorData.getCadence().getValue());
pw.print(Math.min(255, sensorData.getCadence().getValue()));
pw.println("</Cadence>");
}
}
@@ -209,7 +209,7 @@ public class TcxTrackWriter implements TrackFormatWriter {
}
pw.println(" </Lap>");
pw.print(" <Creator xsi:type=\"Device_t\">");
pw.format("<Name>%s</Name>\n", Build.MODEL);
pw.format("<Name>My Tracks running on %s</Name>\n", Build.MODEL);
// The following code is correct. ID is inconsistently capitalized in the
// TCX schema.
@@ -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);
}
@@ -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:
@@ -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
@@ -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;
@@ -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;
@@ -48,7 +48,7 @@ public class XmlDocsGDataParserFactory implements GDataParserFactory {
}
}
@SuppressWarnings("unchecked")
@SuppressWarnings({ "unchecked", "rawtypes" })
@Override
public GDataParser createParser(Class cls, InputStream is)
throws ParseException {
@@ -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;
@@ -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<OnSharedPreferenceChangeListener> preferenceListenerCapture =
new Capture<SharedPreferences.OnSharedPreferenceChangeListener>();
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<ContentObserver> observerCapture = new Capture<ContentObserver>();
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<Cursor> {
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<ContentObserver> observerCapture = new Capture<ContentObserver>();
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<SensorEventListener> listenerCapture = new Capture<SensorEventListener>();
dataSources.registerSensorListener(capture(listenerCapture), same(compass), anyInt());
Capture<LocationListener> locationListenerCapture = new Capture<LocationListener>();
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<Sensor> constructor = Sensor.class.getDeclaredConstructor();
constructor.setAccessible(true);
return constructor.newInstance();
}
private SensorEvent newSensorEvent() throws Exception {
Constructor<SensorEvent> 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<OnSharedPreferenceChangeListener> listenerCapture =
new Capture<OnSharedPreferenceChangeListener>();
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));
}
}
@@ -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<OnSharedPreferenceChangeListener> preferenceListenerCapture =
new Capture<SharedPreferences.OnSharedPreferenceChangeListener>();
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<ContentObserver> observerCapture = new Capture<ContentObserver>();
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<Cursor> {
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<Integer> splitIndexSet = new HashSet<Integer>();
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<ContentObserver> observerCapture = new Capture<ContentObserver>();
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<ContentObserver> observerCapture = new Capture<ContentObserver>();
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<ContentObserver> observerCapture = new Capture<ContentObserver>();
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<ContentObserver> observerCapture = new Capture<ContentObserver>();
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<ContentObserver> observerCapture = new Capture<ContentObserver>();
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<ContentObserver> observerCapture = new Capture<ContentObserver>();
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<SensorEventListener> listenerCapture = new Capture<SensorEventListener>();
dataSources.registerSensorListener(capture(listenerCapture), same(compass), anyInt());
Capture<LocationListener> locationListenerCapture = new Capture<LocationListener>();
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<Sensor> constructor = Sensor.class.getDeclaredConstructor();
constructor.setAccessible(true);
return constructor.newInstance();
}
private SensorEvent newSensorEvent() throws Exception {
Constructor<SensorEvent> 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<OnSharedPreferenceChangeListener> listenerCapture =
new Capture<OnSharedPreferenceChangeListener>();
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);
}
}
@@ -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());
@@ -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<GDataServiceClient> {
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<GDataServiceClient> queryFunction) {
return returnValue;
}
}
@@ -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() {
@@ -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;
@@ -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;
@@ -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();
}