diff --git a/MyTracks/AndroidManifest.xml b/MyTracks/AndroidManifest.xml
index 015c9746e..0a83934c5 100755
--- a/MyTracks/AndroidManifest.xml
+++ b/MyTracks/AndroidManifest.xml
@@ -45,7 +45,8 @@ limitations under the License.
-
+
+
diff --git a/MyTracks/res/menu/track_list.xml b/MyTracks/res/menu/track_list.xml
index b93d1a72f..3d5aae419 100644
--- a/MyTracks/res/menu/track_list.xml
+++ b/MyTracks/res/menu/track_list.xml
@@ -28,17 +28,34 @@ limitations under the License.
android:title="@string/menu_start_gps"/>
+ android:id="@+id/track_list_import_all"
+ android:title="@string/menu_import_all">
+
+
-
-
-
-
- @string/import_gpx_files_zero
- - @string/import_gpx_files_one
- - @string/import_gpx_files_two
- - @string/import_gpx_files_few
- - @string/import_gpx_files_many
- - @string/import_gpx_files_other
+
+ - @string/import_files_zero
+ - @string/import_files_one
+ - @string/import_files_two
+ - @string/import_files_few
+ - @string/import_files_many
+ - @string/import_files_other
- @string/voice_hours_zero
diff --git a/MyTracks/res/values/strings.xml b/MyTracks/res/values/strings.xml
index a4dba9ad1..16f7a9910 100644
--- a/MyTracks/res/values/strings.xml
+++ b/MyTracks/res/values/strings.xml
@@ -604,39 +604,13 @@ limitations under the License.
that the operation was canceled.">
The operation was canceled.
-
- No external storage is found.
+
+ External storage not available.
-
- Unable to import from external storage.
-
-
- No file found in %1$s.
-
-
- Importing from external storage…
-
-
- Finished importing from external storage.
-
-
- Imported %1$d of %2$s from %3$s.
+
+ External storage not writeable.
- %1$d GPX files
+ %1$d files
-
- %1$d GPX files
+ %1$d files
-
- 1 GPX file
+
+ 1 file
-
- %1$d GPX files
+ %1$d files
-
- 2 GPX files
+
+ 2 files
-
- 0 GPX files
+
+ 0 files
+
+
+ %1$s does not exist.
+
+
+ No file found in %1$s.
+
+
+ Importing from %1$s…
+
+
+ Imported %1$d of %2$s from %3$s.
@@ -1149,11 +1162,19 @@ limitations under the License.
Character limit: ~20 (1 line)">
Help
-
- Import from external storage
+ Import all from external storage
+
+
+ Import %1$s
+ Unable to restore from backup.
+
+
+ Finished restoring from backup.
+
+
{
@Override
protected Boolean doInBackground(Void... params) {
- if (!FileUtils.isSdCardAvailable()) {
- messageId = R.string.external_storage_error_no_storage;
+ if (!FileUtils.isExternalStorageWriteable()) {
+ messageId = R.string.external_storage_not_writeable;
return false;
}
diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/backup/ExternalFileBackup.java b/MyTracks/src/com/google/android/apps/mytracks/io/backup/ExternalFileBackup.java
index 82fd11e04..6993c85a7 100644
--- a/MyTracks/src/com/google/android/apps/mytracks/io/backup/ExternalFileBackup.java
+++ b/MyTracks/src/com/google/android/apps/mytracks/io/backup/ExternalFileBackup.java
@@ -93,7 +93,7 @@ class ExternalFileBackup {
return FileUtils.ensureDirectoryExists(dir) ? dir : null;
} else {
// Return it if it already exists, otherwise return null
- return dir.isDirectory() ? dir : null;
+ return FileUtils.isDirectory(dir) ? dir : null;
}
}
diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/backup/RestoreAsyncTask.java b/MyTracks/src/com/google/android/apps/mytracks/io/backup/RestoreAsyncTask.java
index cf4e008dd..665f0e48a 100644
--- a/MyTracks/src/com/google/android/apps/mytracks/io/backup/RestoreAsyncTask.java
+++ b/MyTracks/src/com/google/android/apps/mytracks/io/backup/RestoreAsyncTask.java
@@ -59,7 +59,7 @@ public class RestoreAsyncTask extends AsyncTask {
this.externalFileBackup = new ExternalFileBackup(restoreActivity);
success = false;
completed = false;
- messageId = R.string.external_storage_import_error;
+ messageId = R.string.restore_error;
}
/**
@@ -85,7 +85,7 @@ public class RestoreAsyncTask extends AsyncTask {
protected Boolean doInBackground(Void... params) {
try {
externalFileBackup.restoreFromDate(date);
- messageId = R.string.external_storage_import_success;
+ messageId = R.string.restore_success;
return true;
} catch (IOException e) {
Log.d(TAG, "IO exception", e);
diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/backup/RestoreChooserActivity.java b/MyTracks/src/com/google/android/apps/mytracks/io/backup/RestoreChooserActivity.java
index 0a5955c15..632c8e85b 100644
--- a/MyTracks/src/com/google/android/apps/mytracks/io/backup/RestoreChooserActivity.java
+++ b/MyTracks/src/com/google/android/apps/mytracks/io/backup/RestoreChooserActivity.java
@@ -57,8 +57,8 @@ public class RestoreChooserActivity extends Activity {
ExternalFileBackup externalFileBackup = new ExternalFileBackup(this);
// Get the list of existing backups
- if (!FileUtils.isSdCardAvailable()) {
- Toast.makeText(this, R.string.external_storage_error_no_storage, Toast.LENGTH_LONG).show();
+ if (!FileUtils.isExternalStorageAvailable()) {
+ Toast.makeText(this, R.string.external_storage_not_available, Toast.LENGTH_LONG).show();
finish();
return;
}
diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/file/AbstractImporter.java b/MyTracks/src/com/google/android/apps/mytracks/io/file/AbstractImporter.java
new file mode 100644
index 000000000..2de8d6d13
--- /dev/null
+++ b/MyTracks/src/com/google/android/apps/mytracks/io/file/AbstractImporter.java
@@ -0,0 +1,571 @@
+/*
+ * Copyright 2012 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.file;
+
+import com.google.android.apps.mytracks.Constants;
+import com.google.android.apps.mytracks.content.DescriptionGeneratorImpl;
+import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
+import com.google.android.apps.mytracks.content.Track;
+import com.google.android.apps.mytracks.content.Waypoint;
+import com.google.android.apps.mytracks.content.Waypoint.WaypointType;
+import com.google.android.apps.mytracks.services.TrackRecordingService;
+import com.google.android.apps.mytracks.stats.TripStatistics;
+import com.google.android.apps.mytracks.stats.TripStatisticsUpdater;
+import com.google.android.apps.mytracks.util.LocationUtils;
+import com.google.android.apps.mytracks.util.PreferencesUtils;
+import com.google.android.apps.mytracks.util.StringUtils;
+import com.google.android.apps.mytracks.util.TrackIconUtils;
+import com.google.android.maps.mytracks.R;
+import com.google.common.annotations.VisibleForTesting;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.location.Location;
+import android.location.LocationManager;
+import android.net.Uri;
+import android.util.Log;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+
+import javax.xml.parsers.SAXParser;
+import javax.xml.parsers.SAXParserFactory;
+
+import org.xml.sax.Locator;
+import org.xml.sax.SAXException;
+import org.xml.sax.helpers.DefaultHandler;
+
+/**
+ * Abstract class for various importers like {@link GpxImporter} and
+ * {@link KmlImporter}.
+ *
+ * @author Jimmy Shih
+ */
+abstract class AbstractImporter extends DefaultHandler {
+
+ /**
+ * Data for the current track.
+ *
+ * @author Jimmy Shih
+ */
+ private class TrackData {
+
+ // The current track
+ Track track = new Track();
+
+ // The number of segments processed for the current track
+ int numberOfSegments = 0;
+
+ /*
+ * The last location in the current segment. Null if the current segment
+ * doesn't have a last location.
+ */
+ Location lastLocationInCurrentSegment;
+
+ // The number of locations processed for the current track
+ int numberOfLocations = 0;
+
+ // The trip statistics updater for the current track
+ TripStatisticsUpdater tripStatisticsUpdater;
+
+ // The import time of the track.
+ long importTime = System.currentTimeMillis();
+
+ // The buffered locations
+ Location[] bufferedLocations = new Location[MAX_BUFFERED_LOCATIONS];
+
+ // The number of buffered locations
+ int numBufferedLocations = 0;
+ }
+
+ protected static final String DEFAULT_ALTITUDE = "0";
+
+ // The maximum number of buffered locations for bulk-insertion
+ private static final int MAX_BUFFERED_LOCATIONS = 512;
+
+ private final Context context;
+ private final MyTracksProviderUtils myTracksProviderUtils;
+ private final int minRecordingDistance;
+ private final List trackIds;
+ private final List waypoints;
+
+ // The current track data
+ private TrackData trackData;
+
+ // The SAX locator to get the current line information
+ private Locator locator;
+
+ // The current element content
+ protected String content;
+
+ protected String name;
+ protected String description;
+ protected String category;
+ protected String latitude;
+ protected String longitude;
+ protected String altitude;
+ protected String time;
+ protected String waypointType;
+
+ protected AbstractImporter(Context context) {
+ this(context, MyTracksProviderUtils.Factory.get(context));
+ }
+
+ @VisibleForTesting
+ protected AbstractImporter(
+ Context context, MyTracksProviderUtils myTracksProviderUtils) {
+ this.context = context;
+ this.myTracksProviderUtils = myTracksProviderUtils;
+ this.minRecordingDistance = PreferencesUtils.getInt(context, R.string.min_recording_distance_key,
+ PreferencesUtils.MIN_RECORDING_DISTANCE_DEFAULT);
+ trackIds = new ArrayList();
+ waypoints = new ArrayList();
+ }
+
+ @Override
+ public void setDocumentLocator(Locator locator) {
+ this.locator = locator;
+ }
+
+ @Override
+ public void characters(char[] ch, int start, int length) throws SAXException {
+ String newContent = new String(ch, start, length);
+ if (content == null) {
+ content = newContent;
+ } else {
+ /*
+ * In 99% of the cases, a single call to this method will be made for each
+ * sequence of characters we're interested in, so we'll rarely be
+ * concatenating strings, thus not justifying the use of a StringBuilder.
+ */
+ content += newContent;
+ }
+ }
+
+ /**
+ * Imports a file.
+ *
+ * @param inputStream the input stream.
+ * @return an array of imported track ids.
+ */
+ public long[] importFile(InputStream inputStream) throws Exception {
+ SAXParserFactory saxParserFactory = SAXParserFactory.newInstance();
+ SAXParser saxParser = saxParserFactory.newSAXParser();
+
+ try {
+ long start = System.currentTimeMillis();
+
+ saxParser.parse(inputStream, this);
+
+ long end = System.currentTimeMillis();
+ Log.d(Constants.TAG, "Total import time: " + (end - start) + "ms");
+ } catch (SAXException e) {
+ handleException(e);
+ } catch (IOException e) {
+ handleException(e);
+ }
+ long[] result = new long[trackIds.size()];
+ for (int i = 0; i < result.length; i++) {
+ result[i] = trackIds.get(i);
+ }
+ return result;
+ }
+
+ /**
+ * On file end.
+ */
+ protected void onFileEnd() {
+ // Add waypoints to the last imported track
+ int size = trackIds.size();
+ if (size == 0) {
+ return;
+ }
+ long trackId = trackIds.get(size - 1);
+ Track track = myTracksProviderUtils.getTrack(trackId);
+ if (track == null) {
+ return;
+ }
+ Cursor trackPointCursor = null;
+ try {
+ trackPointCursor = myTracksProviderUtils.getTrackPointCursor(track.getId(), -1L, -1, false);
+ if (trackPointCursor == null) {
+ return;
+ }
+ int waypointPosition = -1;
+ Waypoint waypoint = null;
+ int trackPointPosition = -1;
+ Location trackPoint = null;
+ TripStatisticsUpdater trackTripStatisticstrackUpdater = new TripStatisticsUpdater(
+ track.getTripStatistics().getStartTime());
+ TripStatisticsUpdater markerTripStatisticsUpdater = new TripStatisticsUpdater(
+ track.getTripStatistics().getStartTime());
+
+ while (true) {
+ if (waypoint == null) {
+ waypointPosition++;
+ waypoint = waypointPosition < waypoints.size() ? waypoints.get(waypointPosition) : null;
+ if (waypoint == null) {
+ // No more waypoints
+ return;
+ }
+ }
+ if (trackPoint == null) {
+ trackPointPosition++;
+ trackPoint = trackPointCursor.moveToPosition(trackPointPosition) ? myTracksProviderUtils
+ .createTrackPoint(trackPointCursor)
+ : null;
+ if (trackPoint == null) {
+ // No more track points. Ignore the rest of the waypoints.
+ return;
+ }
+ trackTripStatisticstrackUpdater.addLocation(trackPoint, minRecordingDistance);
+ markerTripStatisticsUpdater.addLocation(trackPoint, minRecordingDistance);
+ }
+ if (waypoint.getLocation().getTime() > trackPoint.getTime()) {
+ trackPoint = null;
+ } else if (waypoint.getLocation().getTime() < trackPoint.getTime()) {
+ waypoint = null;
+ } else {
+ // The waypoint location time matches the track point time
+ if (trackPoint.getLatitude() == waypoint.getLocation().getLatitude()
+ && trackPoint.getLongitude() == waypoint.getLocation().getLongitude()) {
+
+ // Get tripStatistics, description, and icon
+ TripStatistics tripStatistics;
+ String waypointDescription;
+ String icon;
+ if (waypoint.getType() == WaypointType.STATISTICS) {
+ tripStatistics = markerTripStatisticsUpdater.getTripStatistics();
+ markerTripStatisticsUpdater = new TripStatisticsUpdater(trackPoint.getTime());
+ waypointDescription = new DescriptionGeneratorImpl(context)
+ .generateWaypointDescription(tripStatistics);
+ icon = context.getString(R.string.marker_statistics_icon_url);
+ } else {
+ tripStatistics = null;
+ waypointDescription = waypoint.getDescription();
+ icon = context.getString(R.string.marker_waypoint_icon_url);
+ }
+
+ // Get length and duration
+ double length = trackTripStatisticstrackUpdater.getTripStatistics().getTotalDistance();
+ long duration = trackTripStatisticstrackUpdater.getTripStatistics().getTotalTime();
+
+ // Insert waypoint
+ Waypoint newWaypoint = new Waypoint(waypoint.getName(), waypointDescription,
+ waypoint.getCategory(), icon, track.getId(), waypoint.getType(), length, duration,
+ -1L, -1L, trackPoint, tripStatistics);
+ myTracksProviderUtils.insertWaypoint(newWaypoint);
+ }
+ waypoint = null;
+ }
+ }
+ } finally {
+ if (trackPointCursor != null) {
+ trackPointCursor.close();
+ }
+ }
+ }
+
+ /**
+ * On track start.
+ */
+ protected void onTrackStart() {
+ trackData = new TrackData();
+ Uri uri = myTracksProviderUtils.insertTrack(trackData.track);
+ long id = Long.parseLong(uri.getLastPathSegment());
+ trackIds.add(id);
+ trackData.track.setId(id);
+ }
+
+ /**
+ * On track end.
+ */
+ protected void onTrackEnd() {
+ flushLocations(trackData);
+ if (name != null) {
+ trackData.track.setName(name);
+ }
+ if (description != null) {
+ trackData.track.setDescription(description);
+ }
+ if (category != null) {
+ trackData.track.setCategory(category);
+ trackData.track.setIcon(TrackIconUtils.getIconValue(context, category));
+ }
+ if (trackData.tripStatisticsUpdater == null) {
+ trackData.tripStatisticsUpdater = new TripStatisticsUpdater(trackData.importTime);
+ trackData.tripStatisticsUpdater.updateTime(trackData.importTime);
+ }
+ trackData.track.setTripStatistics(trackData.tripStatisticsUpdater.getTripStatistics());
+ trackData.track.setNumberOfPoints(trackData.numberOfLocations);
+ myTracksProviderUtils.updateTrack(trackData.track);
+ insertFirstWaypoint(trackData.track);
+ }
+
+ /**
+ * On track segment start.
+ */
+ protected void onTrackSegmentStart() {
+ trackData.numberOfSegments++;
+
+ /*
+ * If not the first segment, add a pause separator if there is at least one
+ * location in the last segment.
+ */
+ if (trackData.numberOfSegments > 1 && trackData.lastLocationInCurrentSegment != null) {
+ insertLocation(createLocation(TrackRecordingService.PAUSE_LATITUDE, 0.0, 0.0,
+ trackData.lastLocationInCurrentSegment.getTime()));
+ }
+ trackData.lastLocationInCurrentSegment = null;
+ }
+
+ /**
+ * Adds a waypoint.
+ *
+ * @param type the waypoint type
+ */
+ protected void addWaypoint(WaypointType type) throws SAXException {
+ // Waypoint must have a time, else cannot match to the track points
+ if (time == null) {
+ return;
+ }
+
+ Waypoint waypoint = new Waypoint();
+ Location location = createLocation();
+
+ if (!LocationUtils.isValidLocation(location)) {
+ throw new SAXException(createErrorMessage("Invalid location detected: " + location));
+ }
+ waypoint.setLocation(location);
+
+ if (name != null) {
+ waypoint.setName(name);
+ }
+ if (description != null) {
+ waypoint.setDescription(description);
+ }
+ if (category != null) {
+ waypoint.setCategory(category);
+ }
+ waypoint.setType(type);
+ waypoints.add(waypoint);
+ }
+
+ /**
+ * Gets a track point.
+ */
+ protected Location getTrackPoint() throws SAXException {
+ Location location = createLocation();
+
+ // Calculate derived attributes from the previous point
+ if (trackData.lastLocationInCurrentSegment != null
+ && trackData.lastLocationInCurrentSegment.getTime() != 0) {
+ long timeDifference = location.getTime() - trackData.lastLocationInCurrentSegment.getTime();
+
+ // Check for negative time change
+ if (timeDifference <= 0) {
+ Log.w(Constants.TAG, "Time difference not postive.");
+ } else {
+
+ /*
+ * We don't have a speed and bearing in GPX, make something up from the
+ * last two points. GPS points tend to have some inherent imprecision,
+ * speed and bearing will likely be off, so the statistics for things
+ * like max speed will also be off.
+ */
+ float speed = trackData.lastLocationInCurrentSegment.distanceTo(location) * 1000.0f
+ / timeDifference;
+ location.setSpeed(speed);
+ }
+ location.setBearing(trackData.lastLocationInCurrentSegment.bearingTo(location));
+ }
+
+ if (!LocationUtils.isValidLocation(location)) {
+ throw new SAXException(createErrorMessage("Invalid location detected: " + location));
+ }
+
+ if (trackData.numberOfSegments > 1 && trackData.lastLocationInCurrentSegment == null) {
+ /*
+ * If not the first segment, add a resume separator before adding the
+ * first location.
+ */
+ insertLocation(
+ createLocation(TrackRecordingService.RESUME_LATITUDE, 0.0, 0.0, location.getTime()));
+ }
+ trackData.lastLocationInCurrentSegment = location;
+ return location;
+ }
+
+ /**
+ * Inserts a track point.
+ *
+ * @param location the location
+ */
+ protected void insertTrackPoint(Location location) {
+ insertLocation(location);
+
+ if (trackData.track.getStartId() == -1L) {
+ // Flush the location to set the track start id and the track end id
+ flushLocations(trackData);
+ }
+ }
+
+ /**
+ * Creates an error message.
+ *
+ * @param message the message
+ */
+ protected String createErrorMessage(String message) {
+ return String.format(Locale.US, "Parsing error at line: %d column: %d. %s",
+ locator.getLineNumber(), locator.getColumnNumber(), message);
+ }
+
+ /**
+ * Creates a location.
+ */
+ private Location createLocation() throws SAXException {
+ if (latitude == null || longitude == null || altitude == null) {
+ return null;
+ }
+ double longitudeValue;
+ double latitudeValue;
+ double altitudeValue;
+
+ try {
+ longitudeValue = Double.parseDouble(longitude);
+ latitudeValue = Double.parseDouble(latitude);
+ altitudeValue = Double.parseDouble(altitude);
+ } catch (NumberFormatException e) {
+ throw new SAXException(createErrorMessage(String.format(
+ "Unable to parse longitude latitude altitude: %s %s %s", longitude, latitude, altitude)),
+ e);
+ }
+ long timeValue;
+
+ if (time == null) {
+ timeValue = trackData.importTime;
+ } else {
+ try {
+ timeValue = StringUtils.getTime(time);
+ } catch (IllegalArgumentException e) {
+ throw new SAXException(
+ createErrorMessage(String.format("Unable to parse time: %s", time)), e);
+ }
+ }
+ return createLocation(latitudeValue, longitudeValue, altitudeValue, timeValue);
+ }
+
+ /**
+ * Creates a location.
+ *
+ * @param latitudeValue the latitude value
+ * @param longitudeValue the longitude value
+ * @param altitudeValue the altitude value
+ * @param timeValue the time value
+ */
+ private Location createLocation(
+ double latitudeValue, double longitudeValue, double altitudeValue, long timeValue) {
+ Location location = new Location(LocationManager.GPS_PROVIDER);
+ location.setLatitude(latitudeValue);
+ location.setLongitude(longitudeValue);
+ location.setAltitude(altitudeValue);
+ location.setTime(timeValue);
+ location.removeAccuracy();
+ location.removeBearing();
+ location.removeSpeed();
+ return location;
+ }
+
+ /**
+ * Inserts a location.
+ *
+ * @param location the location
+ */
+ private void insertLocation(Location location) {
+ if (trackData.tripStatisticsUpdater == null) {
+ trackData.tripStatisticsUpdater = new TripStatisticsUpdater(
+ location.getTime() != -1L ? location.getTime() : trackData.importTime);
+ }
+ trackData.tripStatisticsUpdater.addLocation(location, minRecordingDistance);
+
+ trackData.bufferedLocations[trackData.numBufferedLocations] = location;
+ trackData.numBufferedLocations++;
+ trackData.numberOfLocations++;
+
+ if (trackData.numBufferedLocations >= MAX_BUFFERED_LOCATIONS) {
+ flushLocations(trackData);
+ }
+ }
+
+ /**
+ * Flushes the locations to the database.
+ *
+ * @param data the track data
+ */
+ private void flushLocations(TrackData data) {
+ if (data.numBufferedLocations <= 0) {
+ return;
+ }
+ myTracksProviderUtils.bulkInsertTrackPoint(
+ data.bufferedLocations, data.numBufferedLocations, data.track.getId());
+ data.numBufferedLocations = 0;
+ if (data.track.getStartId() == -1L) {
+ data.track.setStartId(myTracksProviderUtils.getFirstTrackPointId(data.track.getId()));
+ }
+ data.track.setStopId(myTracksProviderUtils.getLastTrackPointId(data.track.getId()));
+ }
+
+ /**
+ * Inserts the first waypoint, the track statistics waypoint.
+ *
+ * @param track the track
+ */
+ private void insertFirstWaypoint(Track track) {
+ String waypointName = context.getString(R.string.marker_split_name_format, 0);
+ String waypointCategory = "";
+ TripStatisticsUpdater updater = new TripStatisticsUpdater(
+ track.getTripStatistics().getStartTime());
+ TripStatistics tripStatistics = updater.getTripStatistics();
+ String waypointDescription = new DescriptionGeneratorImpl(context).generateWaypointDescription(
+ tripStatistics);
+ String icon = context.getString(R.string.marker_statistics_icon_url);
+ double length = 0.0;
+ long duration = 0L;
+ Location waypointLocation = new Location("");
+ waypointLocation.setLatitude(100);
+ waypointLocation.setLongitude(180);
+ Waypoint waypoint = new Waypoint(waypointName, waypointDescription, waypointCategory, icon,
+ track.getId(), WaypointType.STATISTICS, length, duration, -1L, -1L, waypointLocation,
+ tripStatistics);
+ myTracksProviderUtils.insertWaypoint(waypoint);
+ }
+
+ /**
+ * Handles an exception.
+ *
+ * @param e the exception
+ */
+ private void handleException(Exception e) throws Exception {
+ for (long trackId : trackIds) {
+ myTracksProviderUtils.deleteTrack(trackId);
+ }
+ throw e;
+ }
+}
diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/file/GpxImporter.java b/MyTracks/src/com/google/android/apps/mytracks/io/file/GpxImporter.java
index 0888e9041..c9eee1249 100644
--- a/MyTracks/src/com/google/android/apps/mytracks/io/file/GpxImporter.java
+++ b/MyTracks/src/com/google/android/apps/mytracks/io/file/GpxImporter.java
@@ -16,93 +16,26 @@
package com.google.android.apps.mytracks.io.file;
-import com.google.android.apps.mytracks.Constants;
-import com.google.android.apps.mytracks.content.DescriptionGeneratorImpl;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
-import com.google.android.apps.mytracks.content.Track;
-import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.content.Waypoint.WaypointType;
-import com.google.android.apps.mytracks.services.TrackRecordingService;
-import com.google.android.apps.mytracks.stats.TripStatistics;
-import com.google.android.apps.mytracks.stats.TripStatisticsUpdater;
-import com.google.android.apps.mytracks.util.LocationUtils;
-import com.google.android.apps.mytracks.util.StringUtils;
-import com.google.android.apps.mytracks.util.TrackIconUtils;
-import com.google.android.maps.mytracks.R;
+import com.google.common.annotations.VisibleForTesting;
import android.content.Context;
-import android.database.Cursor;
import android.location.Location;
-import android.location.LocationManager;
-import android.net.Uri;
-import android.util.Log;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Locale;
-
-import javax.xml.parsers.ParserConfigurationException;
-import javax.xml.parsers.SAXParser;
-import javax.xml.parsers.SAXParserFactory;
import org.xml.sax.Attributes;
-import org.xml.sax.Locator;
import org.xml.sax.SAXException;
-import org.xml.sax.helpers.DefaultHandler;
/**
- * Imports GPX file to My Tracks.
+ * Imports a GPX file.
*
- * @author Leif Hendrik Wilden
- * @author Steffen Horlacher
- * @author Rodrigo Damazio
+ * @author Jimmy Shih
*/
-public class GpxImporter extends DefaultHandler {
+public class GpxImporter extends AbstractImporter {
- /**
- * Data for the current track.
- *
- * @author Jimmy Shih
- */
- private class TrackData {
-
- // The current track
- Track track = new Track();
-
- // The parsed depth for the current track
- int parsedDepth = 0;
-
- // The number of segments processed for the current track
- int numberOfSegments = 0;
-
- /*
- * The last location in the current segment. Null if the current segment
- * doesn't have a last location.
- */
- Location lastLocationInCurrentSegment;
-
- // The number of locations processed for the current track
- int numberOfLocations = 0;
-
- // The trip statistics updater for the current track
- TripStatisticsUpdater tripStatisticsUpdater;
-
- // The default location time for the current track. -1L to not use it.
- long defaultLocationTime = -1L;
-
- // The buffered locations
- Location[] bufferedLocations = new Location[MAX_BUFFERED_LOCATIONS];
-
- // The number of buffered locations
- int numBufferedLocations = 0;
- }
-
- // GPX tags
- private static final String TAG_ALTITUDE = "ele";
private static final String TAG_DESCRIPTION = "desc";
private static final String TAG_COMMENT = "cmt";
+ private static final String TAG_ELEVATION = "ele";
private static final String TAG_GPX = "gpx";
private static final String TAG_NAME = "name";
private static final String TAG_TIME = "time";
@@ -112,150 +45,65 @@ public class GpxImporter extends DefaultHandler {
private static final String TAG_TYPE = "type";
private static final String TAG_WAYPOINT = "wpt";
- // GPX attributes
- private static final String ATT_LAT = "lat";
- private static final String ATT_LON = "lon";
+ private static final String ATTRIBUTE_LAT = "lat";
+ private static final String ATTRIBUTE_LON = "lon";
- // The maximum number of buffered locations for bulk-insertion
- private static final int MAX_BUFFERED_LOCATIONS = 512;
-
- private final Context context;
- private final MyTracksProviderUtils myTracksProviderUtils;
- private final int minRecordingDistance;
- private final List tracksIds;
-
- // The SAX locator to get the current line information
- private Locator locator;
-
- // The current element content
- private String content;
-
- // The current track data
- private TrackData trackData = null;
-
- // The current waypoint
- private Waypoint currentWaypoint;
-
- // The list of waypoints
- private List waypoints = new ArrayList();
-
- /*
- * The current location, used for both track point location and waypoint
- * location.
- */
- private Location location;
-
- /**
- * Reads GPS tracks from a GPX file and writes tracks, waypoints, and track
- * points to the database.
- *
- * @param context the context
- * @param inputStream the input stream for the GPX file
- * @param myTracksProviderUtils my tracks provider utils
- * @param minRecordingDistance the min recording distance
- * @return long[] array of track ids written to the database.
- */
- public static long[] importGPXFile(Context context, InputStream inputStream,
- MyTracksProviderUtils myTracksProviderUtils, int minRecordingDistance)
- throws ParserConfigurationException, SAXException, IOException {
- SAXParserFactory saxParserFactory = SAXParserFactory.newInstance();
- SAXParser saxParser = saxParserFactory.newSAXParser();
- GpxImporter gpxImporter = new GpxImporter(context, myTracksProviderUtils, minRecordingDistance);
- long[] trackIds = new long[0];
-
- try {
- long start = System.currentTimeMillis();
-
- saxParser.parse(inputStream, gpxImporter);
-
- long end = System.currentTimeMillis();
- Log.d(Constants.TAG, "Total import time: " + (end - start) + "ms");
-
- trackIds = gpxImporter.getImportedTrackIds();
- if (trackIds.length == 0) {
- throw new IOException("No track imported.");
- }
- } finally {
- // Delete the current track if not finished
- gpxImporter.rollbackUnfinishedTracks();
- }
- return trackIds;
+ public GpxImporter(Context context) {
+ super(context);
}
- public GpxImporter(
- Context context, MyTracksProviderUtils myTracksProviderUtils, int minRecordingDistance) {
- this.context = context;
- this.myTracksProviderUtils = myTracksProviderUtils;
- this.minRecordingDistance = minRecordingDistance;
- this.tracksIds = new ArrayList();
+ @VisibleForTesting
+ public GpxImporter(Context context, MyTracksProviderUtils myTracksProviderUtils) {
+ super(context, myTracksProviderUtils);
}
@Override
- public void characters(char[] ch, int start, int length) throws SAXException {
- String newContent = new String(ch, start, length);
- if (content == null) {
- content = newContent;
- } else {
- /*
- * In 99% of the cases, a single call to this method will be made for each
- * sequence of characters we're interested in, so we'll rarely be
- * concatenating strings, thus not justifying the use of a StringBuilder.
- */
- content += newContent;
- }
- }
-
- @Override
- public void startElement(String uri, String localName, String name, Attributes attributes)
+ public void startElement(String uri, String localName, String tag, Attributes attributes)
throws SAXException {
- if (trackData != null) {
- trackData.parsedDepth++;
- if (localName.equals(TAG_TRACK)) {
- throw new SAXException(createErrorMessage("Invalid GPX. Already inside a track."));
- } else if (localName.equals(TAG_TRACK_SEGMENT)) {
- onTrackSegmentElementStart();
- } else if (localName.equals(TAG_TRACK_POINT)) {
- onTrackPointElementStart(attributes);
- }
- } else {
- if (localName.equals(TAG_TRACK)) {
- onTrackElementStart();
- } else if (localName.equals(TAG_WAYPOINT)) {
- onWaypointElementStart(attributes);
- }
+ if (tag.equals(TAG_WAYPOINT)) {
+ onWaypointStart(attributes);
+ } else if (tag.equals(TAG_TRACK)) {
+ onTrackStart();
+ } else if (tag.equals(TAG_TRACK_SEGMENT)) {
+ onTrackSegmentStart();
+ } else if (tag.equals(TAG_TRACK_POINT)) {
+ onTrackPointStart(attributes);
}
}
@Override
- public void endElement(String uri, String localName, String name) throws SAXException {
- if (localName.equals(TAG_NAME)) {
- onNameElementEnd();
- } else if (localName.equals(TAG_DESCRIPTION)) {
- onDescriptionElementEnd();
- } else if (localName.equals(TAG_TYPE)) {
- onTypeElementEnd();
- } else if (localName.equals(TAG_ALTITUDE)) {
- onAltitudeElementEnd();
- } else if (localName.equals(TAG_TIME)) {
- onTimeElementEnd();
- } else if (localName.equals(TAG_GPX)) {
- onGpxElementEnd();
- }
-
- if (trackData != null) {
- trackData.parsedDepth--;
- if (localName.equals(TAG_TRACK)) {
- onTrackElementEnd();
- } else if (localName.equals(TAG_TRACK_SEGMENT)) {
- onTrackSegmentElementEnd();
- } else if (localName.equals(TAG_TRACK_POINT)) {
- onTrackPointElementEnd();
+ public void endElement(String uri, String localName, String tag) throws SAXException {
+ if (tag.equals(TAG_GPX)) {
+ onFileEnd();
+ } else if (tag.equals(TAG_WAYPOINT)) {
+ onWaypointEnd();
+ } else if (tag.equals(TAG_TRACK)) {
+ onTrackEnd();
+ } else if (tag.equals(TAG_TRACK_POINT)) {
+ onTrackPointEnd();
+ } else if (tag.equals(TAG_NAME)) {
+ if (content != null) {
+ name = content.trim();
}
- } else {
- if (localName.equals(TAG_WAYPOINT)) {
- onWaypointElementEnd();
- } else if (localName.equals(TAG_COMMENT)) {
- onCommentElementEnd();
+ } else if (tag.equals(TAG_DESCRIPTION)) {
+ if (content != null) {
+ description = content.trim();
+ }
+ } else if (tag.equals(TAG_TYPE)) {
+ if (content != null) {
+ category = content.trim();
+ }
+ } else if (tag.equals(TAG_TIME)) {
+ if (content != null) {
+ time = content.trim();
+ }
+ } else if (tag.equals(TAG_ELEVATION)) {
+ if (content != null) {
+ altitude = content.trim();
+ }
+ } else if (tag.equals(TAG_COMMENT)) {
+ if (content != null) {
+ waypointType = content.trim();
}
}
@@ -264,486 +112,57 @@ public class GpxImporter extends DefaultHandler {
}
@Override
- public void setDocumentLocator(Locator locator) {
- this.locator = locator;
+ protected void onTrackStart() {
+ super.onTrackStart();
+ name = null;
+ description = null;
+ category = null;
}
/**
- * Rolls back last track if possible.
- */
- public void rollbackUnfinishedTracks() {
- if (trackData != null) {
- myTracksProviderUtils.deleteTrack(trackData.track.getId());
- trackData = null;
- }
- }
-
- /**
- * On gpx element end.
- */
- private void onGpxElementEnd() {
- int size = tracksIds.size();
- if (size > 0) {
- long trackId = tracksIds.get(size - 1);
- Track track = myTracksProviderUtils.getTrack(trackId);
- if (track != null) {
- addWaypoints(track);
- }
- }
- }
-
- /**
- * On track element start.
- */
- private void onTrackElementStart() {
- trackData = new TrackData();
- Uri uri = myTracksProviderUtils.insertTrack(trackData.track);
- trackData.track.setId(Long.parseLong(uri.getLastPathSegment()));
- }
-
- /**
- * On track element end.
- */
- private void onTrackElementEnd() {
- flushPoints(trackData);
- if (trackData.tripStatisticsUpdater == null) {
- long now = System.currentTimeMillis();
- trackData.tripStatisticsUpdater = new TripStatisticsUpdater(now);
- trackData.tripStatisticsUpdater.updateTime(now);
- }
- trackData.track.setTripStatistics(trackData.tripStatisticsUpdater.getTripStatistics());
- trackData.track.setNumberOfPoints(trackData.numberOfLocations);
- myTracksProviderUtils.updateTrack(trackData.track);
- tracksIds.add(trackData.track.getId());
- insertFirstWaypoint(trackData.track);
- trackData = null;
- }
-
- /**
- * On track segment start.
- */
- private void onTrackSegmentElementStart() {
- trackData.numberOfSegments++;
-
- /*
- * If not the first segment, add a pause separator if there is at least one
- * location in the last segment.
- */
- if (trackData.numberOfSegments > 1 && trackData.lastLocationInCurrentSegment != null) {
- insertPoint(createNewLocation(TrackRecordingService.PAUSE_LATITUDE, 0.0,
- trackData.lastLocationInCurrentSegment.getTime()));
- }
- trackData.lastLocationInCurrentSegment = null;
- }
-
- /**
- * On track segment element end.
- */
- private void onTrackSegmentElementEnd() {
- // Nothing needs to be done
- }
-
- /**
- * On track point element start.
+ * On track point start.
*
* @param attributes the attributes
*/
- private void onTrackPointElementStart(Attributes attributes) throws SAXException {
- parseLocation(attributes);
+ private void onTrackPointStart(Attributes attributes) {
+ latitude = attributes.getValue(ATTRIBUTE_LAT);
+ longitude = attributes.getValue(ATTRIBUTE_LON);
+ altitude = DEFAULT_ALTITUDE;
+ time = null;
}
/**
- * On track point element end.
+ * On track point end.
*/
- private void onTrackPointElementEnd() throws SAXException {
- if (!LocationUtils.isValidLocation(location)) {
- throw new SAXException(createErrorMessage("Invalid location detected: " + location));
+ private void onTrackPointEnd() throws SAXException {
+ Location location = getTrackPoint();
+ if (location == null) {
+ return;
}
-
- if (trackData.numberOfSegments > 1 && trackData.lastLocationInCurrentSegment == null) {
- /*
- * If not the first segment, add a resume separator before adding the
- * first location.
- */
- insertPoint(
- createNewLocation(TrackRecordingService.RESUME_LATITUDE, 0.0, location.getTime()));
- }
-
- insertPoint(location);
-
- if (trackData.track.getStartId() == -1L) {
- // Flush the location to set the track start id and the track end id
- flushPoints(trackData);
- }
-
- trackData.lastLocationInCurrentSegment = location;
- location = null;
+ insertTrackPoint(location);
}
/**
- * On waypoint element start.
+ * On waypoint start.
*
* @param attributes the attributes
*/
- private void onWaypointElementStart(Attributes attributes) throws SAXException {
- if (currentWaypoint != null) {
- throw new SAXException(createErrorMessage("Found waypoint inside another waypoint."));
- }
- parseLocation(attributes);
- currentWaypoint = new Waypoint();
+ private void onWaypointStart(Attributes attributes) {
+ name = null;
+ description = null;
+ category = null;
+ latitude = attributes.getValue(ATTRIBUTE_LAT);
+ longitude = attributes.getValue(ATTRIBUTE_LON);
+ altitude = DEFAULT_ALTITUDE;
+ time = null;
+ waypointType = null;
}
/**
- * On waypoint element end.
+ * On waypoint end.
*/
- private void onWaypointElementEnd() throws SAXException {
- if (!LocationUtils.isValidLocation(location)) {
- throw new SAXException(createErrorMessage("Invalid location detected: " + location));
- }
- currentWaypoint.setLocation(location);
- waypoints.add(currentWaypoint);
- location = null;
- currentWaypoint = null;
- }
-
- /**
- * On name element end.
- */
- private void onNameElementEnd() {
- if (content != null) {
- String name = content.toString().trim();
- if (trackData != null && trackData.parsedDepth == 1) {
- trackData.track.setName(name);
- } else if (currentWaypoint != null) {
- currentWaypoint.setName(name);
- }
- }
- }
-
- /**
- * On description element end.
- */
- private void onDescriptionElementEnd() {
- if (content != null) {
- String description = content.toString().trim();
- if (trackData != null && trackData.parsedDepth == 1) {
- trackData.track.setDescription(description);
- } else if (currentWaypoint != null) {
- currentWaypoint.setDescription(description);
- }
- }
- }
-
- /**
- * On type element end.
- */
- private void onTypeElementEnd() {
- if (content != null) {
- String type = content.toString().trim();
- if (trackData != null && trackData.parsedDepth == 1) {
- trackData.track.setCategory(type);
- trackData.track.setIcon(TrackIconUtils.getIconValue(context, type));
- } else if (currentWaypoint != null) {
- currentWaypoint.setCategory(type);
- }
- }
- }
-
- /**
- * On altitude element end.
- */
- private void onAltitudeElementEnd() throws SAXException {
- if (location == null || content == null) {
- return;
- }
-
- try {
- location.setAltitude(Double.parseDouble(content));
- } catch (NumberFormatException e) {
- throw new SAXException(createErrorMessage("Unable to parse altitude: " + content), e);
- }
- }
-
- /**
- * On time element end. Sets location time and doing additional calculations
- * as this is the last value required for the location. Also sets the start
- * time for the trip statistics builder as there is no start time in the track
- * root element.
- */
- private void onTimeElementEnd() throws SAXException {
- if (location == null || content == null) {
- return;
- }
-
- // Parse the time
- long time;
- try {
- time = StringUtils.getTime(content.trim());
- } catch (IllegalArgumentException e) {
- throw new SAXException(createErrorMessage("Unable to parse time: " + content), e);
- }
- location.setTime(time);
-
- // Calculate derived attributes from the previous point
- if (trackData != null && trackData.lastLocationInCurrentSegment != null
- && trackData.lastLocationInCurrentSegment.getTime() != 0) {
- long timeDifference = time - trackData.lastLocationInCurrentSegment.getTime();
-
- // Check for negative time change
- if (timeDifference <= 0) {
- Log.w(Constants.TAG, "Time difference not postive.");
- } else {
-
- /*
- * We don't have a speed and bearing in GPX, make something up from the
- * last two points. GPS points tend to have some inherent imprecision,
- * speed and bearing will likely be off, so the statistics for things
- * like max speed will also be off.
- */
- float speed = trackData.lastLocationInCurrentSegment.distanceTo(location) * 1000.0f
- / timeDifference;
- location.setSpeed(speed);
- }
- location.setBearing(trackData.lastLocationInCurrentSegment.bearingTo(location));
- }
- }
-
- /**
- * On comment element end.
- */
- private void onCommentElementEnd() {
- if (content != null) {
- String comment = content.toString().trim();
- if (currentWaypoint != null) {
- WaypointType waypointType = WaypointType.STATISTICS.name().equals(comment) ? WaypointType.STATISTICS
- : WaypointType.WAYPOINT;
- currentWaypoint.setType(waypointType);
-
- }
- }
- }
-
- /**
- * Parses a location.
- *
- * @param attributes the attributes
- */
- private void parseLocation(Attributes attributes) throws SAXException {
- if (location != null) {
- throw new SAXException(createErrorMessage("Found a location inside another one."));
- }
- String latitude = attributes.getValue(ATT_LAT);
- String longitude = attributes.getValue(ATT_LON);
-
- if (latitude == null || longitude == null) {
- throw new SAXException(createErrorMessage("Point with no longitude or latitude."));
- }
- double latitudeValue;
- double longitudeValue;
- try {
- latitudeValue = Double.parseDouble(latitude);
- longitudeValue = Double.parseDouble(longitude);
- } catch (NumberFormatException e) {
- throw new SAXException(
- createErrorMessage("Unable to parse latitude/longitude: " + latitude + "/" + longitude),
- e);
- }
- location = createNewLocation(latitudeValue, longitudeValue, -1L);
- }
-
- /**
- * Creates a new location
- *
- * @param latitude location latitude
- * @param longitude location longitude
- * @param time location time
- */
- private Location createNewLocation(double latitude, double longitude, long time) {
- Location newLocation = new Location(LocationManager.GPS_PROVIDER);
- newLocation.setLatitude(latitude);
- newLocation.setLongitude(longitude);
- newLocation.setAltitude(0.0f);
- newLocation.setTime(time);
- newLocation.removeAccuracy();
- newLocation.removeBearing();
- newLocation.removeSpeed();
- return newLocation;
- }
-
- /**
- * Inserts a point.
- *
- * @param newLocation the location
- */
- private void insertPoint(Location newLocation) {
- if (trackData.tripStatisticsUpdater == null) {
- // Set the default location time if the newLocation does not have a time
- trackData.defaultLocationTime = newLocation.getTime() == -1L ? System.currentTimeMillis()
- : -1L;
- trackData.tripStatisticsUpdater = new TripStatisticsUpdater(
- newLocation.getTime() != -1L ? newLocation.getTime() : trackData.defaultLocationTime);
- }
- if (trackData.defaultLocationTime != -1L) {
- newLocation.setTime(trackData.defaultLocationTime);
- }
- trackData.tripStatisticsUpdater.addLocation(newLocation, minRecordingDistance);
-
- trackData.bufferedLocations[trackData.numBufferedLocations] = newLocation;
- trackData.numBufferedLocations++;
- trackData.numberOfLocations++;
-
- if (trackData.numBufferedLocations >= MAX_BUFFERED_LOCATIONS) {
- flushPoints(trackData);
- }
- }
-
- /**
- * Flushes the points to the database.
- *
- * @param data the track data
- */
- private void flushPoints(TrackData data) {
- if (data.numBufferedLocations <= 0) {
- return;
- }
- myTracksProviderUtils.bulkInsertTrackPoint(
- data.bufferedLocations, data.numBufferedLocations, data.track.getId());
- data.numBufferedLocations = 0;
- if (data.track.getStartId() == -1L) {
- data.track.setStartId(myTracksProviderUtils.getFirstTrackPointId(data.track.getId()));
- }
- data.track.setStopId(myTracksProviderUtils.getLastTrackPointId(data.track.getId()));
- }
-
- /**
- * Gets the imported track ids.
- */
- private long[] getImportedTrackIds() {
- long[] result = new long[tracksIds.size()];
- for (int i = 0; i < result.length; i++) {
- result[i] = tracksIds.get(i);
- }
- return result;
- }
-
- /**
- * Creates an error message.
- *
- * @param message the message
- */
- private String createErrorMessage(String message) {
- return String.format(Locale.US, "Parsing error at line: %d column: %d. %s",
- locator.getLineNumber(), locator.getColumnNumber(), message);
- }
-
- /**
- * Inserts the first track waypoint, the track statistics waypoint.
- *
- * @param track the track
- */
- private void insertFirstWaypoint(Track track) {
- String name = context.getString(R.string.marker_split_name_format, 0);
- String category = "";
- TripStatisticsUpdater updater = new TripStatisticsUpdater(
- track.getTripStatistics().getStartTime());
- TripStatistics tripStatistics = updater.getTripStatistics();
- String description = new DescriptionGeneratorImpl(context).generateWaypointDescription(
- tripStatistics);
- String icon = context.getString(R.string.marker_statistics_icon_url);
- double length = 0.0;
- long duration = 0L;
- Location waypointLocation = new Location("");
- waypointLocation.setLatitude(100);
- waypointLocation.setLongitude(180);
- Waypoint waypoint = new Waypoint(name, description, category, icon, track.getId(),
- WaypointType.STATISTICS, length, duration, -1L, -1L, waypointLocation, tripStatistics);
- myTracksProviderUtils.insertWaypoint(waypoint);
- }
-
- /**
- * Adds waypoints to a track.
- *
- * @param track the track
- */
- private void addWaypoints(Track track) {
- Cursor trackPointCursor = null;
- try {
- trackPointCursor = myTracksProviderUtils.getTrackPointCursor(track.getId(), -1L, -1, false);
- if (trackPointCursor == null) {
- return;
- }
- int waypointPosition = -1;
- Waypoint waypoint = null;
- int trackPointPosition = -1;
- Location trackPoint = null;
- TripStatisticsUpdater trackTripStatisticstrackUpdater = new TripStatisticsUpdater(
- track.getTripStatistics().getStartTime());
- TripStatisticsUpdater markerTripStatisticsUpdater = new TripStatisticsUpdater(
- track.getTripStatistics().getStartTime());
-
- while (true) {
- if (waypoint == null) {
- waypointPosition++;
- waypoint = waypointPosition < waypoints.size() ? waypoints.get(waypointPosition) : null;
- if (waypoint == null) {
- // No more waypoints
- return;
- }
- }
- if (trackPoint == null) {
- trackPointPosition++;
- trackPoint = trackPointCursor.moveToPosition(trackPointPosition) ? myTracksProviderUtils
- .createTrackPoint(trackPointCursor)
- : null;
- if (trackPoint == null) {
- // No more track points. Ignore the rest of the waypoints.
- return;
- }
- trackTripStatisticstrackUpdater.addLocation(trackPoint, minRecordingDistance);
- markerTripStatisticsUpdater.addLocation(trackPoint, minRecordingDistance);
- }
- if (waypoint.getLocation().getTime() > trackPoint.getTime()) {
- trackPoint = null;
- } else if (waypoint.getLocation().getTime() < trackPoint.getTime()) {
- waypoint = null;
- } else {
- // The waypoint location time matches the track point time
- if (trackPoint.getLatitude() == waypoint.getLocation().getLatitude()
- && trackPoint.getLongitude() == waypoint.getLocation().getLongitude()) {
-
- // Get tripStatistics, description, and icon
- TripStatistics tripStatistics;
- String description;
- String icon;
- if (waypoint.getType() == WaypointType.STATISTICS) {
- tripStatistics = markerTripStatisticsUpdater.getTripStatistics();
- markerTripStatisticsUpdater = new TripStatisticsUpdater(trackPoint.getTime());
- description = new DescriptionGeneratorImpl(context).generateWaypointDescription(
- tripStatistics);
- icon = context.getString(R.string.marker_statistics_icon_url);
- } else {
- tripStatistics = null;
- description = waypoint.getDescription();
- icon = context.getString(R.string.marker_waypoint_icon_url);
- }
-
- // Get length and duration
- double length = trackTripStatisticstrackUpdater.getTripStatistics().getTotalDistance();
- long duration = trackTripStatisticstrackUpdater.getTripStatistics().getTotalTime();
-
- // Insert waypoint
- Waypoint newWaypoint = new Waypoint(waypoint.getName(), description,
- waypoint.getCategory(), icon, track.getId(), waypoint.getType(), length, duration,
- -1L, -1L, trackPoint, tripStatistics);
- myTracksProviderUtils.insertWaypoint(newWaypoint);
- }
- waypoint = null;
- }
- }
- } finally {
- if (trackPointCursor != null) {
- trackPointCursor.close();
- }
- }
+ private void onWaypointEnd() throws SAXException {
+ addWaypoint(WaypointType.STATISTICS.name().equals(waypointType) ? WaypointType.STATISTICS
+ : WaypointType.WAYPOINT);
}
}
diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/file/ImportActivity.java b/MyTracks/src/com/google/android/apps/mytracks/io/file/ImportActivity.java
index e9b747c72..2cae4923c 100644
--- a/MyTracks/src/com/google/android/apps/mytracks/io/file/ImportActivity.java
+++ b/MyTracks/src/com/google/android/apps/mytracks/io/file/ImportActivity.java
@@ -35,36 +35,39 @@ import android.support.v4.app.TaskStackBuilder;
import android.util.Log;
import android.widget.Toast;
+import java.io.File;
+
/**
- * An activity to import GPX files from the SD card. Optionally to import one
- * GPX file and display it in My Tracks.
- *
+ * An activity to import files from the external storage. Optionally to import
+ * one specific file.
+ *
* @author Rodrigo Damazio
*/
public class ImportActivity extends Activity {
public static final String EXTRA_IMPORT_ALL = "import_all";
+ public static final String EXTRA_TRACK_FILE_FORMAT = "track_file_format";
private static final String TAG = ImportActivity.class.getSimpleName();
-
private static final int DIALOG_PROGRESS_ID = 0;
private static final int DIALOG_RESULT_ID = 1;
private ImportAsyncTask importAsyncTask;
private ProgressDialog progressDialog;
-
+
private boolean importAll;
-
- // path on the SD card to import
+ private TrackFileFormat trackFileFormat;
+
+ // the path on the external storage to import
private String path;
- // number of succesfully imported files
+ // the number of successfully imported files
private int successCount;
-
- // number of files to import
+
+ // the number of files to import
private int totalCount;
-
- // last successfully imported track id
+
+ // the last successfully imported track id
private long trackId;
@Override
@@ -73,8 +76,25 @@ public class ImportActivity extends Activity {
Intent intent = getIntent();
importAll = intent.getBooleanExtra(EXTRA_IMPORT_ALL, false);
+ trackFileFormat = intent.getParcelableExtra(EXTRA_TRACK_FILE_FORMAT);
+ if (trackFileFormat == null) {
+ trackFileFormat = TrackFileFormat.GPX;
+ }
+
+ if (!FileUtils.isExternalStorageAvailable()) {
+ Toast.makeText(this, R.string.external_storage_not_available, Toast.LENGTH_LONG).show();
+ finish();
+ return;
+ }
if (importAll) {
- path = FileUtils.buildExternalDirectoryPath("gpx");
+ path = FileUtils.buildExternalDirectoryPath(
+ trackFileFormat == TrackFileFormat.KML ? "kml" : "gpx");
+ if (!FileUtils.isDirectory(new File(path))) {
+ Toast.makeText(this, getString(R.string.import_no_directory, path), Toast.LENGTH_LONG)
+ .show();
+ finish();
+ return;
+ }
} else {
String action = intent.getAction();
if (!(Intent.ACTION_ATTACH_DATA.equals(action) || Intent.ACTION_VIEW.equals(action))) {
@@ -97,7 +117,7 @@ public class ImportActivity extends Activity {
importAsyncTask = (ImportAsyncTask) retained;
importAsyncTask.setActivity(this);
} else {
- importAsyncTask = new ImportAsyncTask(this, importAll, path);
+ importAsyncTask = new ImportAsyncTask(this, importAll, trackFileFormat, path);
importAsyncTask.execute();
}
}
@@ -112,41 +132,36 @@ public class ImportActivity extends Activity {
protected Dialog onCreateDialog(int id) {
switch (id) {
case DIALOG_PROGRESS_ID:
- progressDialog = DialogUtils.createHorizontalProgressDialog(this,
- R.string.external_storage_import_progress_message,
- new DialogInterface.OnCancelListener() {
+ progressDialog = DialogUtils.createHorizontalProgressDialog(
+ this, R.string.import_progress_message, new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
importAsyncTask.cancel(true);
finish();
}
- });
+ }, path);
return progressDialog;
case DIALOG_RESULT_ID:
String message;
- if (successCount == 0) {
- message = getString(R.string.external_storage_import_error_no_file, path);
+ if (totalCount == 0) {
+ message = getString(R.string.import_no_file, path);
} else {
String totalFiles = getResources()
- .getQuantityString(R.plurals.importGpxFiles, totalCount, totalCount);
- message = getString(
- R.string.external_storage_import_success_count, successCount, totalFiles, path);
+ .getQuantityString(R.plurals.importFiles, totalCount, totalCount);
+ message = getString(R.string.import_success, successCount, totalFiles, path);
}
- return new AlertDialog.Builder(this)
- .setCancelable(true)
- .setMessage(message)
- .setOnCancelListener(new DialogInterface.OnCancelListener() {
- @Override
+ return new AlertDialog.Builder(this).setCancelable(true)
+ .setMessage(message).setOnCancelListener(new DialogInterface.OnCancelListener() {
+ @Override
public void onCancel(DialogInterface dialog) {
finish();
}
- })
- .setPositiveButton(R.string.generic_ok, new DialogInterface.OnClickListener() {
- @Override
+ }).setPositiveButton(R.string.generic_ok, new DialogInterface.OnClickListener() {
+ @Override
public void onClick(DialogInterface dialog, int which) {
if (!importAll && trackId != -1L) {
- Intent intent = IntentUtils
- .newIntent(ImportActivity.this, TrackDetailActivity.class)
+ Intent intent = IntentUtils.newIntent(
+ ImportActivity.this, TrackDetailActivity.class)
.putExtra(TrackDetailActivity.EXTRA_TRACK_ID, trackId);
TaskStackBuilder taskStackBuilder = TaskStackBuilder.create(ImportActivity.this);
taskStackBuilder.addNextIntent(intent);
@@ -154,8 +169,7 @@ public class ImportActivity extends Activity {
}
finish();
}
- })
- .create();
+ }).create();
default:
return null;
}
@@ -163,23 +177,17 @@ public class ImportActivity extends Activity {
/**
* Invokes when the associated AsyncTask completes.
- *
- * @param success true if the AsyncTask is successful
- * @param imported the number of files successfully imported
- * @param total the total number of files to import
- * @param id the last successfully imported track id
+ *
+ * @param aSuccessCount the number of files successfully imported
+ * @param aTotalCount the total number of files to import
+ * @param aTrackId the last successfully imported track id
*/
- public void onAsyncTaskCompleted(boolean success, int imported, int total, long id) {
- successCount = imported;
- totalCount = total;
- trackId = id;
+ public void onAsyncTaskCompleted(int aSuccessCount, int aTotalCount, long aTrackId) {
+ successCount = aSuccessCount;
+ totalCount = aTotalCount;
+ trackId = aTrackId;
removeDialog(DIALOG_PROGRESS_ID);
- if (success) {
- showDialog(DIALOG_RESULT_ID);
- } else {
- Toast.makeText(this, R.string.external_storage_import_error, Toast.LENGTH_LONG).show();
- finish();
- }
+ showDialog(DIALOG_RESULT_ID);
}
/**
@@ -191,7 +199,7 @@ public class ImportActivity extends Activity {
/**
* Sets the progress dialog value.
- *
+ *
* @param number the number of files imported
* @param max the maximum number of files
*/
diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/file/ImportAsyncTask.java b/MyTracks/src/com/google/android/apps/mytracks/io/file/ImportAsyncTask.java
index 0b714b4d7..2e2cba2dc 100644
--- a/MyTracks/src/com/google/android/apps/mytracks/io/file/ImportAsyncTask.java
+++ b/MyTracks/src/com/google/android/apps/mytracks/io/file/ImportAsyncTask.java
@@ -16,8 +16,6 @@
package com.google.android.apps.mytracks.io.file;
-import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
-import com.google.android.apps.mytracks.util.FileUtils;
import com.google.android.apps.mytracks.util.PreferencesUtils;
import com.google.android.apps.mytracks.util.SystemUtils;
import com.google.android.maps.mytracks.R;
@@ -28,18 +26,12 @@ import android.util.Log;
import java.io.File;
import java.io.FileInputStream;
-import java.io.FileNotFoundException;
-import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
-import javax.xml.parsers.ParserConfigurationException;
-
-import org.xml.sax.SAXException;
-
/**
- * AsyncTask to import GPX files from the SD card.
- *
+ * AsyncTask to import files from the external storage.
+ *
* @author Jimmy Shih
*/
public class ImportAsyncTask extends AsyncTask {
@@ -48,20 +40,17 @@ public class ImportAsyncTask extends AsyncTask {
private ImportActivity importActivity;
private final boolean importAll;
+ private final TrackFileFormat trackFileFormat;
private final String path;
- private final MyTracksProviderUtils myTracksProviderUtils;
private WakeLock wakeLock;
- // true if the AsyncTask result is success
- private boolean success;
-
// true if the AsyncTask has completed
private boolean completed;
- // number of files successfully imported
+ // the number of files successfully imported
private int successCount;
- // number of files to import
+ // the number of files to import
private int totalCount;
// the last successfully imported track id
@@ -69,18 +58,19 @@ public class ImportAsyncTask extends AsyncTask {
/**
* Creates an AsyncTask.
- *
+ *
* @param importActivity the activity currently associated with this AsyncTask
* @param importAll true to import all GPX files
+ * @param trackFileFormat the track file format
* @param path path to import GPX files
*/
- public ImportAsyncTask(ImportActivity importActivity, boolean importAll, String path) {
+ public ImportAsyncTask(ImportActivity importActivity, boolean importAll,
+ TrackFileFormat trackFileFormat, String path) {
this.importActivity = importActivity;
this.importAll = importAll;
+ this.trackFileFormat = trackFileFormat;
this.path = path;
- myTracksProviderUtils = MyTracksProviderUtils.Factory.get(importActivity);
-
// Get the wake lock if not recording or paused
if (PreferencesUtils.getLong(importActivity, R.string.recording_track_id_key)
== PreferencesUtils.RECORDING_TRACK_ID_DEFAULT || PreferencesUtils.getBoolean(
@@ -89,7 +79,6 @@ public class ImportAsyncTask extends AsyncTask {
wakeLock = SystemUtils.acquireWakeLock(importActivity, wakeLock);
}
- success = false;
completed = false;
successCount = 0;
totalCount = 0;
@@ -98,13 +87,13 @@ public class ImportAsyncTask extends AsyncTask {
/**
* Sets the current {@link ImportActivity} associated with this AyncTask.
- *
+ *
* @param importActivity the current {@link ImportActivity}, can be null
*/
public void setActivity(ImportActivity importActivity) {
this.importActivity = importActivity;
if (completed && importActivity != null) {
- importActivity.onAsyncTaskCompleted(success, successCount, totalCount, trackId);
+ importActivity.onAsyncTaskCompleted(successCount, totalCount, trackId);
}
}
@@ -118,10 +107,6 @@ public class ImportAsyncTask extends AsyncTask {
@Override
protected Boolean doInBackground(Void... params) {
try {
- if (!FileUtils.isSdCardAvailable()) {
- return false;
- }
-
List files = getFiles();
totalCount = files.size();
if (totalCount == 0) {
@@ -133,8 +118,7 @@ public class ImportAsyncTask extends AsyncTask {
// If cancelled, return true to show the number of files imported
return true;
}
- File file = files.get(i);
- if (importFile(file)) {
+ if (importFile(files.get(i))) {
successCount++;
}
publishProgress(i + 1, totalCount);
@@ -157,46 +141,36 @@ public class ImportAsyncTask extends AsyncTask {
@Override
protected void onPostExecute(Boolean result) {
- success = result;
completed = true;
if (importActivity != null) {
- importActivity.onAsyncTaskCompleted(success, successCount, totalCount, trackId);
+ importActivity.onAsyncTaskCompleted(successCount, totalCount, trackId);
}
}
/**
- * Imports a GPX file.
- *
+ * Imports a file.
+ *
* @param file the file
*/
private boolean importFile(final File file) {
try {
- int minRecordingDistance = PreferencesUtils.getInt(importActivity,
- R.string.min_recording_distance_key, PreferencesUtils.MIN_RECORDING_DISTANCE_DEFAULT);
- long trackIds[] = GpxImporter.importGPXFile(importActivity,
- new FileInputStream(file), myTracksProviderUtils, minRecordingDistance);
+ AbstractImporter importer = trackFileFormat == TrackFileFormat.KML ? new KmlImporter(
+ importActivity)
+ : new GpxImporter(importActivity);
+ long trackIds[] = importer.importFile(new FileInputStream(file));
int length = trackIds.length;
if (length > 0) {
trackId = trackIds[length - 1];
}
return true;
- } catch (FileNotFoundException e) {
- Log.d(TAG, "file: " + file.getAbsolutePath(), e);
- return false;
- } catch (ParserConfigurationException e) {
- Log.d(TAG, "file: " + file.getAbsolutePath(), e);
- return false;
- } catch (SAXException e) {
- Log.d(TAG, "file: " + file.getAbsolutePath(), e);
- return false;
- } catch (IOException e) {
+ } catch (Exception e) {
Log.d(TAG, "file: " + file.getAbsolutePath(), e);
return false;
}
}
/**
- * Gets a list of GPX files. If importAll is true, returns a list of GPX files
+ * Gets a list of files. If importAll is true, returns a list of the files
* under the path directory. If importAll is false, returns a list containing
* just the path file.
*/
@@ -207,7 +181,8 @@ public class ImportAsyncTask extends AsyncTask {
File[] candidates = file.listFiles();
if (candidates != null) {
for (File candidate : candidates) {
- if (!candidate.isDirectory() && candidate.getName().endsWith(".gpx")) {
+ if (!candidate.isDirectory() && candidate.getName()
+ .endsWith(trackFileFormat == TrackFileFormat.KML ? ".kml" : ".gpx")) {
files.add(candidate);
}
}
diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/file/KmlImporter.java b/MyTracks/src/com/google/android/apps/mytracks/io/file/KmlImporter.java
new file mode 100644
index 000000000..e9a74222f
--- /dev/null
+++ b/MyTracks/src/com/google/android/apps/mytracks/io/file/KmlImporter.java
@@ -0,0 +1,280 @@
+/*
+ * Copyright 2012 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.file;
+
+import com.google.android.apps.mytracks.content.MyTracksLocation;
+import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
+import com.google.android.apps.mytracks.content.Sensor;
+import com.google.android.apps.mytracks.content.Sensor.SensorDataSet;
+import com.google.android.apps.mytracks.content.Waypoint.WaypointType;
+import com.google.common.annotations.VisibleForTesting;
+
+import android.content.Context;
+import android.location.Location;
+
+import java.util.ArrayList;
+
+import org.xml.sax.Attributes;
+import org.xml.sax.SAXException;
+
+/**
+ * Imports a KML file.
+ *
+ * @author Jimmy Shih
+ */
+public class KmlImporter extends AbstractImporter {
+
+ private static final String CADENCE = "cadence";
+ private static final String HEART_RATE = "heart_rate";
+ private static final String POWER = "power";
+
+ private static final String STATISTICS_STYLE = "#statistics";
+ private static final String WAYPOINT_STYLE = "#waypoint";
+
+ private static final String TAG_COORDINATES = "coordinates";
+ private static final String TAG_DESCRIPTION = "description";
+ private static final String TAG_GX_COORD = "gx:coord";
+ private static final String TAG_GX_MULTI_TRACK = "gx:MultiTrack";
+ private static final String TAG_GX_SIMPLE_ARRAY_DATA = "gx:SimpleArrayData";
+ private static final String TAG_GX_TRACK = "gx:Track";
+ private static final String TAG_GX_VALUE = "gx:value";
+ private static final String TAG_KML = "kml";
+ private static final String TAG_NAME = "name";
+ private static final String TAG_PLACEMARK = "Placemark";
+ private static final String TAG_STYLE_URL = "styleUrl";
+ private static final String TAG_VALUE = "value";
+ private static final String TAG_WHEN = "when";
+
+ private static final String ATTRIBUTE_NAME = "name";
+
+ private String sensorName;
+ private ArrayList locationList;
+ private ArrayList cadenceList;
+ private ArrayList heartRateList;
+ private ArrayList powerList;
+
+ public KmlImporter(Context context) {
+ super(context);
+ }
+
+ @VisibleForTesting
+ public KmlImporter(Context context, MyTracksProviderUtils myTracksProviderUtils) {
+ super(context, myTracksProviderUtils);
+ }
+
+ @Override
+ public void startElement(String uri, String localName, String tag, Attributes attributes)
+ throws SAXException {
+ if (tag.equals(TAG_PLACEMARK)) {
+ onWaypointStart();
+ } else if (tag.equals(TAG_GX_MULTI_TRACK)) {
+ onTrackStart();
+ } else if (tag.equals(TAG_GX_TRACK)) {
+ onTrackSegmentStart();
+ } else if (tag.equals(TAG_GX_SIMPLE_ARRAY_DATA)) {
+ onSensorDataStart(attributes);
+ }
+ }
+
+ @Override
+ public void endElement(String uri, String localName, String tag) throws SAXException {
+ if (tag.equals(TAG_KML)) {
+ onFileEnd();
+ } else if (tag.equals(TAG_PLACEMARK)) {
+ onWaypointEnd();
+ } else if (localName.equals(TAG_COORDINATES)) {
+ onWaypointLocationEnd();
+ } else if (tag.equals(TAG_GX_MULTI_TRACK)) {
+ onTrackEnd();
+ } else if (tag.equals(TAG_GX_TRACK)) {
+ onTrackSegmentEnd();
+ } else if (tag.equals(TAG_GX_COORD)) {
+ onTrackPointEnd();
+ } else if (tag.equals(TAG_GX_VALUE)) {
+ onSensorValueEnd();
+ } else if (tag.equals(TAG_NAME)) {
+ if (content != null) {
+ name = content.trim();
+ }
+ } else if (localName.equals(TAG_DESCRIPTION)) {
+ if (content != null) {
+ description = content.trim();
+ }
+ } else if (localName.equals(TAG_VALUE)) {
+ if (content != null) {
+ category = content.trim();
+ }
+ } else if (localName.equals(TAG_WHEN)) {
+ if (content != null) {
+ time = content.trim();
+ }
+ } else if (localName.equals(TAG_STYLE_URL)) {
+ if (content != null) {
+ waypointType = content.trim();
+ }
+ }
+
+ // Reset element content
+ content = null;
+ }
+
+ /**
+ * On waypoint start.
+ */
+ private void onWaypointStart() {
+ // Reset all Placemark variables
+ name = null;
+ description = null;
+ category = null;
+ latitude = null;
+ longitude = null;
+ altitude = DEFAULT_ALTITUDE;
+ time = null;
+ waypointType = null;
+ }
+
+ /**
+ * On waypoint end.
+ */
+ private void onWaypointEnd() throws SAXException {
+ // Add a waypoint if the waypointType matches
+ WaypointType type = null;
+ if (WAYPOINT_STYLE.equals(waypointType)) {
+ type = WaypointType.WAYPOINT;
+ } else if (STATISTICS_STYLE.equals(waypointType)) {
+ type = WaypointType.STATISTICS;
+ }
+ if (type == null) {
+ return;
+ }
+ addWaypoint(type);
+ }
+
+ /**
+ * On waypoint location end.
+ */
+ private void onWaypointLocationEnd() {
+ if (content != null) {
+ String parts[] = content.trim().split(",");
+ if (parts.length != 2 && parts.length != 3) {
+ return;
+ }
+ longitude = parts[0];
+ latitude = parts[1];
+ if (parts.length == 3) {
+ altitude = parts[2];
+ }
+ }
+ }
+
+ @Override
+ protected void onTrackSegmentStart() {
+ super.onTrackSegmentStart();
+ locationList = new ArrayList();
+ powerList = new ArrayList();
+ cadenceList = new ArrayList();
+ heartRateList = new ArrayList();
+ }
+
+ /**
+ * On track segment end.
+ */
+ private void onTrackSegmentEnd() {
+ // Close a track segment by inserting the segment locations
+ boolean hasPower = powerList.size() == locationList.size();
+ boolean hasCadence = cadenceList.size() == locationList.size();
+ boolean hasHeartRate = heartRateList.size() == locationList.size();
+
+ for (int i = 0; i < locationList.size(); i++) {
+ Location location = locationList.get(i);
+
+ if (!hasPower && !hasCadence && !hasHeartRate) {
+ insertTrackPoint(location);
+ } else {
+ SensorDataSet.Builder builder = Sensor.SensorDataSet.newBuilder();
+ if (hasPower) {
+ builder.setPower(Sensor.SensorData.newBuilder()
+ .setValue(powerList.get(i)).setState(Sensor.SensorState.SENDING));
+ }
+ if (hasCadence) {
+ builder.setCadence(Sensor.SensorData.newBuilder()
+ .setValue(cadenceList.get(i)).setState(Sensor.SensorState.SENDING));
+ }
+ if (hasHeartRate) {
+ builder.setHeartRate(Sensor.SensorData.newBuilder()
+ .setValue(heartRateList.get(i)).setState(Sensor.SensorState.SENDING));
+ }
+ SensorDataSet sensorDataSet = builder.setCreationTime(location.getTime()).build();
+ MyTracksLocation myTracksLocation = new MyTracksLocation(location, sensorDataSet);
+ insertTrackPoint(myTracksLocation);
+ }
+ }
+ }
+
+ /**
+ * On track point end. gx:coord end tag.
+ */
+ private void onTrackPointEnd() throws SAXException {
+ // Add location to locationList
+ if (content == null) {
+ return;
+ }
+ String parts[] = content.trim().split(" ");
+ if (parts.length != 3) {
+ return;
+ }
+ longitude = parts[0];
+ latitude = parts[1];
+ altitude = parts[2];
+ Location location = getTrackPoint();
+ if (location == null) {
+ return;
+ }
+ locationList.add(location);
+ time = null;
+ }
+
+ /**
+ * On sensor data start. gx:SimpleArrayData start tag.
+ *
+ * @param attributes
+ */
+ private void onSensorDataStart(Attributes attributes) {
+ sensorName = attributes.getValue(ATTRIBUTE_NAME);
+ }
+
+ /**
+ * On sensor value end. gx:value end tag.
+ */
+ private void onSensorValueEnd() throws SAXException {
+ if (content != null) {
+ int value;
+ try {
+ value = Integer.parseInt(content.trim());
+ } catch (NumberFormatException e) {
+ throw new SAXException(createErrorMessage("Unable to parse gx:value:" + content), e);
+ }
+ if (POWER.equals(sensorName)) {
+ powerList.add(value);
+ } else if (HEART_RATE.equals(sensorName)) {
+ heartRateList.add(value);
+ } else if (CADENCE.equals(sensorName)) {
+ cadenceList.add(value);
+ }
+ }
+ }
+}
diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/file/KmlTrackWriter.java b/MyTracks/src/com/google/android/apps/mytracks/io/file/KmlTrackWriter.java
index 99fd29408..25e7f0539 100644
--- a/MyTracks/src/com/google/android/apps/mytracks/io/file/KmlTrackWriter.java
+++ b/MyTracks/src/com/google/android/apps/mytracks/io/file/KmlTrackWriter.java
@@ -303,6 +303,8 @@ public class KmlTrackWriter implements TrackFormatWriter {
printWriter.println("" + StringUtils.formatCData(name) + "");
printWriter.println(
"" + StringUtils.formatCData(description) + "");
+ printWriter.println(""
+ + StringUtils.formatDateTimeIso8601(location.getTime()) + "");
printWriter.println("#" + styleName + "");
writeCategory(category);
printWriter.println("");
diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/file/TrackWriterImpl.java b/MyTracks/src/com/google/android/apps/mytracks/io/file/TrackWriterImpl.java
index a97d07431..30c37782b 100644
--- a/MyTracks/src/com/google/android/apps/mytracks/io/file/TrackWriterImpl.java
+++ b/MyTracks/src/com/google/android/apps/mytracks/io/file/TrackWriterImpl.java
@@ -194,9 +194,8 @@ class TrackWriterImpl implements TrackWriter {
* Checks and returns whether we're ready to create the output file.
*/
protected boolean canWriteFile() {
- if (!FileUtils.isSdCardAvailable()) {
- Log.i(Constants.TAG, "Could not find SD card.");
- errorMessage = R.string.external_storage_error_no_storage;
+ if (!FileUtils.isExternalStorageWriteable()) {
+ errorMessage = R.string.external_storage_not_writeable;
return false;
}
diff --git a/MyTracks/src/com/google/android/apps/mytracks/util/FileUtils.java b/MyTracks/src/com/google/android/apps/mytracks/util/FileUtils.java
index 11d7ff5da..659552e65 100644
--- a/MyTracks/src/com/google/android/apps/mytracks/util/FileUtils.java
+++ b/MyTracks/src/com/google/android/apps/mytracks/util/FileUtils.java
@@ -37,29 +37,45 @@ public class FileUtils {
*/
@VisibleForTesting
static final int MAX_FAT32_PATH_LENGTH = 260;
-
+
/**
- * Returns whether the SD card is available.
+ * Returns true if the external storage is available.
*/
- public static boolean isSdCardAvailable() {
- return Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState());
+ public static boolean isExternalStorageAvailable() {
+ String state = Environment.getExternalStorageState();
+ return Environment.MEDIA_MOUNTED.equals(state)
+ || Environment.MEDIA_MOUNTED_READ_ONLY.equals(state);
+ }
+
+ /**
+ * Returns true if the external storage is writable.
+ */
+ public static boolean isExternalStorageWriteable() {
+ String state = Environment.getExternalStorageState();
+ return Environment.MEDIA_MOUNTED.equals(state);
+ }
+
+ /**
+ * Returns true if the directory exists.
+ *
+ * @param dir the directory
+ */
+ public static boolean isDirectory(File dir) {
+ return dir.exists() && dir.isDirectory();
}
/**
- * Ensures the given directory exists by creating it and its parents if
+ * Ensures the directory exists by creating it and its parents if
* necessary.
*
* @return whether the directory exists (either already existed or was
* successfully created)
*/
public static boolean ensureDirectoryExists(File dir) {
- if (dir.exists() && dir.isDirectory()) {
+ if (isDirectory(dir)) {
return true;
}
- if (dir.mkdirs()) {
- return true;
- }
- return false;
+ return dir.mkdirs();
}
/**
diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/io/file/AbstractTestImporter.java b/MyTracksTest/src/com/google/android/apps/mytracks/io/file/AbstractTestImporter.java
new file mode 100644
index 000000000..55059418e
--- /dev/null
+++ b/MyTracksTest/src/com/google/android/apps/mytracks/io/file/AbstractTestImporter.java
@@ -0,0 +1,164 @@
+/*
+ * Copyright 2012 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.file;
+
+import static com.google.android.testing.mocking.AndroidMock.eq;
+import static com.google.android.testing.mocking.AndroidMock.expect;
+
+import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
+import com.google.android.apps.mytracks.content.MyTracksProviderUtils.Factory;
+import com.google.android.apps.mytracks.content.Track;
+import com.google.android.apps.mytracks.content.TracksColumns;
+import com.google.android.apps.mytracks.content.Waypoint;
+import com.google.android.apps.mytracks.content.WaypointsColumns;
+import com.google.android.apps.mytracks.testing.TestingProviderUtilsFactory;
+import com.google.android.testing.mocking.AndroidMock;
+import com.google.android.testing.mocking.UsesMocks;
+
+import android.content.ContentUris;
+import android.location.Location;
+import android.location.LocationManager;
+import android.net.Uri;
+import android.test.AndroidTestCase;
+
+import java.text.SimpleDateFormat;
+import java.util.Locale;
+import java.util.SimpleTimeZone;
+
+import org.easymock.Capture;
+
+/**
+ * Abstract class for testing importer.
+ *
+ * @author Jimmy Shih.
+ */
+public class AbstractTestImporter extends AndroidTestCase {
+
+ protected static final String TRACK_NAME_0 = "blablub";
+ protected static final String TRACK_DESCRIPTION_0 = "s'Laebe isch koi Schlotzer";
+
+ protected static final String TRACK_NAME_1 = "another track";
+ protected static final String TRACK_DESCRIPTION_1 = "another description";
+
+ protected static final double TRACK_LATITUDE = 48.768364;
+ protected static final double TRACK_LONGITUDE = 9.177886;
+ protected static final double TRACK_ELEVATION = 324.0;
+
+ protected static final String TRACK_TIME_0 = "2010-04-22T18:21:00Z";
+ protected static final String TRACK_TIME_1 = "2010-04-22T18:21:50.123";
+ protected static final String TRACK_TIME_2 = "2010-04-22T18:23:00.123";
+ protected static final String TRACK_TIME_3 = "2010-04-22T18:24:50.123";
+
+ protected static final SimpleDateFormat DATE_FORMAT_0 = new SimpleDateFormat(
+ "yyyy-MM-dd'T'hh:mm:ss'Z'", Locale.US);
+ protected static final SimpleDateFormat DATE_FORMAT_1 = new SimpleDateFormat(
+ "yyyy-MM-dd'T'HH:mm:ss.SSS", Locale.US);
+
+ static {
+ /*
+ * We can't omit the timezones in the test, otherwise it'll use the local
+ * timezone and fail depending on where the test runner is.
+ */
+ SimpleTimeZone utc = new SimpleTimeZone(0, "UTC");
+ DATE_FORMAT_0.setTimeZone(utc);
+ DATE_FORMAT_1.setTimeZone(utc);
+ }
+
+ protected static final long TRACK_ID_0 = 1;
+ protected static final long TRACK_ID_1 = 2;
+ protected static final long TRACK_POINT_ID_0 = 1;
+ protected static final long TRACK_POINT_ID_1 = 2;
+ protected static final long TRACK_POINT_ID_3 = 4;
+ protected static final long WAYPOINT_ID_0 = 1;
+
+ protected static final Uri TRACK_ID_0_URI = ContentUris.appendId(
+ TracksColumns.CONTENT_URI.buildUpon(), TRACK_ID_0).build();
+ protected static final Uri TRACK_ID_1_URI = ContentUris.appendId(
+ TracksColumns.CONTENT_URI.buildUpon(), TRACK_ID_1).build();
+ protected static final Uri WAYPOINT_ID_O_URI = ContentUris.appendId(
+ WaypointsColumns.CONTENT_URI.buildUpon(), WAYPOINT_ID_0).build();
+
+ protected MyTracksProviderUtils myTracksProviderUtils;
+
+ private Factory oldMyTracksProviderUtilsFactory;
+
+ @UsesMocks(MyTracksProviderUtils.class)
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ myTracksProviderUtils = AndroidMock.createMock(MyTracksProviderUtils.class);
+ oldMyTracksProviderUtilsFactory = TestingProviderUtilsFactory.installWithInstance(
+ myTracksProviderUtils);
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ TestingProviderUtilsFactory.restoreOldFactory(oldMyTracksProviderUtilsFactory);
+ super.tearDown();
+ }
+
+ protected Location createLocation(int index, long time) {
+ Location location = new Location(LocationManager.GPS_PROVIDER);
+ location.setLatitude(TRACK_LATITUDE + index);
+ location.setLongitude(TRACK_LONGITUDE + index);
+ location.setAltitude(TRACK_ELEVATION + index);
+ location.setTime(time);
+ return location;
+ }
+
+ /**
+ * Expects the first track point to be added.
+ *
+ * @param location the location
+ * @param trackId the track id
+ * @param trackPointId the track point id
+ */
+ protected void expectFirstTrackPoint(Location location, long trackId, long trackPointId) {
+ expect(myTracksProviderUtils.bulkInsertTrackPoint(
+ location != null ? LocationsMatcher.eqLoc(location) : (Location[]) AndroidMock.anyObject(),
+ eq(1), eq(trackId))).andReturn(1);
+ expect(myTracksProviderUtils.getFirstTrackPointId(trackId)).andReturn(trackPointId);
+ expect(myTracksProviderUtils.getLastTrackPointId(trackId)).andReturn(trackPointId);
+ }
+
+ /**
+ * Expects the track to be updated.
+ *
+ * @param track the track
+ * @param lastTrack true if it is the last track in the gpx
+ * @param trackId the track id
+ */
+ protected void expectUpdateTrack(Capture