() {
+ @Override
+ public void query(GDataServiceClient client)
+ throws GDataWrapper.AuthenticationException, IOException, GDataWrapper.ParseException, GDataWrapper.HttpException {
+ String uri = String.format(DOCS_WORKSHEETS_URL_FORMAT, spreadsheetId);
+ GDataParser sheetParser;
+ try {
+ sheetParser = ((SpreadsheetsClient) client).getParserForWorksheetsFeed(uri,
+ trixWrapper.getAuthManager().getAuthToken());
+ sheetParser.init();
+ if (!sheetParser.hasMoreData()) {
+ Log.i(MyTracksConstants.TAG, "Found no worksheets");
+ return;
+ }
+
+ // Grab the first worksheet.
+ WorksheetEntry worksheetEntry =
+ (WorksheetEntry) sheetParser.readNextEntry(new WorksheetEntry());
+
+ int lastSlash = worksheetEntry.getId().lastIndexOf('/');
+ idSaver.set(worksheetEntry.getId().substring(lastSlash + 1));
+ } catch (ParseException e) {
+ throw new GDataWrapper.ParseException(e);
+ } catch (AuthenticationException e) {
+ throw new GDataWrapper.AuthenticationException(e);
+ }
+ }
+ });
+
+ if (!result) {
+ throw newIOException(trixWrapper, "Failed to retrieve worksheet ID.");
+ }
+
+ return idSaver.get();
+ }
+
+ /**
+ * Add a row to a worksheet containing the stats for a given track.
+ *
+ * @param context The context associated with this request.
+ * @param trixAuth The GData authorization for the spreadsheet service.
+ * @param spreadsheetId The spreadsheet to be modified.
+ * @param worksheetId The worksheet to be modified.
+ * @param track The track whose stats are to be written.
+ * @param metricUnits True if metric units are to be used. If false,
+ * imperial units will be used.
+ * @throws IOException If an error occurs while updating the worksheet.
+ */
+ public void addTrackRow(Context context, AuthManager trixAuth,
+ String spreadsheetId, String worksheetId, Track track,
+ boolean metricUnits) throws IOException {
+
+ String worksheetUri = String.format(DOCS_SPREADSHEET_URL_FORMAT,
+ spreadsheetId, worksheetId);
+ TripStatistics stats = track.getStatistics();
+
+ String distanceUnit = context.getString(metricUnits ?
+ R.string.kilometer : R.string.mile);
+ String speedUnit = context.getString(metricUnits ?
+ R.string.kilometer_per_hour : R.string.mile_per_hour);
+ String elevationUnit = context.getString(metricUnits ?
+ R.string.meter : R.string.feet);
+
+ // Prepare the Post-Text we are going to send.
+ DocsTagBuilder tagBuilder = new DocsTagBuilder(metricUnits)
+ .append("name", track.getName())
+ .append("description", track.getDescription())
+ .append("date", String.format("%tc", stats.getStartTime()))
+ .append("totaltime", StringUtils.formatTimeAlwaysShowingHours(
+ stats.getTotalTime()))
+ .append("movingtime", StringUtils.formatTimeAlwaysShowingHours(
+ stats.getMovingTime()))
+ .appendLargeUnits("distance", stats.getTotalDistance() / 1000)
+ .append("distanceunit", distanceUnit)
+ .appendLargeUnits("averagespeed", stats.getAverageSpeed() * 3.6)
+ .appendLargeUnits("averagemovingspeed",
+ stats.getAverageMovingSpeed() * 3.6)
+ .appendLargeUnits("maxspeed", stats.getMaxSpeed() * 3.6)
+ .append("speedunit", speedUnit)
+ .appendSmallUnits("elevationgain", stats.getTotalElevationGain())
+ .appendSmallUnits("minelevation", stats.getMinElevation())
+ .appendSmallUnits("maxelevation", stats.getMaxElevation())
+ .append("elevationunit", elevationUnit);
+
+ if (track.getMapId().length() > 0) {
+ tagBuilder.append("map", String.format("%s?msa=0&msid=%s",
+ MyTracksConstants.MAPSHOP_BASE_URL, track.getMapId()));
+ }
+
+ String postText = new StringBuilder()
+ .append("")
+ .append(tagBuilder.build())
+ .append("")
+ .toString();
+
+ Log.i(MyTracksConstants.TAG,
+ "Inserting at: " + spreadsheetId + " => " + worksheetUri);
+
+ Log.i(MyTracksConstants.TAG, postText);
+
+ writeRowData(trixAuth, worksheetUri, postText);
+
+ Log.i(MyTracksConstants.TAG, "Post finished.");
+ }
+
+ /**
+ * Writes spreadsheet row data to the indicated worksheet.
+ *
+ * @param trixAuth The GData authorization for the spreadsheet service.
+ * @param worksheetUri The URI of the worksheet to be altered.
+ * @param postText The XML tags describing the change to be made.
+ * @throws IOException Thrown if an error occurs during the write.
+ */
+ protected void writeRowData(AuthManager trixAuth, String worksheetUri,
+ String postText) throws IOException {
+ // No need for a wrapper because we know that the authorization was good
+ // enough to get this far.
+ URL url = new URL(worksheetUri);
+ URLConnection conn = url.openConnection();
+ conn.addRequestProperty(CONTENT_TYPE_PARAM, ATOM_FEED_MIME_TYPE);
+ conn.addRequestProperty("Authorization",
+ "GoogleLogin auth=" + trixAuth.getAuthToken());
+ conn.setDoOutput(true);
+ OutputStreamWriter wr = new OutputStreamWriter(conn.getOutputStream());
+ wr.write(postText);
+ wr.flush();
+
+ // Get the response.
+ // TODO: Should we parse the response, rather than simply throwing it away?
+ BufferedReader rd =
+ new BufferedReader(new InputStreamReader(conn.getInputStream()));
+ String line;
+ while ((line = rd.readLine()) != null) {
+ // Process line.
+ Log.i(MyTracksConstants.TAG, "r: " + line);
+ }
+ wr.close();
+ rd.close();
+ }
+
+ private static IOException newIOException(GDataWrapper wrapper,
+ String message) {
+ return new IOException(String.format("%s: %d: %s", message,
+ wrapper.getErrorType(), wrapper.getErrorMessage()));
+ }
+}
diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/docs/DocsTagBuilder.java b/MyTracks/src/com/google/android/apps/mytracks/io/docs/DocsTagBuilder.java
new file mode 100644
index 000000000..50ab27057
--- /dev/null
+++ b/MyTracks/src/com/google/android/apps/mytracks/io/docs/DocsTagBuilder.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright 2010 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.android.apps.mytracks.io.docs;
+
+import com.google.android.apps.mytracks.util.StringUtils;
+import com.google.android.apps.mytracks.util.UnitConversions;
+
+import java.text.DecimalFormat;
+import java.text.NumberFormat;
+
+/**
+ * This class builds a string of XML tags used to talk to Docs using GData.
+ *
+ *
Sample Usage:
+ *
+ *
+ * String tags = new DocsTagBuilder(false)
+ * .append("tagName", "tagValue")
+ * .appendLargeUnits("bigTagName", 1.0)
+ * .build();
+ *
+ *
+ *
results in:
+ *
+ *
+ *
+ *
+ *
+ *
+ * @author Matthew Simmons
+ */
+class DocsTagBuilder {
+ // TODO(simmonmt): These formats aren't I18N-compatible. Not everyone uses
+ // commas for thousands and dots for the decimal point.
+ private static final NumberFormat LARGE_UNIT_FORMAT =
+ new DecimalFormat("#,###,###.00");
+ private static final NumberFormat SMALL_UNIT_FORMAT =
+ new DecimalFormat("###,###");
+
+ protected final boolean metricUnits;
+ protected final StringBuilder stringBuilder;
+
+ /**
+ * @param metricUnits True if metric units are to be used. If false,
+ * imperial units will be used.
+ */
+ DocsTagBuilder(boolean metricUnits) {
+ this.metricUnits = metricUnits;
+ stringBuilder = new StringBuilder();
+ }
+
+ /** Appends a tag containing a string value */
+ DocsTagBuilder append(String name, String value) {
+ appendTag(name, value);
+ return this;
+ }
+
+ /**
+ * Appends a tag containing a numeric value. The value will be formatted
+ * according to the large distance of the specified measurement system (i.e.
+ * kilometers or miles).
+ *
+ * @param name The tag name.
+ * @param d The value to be formatted, in kilometers.
+ */
+ DocsTagBuilder appendLargeUnits(String name, double d) {
+ double value = metricUnits ? d : (d * UnitConversions.KM_TO_MI);
+ appendTag(name, LARGE_UNIT_FORMAT.format(value));
+ return this;
+ }
+
+ /**
+ * Appends a tag containing a numeric value. The value will be formatted
+ * according to the small distance of the specified measurement system (i.e.
+ * meters or feet).
+ *
+ * @param name The tag name.
+ * @param d The value to be formatted, in meters.
+ */
+ DocsTagBuilder appendSmallUnits(String name, double d) {
+ double value = metricUnits ? d : (d * UnitConversions.M_TO_FT);
+ appendTag(name, SMALL_UNIT_FORMAT.format(value));
+ return this;
+ }
+
+ /** Returns a string containing all tags which have been added. */
+ String build() {
+ return stringBuilder.toString();
+ }
+
+ private void appendTag(String name, String value) {
+ stringBuilder.append("")
+ .append(StringUtils.stringAsCData(value))
+ .append("");
+ }
+}
diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/gdata/GDataWrapper.java b/MyTracks/src/com/google/android/apps/mytracks/io/gdata/GDataWrapper.java
index 48de71ebe..f56497876 100644
--- a/MyTracks/src/com/google/android/apps/mytracks/io/gdata/GDataWrapper.java
+++ b/MyTracks/src/com/google/android/apps/mytracks/io/gdata/GDataWrapper.java
@@ -289,6 +289,10 @@ public class GDataWrapper {
this.auth = auth;
}
+ public AuthManager getAuthManager() {
+ return auth;
+ }
+
public void setRetryOnAuthFailure(boolean retry) {
retryOnAuthFailure = retry;
}
diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/SplitManager.java b/MyTracks/src/com/google/android/apps/mytracks/services/SplitManager.java
index e8a92bfc7..f63e87531 100644
--- a/MyTracks/src/com/google/android/apps/mytracks/services/SplitManager.java
+++ b/MyTracks/src/com/google/android/apps/mytracks/services/SplitManager.java
@@ -16,6 +16,7 @@
package com.google.android.apps.mytracks.services;
import com.google.android.apps.mytracks.MyTracksConstants;
+import com.google.android.apps.mytracks.content.WaypointCreationRequest;
import com.google.android.apps.mytracks.util.UnitConversions;
import android.util.Log;
@@ -111,7 +112,7 @@ public class SplitManager {
}
if (distance > this.nextSplitDistance) {
- service.insertStatisticsMarker(service.getLastLocation());
+ service.insertWaypoint(WaypointCreationRequest.DEFAULT_STATISTICS);
calculateNextSplit();
}
}
diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/TimeSplitTask.java b/MyTracks/src/com/google/android/apps/mytracks/services/TimeSplitTask.java
index 9535ad4b0..333ce6ae4 100644
--- a/MyTracks/src/com/google/android/apps/mytracks/services/TimeSplitTask.java
+++ b/MyTracks/src/com/google/android/apps/mytracks/services/TimeSplitTask.java
@@ -16,6 +16,9 @@
package com.google.android.apps.mytracks.services;
+import com.google.android.apps.mytracks.content.WaypointCreationRequest;
+
+
/**
* A simple task to insert statistics markers every n minutes.
* @author Sandor Dornbush
@@ -24,7 +27,7 @@ public class TimeSplitTask implements PeriodicTask {
@Override
public void run(TrackRecordingService service) {
- service.insertStatisticsMarker(service.getLastLocation());
+ service.insertWaypoint(WaypointCreationRequest.DEFAULT_STATISTICS);
}
@Override
diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/TrackRecordingService.java b/MyTracks/src/com/google/android/apps/mytracks/services/TrackRecordingService.java
index b3610bd44..f44823bce 100644
--- a/MyTracks/src/com/google/android/apps/mytracks/services/TrackRecordingService.java
+++ b/MyTracks/src/com/google/android/apps/mytracks/services/TrackRecordingService.java
@@ -26,6 +26,7 @@ import com.google.android.apps.mytracks.content.Sensor;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.content.TracksColumns;
import com.google.android.apps.mytracks.content.Waypoint;
+import com.google.android.apps.mytracks.content.WaypointCreationRequest;
import com.google.android.apps.mytracks.content.WaypointsColumns;
import com.google.android.apps.mytracks.content.Sensor.SensorDataSet;
import com.google.android.apps.mytracks.services.sensors.SensorManager;
@@ -70,9 +71,6 @@ import java.util.TimerTask;
*/
public class TrackRecordingService extends Service implements LocationListener {
- private static final String STATISTICS_ICON_URL =
- "http://maps.google.com/mapfiles/ms/micons/ylw-pushpin.png";
-
static final int MAX_AUTO_RESUME_TRACK_RETRY_ATTEMPTS = 3;
private NotificationManager notificationManager;
@@ -429,7 +427,7 @@ public class TrackRecordingService extends Service implements LocationListener {
lastValidLocation = null;
Waypoint waypoint = providerUtils.getFirstWaypoint(recordingTrackId);
- if (waypoint != null) {
+ if (waypoint != null && waypoint.getStatistics() != null) {
currentWaypointId = waypoint.getId();
waypointStatsBuilder = new TripStatisticsBuilder(
waypoint.getStatistics());
@@ -646,7 +644,6 @@ public class TrackRecordingService extends Service implements LocationListener {
locationManager = (LocationManager) getSystemService(LOCATION_SERVICE);
splitManager = new SplitManager(this);
- sensorManager = SensorManagerFactory.getSensorManager(this);
prefManager = new PreferenceManager(this);
registerLocationListener();
@@ -733,7 +730,7 @@ public class TrackRecordingService extends Service implements LocationListener {
splitManager.shutdown();
splitManager = null;
if (sensorManager != null) {
- sensorManager.onDestroy();
+ sensorManager.shutdown();
sensorManager = null;
}
@@ -860,50 +857,62 @@ public class TrackRecordingService extends Service implements LocationListener {
return isRecording;
}
- public long insertWaypointMarker(Waypoint waypoint) {
+ public long insertWaypoint(WaypointCreationRequest request) {
if (!isRecording()) {
throw new IllegalStateException(
"Unable to insert waypoint marker while not recording!");
}
-
- if (waypoint.getLocation() == null) {
- if (lastValidLocation == null) {
- Log.w(MyTracksConstants.TAG, "Cannot insert waypoint with no location");
- return -1;
- }
-
- waypoint.setLocation(lastValidLocation);
+ if (request == null) {
+ request = WaypointCreationRequest.DEFAULT_MARKER;
}
-
- if (waypoint.getTrackId() < 0) {
- waypoint.setTrackId(recordingTrackId);
+ Waypoint wpt = new Waypoint();
+ switch (request.getType()) {
+ case MARKER:
+ buildMarker(wpt, request);
+ break;
+ case STATISTICS:
+ buildStatisticsMarker(wpt);
+ break;
}
-
- waypoint.setLength(length);
- waypoint.setDuration(waypoint.getLocation().getTime()
- - statsBuilder.getStatistics().getStartTime());
- Uri uri = providerUtils.insertWaypoint(waypoint);
+ wpt.setTrackId(recordingTrackId);
+ wpt.setLength(length);
+ if (lastValidLocation != null) {
+ // A null location is ok, and expected on track start.
+ wpt.setLocation(lastLocation);
+ wpt.setDuration(lastLocation.getTime()
+ - statsBuilder.getStatistics().getStartTime());
+ }
+ Uri uri = providerUtils.insertWaypoint(wpt);
return Long.parseLong(uri.getLastPathSegment());
}
+ private void buildMarker(Waypoint wpt, WaypointCreationRequest request) {
+ wpt.setType(Waypoint.TYPE_WAYPOINT);
+ if (request.getIconUrl() == null) {
+ wpt.setIcon(getString(R.string.waypoint_icon_url));
+ } else {
+ wpt.setIcon(request.getIconUrl());
+ }
+ if (request.getName() == null) {
+ wpt.setName(getString(R.string.waypoint));
+ } else {
+ wpt.setName(request.getName());
+ }
+ if (request.getDescription() != null) {
+ wpt.setDescription(request.getDescription());
+ }
+ }
+
/**
- * Inserts a statistics marker. A statistics marker holds the stats for the
- * last segment up to this marker.
+ * Build a statistics marker.
+ * A statistics marker holds the stats for the* last segment up to this marker.
*
- * @param location the location where to insert
+ * @param Waypoint The waypoint which will be populated with stats data.
* @return the unique id of the inserted marker
*/
- public long insertStatisticsMarker(Location location) {
- if (!isRecording()) {
- throw new IllegalStateException(
- "Unable to insert statistics marker while not recording!");
- }
-
+ private void buildStatisticsMarker(Waypoint waypoint) {
StringUtils utils = new StringUtils(TrackRecordingService.this);
- // Create a new waypoint to save
- Waypoint waypoint = new Waypoint();
-
// Set stop and total time in the stats data
final long time = System.currentTimeMillis();
waypointStatsBuilder.pauseAt(time);
@@ -913,24 +922,18 @@ public class TrackRecordingService extends Service implements LocationListener {
waypoint.setDuration(time - statsBuilder.getStatistics().getStartTime());
// Set the rest of the waypoint data
- waypoint.setTrackId(recordingTrackId);
waypoint.setType(Waypoint.TYPE_STATISTICS);
- waypoint.setName(TrackRecordingService.this.getString(R.string.statistics));
+ waypoint.setName(getString(R.string.statistics));
waypoint.setStatistics(waypointStatsBuilder.getStatistics());
waypoint.setDescription(utils.generateWaypointDescription(waypoint));
- waypoint.setLocation(location);
- waypoint.setIcon(STATISTICS_ICON_URL);
- waypoint.setLength(length);
+ waypoint.setIcon(getString(R.string.stats_icon_url));
waypoint.setStartId(providerUtils.getLastLocationId(recordingTrackId));
- Uri uri = providerUtils.insertWaypoint(waypoint);
- // Create a new stats keeper for the next marker
+ // Create a new stats keeper for the next marker.
waypointStatsBuilder = new TripStatisticsBuilder(time);
- updateCurrentWaypoint();
- return Long.parseLong(uri.getLastPathSegment());
}
-
+
private ServiceBinder binder = new ServiceBinder(this);
/**
@@ -961,79 +964,54 @@ public class TrackRecordingService extends Service implements LocationListener {
@Override
public boolean isRecording() {
+ checkService();
+ return service.isRecording();
+ }
+
+ private void checkService() {
if (service == null) {
throw new IllegalStateException("The service has been already detached!");
}
- return service.isRecording();
}
@Override
public long getRecordingTrackId() {
- if (service == null) {
- throw new IllegalStateException("The service has been already detached!");
- }
+ checkService();
return service.recordingTrackId;
}
@Override
public boolean hasRecorded() {
- if (service == null) {
- throw new IllegalStateException("The service has been already detached!");
- }
+ checkService();
return service.providerUtils.getLastTrackId() >= 0;
}
@Override
public long startNewTrack() {
- if (service == null) {
- throw new IllegalStateException("The service has been already detached!");
- }
+ checkService();
return service.startNewTrack();
}
/**
- * Insert the given waypoint marker. Users can insert waypoint markers
- * to tag locations with a name, description, category etc.
+ * Inserts a waypoint marker in the track being recorded.
*
- * @param waypoint a waypoint
- * @return the unique id of the inserted marker
+ * @param request Details of the waypoint to insert
+ * @return the unique ID of the inserted marker
*/
- @Override
- public long insertWaypointMarker(Waypoint waypoint) {
- if (service == null) {
- throw new IllegalStateException("The service has been already detached!");
- }
- return service.insertWaypointMarker(waypoint);
- }
-
- /**
- * Insert a statistics marker. A statistics marker holds the stats for
- * the last segment up to this marker.
- *
- * @param location the location where to insert
- * @return the unique id of the inserted marker
- */
- @Override
- public long insertStatisticsMarker(Location location) {
- if (service == null) {
- throw new IllegalStateException("The service has been already detached!");
- }
- return service.insertStatisticsMarker(location);
+ public long insertWaypoint(WaypointCreationRequest request) {
+ checkService();
+ return service.insertWaypoint(request);
}
@Override
public void endCurrentTrack() {
- if (service == null) {
- throw new IllegalStateException("The service has been already detached!");
- }
+ checkService();
service.endCurrentTrack();
}
@Override
public void deleteAllTracks() {
- if (service == null) {
- throw new IllegalStateException("The service has been already detached!");
- }
+ checkService();
if (isRecording()) {
throw new IllegalStateException("Cannot delete all tracks while recording!");
}
@@ -1042,14 +1020,13 @@ public class TrackRecordingService extends Service implements LocationListener {
@Override
public void recordLocation(Location loc) {
- if (service == null) {
- throw new IllegalStateException("The service has been already detached!");
- }
+ checkService();
service.onLocationChanged(loc);
}
@Override
public byte[] getSensorData() {
+ checkService();
if (service.sensorManager == null) {
Log.d(MyTracksConstants.TAG, "No sensor manager for data.");
return null;
@@ -1063,6 +1040,7 @@ public class TrackRecordingService extends Service implements LocationListener {
@Override
public int getSensorState() {
+ checkService();
if (service.sensorManager == null) {
Log.d(MyTracksConstants.TAG, "No sensor manager for data.");
return Sensor.SensorState.NONE.getNumber();
@@ -1095,12 +1073,13 @@ public class TrackRecordingService extends Service implements LocationListener {
providerUtils.updateTrack(track);
statsBuilder = new TripStatisticsBuilder(startTime);
waypointStatsBuilder = new TripStatisticsBuilder(startTime);
- currentWaypointId = insertStatisticsMarker(null);
+ currentWaypointId = insertWaypoint(WaypointCreationRequest.DEFAULT_STATISTICS);
setUpAnnouncer();
length = 0;
showNotification();
registerLocationListener();
splitManager.restore();
+ sensorManager = SensorManagerFactory.getSensorManager(this);
if (sensorManager != null) {
sensorManager.onStartTrack();
}
@@ -1149,6 +1128,7 @@ public class TrackRecordingService extends Service implements LocationListener {
if (sensorManager != null) {
sensorManager.shutdown();
+ sensorManager = null;
}
releaseWakeLock();
diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/sensors/AntDirectSensorManager.java b/MyTracks/src/com/google/android/apps/mytracks/services/sensors/AntDirectSensorManager.java
new file mode 100644
index 000000000..a26d0e8cd
--- /dev/null
+++ b/MyTracks/src/com/google/android/apps/mytracks/services/sensors/AntDirectSensorManager.java
@@ -0,0 +1,169 @@
+/*
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.android.apps.mytracks.services.sensors;
+
+import static com.google.android.apps.mytracks.MyTracksConstants.TAG;
+import com.google.android.apps.mytracks.MyTracksSettings;
+import com.google.android.apps.mytracks.content.Sensor;
+import com.google.android.maps.mytracks.R;
+
+import com.dsi.ant.AntDefine;
+import com.dsi.ant.AntMesg;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.util.Log;
+
+/**
+ * A sensor manager to connect ANT+ sensors.
+ * This can include heart rate monitors.
+ *
+ * @author Sandor Dornbush
+ */
+public class AntDirectSensorManager extends AntSensorManager {
+
+ /*
+ * These constants are defined by the ANT+ heart rate monitor spec.
+ */
+ public static final byte HRM_CHANNEL = 0;
+ public static final byte NETWORK_NUMBER = 1;
+ public static final byte HEART_RATE_DEVICE_TYPE = 120;
+ public static final byte POWER_DEVICE_TYPE = 11;
+ public static final byte MANUFACTURER_ID = 1;
+ public static final short CHANNEL_PERIOD = 8070;
+ public static final byte RF_FREQUENCY = 57;
+
+ private short deviceNumberHRM;
+
+ public AntDirectSensorManager(Context context) {
+ super(context);
+
+ deviceNumberHRM = WILDCARD;
+
+ // First read the the device id that we will be pairing with.
+ SharedPreferences prefs = context.getSharedPreferences(
+ MyTracksSettings.SETTINGS_NAME, 0);
+ if (prefs != null) {
+ deviceNumberHRM =
+ (short) prefs.getInt(context.getString(R.string.ant_heart_rate_sensor_id_key), 0);
+ }
+ Log.i(TAG, "Pairing with heart rate monitor: " + deviceNumberHRM);
+ }
+
+ @Override
+ public void handleMessage(byte[] antMessage) {
+ int channel =
+ antMessage[AntMesg.MESG_DATA_OFFSET] & AntDefine.CHANNEL_NUMBER_MASK;
+ switch (channel) {
+ case HRM_CHANNEL:
+ antDecodeHRM(antMessage);
+ break;
+ default:
+ Log.d(TAG, "Unhandled message: " + channel);
+ }
+ }
+
+ /**
+ * Decode an ant heart rate monitor message.
+ * @param antMessage The byte array received from the heart rate monitor.
+ */
+ private void antDecodeHRM(byte[] antMessage) {
+ // message ID == broadcast
+ switch (antMessage[AntMesg.MESG_ID_OFFSET]) {
+ case AntMesg.MESG_BROADCAST_DATA_ID:
+ handleBroadcastData(antMessage);
+ break;
+ case AntMesg.MESG_RESPONSE_EVENT_ID:
+ handleMessageResponse(antMessage);
+ break;
+ case AntMesg.MESG_CHANNEL_ID_ID:
+ handleChannelId(antMessage);
+ break;
+ default:
+ Log.e(TAG, "Unexpected message id: " + antMessage[3]);
+ }
+ }
+
+ private void handleBroadcastData(byte[] antMessage) {
+ if (deviceNumberHRM == WILDCARD) {
+ getAntReceiver().ANTRequestMessage(HRM_CHANNEL,
+ AntMesg.MESG_CHANNEL_ID_ID);
+ Log.d(TAG, "Requesting channel id id.");
+ }
+
+ setSensorState(Sensor.SensorState.CONNECTED);
+ int bpm = (int) antMessage[10] & 0xFF;
+ Sensor.SensorData.Builder b = Sensor.SensorData.newBuilder()
+ .setValue(bpm)
+ .setState(Sensor.SensorState.SENDING);
+ sensorData =
+ Sensor.SensorDataSet.newBuilder()
+ .setCreationTime(System.currentTimeMillis())
+ .setHeartRate(b)
+ .build();
+ }
+
+ void handleChannelId(byte[] antMessage) {
+ // Store the device id.
+ deviceNumberHRM =
+ (short) (((int) antMessage[3] & 0xFF |
+ ((int) (antMessage[4] & 0xFF) << 8)) & 0xFFFF);
+ Log.i(TAG, "Found device id: " + deviceNumberHRM);
+
+ SharedPreferences prefs = context.getSharedPreferences(
+ MyTracksSettings.SETTINGS_NAME, Context.MODE_PRIVATE);
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putInt(context.getString(R.string.ant_heart_rate_sensor_id_key), deviceNumberHRM);
+ editor.commit();
+ }
+
+ private void handleMessageResponse(byte[] antMessage) {
+ if (antMessage[3] == AntMesg.MESG_EVENT_ID &&
+ antMessage[4] == AntDefine.EVENT_RX_SEARCH_TIMEOUT) {
+ // Search timeout
+ Log.w(TAG, "Search timed out. Unassigning channel.");
+ getAntReceiver().ANTUnassignChannel((byte) 0);
+ setSensorState(Sensor.SensorState.DISCONNECTED);
+ } else if (antMessage[3] == AntMesg.MESG_UNASSIGN_CHANNEL_ID) {
+ setSensorState(Sensor.SensorState.DISCONNECTED);
+ Log.i(TAG, "Disconnected from the sensor: " + getSensorState());
+ }
+ }
+
+ /**
+ * Open a channel to the SRM head unit.
+ */
+ @Override
+ public void setupChannel() {
+ super.setupChannel();
+ antChannelSetup(NETWORK_NUMBER,
+ HRM_CHANNEL,
+ deviceNumberHRM,
+ HEART_RATE_DEVICE_TYPE,
+ (byte) 0x01,
+ CHANNEL_PERIOD,
+ RF_FREQUENCY,
+ (byte) 0);
+ setSensorState(Sensor.SensorState.CONNECTING);
+ }
+
+ public short getDeviceNumberHRM() {
+ return deviceNumberHRM;
+ }
+
+ void setDeviceNumberHRM(short deviceNumberHRM) {
+ this.deviceNumberHRM = deviceNumberHRM;
+ }
+}
diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/sensors/AntSRMSensorManager.java b/MyTracks/src/com/google/android/apps/mytracks/services/sensors/AntSRMSensorManager.java
new file mode 100644
index 000000000..3d038a922
--- /dev/null
+++ b/MyTracks/src/com/google/android/apps/mytracks/services/sensors/AntSRMSensorManager.java
@@ -0,0 +1,226 @@
+/*
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.android.apps.mytracks.services.sensors;
+
+import com.google.android.apps.mytracks.MyTracksConstants;
+import com.google.android.apps.mytracks.MyTracksSettings;
+import com.google.android.apps.mytracks.content.Sensor;
+import com.google.android.maps.mytracks.R;
+
+import com.dsi.ant.AntDefine;
+import com.dsi.ant.AntMesg;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.util.Log;
+
+/**
+ * A sensor manager that can connect to an SRM Power Control 7 head unit.
+ *
+ * @author Sandor Dornbush
+ */
+public class AntSRMSensorManager extends AntSensorManager {
+
+ /*
+ * These constants are defined by the SRM Headunit spec.
+ */
+ public static final byte NETWORK_NUMBER = (byte) 0;
+ public static final byte DEVICE_TYPE = (byte) 12;
+ public static final byte MANUFACTURER_ID = (byte) 1;
+ public static final short CHANNEL_PERIOD = (short) 8192;
+ public static final byte RF_FREQUENCY = (byte) 50;
+
+ public static final int MESSAGE_TYPE_INDEX = 3;
+ public static final int INITIALS_MESSAGE_TYPE = 5;
+ public static final int SENSOR_DATA_MESSAGE_TYPE = 6;
+
+ public static final int MESSAGE_ID_INDEX = 4;
+
+ /**
+ * This can be any value between 0..8
+ */
+ private byte channel = (byte) 5;
+
+ /**
+ * This is the id of this device.
+ * The head unit must be programmed to expect this number.
+ */
+ private byte deviceId;
+
+ /**
+ * The id of the last message received from the SRM head unit.
+ */
+ private int lastMessageId;
+
+ public AntSRMSensorManager(Context context) {
+ super(context);
+
+ // First read the the device id that we will be announcing.
+ SharedPreferences prefs = context.getSharedPreferences(
+ MyTracksSettings.SETTINGS_NAME, 0);
+ if (prefs != null) {
+ deviceId = (byte) prefs.getInt(
+ context.getString(R.string.ant_srm_bridge_sensor_id_key),
+ WILDCARD);
+ }
+ }
+
+ @Override
+ public void handleMessage(byte[] antMessage) {
+ // Parse channel number
+ byte recievedChannel = (byte) (antMessage[AntMesg.MESG_DATA_OFFSET]
+ & AntDefine.CHANNEL_NUMBER_MASK);
+ if (recievedChannel != channel) {
+ Log.d(MyTracksConstants.TAG, "Unexpected channel: " + recievedChannel);
+ return;
+ }
+ switch (antMessage[AntMesg.MESG_ID_OFFSET]) {
+ case AntMesg.MESG_BROADCAST_DATA_ID:
+ handleBroadcastData(antMessage);
+ break;
+ case AntMesg.MESG_RESPONSE_EVENT_ID:
+ handleMessageResponse(antMessage);
+ break;
+ case AntMesg.MESG_CHANNEL_ID_ID:
+ handleChannelId(antMessage);
+ break;
+ default:
+ Log.e(MyTracksConstants.TAG,
+ "Unexpected message id: " + antMessage[3]);
+ }
+ }
+
+ private void handleBroadcastData(byte[] antMessage) {
+ if (deviceId == WILDCARD) {
+ getAntReceiver().ANTRequestMessage(channel, AntMesg.MESG_CHANNEL_ID_ID);
+ Log.d(MyTracksConstants.TAG, "Requesting channel id id.");
+ }
+ setSensorState(Sensor.SensorState.CONNECTED);
+ switch(antMessage[MESSAGE_TYPE_INDEX]) {
+ case INITIALS_MESSAGE_TYPE:
+ // TODO handle initials message.
+ break;
+ case SENSOR_DATA_MESSAGE_TYPE:
+ parseSensorData(antMessage);
+ break;
+ default:
+ Log.e(MyTracksConstants.TAG,
+ "Unexpected message type: " + antMessage[MESSAGE_TYPE_INDEX]);
+ }
+ }
+
+ private void handleChannelId(byte[] antMessage) {
+ // Store the device id.
+ deviceId = antMessage[3];
+ Log.i(MyTracksConstants.TAG, "Found device id: " + deviceId);
+
+ SharedPreferences prefs = context.getSharedPreferences(
+ MyTracksSettings.SETTINGS_NAME, Context.MODE_PRIVATE);
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putInt(context.getString(R.string.ant_srm_bridge_sensor_id_key), deviceId);
+ editor.commit();
+ }
+
+ private void handleMessageResponse(byte[] antMessage) {
+ if (antMessage[3] == AntMesg.MESG_EVENT_ID
+ && antMessage[4] == AntDefine.EVENT_RX_SEARCH_TIMEOUT) {
+ // Search timeout
+ Log.w(MyTracksConstants.TAG, "Search timed out. Unassigning channel.");
+ getAntReceiver().ANTUnassignChannel(channel);
+ setSensorState(Sensor.SensorState.DISCONNECTED);
+ } else if (antMessage[3] == AntMesg.MESG_UNASSIGN_CHANNEL_ID) {
+ setSensorState(Sensor.SensorState.DISCONNECTED);
+ Log.i(MyTracksConstants.TAG,
+ "Disconnected from the sensor: " + getSensorState());
+ }
+ }
+
+ private void parseSensorData(byte[] antMessage) {
+ if (antMessage.length != 11) {
+ Log.e(MyTracksConstants.TAG,
+ "Unexpected ant message length: " + antMessage.length);
+ return;
+ }
+
+ int newMessageId = antMessage[MESSAGE_ID_INDEX] & 0xFF;
+ if (lastMessageId == newMessageId) {
+ // Repeated message.
+ Log.i(MyTracksConstants.TAG,
+ String.format("SRM ignoring repeat: 0x%X", newMessageId));
+ return;
+ }
+ if (newMessageId < lastMessageId) {
+ if (!(newMessageId < 20 && lastMessageId > 200)) {
+ Log.i(MyTracksConstants.TAG,
+ String.format("SRM ignoring repeat: 0x%X", newMessageId));
+ return;
+ } // else assume the byte overflowed to 0.
+ }
+ lastMessageId = newMessageId;
+
+ Sensor.SensorDataSet.Builder builder = Sensor.SensorDataSet.newBuilder()
+ .setCreationTime(System.currentTimeMillis());
+
+ int power = SensorUtils.unsignedShortToInt(antMessage, 5);
+ if (power >= 0) {
+ Sensor.SensorData.Builder b = Sensor.SensorData.newBuilder()
+ .setValue(power)
+ .setState(Sensor.SensorState.SENDING);
+ builder.setPower(b);
+ }
+
+ int speed = SensorUtils.unsignedShortToInt(antMessage, 7);
+ if (speed >= 0) {
+ // The speed from srm is in 1/10 km/hr.
+ // Sensor data expects the speed as m/s.
+ // TODO finish this.
+ //sensorData.setWheelSpeed(UnitConversions.TENTH_KMH_TO_MPS * speed);
+ }
+
+ int cadence = antMessage[9] & 0xFF;
+ if (cadence >= 0) {
+ Sensor.SensorData.Builder b = Sensor.SensorData.newBuilder()
+ .setValue(cadence)
+ .setState(Sensor.SensorState.SENDING);
+ builder.setCadence(b);
+ }
+
+ int bpm = antMessage[10] & 0xFF;
+ if (bpm > 0) {
+ Sensor.SensorData.Builder b = Sensor.SensorData.newBuilder()
+ .setValue(bpm)
+ .setState(Sensor.SensorState.SENDING);
+ builder.setHeartRate(b);
+ }
+ sensorData = builder.build();
+ }
+
+ /**
+ * Open a channel to the SRM head unit.
+ */
+ @Override
+ public void setupChannel() {
+ antChannelSetup(NETWORK_NUMBER,
+ channel,
+ deviceId,
+ DEVICE_TYPE,
+ MANUFACTURER_ID,
+ CHANNEL_PERIOD,
+ RF_FREQUENCY,
+ (byte) 0);
+ lastMessageId = 0;
+ setSensorState(Sensor.SensorState.CONNECTING);
+ }
+}
diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/sensors/AntSensorManager.java b/MyTracks/src/com/google/android/apps/mytracks/services/sensors/AntSensorManager.java
new file mode 100644
index 000000000..eb2230f10
--- /dev/null
+++ b/MyTracks/src/com/google/android/apps/mytracks/services/sensors/AntSensorManager.java
@@ -0,0 +1,228 @@
+/*
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.android.apps.mytracks.services.sensors;
+
+import com.google.android.apps.mytracks.MyTracksConstants;
+import com.google.android.apps.mytracks.content.Sensor.SensorDataSet;
+
+import com.dsi.ant.AntDefine;
+import com.dsi.ant.AntInterface;
+import com.dsi.ant.AntInterfaceIntent;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Handler;
+import android.os.Message;
+import android.util.Log;
+
+/**
+ * An abstract class for ANT+ based sensors.
+ *
+ * @author Sandor Dornbush
+ */
+public abstract class AntSensorManager extends SensorManager {
+
+ // ANT Service
+ public static final int EVENT_ANT_ENABLED = 1;
+ public static final int EVENT_ANT_DISABLED = 2;
+
+ // Pairing
+ protected static final short WILDCARD = 0;
+
+ private AntInterface antReceiver;
+
+ private boolean status;
+
+ // Flag to know if the ANT App was interrupted
+ // TODO: This code path is not used but probably should be.
+ private boolean antInterrupted;
+
+ /**
+ * The data from the sensors.
+ */
+ protected SensorDataSet sensorData;
+
+ protected Context context;
+
+ private static final boolean DEBUGGING = false;
+
+ /**
+ * Receives all of the ANT intents and dispatches to the proper handler.
+ */
+ private final BroadcastReceiver receiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String antAction = intent.getAction();
+
+ Log.i(MyTracksConstants.TAG, "enter onReceive" + antAction);
+ if (antAction.equals(AntInterfaceIntent.ANT_ENABLED_ACTION)) {
+ Log.i(MyTracksConstants.TAG, "enter onReceive ANT_ENABLED_ACTION "
+ + antAction);
+ handler.sendMessage(handler.obtainMessage(EVENT_ANT_ENABLED, 0));
+ }
+ if (antAction.equals(AntInterfaceIntent.ANT_DISABLED_ACTION)) {
+ Log.i(MyTracksConstants.TAG, "enter onReceive ANT_DISABLED_ACTION "
+ + antAction);
+ handler.sendMessage(handler.obtainMessage(EVENT_ANT_DISABLED, 0));
+ }
+
+ if (antAction.equals(AntInterfaceIntent.ANT_RX_MESSAGE_ACTION)) {
+ byte[] antMessage =
+ intent.getByteArrayExtra(AntInterfaceIntent.ANT_MESSAGE);
+ if (DEBUGGING) {
+ StringBuilder sb =
+ new StringBuilder("ANT_RX_MESSAGE_ACTION Rx:");
+
+ for (int i = 0; i < antMessage.length; i++)
+ sb.append(String.format("[%X]]", (antMessage[i] & 0xFF)));
+
+ Log.d(MyTracksConstants.TAG, sb.toString());
+ }
+
+ handleMessage(antMessage);
+ }
+ }
+ };
+
+ /**
+ * Handler for all the ANT related events. The events will be handled only
+ * when we are in the ANT application main menu
+ **/
+ private Handler handler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case EVENT_ANT_ENABLED:
+ Log.i(MyTracksConstants.TAG,
+ "enter handleMessage ----EVENT_ANT_ENABLED");
+ break;
+ case EVENT_ANT_DISABLED:
+ Log.i(MyTracksConstants.TAG,
+ "enter handleMessage ----EVENT_ANT_DISABLED");
+ break;
+ }
+ }
+ };
+
+ public AntSensorManager(Context context) {
+ this.context = context;
+
+ // Need to enable the ANT if it is not enabled earlier
+ antReceiver = AntInterface.getInstance(context, null);
+ if (antReceiver == null) {
+ return;
+ }
+
+ status = antReceiver.isEnabled();
+ if (!status) {
+ // Make sure not to call AntInterface.enable() again, if it has been
+ // already called before
+ if (antInterrupted == false) {
+ status = antReceiver.enable();
+ if (!status) {
+ Log.e(MyTracksConstants.TAG, "Can not enable ANT interface");
+ } else {
+ Log.i(MyTracksConstants.TAG, "Powering on Radio");
+ }
+ } else {
+ Log.i(MyTracksConstants.TAG, "Radio already enabled");
+ }
+ }
+ }
+
+ @Override
+ public void setupChannel() {
+ Log.i(MyTracksConstants.TAG, "Registering for ant intents.");
+ // Register for ANT intent broadcasts.
+ IntentFilter intentFilter = new IntentFilter();
+ intentFilter.addAction(AntInterfaceIntent.ANT_ENABLED_ACTION);
+ intentFilter.addAction(AntInterfaceIntent.ANT_DISABLED_ACTION);
+ intentFilter.addAction(AntInterfaceIntent.ANT_RX_MESSAGE_ACTION);
+ context.registerReceiver(receiver, intentFilter);
+ }
+
+ @Override
+ public SensorDataSet getSensorDataSet() {
+ return sensorData;
+ }
+
+ @Override
+ public boolean isEnabled() {
+ return true;
+ }
+
+ public abstract void handleMessage(byte[] antMessage);
+
+ @Override
+ public void onDestroy() {
+ Log.i(MyTracksConstants.TAG, "Unregistering for ant intents.");
+ context.unregisterReceiver(receiver);
+ }
+
+ /**
+ * Set up an ANT+ channel.
+ *
+ * @return True on success.
+ */
+ protected boolean antChannelSetup(byte networkNumber, byte channelNumber,
+ short deviceNumber, byte deviceType, byte txType, short channelPeriod,
+ byte radioFreq, byte proxSearch) {
+
+ // Assign as slave channel on selected network (0 = public, 1 = ANT+, 2 =
+ // ANTFS)
+ if (!antReceiver.ANTAssignChannel(channelNumber,
+ AntDefine.PARAMETER_RX_NOT_TX, networkNumber)) {
+ return false;
+ }
+
+ if (!antReceiver.ANTSetChannelId(channelNumber, deviceNumber, deviceType,
+ txType)) {
+ return false;
+ }
+
+ if (!antReceiver.ANTSetChannelPeriod(channelNumber, channelPeriod)) {
+ return false;
+ }
+
+ if (!antReceiver.ANTSetChannelRFFreq(channelNumber, radioFreq)) {
+ return false;
+ }
+
+ // Disable high priority search
+ if (!antReceiver.ANTSetChannelSearchTimeout(channelNumber, (byte) 0)) {
+ return false;
+ }
+
+ // Set search timeout to 30 seconds (low priority search))
+ if (!antReceiver.ANTSetLowPriorityChannelSearchTimeout(channelNumber, (byte) 12)) {
+ return false;
+ }
+
+ if (deviceNumber == WILDCARD) {
+ // Configure proximity search, if using wild card search
+ if (!antReceiver.ANTSetProximitySearch(channelNumber, proxSearch)) {
+ return false;
+ }
+ }
+
+ return !antReceiver.ANTOpenChannel(channelNumber);
+ }
+
+ public AntInterface getAntReceiver() {
+ return antReceiver;
+ }
+}
diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/sensors/SensorManagerFactory.java b/MyTracks/src/com/google/android/apps/mytracks/services/sensors/SensorManagerFactory.java
index 01a8d8bf5..cad3e707b 100644
--- a/MyTracks/src/com/google/android/apps/mytracks/services/sensors/SensorManagerFactory.java
+++ b/MyTracks/src/com/google/android/apps/mytracks/services/sensors/SensorManagerFactory.java
@@ -13,7 +13,6 @@
* License for the specific language governing permissions and limitations under
* the License.
*/
-
package com.google.android.apps.mytracks.services.sensors;
import com.google.android.apps.mytracks.MyTracksConstants;
@@ -46,15 +45,19 @@ public class SensorManagerFactory {
return null;
}
- String sensor = prefs.getString(context.getString(R.string.sensor_type_key),
- null);
+ String sensor = prefs.getString(context.getString(R.string.sensor_type_key), null);
Log.i(MyTracksConstants.TAG, "Creating sensor of type: " + sensor);
if (sensor == null) {
return null;
+ } else if (sensor.equals(context.getString(R.string.ant_sensor_type))) {
+ return new AntDirectSensorManager(context);
+ } else if (sensor.equals(context.getString(R.string.srm_ant_bridge_sensor_type))) {
+ return new AntSRMSensorManager(context);
} else if (sensor.equals(context.getString(R.string.zephyr_sensor_type))) {
return new ZephyrSensorManager(context);
} else {
+ Log.w(MyTracksConstants.TAG, "Unable to find sensor type: " + sensor);
return null;
}
}
diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/sensors/ZephyrMessageParser.java b/MyTracks/src/com/google/android/apps/mytracks/services/sensors/ZephyrMessageParser.java
index 8cf8f951f..3a2fbf9e4 100644
--- a/MyTracks/src/com/google/android/apps/mytracks/services/sensors/ZephyrMessageParser.java
+++ b/MyTracks/src/com/google/android/apps/mytracks/services/sensors/ZephyrMessageParser.java
@@ -35,8 +35,9 @@ public class ZephyrMessageParser implements MessageParser {
}
Log.w(MyTracksConstants.TAG, "Got zephyr data: " + sb);
// The provided units are 1/16 strides per minute.
- int cadence = SensorUtils.unsignedShortToInt(buffer, 56);
- // Heart Rate
+ // 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);
diff --git a/MyTracks/src/com/google/android/apps/mytracks/util/ApiFeatures.java b/MyTracks/src/com/google/android/apps/mytracks/util/ApiFeatures.java
index 06727cd2d..7f1016856 100644
--- a/MyTracks/src/com/google/android/apps/mytracks/util/ApiFeatures.java
+++ b/MyTracks/src/com/google/android/apps/mytracks/util/ApiFeatures.java
@@ -62,24 +62,18 @@ public class ApiFeatures {
* Allow subclasses for mocking, but no direct instantiation.
*/
protected ApiFeatures() {
- if (getApiLevel() >= 5) {
- try {
- Class> clazz = Class.forName(
- "com.google.android.apps.mytracks.util.EclairPlatformAdapter");
- apiPlatformAdapter = (ApiPlatformAdapter) clazz.newInstance();
- } catch (Exception e) {
- Log.i(MyTracksConstants.TAG, "ApiFeatures: Unable to instantiate Eclair"
- + " platform adapter", e);
- }
- }
- if (apiPlatformAdapter == null) {
+ if (getApiLevel() >= 9) {
+ apiPlatformAdapter = new GingerbreadPlatformAdapter();
+ } else if (getApiLevel() >= 5) {
+ apiPlatformAdapter = new EclairPlatformAdapter();
+ } else {
Log.i(MyTracksConstants.TAG,
"ApiFeatures: Using default platform adapter");
// Cupcake adapter is always supported, so it's safe to do static linkage.
apiPlatformAdapter = new CupcakePlatformAdapter();
}
}
-
+
public ApiPlatformAdapter getApiPlatformAdapter() {
return apiPlatformAdapter;
}
@@ -107,7 +101,11 @@ public class ApiFeatures {
return true;
}
-
+
+ public boolean hasStrictMode() {
+ return getApiLevel() >= 9;
+ }
+
// Visible for testing.
protected int getApiLevel() {
return ANDROID_API_LEVEL;
diff --git a/MyTracks/src/com/google/android/apps/mytracks/util/ApiPlatformAdapter.java b/MyTracks/src/com/google/android/apps/mytracks/util/ApiPlatformAdapter.java
index 1e2a5210a..c68832735 100644
--- a/MyTracks/src/com/google/android/apps/mytracks/util/ApiPlatformAdapter.java
+++ b/MyTracks/src/com/google/android/apps/mytracks/util/ApiPlatformAdapter.java
@@ -18,6 +18,7 @@ package com.google.android.apps.mytracks.util;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.Service;
+import android.content.SharedPreferences;
import android.os.HandlerThread;
/**
@@ -57,4 +58,15 @@ public interface ApiPlatformAdapter {
* @return true whether the handler has been stopped or not.
*/
boolean stopHandlerThread(HandlerThread handlerThread);
+
+ /**
+ * Applies all changes done to the given preferences editor.
+ * Changes may or may not be applied immediately.
+ */
+ void applyPreferenceChanges(SharedPreferences.Editor editor);
+
+ /**
+ * Enables strict mode where supported, only if this is a development build.
+ */
+ void enableStrictMode();
}
diff --git a/MyTracks/src/com/google/android/apps/mytracks/util/CupcakePlatformAdapter.java b/MyTracks/src/com/google/android/apps/mytracks/util/CupcakePlatformAdapter.java
index 272b47218..354f0a307 100644
--- a/MyTracks/src/com/google/android/apps/mytracks/util/CupcakePlatformAdapter.java
+++ b/MyTracks/src/com/google/android/apps/mytracks/util/CupcakePlatformAdapter.java
@@ -18,6 +18,7 @@ package com.google.android.apps.mytracks.util;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.Service;
+import android.content.SharedPreferences.Editor;
import android.os.HandlerThread;
/**
@@ -50,4 +51,14 @@ public class CupcakePlatformAdapter implements ApiPlatformAdapter {
// Do nothing, as Cupcake doesn't provide quit().
return false;
}
+
+ @Override
+ public void applyPreferenceChanges(Editor editor) {
+ editor.commit();
+ }
+
+ @Override
+ public void enableStrictMode() {
+ // Not supported
+ }
}
diff --git a/MyTracks/src/com/google/android/apps/mytracks/util/EclairPlatformAdapter.java b/MyTracks/src/com/google/android/apps/mytracks/util/EclairPlatformAdapter.java
index 9c32946d3..91c1ea1d3 100644
--- a/MyTracks/src/com/google/android/apps/mytracks/util/EclairPlatformAdapter.java
+++ b/MyTracks/src/com/google/android/apps/mytracks/util/EclairPlatformAdapter.java
@@ -18,6 +18,7 @@ package com.google.android.apps.mytracks.util;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.Service;
+import android.content.SharedPreferences.Editor;
import android.os.HandlerThread;
/**
@@ -45,4 +46,14 @@ public class EclairPlatformAdapter implements ApiPlatformAdapter {
public boolean stopHandlerThread(HandlerThread handlerThread) {
return handlerThread.quit();
}
+
+ @Override
+ public void applyPreferenceChanges(Editor editor) {
+ editor.commit();
+ }
+
+ @Override
+ public void enableStrictMode() {
+ // Not supported
+ }
}
diff --git a/MyTracks/src/com/google/android/apps/mytracks/util/GingerbreadPlatformAdapter.java b/MyTracks/src/com/google/android/apps/mytracks/util/GingerbreadPlatformAdapter.java
new file mode 100644
index 000000000..caa73a170
--- /dev/null
+++ b/MyTracks/src/com/google/android/apps/mytracks/util/GingerbreadPlatformAdapter.java
@@ -0,0 +1,36 @@
+package com.google.android.apps.mytracks.util;
+
+import com.google.android.apps.mytracks.MyTracksConstants;
+
+import android.content.SharedPreferences.Editor;
+import android.os.StrictMode;
+import android.util.Log;
+
+/**
+ * The Gingerbread (API level 9) specific implememntation of the
+ * {@link ApiPlatformAdapter}.
+ *
+ * @author Rodrigo Damazio
+ */
+public class GingerbreadPlatformAdapter extends EclairPlatformAdapter {
+ @Override
+ public void applyPreferenceChanges(Editor editor) {
+ // Apply asynchronously
+ editor.apply();
+ }
+
+ @Override
+ public void enableStrictMode() {
+ Log.d(MyTracksConstants.TAG, "Enabling strict mode");
+ StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
+ .detectDiskWrites()
+ .detectNetwork()
+ .penaltyLog()
+ .build());
+ StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
+ .detectAll()
+ .penaltyLog()
+ .penaltyDeath()
+ .build());
+ }
+}
diff --git a/MyTracks/src/com/google/android/apps/mytracks/util/MyTracksUtils.java b/MyTracks/src/com/google/android/apps/mytracks/util/MyTracksUtils.java
index 4d83ba992..4d6da6786 100644
--- a/MyTracks/src/com/google/android/apps/mytracks/util/MyTracksUtils.java
+++ b/MyTracks/src/com/google/android/apps/mytracks/util/MyTracksUtils.java
@@ -25,6 +25,7 @@ import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.Signature;
import android.location.Location;
import android.util.Log;
@@ -38,6 +39,7 @@ import java.util.Stack;
* @author Leif Hendrik Wilden
*/
public class MyTracksUtils {
+ private static final int RELEASE_SIGNATURE_HASHCODE = -1855564782;
/**
* Computes the distance on the two sphere between the point c0 and the line
@@ -250,6 +252,24 @@ public class MyTracksUtils {
(int) (location.getLongitude() * 1E6));
}
+ /**
+ * Returns whether or not this is a release build.
+ */
+ public static boolean isRelease(Context context) {
+ try {
+ Signature [] sigs = context.getPackageManager().getPackageInfo(
+ context.getPackageName(), PackageManager.GET_SIGNATURES).signatures;
+ for (Signature sig : sigs) {
+ if (sig.hashCode() == RELEASE_SIGNATURE_HASHCODE) {
+ return true;
+ }
+ }
+ } catch (NameNotFoundException e) {
+ Log.e(MyTracksConstants.TAG, "Unable to get signatures", e);
+ }
+ return false;
+ }
+
/**
* Get the My Tracks version from the manifest.
*
@@ -258,17 +278,17 @@ public class MyTracksUtils {
public static String getMyTracksVersion(Context context) {
try {
PackageInfo pi = context.getPackageManager().getPackageInfo(
- "com.google.android.maps.mytracks",
+ "com.google.android.maps.mytracks",
PackageManager.GET_META_DATA);
return pi.versionName;
- } catch (NameNotFoundException e) {
+ } catch (NameNotFoundException e) {
Log.w(MyTracksConstants.TAG, "Failed to get version info.", e);
return "";
}
}
/**
- * This is a utility class w/ only static memebers.
+ * This is a utility class w/ only static members.
*/
protected MyTracksUtils() {
}
diff --git a/MyTracksLib/default.properties b/MyTracksLib/default.properties
index 948be06ed..1694c805b 100644
--- a/MyTracksLib/default.properties
+++ b/MyTracksLib/default.properties
@@ -8,5 +8,5 @@
# project structure.
# Project target.
-target=android-8
+target=android-9
android.library=true
diff --git a/MyTracksLib/src/com/google/android/apps/mytracks/content/MyTracksProviderUtils.java b/MyTracksLib/src/com/google/android/apps/mytracks/content/MyTracksProviderUtils.java
index 56c70db13..586228b25 100644
--- a/MyTracksLib/src/com/google/android/apps/mytracks/content/MyTracksProviderUtils.java
+++ b/MyTracksLib/src/com/google/android/apps/mytracks/content/MyTracksProviderUtils.java
@@ -21,6 +21,7 @@ import android.database.Cursor;
import android.location.Location;
import android.net.Uri;
+import java.util.Iterator;
import java.util.List;
/**
@@ -179,8 +180,9 @@ public interface MyTracksProviderUtils {
/**
* Finds a track by given unique track id.
- * Note that the returned track object does not have any track points
- * attached. Use {@link #getTrackPoints(Track, int)} to load the track points.
+ * Note that the returned track object does not have any track points attached.
+ * Use {@link #getLocationIterator(long, long, boolean, LocationFactory)} to load
+ * the track points.
*
* @param id desired unique track id
* @return a Track object, or null if not found
@@ -188,24 +190,14 @@ public interface MyTracksProviderUtils {
Track getTrack(long id);
/**
- * Retrieves all tracks without track points. If no tracks exist an empty
- * list will be returned. Use {@link #getTrackPoints(Track, int)} to load
- * the track points.
+ * Retrieves all tracks without track points. If no tracks exist, an empty
+ * list will be returned. Use {@link #getLocationIterator(long, long, boolean, LocationFactory)}
+ * to load the track points.
*
* @return a list of all the recorded tracks
*/
List