Add KmlImporter.

This commit is contained in:
Jimmy Shih
2013-01-07 15:54:04 -08:00
parent c14f2af060
commit 2e69c47e2a
24 changed files with 1613 additions and 1075 deletions
@@ -60,6 +60,7 @@ import android.widget.TabHost;
import android.widget.TabHost.TabSpec;
import java.util.List;
import java.util.Locale;
/**
* An activity to show the track detail.
@@ -506,7 +507,8 @@ public class TrackDetailActivity extends AbstractMyTracksActivity implements Del
* @param trackFileFormat the track file format
*/
private void startSaveActivity(TrackFileFormat trackFileFormat) {
AnalyticsUtils.sendPageViews(this, "/action/save");
AnalyticsUtils.sendPageViews(
this, "/action/save_" + trackFileFormat.name().toLowerCase(Locale.US));
Intent intent = IntentUtils.newIntent(this, SaveActivity.class)
.putExtra(SaveActivity.EXTRA_TRACK_ID, trackId)
.putExtra(SaveActivity.EXTRA_TRACK_FILE_FORMAT, (Parcelable) trackFileFormat);
@@ -81,6 +81,7 @@ import android.widget.ListView;
import android.widget.Toast;
import java.util.EnumSet;
import java.util.Locale;
/**
* An activity displaying a list of tracks.
@@ -323,7 +324,7 @@ public class TrackListActivity extends FragmentActivity implements DeleteOneTrac
// Menu items
private MenuItem searchMenuItem;
private MenuItem startGpsMenuItem;
private MenuItem importMenuItem;
private MenuItem importAllMenuItem;
private MenuItem saveAllMenuItem;
private MenuItem deleteAllMenuItem;
@@ -486,6 +487,12 @@ public class TrackListActivity extends FragmentActivity implements DeleteOneTrac
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.track_list, menu);
String fileTypes[] = getResources().getStringArray(R.array.file_types);
// Import all submenu titles
menu.findItem(R.id.track_list_import_all_gpx)
.setTitle(getString(R.string.menu_import_all_format, fileTypes[0]));
menu.findItem(R.id.track_list_import_all_kml)
.setTitle(getString(R.string.menu_import_all_format, fileTypes[1]));
// Save all submenu titles
menu.findItem(R.id.track_list_save_all_gpx)
.setTitle(getString(R.string.menu_save_format, fileTypes[0]));
menu.findItem(R.id.track_list_save_all_kml)
@@ -497,7 +504,7 @@ public class TrackListActivity extends FragmentActivity implements DeleteOneTrac
searchMenuItem = menu.findItem(R.id.track_list_search);
startGpsMenuItem = menu.findItem(R.id.track_list_start_gps);
importMenuItem = menu.findItem(R.id.track_list_import);
importAllMenuItem = menu.findItem(R.id.track_list_import_all);
saveAllMenuItem = menu.findItem(R.id.track_list_save_all);
deleteAllMenuItem = menu.findItem(R.id.track_list_delete_all);
@@ -529,11 +536,11 @@ public class TrackListActivity extends FragmentActivity implements DeleteOneTrac
handleStartGps();
updateMenuItems(recordingTrackId != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT);
return true;
case R.id.track_list_import:
AnalyticsUtils.sendPageViews(this, "/action/import");
intent = IntentUtils.newIntent(this, ImportActivity.class)
.putExtra(ImportActivity.EXTRA_IMPORT_ALL, true);
startActivity(intent);
case R.id.track_list_import_all_gpx:
startImportActivity(TrackFileFormat.GPX);
return true;
case R.id.track_list_import_all_kml:
startImportActivity(TrackFileFormat.KML);
return true;
case R.id.track_list_save_all_gpx:
startSaveActivity(TrackFileFormat.GPX);
@@ -653,8 +660,8 @@ public class TrackListActivity extends FragmentActivity implements DeleteOneTrac
startGpsMenuItem.setTitle(startGps ? R.string.menu_stop_gps : R.string.menu_start_gps);
startGpsMenuItem.setVisible(!isRecording);
}
if (importMenuItem != null) {
importMenuItem.setVisible(!isRecording);
if (importAllMenuItem != null) {
importAllMenuItem.setVisible(!isRecording);
}
if (saveAllMenuItem != null) {
saveAllMenuItem.setVisible(!isRecording);
@@ -680,13 +687,28 @@ public class TrackListActivity extends FragmentActivity implements DeleteOneTrac
bindChangedCallback.run();
}
/**
* Starts the {@link ImportActivity} to import all tracks.
*
* @param trackFileFormat the track file format
*/
private void startImportActivity(TrackFileFormat trackFileFormat) {
AnalyticsUtils.sendPageViews(
this, "/action/import_all_" + trackFileFormat.name().toLowerCase(Locale.US));
Intent intent = IntentUtils.newIntent(this, ImportActivity.class)
.putExtra(ImportActivity.EXTRA_IMPORT_ALL, true)
.putExtra(ImportActivity.EXTRA_TRACK_FILE_FORMAT, (Parcelable) trackFileFormat);
startActivity(intent);
}
/**
* Starts the {@link SaveActivity} to save all tracks.
*
* @param trackFileFormat the track file format
*/
private void startSaveActivity(TrackFileFormat trackFileFormat) {
AnalyticsUtils.sendPageViews(this, "/action/save_all");
AnalyticsUtils.sendPageViews(
this, "/action/save_all_" + trackFileFormat.name().toLowerCase(Locale.US));
Intent intent = IntentUtils.newIntent(this, SaveActivity.class)
.putExtra(SaveActivity.EXTRA_TRACK_FILE_FORMAT, (Parcelable) trackFileFormat);
startActivity(intent);
@@ -80,8 +80,8 @@ public class BackupAsyncTask extends AsyncTask<Void, Integer, Boolean> {
@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;
}
@@ -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;
}
}
@@ -59,7 +59,7 @@ public class RestoreAsyncTask extends AsyncTask<Void, Integer, Boolean> {
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<Void, Integer, Boolean> {
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);
@@ -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;
}
@@ -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<Long> trackIds;
private final List<Waypoint> 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<Long>();
waypoints = new ArrayList<Waypoint>();
}
@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;
}
}
@@ -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<Long> 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<Waypoint> waypoints = new ArrayList<Waypoint>();
/*
* 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<Long>();
@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);
}
}
@@ -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
*/
@@ -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<Void, Integer, Boolean> {
@@ -48,20 +40,17 @@ public class ImportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
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<Void, Integer, Boolean> {
/**
* 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<Void, Integer, Boolean> {
wakeLock = SystemUtils.acquireWakeLock(importActivity, wakeLock);
}
success = false;
completed = false;
successCount = 0;
totalCount = 0;
@@ -98,13 +87,13 @@ public class ImportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
/**
* 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<Void, Integer, Boolean> {
@Override
protected Boolean doInBackground(Void... params) {
try {
if (!FileUtils.isSdCardAvailable()) {
return false;
}
List<File> files = getFiles();
totalCount = files.size();
if (totalCount == 0) {
@@ -133,8 +118,7 @@ public class ImportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
// 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<Void, Integer, Boolean> {
@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<Void, Integer, Boolean> {
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);
}
}
@@ -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<Location> locationList;
private ArrayList<Integer> cadenceList;
private ArrayList<Integer> heartRateList;
private ArrayList<Integer> 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<Location>();
powerList = new ArrayList<Integer>();
cadenceList = new ArrayList<Integer>();
heartRateList = new ArrayList<Integer>();
}
/**
* 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);
}
}
}
}
@@ -303,6 +303,8 @@ public class KmlTrackWriter implements TrackFormatWriter {
printWriter.println("<name>" + StringUtils.formatCData(name) + "</name>");
printWriter.println(
"<description>" + StringUtils.formatCData(description) + "</description>");
printWriter.println("<TimeStamp><when>"
+ StringUtils.formatDateTimeIso8601(location.getTime()) + "</when></TimeStamp>");
printWriter.println("<styleUrl>#" + styleName + "</styleUrl>");
writeCategory(category);
printWriter.println("<Point>");
@@ -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;
}
@@ -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();
}
/**