diff --git a/MyTracks/AndroidManifest.xml b/MyTracks/AndroidManifest.xml index 8834d5840..d085d37e9 100755 --- a/MyTracks/AndroidManifest.xml +++ b/MyTracks/AndroidManifest.xml @@ -17,8 +17,8 @@ limitations under the License. + android:versionCode="32" + android:versionName="1.1.9"> @@ -79,6 +79,17 @@ limitations under the License. + + + + + + + + diff --git a/MyTracks/res/values/strings.xml b/MyTracks/res/values/strings.xml index adacc5917..afe2a8f2a 100644 --- a/MyTracks/res/values/strings.xml +++ b/MyTracks/res/values/strings.xml @@ -83,7 +83,8 @@ limitations under the License. Writing file to SD card. This may take a while. Updating track statistics... - + + Delete track Are you sure? All tracks and markers will be deleted from this phone. The track and its markers will be permanently deleted from this phone. @@ -509,5 +510,5 @@ limitations under the License. SRM ANT+ Bridge Polar Bluetooth Zephyr Bluetooth - + diff --git a/MyTracks/src/com/google/android/apps/mytracks/Constants.java b/MyTracks/src/com/google/android/apps/mytracks/Constants.java index 063bb6679..91defa8b2 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/Constants.java +++ b/MyTracks/src/com/google/android/apps/mytracks/Constants.java @@ -1,12 +1,12 @@ /* * Copyright 2008 Google Inc. - * + * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the @@ -26,7 +26,7 @@ public abstract class Constants { * Should be used by all log statements */ public static final String TAG = "MyTracks"; - + /** * Name of the top-level directory inside the SD card where our files will * be read from/written to. @@ -39,32 +39,21 @@ public abstract class Constants { public static final int GET_LOGIN = 0; public static final int GET_MAP = 1; - public static final int CREATE_MAP = 2; - public static final int SHOW_TRACK = 3; - public static final int ADD_LIST = 4; - public static final int FEATURE_DETAILS = 5; - public static final int START_RECORDING = 6; - public static final int STOP_RECORDING = 7; - public static final int AUTHENTICATE_TO_MY_MAPS = 8; - public static final int AUTHENTICATE_TO_FUSION_TABLES = 9; - public static final int AUTHENTICATE_TO_DOCLIST = 10; - public static final int AUTHENTICATE_TO_TRIX = 11; - public static final int DELETE_TRACK = 13; - public static final int SEND_TO_GOOGLE = 14; - public static final int SHARE_LINK = 15; - public static final int SHARE_GPX_FILE = 16; - public static final int SHARE_KML_FILE = 17; - public static final int SHARE_CSV_FILE = 18; - public static final int SHARE_TCX_FILE = 19; - public static final int EDIT_DETAILS = 20; - public static final int SAVE_GPX_FILE = 21; - public static final int SAVE_KML_FILE = 22; - public static final int SAVE_CSV_FILE = 23; - public static final int SAVE_TCX_FILE = 24; - public static final int CLEAR_MAP = 25; - public static final int SHOW_WAYPOINT = 26; - public static final int EDIT_WAYPOINT = 27; - public static final int WELCOME = 28; + public static final int SHOW_TRACK = 2; + public static final int AUTHENTICATE_TO_MY_MAPS = 3; + public static final int AUTHENTICATE_TO_FUSION_TABLES = 4; + public static final int AUTHENTICATE_TO_DOCLIST = 5; + public static final int AUTHENTICATE_TO_TRIX = 6; + public static final int SHARE_GPX_FILE = 7; + public static final int SHARE_KML_FILE = 8; + public static final int SHARE_CSV_FILE = 9; + public static final int SHARE_TCX_FILE = 10; + public static final int SAVE_GPX_FILE = 11; + public static final int SAVE_KML_FILE = 12; + public static final int SAVE_CSV_FILE = 13; + public static final int SAVE_TCX_FILE = 14; + public static final int SHOW_WAYPOINT = 15; + public static final int WELCOME = 16; /* * Menu ids: @@ -179,7 +168,7 @@ public abstract class Constants { * The type of account that we can use for gdata uploads. */ public static final String ACCOUNT_TYPE = "com.google"; - + /** * The name of extra intent property to indicate whether we want to resume * a previously recorded track. @@ -189,14 +178,6 @@ public abstract class Constants { public static int getActionFromMenuId(int menuId) { switch (menuId) { - case Constants.MENU_SEND_TO_GOOGLE: - return Constants.SEND_TO_GOOGLE; - case Constants.MENU_EDIT: - return Constants.EDIT_DETAILS; - case Constants.MENU_DELETE: - return Constants.DELETE_TRACK; - case Constants.MENU_SHARE_LINK: - return Constants.SHARE_LINK; case Constants.MENU_SHARE_KML_FILE: return Constants.SHARE_KML_FILE; case Constants.MENU_SHARE_GPX_FILE: @@ -213,8 +194,6 @@ public abstract class Constants { return Constants.SAVE_CSV_FILE; case Constants.MENU_SAVE_TCX_FILE: return Constants.SAVE_TCX_FILE; - case Constants.MENU_CLEAR_MAP: - return Constants.CLEAR_MAP; default: return -1; } diff --git a/MyTracks/src/com/google/android/apps/mytracks/DeleteTrack.java b/MyTracks/src/com/google/android/apps/mytracks/DeleteTrack.java new file mode 100644 index 000000000..1b73e8f68 --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/DeleteTrack.java @@ -0,0 +1,114 @@ +/* + * 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.apps.mytracks.Constants.TAG; + +import com.google.android.apps.mytracks.content.MyTracksProviderUtils; +import com.google.android.apps.mytracks.content.TracksColumns; +import com.google.android.apps.mytracks.util.ApiFeatures; +import com.google.android.apps.mytracks.util.UriUtils; +import com.google.android.maps.mytracks.R; + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.Dialog; +import android.content.ContentUris; +import android.content.DialogInterface; +import android.content.DialogInterface.OnCancelListener; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.SharedPreferences.Editor; +import android.net.Uri; +import android.os.Bundle; +import android.util.Log; + +/** + * Activity used to delete a track. + * + * @author Rodrigo Damazio + */ +public class DeleteTrack extends Activity + implements DialogInterface.OnClickListener, OnCancelListener { + private static final int CONFIRM_DIALOG = 1; + + private MyTracksProviderUtils providerUtils; + + private long deleteTrackId; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + providerUtils = MyTracksProviderUtils.Factory.get(this); + + Intent intent = getIntent(); + String action = intent.getAction(); + Uri data = intent.getData(); + if (!Intent.ACTION_DELETE.equals(action) || + !UriUtils.matchesContentUri(data, TracksColumns.CONTENT_URI)) { + Log.e(TAG, "Got bad delete intent: " + intent); + finish(); + } + + deleteTrackId = ContentUris.parseId(data); + + showDialog(CONFIRM_DIALOG); + } + + @Override + protected Dialog onCreateDialog(int id) { + if (id != CONFIRM_DIALOG) { + Log.e(TAG, "Unknown dialog " + id); + return null; + } + + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setMessage(getString(R.string.track_will_be_permanently_deleted)); + builder.setTitle(getString(R.string.are_you_sure_question)); + builder.setIcon(android.R.drawable.ic_dialog_alert); + builder.setPositiveButton(getString(R.string.yes), this); + builder.setNegativeButton(getString(R.string.no), this); + builder.setOnCancelListener(this); + return builder.create(); + } + + @Override + public void onClick(DialogInterface dialogInterface, int which) { + dialogInterface.dismiss(); + if (which == DialogInterface.BUTTON_POSITIVE) { + deleteTrack(); + } + finish(); + } + + @Override + public void onCancel(DialogInterface dialog) { + onClick(dialog, DialogInterface.BUTTON_NEGATIVE); + } + + private void deleteTrack() { + providerUtils.deleteTrack(deleteTrackId); + + // If the track we just deleted was selected, unselect it. + String selectedKey = getString(R.string.selected_track_key); + SharedPreferences preferences = getSharedPreferences(Constants.SETTINGS_NAME, 0); + if (preferences.getLong(selectedKey, -1) == deleteTrackId) { + Editor editor = preferences.edit().putLong(selectedKey, -1); + ApiFeatures.getInstance().getApiPlatformAdapter().applyPreferenceChanges(editor); + } + } +} diff --git a/MyTracks/src/com/google/android/apps/mytracks/MapActivity.java b/MyTracks/src/com/google/android/apps/mytracks/MapActivity.java index d7391a211..f168b3bc7 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/MapActivity.java +++ b/MyTracks/src/com/google/android/apps/mytracks/MapActivity.java @@ -22,6 +22,7 @@ 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.TracksColumns; import com.google.android.apps.mytracks.content.Waypoint; import com.google.android.apps.mytracks.io.file.SaveActivity; import com.google.android.apps.mytracks.io.sendtogoogle.SendActivity; @@ -35,8 +36,10 @@ import com.google.android.maps.MapController; import com.google.android.maps.MapView; import com.google.android.maps.mytracks.R; +import android.content.ContentUris; import android.content.Intent; import android.location.Location; +import android.net.Uri; import android.os.Bundle; import android.provider.Settings; import android.util.Log; @@ -412,6 +415,23 @@ public class MapActivity extends com.google.android.maps.MapActivity SaveActivity.handleExportTrackAction(this, dataHub.getSelectedTrackId(), Constants.getActionFromMenuId(item.getItemId())); return true; + case Constants.MENU_EDIT: { + Intent intent = new Intent(this, TrackDetails.class); + // TODO: Pass in a content URI + intent.putExtra("trackid", dataHub.getSelectedTrackId()); + startActivity(intent); + return true; + } + case Constants.MENU_DELETE: { + Uri uri = ContentUris.withAppendedId( + TracksColumns.CONTENT_URI, dataHub.getSelectedTrackId()); + Intent intent = new Intent(Intent.ACTION_DELETE, uri); + startActivity(intent); + return true; + } + case Constants.MENU_CLEAR_MAP: + dataHub.unloadCurrentTrack(); + return true; default: return super.onMenuItemSelected(featureId, item); } diff --git a/MyTracks/src/com/google/android/apps/mytracks/MenuManager.java b/MyTracks/src/com/google/android/apps/mytracks/MenuManager.java index 97ef2adae..58f9d1f63 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/MenuManager.java +++ b/MyTracks/src/com/google/android/apps/mytracks/MenuManager.java @@ -1,12 +1,12 @@ /* * Copyright 2010 Google Inc. - * + * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the @@ -24,7 +24,7 @@ import android.view.MenuItem; /** * Manage the application menus. - * + * * @author Sandor Dornbush */ class MenuManager { @@ -34,7 +34,7 @@ class MenuManager { public MenuManager(MyTracks activity) { this.activity = activity; } - + public boolean onCreateOptionsMenu(Menu menu) { activity.getMenuInflater().inflate(R.menu.main, menu); return true; @@ -52,7 +52,7 @@ class MenuManager { .setEnabled(isRecording) .setVisible(isRecording); } - + public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.menu_start_recording: { @@ -86,10 +86,6 @@ class MenuManager { case R.id.menu_help: { return startActivity(WelcomeActivity.class); } - case Constants.MENU_CLEAR_MAP: { - activity.clearSelectedTrack(); - return true; - } } return false; } diff --git a/MyTracks/src/com/google/android/apps/mytracks/MyTracks.java b/MyTracks/src/com/google/android/apps/mytracks/MyTracks.java index 2a72052cb..8a9555a8e 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/MyTracks.java +++ b/MyTracks/src/com/google/android/apps/mytracks/MyTracks.java @@ -33,10 +33,8 @@ import com.google.android.apps.mytracks.util.UriUtils; import com.google.android.maps.mytracks.R; import android.app.Activity; -import android.app.AlertDialog; import android.app.TabActivity; import android.content.ContentUris; -import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; import android.content.res.Resources; @@ -61,6 +59,7 @@ import android.widget.Toast; * @author Leif Hendrik Wilden * @author Rodrigo Damazio */ +@SuppressWarnings("deprecation") public class MyTracks extends TabActivity implements OnTouchListener { private TrackDataHub dataHub; @@ -314,7 +313,7 @@ public class MyTracks extends TabActivity implements OnTouchListener { dataHub.loadTrack(trackId); // The track list passed the requested action as result code. Hand - // it off to the onAcitivtyResult for further processing: + // it off to the onActivityResult for further processing: if (resultCode != Constants.SHOW_TRACK) { onActivityResult(resultCode, Activity.RESULT_OK, results); } @@ -336,24 +335,6 @@ public class MyTracks extends TabActivity implements OnTouchListener { } break; } - case Constants.DELETE_TRACK: { - if (results != null && resultCode == RESULT_OK) { - deleteTrack(trackId); - } - break; - } - case Constants.EDIT_DETAILS: { - if (results != null && resultCode == RESULT_OK) { - Intent intent = new Intent(this, TrackDetails.class); - intent.putExtra("trackid", trackId); - startActivity(intent); - } - break; - } - case Constants.CLEAR_MAP: { - dataHub.unloadCurrentTrack(); - break; - } case Constants.WELCOME: { CheckUnits.check(this); break; @@ -373,36 +354,6 @@ public class MyTracks extends TabActivity implements OnTouchListener { return false; } - /** - * Deletes the track with the given id. - * Prompts the user if he want to really delete the track first. - * If the selected track is deleted, the selection will be removed. - */ - public void deleteTrack(final long trackId) { - AlertDialog dialog = null; - AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setMessage(getString(R.string.track_will_be_permanently_deleted)); - builder.setTitle(getString(R.string.are_you_sure_question)); - builder.setIcon(android.R.drawable.ic_dialog_alert); - builder.setPositiveButton(getString(R.string.yes), - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialogInterface, int i) { - dialogInterface.dismiss(); - providerUtils.deleteTrack(trackId); - if (trackId == dataHub.getSelectedTrackId()) { - dataHub.unloadCurrentTrack(); - } - }}); - builder.setNegativeButton(getString(R.string.no), - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialogInterface, int i) { - dialogInterface.dismiss(); - } - }); - dialog = builder.create(); - dialog.show(); - } - /** * Inserts a waypoint marker. * @@ -493,10 +444,6 @@ public class MyTracks extends TabActivity implements OnTouchListener { } } - void clearSelectedTrack() { - dataHub.unloadCurrentTrack(); - } - long getSelectedTrackId() { return dataHub.getSelectedTrackId(); } diff --git a/MyTracks/src/com/google/android/apps/mytracks/TrackList.java b/MyTracks/src/com/google/android/apps/mytracks/TrackList.java index 17e927f28..cf0defa44 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/TrackList.java +++ b/MyTracks/src/com/google/android/apps/mytracks/TrackList.java @@ -15,6 +15,8 @@ */ package com.google.android.apps.mytracks; +import static com.google.android.apps.mytracks.Constants.TAG; + import com.google.android.apps.mytracks.content.TracksColumns; import com.google.android.apps.mytracks.io.file.SaveActivity; import com.google.android.apps.mytracks.io.sendtogoogle.SendActivity; @@ -25,11 +27,14 @@ import com.google.android.apps.mytracks.util.UnitConversions; import com.google.android.maps.mytracks.R; import android.app.ListActivity; +import android.content.ContentUris; import android.content.Intent; import android.content.SharedPreferences; import android.database.Cursor; +import android.net.Uri; import android.os.Bundle; import android.os.Handler; +import android.util.Log; import android.view.ContextMenu; import android.view.ContextMenu.ContextMenuInfo; import android.view.MenuItem; @@ -182,14 +187,16 @@ public class TrackList extends ListActivity SaveActivity.handleExportTrackAction(this, trackId, Constants.getActionFromMenuId(item.getItemId())); return true; - default: { - Intent result = new Intent(); - result.putExtra("trackid", trackId); - setResult( - Constants.getActionFromMenuId(item.getItemId()), result); - finish(); + case Constants.MENU_DELETE: { + Intent intent = new Intent(Intent.ACTION_DELETE); + Uri uri = ContentUris.withAppendedId(TracksColumns.CONTENT_URI, trackId); + intent.setDataAndType(uri, TracksColumns.CONTENT_ITEMTYPE); + startActivity(intent); return true; } + default: + Log.w(TAG, "Unknown menu item: " + item.getItemId() + "(" + item.getTitle() + ")"); + return super.onMenuItemSelected(featureId, item); } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/WaypointsList.java b/MyTracks/src/com/google/android/apps/mytracks/WaypointsList.java index f3f713902..ecfe1b733 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/WaypointsList.java +++ b/MyTracks/src/com/google/android/apps/mytracks/WaypointsList.java @@ -96,11 +96,7 @@ public class WaypointsList extends ListActivity @Override protected void onListItemClick(ListView l, View v, int position, long id) { - Intent result = new Intent(); - result.putExtra("trackid", trackId); - result.putExtra(WaypointDetails.WAYPOINT_ID_EXTRA, id); - setResult(Constants.EDIT_WAYPOINT, result); - finish(); + editWaypoint(id); } @Override @@ -108,14 +104,15 @@ public class WaypointsList extends ListActivity if (!super.onMenuItemSelected(featureId, item)) { switch (item.getItemId()) { case Constants.MENU_SHOW: { - onListItemClick(null, null, 0, selectedWaypointId); + Intent result = new Intent(); + result.putExtra("trackid", trackId); + result.putExtra(WaypointDetails.WAYPOINT_ID_EXTRA, selectedWaypointId); + setResult(RESULT_OK, result); + finish(); return true; } case Constants.MENU_EDIT: { - Intent intent = new Intent(this, WaypointDetails.class); - intent.putExtra("trackid", trackId); - intent.putExtra(WaypointDetails.WAYPOINT_ID_EXTRA, selectedWaypointId); - startActivity(intent); + editWaypoint(selectedWaypointId); return true; } case Constants.MENU_DELETE: { @@ -126,6 +123,13 @@ public class WaypointsList extends ListActivity return false; } + private void editWaypoint(long waypointId) { + Intent intent = new Intent(this, WaypointDetails.class); + intent.putExtra("trackid", trackId); + intent.putExtra(WaypointDetails.WAYPOINT_ID_EXTRA, waypointId); + startActivity(intent); + } + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); diff --git a/MyTracks/src/com/google/android/apps/mytracks/content/TrackDataHub.java b/MyTracks/src/com/google/android/apps/mytracks/content/TrackDataHub.java index 26995fd09..d5e53c109 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/content/TrackDataHub.java +++ b/MyTracks/src/com/google/android/apps/mytracks/content/TrackDataHub.java @@ -412,10 +412,10 @@ public class TrackDataHub { return; } - // Save the selection to memory and flash. + // Save the selection to memory and flush. + selectedTrackId = trackId; ApiFeatures.getInstance().getApiPlatformAdapter().applyPreferenceChanges( preferences.edit().putLong(SELECTED_TRACK_KEY, trackId)); - selectedTrackId = trackId; // Force it to reload data from the beginning. Log.d(TAG, "Loading track"); @@ -642,6 +642,9 @@ public class TrackDataHub { } else if (SPEED_REPORTING_KEY.equals(key)) { reportSpeed = preferences.getBoolean(SPEED_REPORTING_KEY, true); notifySpeedReportingChanged(); + } else if (SELECTED_TRACK_KEY.equals(key)) { + long trackId = preferences.getLong(SELECTED_TRACK_KEY, -1); + loadTrack(trackId); } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/SendToFusionTables.java b/MyTracks/src/com/google/android/apps/mytracks/io/SendToFusionTables.java index c08269ffe..c43220f75 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/SendToFusionTables.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/SendToFusionTables.java @@ -16,6 +16,7 @@ package com.google.android.apps.mytracks.io; import static com.google.android.apps.mytracks.Constants.TAG; + import com.google.android.apps.mytracks.Constants; import com.google.android.apps.mytracks.ProgressIndicator; import com.google.android.apps.mytracks.content.MyTracksProviderUtils; @@ -50,7 +51,6 @@ import android.util.Log; import java.io.ByteArrayInputStream; import java.io.IOException; import java.net.URLEncoder; -import java.nio.charset.Charset; import java.util.ArrayList; import java.util.List; import java.util.Locale; @@ -695,7 +695,7 @@ public class SendToFusionTables implements Runnable { if (success) { byte[] result = new byte[1024]; int read = response.getContent().read(result); - String s = new String(result, 0, read, Charset.forName("UTF8")); + String s = new String(result, 0, read, "UTF8"); String[] lines = s.split(Strings.LINE_SEPARATOR); if (lines[0].equals("tableid")) { tableId = lines[1]; diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/file/TcxTrackWriter.java b/MyTracks/src/com/google/android/apps/mytracks/io/file/TcxTrackWriter.java index 6775ec977..49961fbae 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/file/TcxTrackWriter.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/file/TcxTrackWriter.java @@ -44,6 +44,7 @@ import java.util.TimeZone; * Garmin Training Center 3.5.3. * * @author Sandor Dornbush + * @author Dominik Ršttsches */ public class TcxTrackWriter implements TrackFormatWriter { protected static final String TIMESTAMP_FORMAT = "yyyy-MM-dd'T'HH:mm:ss'Z'"; @@ -64,6 +65,9 @@ public class TcxTrackWriter implements TrackFormatWriter { private PrintWriter pw = null; private Track track; + // Determines whether to encode cadence value as running or cycling cadence. + private boolean sportIsCycling; + public TcxTrackWriter(Context context) { this.context = context; @@ -76,7 +80,7 @@ public class TcxTrackWriter implements TrackFormatWriter { public void prepare(Track track, OutputStream out) { this.track = track; this.pw = new PrintWriter(out); - + this.sportIsCycling = categoryToTcxSport(track.getCategory()).equals(TCX_SPORT_BIKING); } @Override @@ -175,41 +179,38 @@ public class TcxTrackWriter implements TrackFormatWriter { pw.println(""); } - boolean cadenceAsCyclingCadence = categoryToTcxSport(track.getCategory()).equals(TCX_SPORT_BIKING); - - boolean cadenceValueAvailable = sensorData.hasCadence() + boolean cadenceAvailable = sensorData.hasCadence() && sensorData.getCadence().getState() == Sensor.SensorState.SENDING && sensorData.getCadence().hasValue(); - // TCX Trackpoint_t contains a sequence, - // legacy XML element needs to be put before - // Only for the case that activity was marked as biking. - // Otherwise cadence we interpret it as running cadence data - // and put it into . - if (cadenceAsCyclingCadence && cadenceValueAvailable) { + // TCX Trackpoint_t contains a sequence. Thus, the legacy XML element + // needs to be put before . + // This field should only be used for the case that activity was marked as biking. + // Otherwise cadence is interpreted as running cadence data which + // is written in the as . + if (sportIsCycling && cadenceAvailable) { pw.print(" "); pw.print(Math.min(254, sensorData.getCadence().getValue())); pw.println(""); } - boolean powerValueAvailable = sensorData.hasPower() + boolean powerAvailable = sensorData.hasPower() && sensorData.getPower().getState() == Sensor.SensorState.SENDING && sensorData.getPower().hasValue(); - if(powerValueAvailable - || (!cadenceAsCyclingCadence && cadenceValueAvailable)) { + if(powerAvailable || (!sportIsCycling && cadenceAvailable)) { pw.print(" "); pw.print(""); // RunCadence needs to be put before power in order to be understood // by Garmin Training Center. - if (!cadenceAsCyclingCadence && cadenceValueAvailable) { + if (!sportIsCycling && cadenceAvailable) { pw.print(""); - pw.print(sensorData.getCadence().getValue()); + pw.print(Math.min(254, sensorData.getCadence().getValue())); pw.print(""); } - if (powerValueAvailable) { + if (powerAvailable) { pw.print(""); pw.print(sensorData.getPower().getValue()); pw.print(""); diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/sensors/StrideReadings.java b/MyTracks/src/com/google/android/apps/mytracks/services/sensors/StrideReadings.java index 062b6b4f5..d01987175 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/services/sensors/StrideReadings.java +++ b/MyTracks/src/com/google/android/apps/mytracks/services/sensors/StrideReadings.java @@ -1,5 +1,5 @@ /* - * Copyright 2010 Google Inc. + * 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 @@ -19,61 +19,62 @@ import java.util.LinkedList; import java.util.List; /** - * Storage of a history of readings of the Zephyr stride counter, - * in order to derive a correct cadence value from it, - * working around an issue with the HxM's firmware. + * A history of Zephyr stride counter reading. + * These can be used as an alternate method to calculate the correct cadence. + * This is a work around for an issue with some HxM firmware. * * @author Dominik Ršttsches */ public class StrideReadings { - private static class StrideReading { - // TODO: Check whether 1Hz assumption is okay for cadence calculation - // otherwise use timeMs, which is taken from heart beat timestamp. - @SuppressWarnings("unused") - public int timeMs; - public int numStrides; - StrideReading(int newTimeMs, int newNumStrides) { - timeMs = newTimeMs; - numStrides = newNumStrides; - } - } - - private static final int NUM_READINGS_FOR_AVERAGE = 10; - private static final int MIN_READINGS_FOR_AVERAGE = 5; + // visible for testing + protected static final int NUM_READINGS_FOR_AVERAGE = 10; + protected static final int MIN_READINGS_FOR_AVERAGE = 5; protected static final int CADENCE_NOT_AVAILABLE = -1; - private List strideReadingsHistory; + // TODO: Check whether 1Hz assumption is okay for cadence calculation + // otherwise add heart beat timestamp to this list and compute + // cadence from these timestamps. + private final List strideReadingsHistory; public StrideReadings() { - strideReadingsHistory = new LinkedList(); + strideReadingsHistory = new LinkedList(); } - public void updateStrideReading(int timeInMs, int numStrides) { + public void updateStrideReading(int numStrides) { // HRM/HxM documentation says, transmission frequency is 1 Hz, // let's keep last NUM_READINGS_FOR_AVERAGE readings. // TODO: Calibrate this using a reliable footpod / cadence sensor, // otherwise use heartbeat timestamp for calculation. - strideReadingsHistory.add(0, new StrideReading(timeInMs, numStrides)); - while(strideReadingsHistory.size() > NUM_READINGS_FOR_AVERAGE) { + strideReadingsHistory.add(0, numStrides); + while (strideReadingsHistory.size() > NUM_READINGS_FOR_AVERAGE) { strideReadingsHistory.remove(strideReadingsHistory.size()-1); } } public int getCadence() { - if(strideReadingsHistory.size() < MIN_READINGS_FOR_AVERAGE) { + if (strideReadingsHistory.size() < MIN_READINGS_FOR_AVERAGE) { // Bail out if we cannot really get a meaningful average yet. return CADENCE_NOT_AVAILABLE; } - // compute assuming 1 stride reading/second + // Compute assuming 1 stride reading/second. int timeSinceOldestReadingSecs = strideReadingsHistory.size() - 1; - int stridesThen = strideReadingsHistory - .get(strideReadingsHistory.size()-1).numStrides; - int stridesNow = strideReadingsHistory - .get(0).numStrides; - // Contrary to documentation stride value seems to roll over every 127 strides. - return Math.round( (float)((stridesNow - stridesThen) % 127) / + int stridesThen = strideReadingsHistory.get(strideReadingsHistory.size()-1); + int stridesNow = strideReadingsHistory.get(0); + // Contrary to documentation stride value seems to roll over at 128. + return Math.round( (float)(mod((stridesNow - stridesThen), 128)) / timeSinceOldestReadingSecs * 60); } + + /** + * Modulo operation with positive return values, Java's remainder operator doesn't change sign. + * + * @return x mod y + */ + private static int mod(int x, int y) + { + int result = x % y; + return result < 0 ? result + y : result; + } } 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 77f99ea7b..24d93c2fd 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 @@ -15,10 +15,9 @@ */ package com.google.android.apps.mytracks.services.sensors; -import com.google.android.apps.mytracks.Constants; import com.google.android.apps.mytracks.content.Sensor; -import android.util.Log; +import java.util.Arrays; /** * An implementation of a Sensor MessageParser for Zephyr. @@ -32,27 +31,12 @@ public class ZephyrMessageParser implements MessageParser { public static final int ZEPHYR_HXM_BYTE_CRC = 58; public static final int ZEPHYR_HXM_BYTE_ETX = 59; - private static final String CADENCE_BUG_FW_ID = "1A00316550003162"; + private static final byte[] CADENCE_BUG_FW_ID = {0x1A, 0x00, 0x31, 0x65, 0x50, 0x00, 0x31, 0x62}; private StrideReadings strideReadings; @Override public Sensor.SensorDataSet parseBuffer(byte[] buffer) { - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < buffer.length; i++) { - sb.append(String.format("%02X", buffer[i])); - } - Log.w(Constants.TAG, "Got zephyr data: " + sb); - - // Device Firmware ID, Firmware Version, Hardware ID, Hardware Version - // 0x1A00316550003162 produces erroneous values for Cadence and needs - // a workaround based on the stride counter. - // Firmware values range from field 3 to 11 of the byte buffer, - // since in hex there are two characters for each byte, doubling the values. - String hardwareFirmwareId = sb.substring(6, 22); - boolean computeCadenceFromStrides = hardwareFirmwareId.equals(CADENCE_BUG_FW_ID); - Log.d(Constants.TAG, "FW & HW Ids & Version " + hardwareFirmwareId + " needs workaround: " + computeCadenceFromStrides); - Sensor.SensorDataSet.Builder sds = Sensor.SensorDataSet.newBuilder() .setCreationTime(System.currentTimeMillis()); @@ -60,42 +44,42 @@ public class ZephyrMessageParser implements MessageParser { Sensor.SensorData.Builder heartrate = Sensor.SensorData.newBuilder() .setValue(buffer[12] & 0xFF) .setState(Sensor.SensorState.SENDING); - sds = sds.setHeartRate(heartrate); + sds.setHeartRate(heartrate); Sensor.SensorData.Builder batteryLevel = Sensor.SensorData.newBuilder() .setValue(buffer[11]) .setState(Sensor.SensorState.SENDING); - sds = sds.setBatteryLevel(batteryLevel); + sds.setBatteryLevel(batteryLevel); - // Appends cadence to SensorDataSet builder if available. - parseOrComputeCadence(sds, buffer, computeCadenceFromStrides); + setCadence(sds, buffer); return sds.build(); } - private void parseOrComputeCadence(Sensor.SensorDataSet.Builder sds, byte[] buffer, boolean computeFromStrides) { - + private void setCadence(Sensor.SensorDataSet.Builder sds, byte[] buffer) { + // Device Firmware ID, Firmware Version, Hardware ID, Hardware Version + // 0x1A00316550003162 produces erroneous values for Cadence and needs + // a workaround based on the stride counter. + // Firmware values range from field 3 to 10 (inclusive) of the byte buffer. + byte[] hardwareFirmwareId = Arrays.copyOfRange(buffer, 3, 11); + Sensor.SensorData.Builder cadence = Sensor.SensorData.newBuilder(); - if(!computeFromStrides) { - cadence = cadence - .setValue(SensorUtils.unsignedShortToIntLittleEndian(buffer, 56) / 16) - .setState(Sensor.SensorState.SENDING); - sds.setCadence(cadence); - } else { - if(strideReadings == null) { + if (Arrays.equals(hardwareFirmwareId, CADENCE_BUG_FW_ID)) { + if (strideReadings == null) { strideReadings = new StrideReadings(); } - strideReadings.updateStrideReading( - SensorUtils.unsignedShortToIntLittleEndian(buffer, 14), - buffer[54] & 0xFF); + strideReadings.updateStrideReading(buffer[54] & 0xFF); - if(strideReadings.getCadence() != StrideReadings.CADENCE_NOT_AVAILABLE) { - cadence = cadence.setValue(strideReadings.getCadence()) - .setState(Sensor.SensorState.SENDING); - sds.setCadence(cadence); + if (strideReadings.getCadence() != StrideReadings.CADENCE_NOT_AVAILABLE) { + cadence.setValue(strideReadings.getCadence()).setState(Sensor.SensorState.SENDING); } + } else { + cadence + .setValue(SensorUtils.unsignedShortToIntLittleEndian(buffer, 56) / 16) + .setState(Sensor.SensorState.SENDING); } + sds.setCadence(cadence); } @Override diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/services/sensors/StrideReadingsTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/services/sensors/StrideReadingsTest.java index 75fbf035d..23a0276b4 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/services/sensors/StrideReadingsTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/services/sensors/StrideReadingsTest.java @@ -1,29 +1,63 @@ +/* + * 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.services.sensors; import junit.framework.TestCase; +/** + * @author Dominik Ršttsches + */ public class StrideReadingsTest extends TestCase { public void testNoReadingOnStartup() { StrideReadings strideReadings = new StrideReadings(); - assertTrue(strideReadings.getCadence() == StrideReadings.CADENCE_NOT_AVAILABLE); + assertEquals(StrideReadings.CADENCE_NOT_AVAILABLE, strideReadings.getCadence()); } public void testAverageCadenceAvailable() { StrideReadings strideReadings = new StrideReadings(); - for(int i=0;i<30;i++) { - strideReadings.updateStrideReading(i*1000, i*2); + // 2 steps / second => Cadence is 120 / minute + for (int i = 1; i <= 30; i++) { + strideReadings.updateStrideReading(i*2); + if (i >= StrideReadings.MIN_READINGS_FOR_AVERAGE) { + assertEquals(120, strideReadings.getCadence()); + } else { + assertEquals(StrideReadings.CADENCE_NOT_AVAILABLE, strideReadings.getCadence()); + } } - assertTrue(strideReadings.getCadence() > 0); } - /* testing rollover at 127, just like the HxM seems to do it */ + /** + * Tests for correct calculation after rolling over at 128 strides, + * just like the HxM seems to do it. + */ public void testRollover() { StrideReadings strideReadings = new StrideReadings(); - for(int i=0;i<=10;i++) { - strideReadings.updateStrideReading(i*1000, (120 + i*2) ); - } - assertTrue(strideReadings.getCadence() > 0); - } + // 1 step per second => Cadence is 60 / minute + // Updating readings counting upwards from initialStrides - + // initialStrides set to a value below 128 to ensure rollover. + int initialStrides = 128 - StrideReadings.NUM_READINGS_FOR_AVERAGE - 5; + for (int i = 1; i <= StrideReadings.NUM_READINGS_FOR_AVERAGE + 10; i++) { + strideReadings.updateStrideReading((initialStrides + i) % 128); + if (i >= StrideReadings.MIN_READINGS_FOR_AVERAGE) { + assertEquals(60, strideReadings.getCadence()); + } else { + assertEquals(StrideReadings.CADENCE_NOT_AVAILABLE, strideReadings.getCadence()); + } + } + } }