wrapper,
+ String message) {
+ return new IOException(String.format("%s: %d: %s", message,
+ wrapper.getErrorType(), wrapper.getErrorMessage()));
+ }
+}
diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/docs/DocsTagBuilder.java b/MyTracks/src/com/google/android/apps/mytracks/io/docs/DocsTagBuilder.java
new file mode 100644
index 000000000..b20ae2eb6
--- /dev/null
+++ b/MyTracks/src/com/google/android/apps/mytracks/io/docs/DocsTagBuilder.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright 2010 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.android.apps.mytracks.io.docs;
+
+import com.google.android.apps.mytracks.util.StringUtils;
+import com.google.android.apps.mytracks.util.UnitConversions;
+
+import java.text.DecimalFormat;
+import java.text.DecimalFormatSymbols;
+import java.text.NumberFormat;
+import java.util.Locale;
+
+/**
+ * This class builds a string of XML tags used to talk to Docs using GData.
+ *
+ *
Sample Usage:
+ *
+ *
+ * String tags = new DocsTagBuilder(false)
+ * .append("tagName", "tagValue")
+ * .appendLargeUnits("bigTagName", 1.0)
+ * .build();
+ *
+ *
+ *
results in:
+ *
+ *
+ *
+ *
+ *
+ *
+ * @author Matthew Simmons
+ */
+class DocsTagBuilder {
+ private static final DecimalFormatSymbols FORMAT_SYMBOLS =
+ DecimalFormatSymbols.getInstance(Locale.ENGLISH);
+ private static final NumberFormat LARGE_UNIT_FORMAT =
+ new DecimalFormat("#,###,###.00", FORMAT_SYMBOLS);
+ private static final NumberFormat SMALL_UNIT_FORMAT =
+ new DecimalFormat("###,###", FORMAT_SYMBOLS);
+
+ protected final boolean metricUnits;
+ protected final StringBuilder stringBuilder;
+
+ /**
+ * @param metricUnits True if metric units are to be used. If false,
+ * imperial units will be used.
+ */
+ DocsTagBuilder(boolean metricUnits) {
+ this.metricUnits = metricUnits;
+ this.stringBuilder = new StringBuilder();
+ }
+
+ /** Appends a tag containing a string value */
+ DocsTagBuilder append(String name, String value) {
+ appendTag(name, value);
+ return this;
+ }
+
+ /**
+ * Appends a tag containing a numeric value. The value will be formatted
+ * according to the large distance of the specified measurement system (i.e.
+ * kilometers or miles).
+ *
+ * @param name The tag name.
+ * @param d The value to be formatted, in kilometers.
+ */
+ DocsTagBuilder appendLargeUnits(String name, double d) {
+ double value = metricUnits ? d : (d * UnitConversions.KM_TO_MI);
+ appendTag(name, LARGE_UNIT_FORMAT.format(value));
+ return this;
+ }
+
+ /**
+ * Appends a tag containing a numeric value. The value will be formatted
+ * according to the small distance of the specified measurement system (i.e.
+ * meters or feet).
+ *
+ * @param name The tag name.
+ * @param d The value to be formatted, in meters.
+ */
+ DocsTagBuilder appendSmallUnits(String name, double d) {
+ double value = metricUnits ? d : (d * UnitConversions.M_TO_FT);
+ appendTag(name, SMALL_UNIT_FORMAT.format(value));
+ return this;
+ }
+
+ /** Returns a string containing all tags which have been added. */
+ String build() {
+ return stringBuilder.toString();
+ }
+
+ private void appendTag(String name, String value) {
+ stringBuilder.append("")
+ .append(StringUtils.stringAsCData(value))
+ .append("");
+ }
+}
diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/CsvTrackWriter.java b/MyTracks/src/com/google/android/apps/mytracks/io/file/CsvTrackWriter.java
similarity index 88%
rename from MyTracks/src/com/google/android/apps/mytracks/io/CsvTrackWriter.java
rename to MyTracks/src/com/google/android/apps/mytracks/io/file/CsvTrackWriter.java
index 22797b4af..698557e96 100644
--- a/MyTracks/src/com/google/android/apps/mytracks/io/CsvTrackWriter.java
+++ b/MyTracks/src/com/google/android/apps/mytracks/io/file/CsvTrackWriter.java
@@ -1,28 +1,29 @@
/*
* Copyright 2010 Google Inc.
- *
+ *
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
-package com.google.android.apps.mytracks.io;
+package com.google.android.apps.mytracks.io.file;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.content.Waypoint;
-import com.google.android.apps.mytracks.io.TrackWriterFactory.TrackFileFormat;
+import com.google.android.apps.mytracks.io.file.TrackWriterFactory.TrackFileFormat;
import android.location.Location;
import java.io.OutputStream;
import java.io.PrintWriter;
+import java.text.NumberFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.TimeZone;
@@ -44,9 +45,13 @@ import java.util.TimeZone;
* @author Rodrigo Damazio
*/
public class CsvTrackWriter implements TrackFormatWriter {
+
+ static final NumberFormat SHORT_FORMAT = NumberFormat.getInstance();
static final SimpleDateFormat TIMESTAMP_FORMAT =
new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
+
static {
+ SHORT_FORMAT.setMaximumFractionDigits(4);
TIMESTAMP_FORMAT.setTimeZone(TimeZone.getTimeZone("UTC"));
}
@@ -60,6 +65,7 @@ public class CsvTrackWriter implements TrackFormatWriter {
return TrackFileFormat.CSV.getExtension();
}
+ @SuppressWarnings("hiding")
@Override
public void prepare(Track track, OutputStream out) {
this.track = track;
@@ -94,8 +100,8 @@ public class CsvTrackWriter implements TrackFormatWriter {
Double.toString(location.getLongitude()),
Double.toString(location.getAltitude()),
Double.toString(location.getBearing()),
- Double.toString(location.getAccuracy()),
- Double.toString(location.getSpeed()),
+ SHORT_FORMAT.format(location.getAccuracy()),
+ SHORT_FORMAT.format(location.getSpeed()),
null, null,
Integer.toString(segmentIdx));
}
@@ -110,8 +116,8 @@ public class CsvTrackWriter implements TrackFormatWriter {
Double.toString(location.getLongitude()),
Double.toString(location.getAltitude()),
Double.toString(location.getBearing()),
- Double.toString(location.getAccuracy()),
- Double.toString(location.getSpeed()),
+ SHORT_FORMAT.format(location.getAccuracy()),
+ SHORT_FORMAT.format(location.getSpeed()),
waypoint.getName(),
waypoint.getDescription(),
null);
diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/GpxImporter.java b/MyTracks/src/com/google/android/apps/mytracks/io/file/GpxImporter.java
similarity index 89%
rename from MyTracks/src/com/google/android/apps/mytracks/io/GpxImporter.java
rename to MyTracks/src/com/google/android/apps/mytracks/io/file/GpxImporter.java
index 571859cc9..66afa09e1 100644
--- a/MyTracks/src/com/google/android/apps/mytracks/io/GpxImporter.java
+++ b/MyTracks/src/com/google/android/apps/mytracks/io/file/GpxImporter.java
@@ -13,13 +13,13 @@
* License for the specific language governing permissions and limitations under
* the License.
*/
-package com.google.android.apps.mytracks.io;
+package com.google.android.apps.mytracks.io.file;
-import com.google.android.apps.mytracks.MyTracksConstants;
+import com.google.android.apps.mytracks.Constants;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.stats.TripStatisticsBuilder;
-import com.google.android.apps.mytracks.util.MyTracksUtils;
+import com.google.android.apps.mytracks.util.LocationUtils;
import com.google.android.apps.mytracks.util.StringUtils;
import android.location.Location;
@@ -174,7 +174,7 @@ public class GpxImporter extends DefaultHandler {
parser.parse(is, handler);
long end = System.currentTimeMillis();
- Log.d(MyTracksConstants.TAG, "Total import time: " + (end - start) + "ms");
+ Log.d(Constants.TAG, "Total import time: " + (end - start) + "ms");
trackIds = handler.getImportedTrackIds();
} finally {
@@ -229,7 +229,10 @@ public class GpxImporter extends DefaultHandler {
@Override
public void endElement(String uri, String localName, String name)
throws SAXException {
- if (!isInTrackElement) { return; }
+ if (!isInTrackElement) {
+ content = null;
+ return;
+ }
// process these elements only as sub-elements of track
if (localName.equals(TAG_TRACK_POINT)) {
@@ -342,8 +345,14 @@ public class GpxImporter extends DefaultHandler {
// create new location and set attributes
Location loc = new Location(LocationManager.GPS_PROVIDER);
- loc.setLatitude(Double.parseDouble(latitude));
- loc.setLongitude(Double.parseDouble(longitude));
+ try {
+ loc.setLatitude(Double.parseDouble(latitude));
+ loc.setLongitude(Double.parseDouble(longitude));
+ } catch (NumberFormatException e) {
+ String msg = createErrorMessage(
+ "Unable to parse lat/long: " + latitude + "/" + longitude);
+ throw new SAXException(msg, e);
+ }
return loc;
}
@@ -353,7 +362,11 @@ public class GpxImporter extends DefaultHandler {
* @throws SAXException - thrown if track point is invalid
*/
private void onTrackPointElementEnd() throws SAXException {
- if (MyTracksUtils.isValidLocation(location)) {
+ if (LocationUtils.isValidLocation(location)) {
+ if (statsBuilder == null) {
+ // first point did not have a time, start stats builder without it
+ statsBuilder = new TripStatisticsBuilder(0);
+ }
statsBuilder.addLocation(location, location.getTime());
// insert in db
@@ -449,18 +462,18 @@ public class GpxImporter extends DefaultHandler {
// check for negative time change
if (timeDifference < 0) {
- String msg = createErrorMessage("Found negative time change.");
- throw new SAXException(msg);
- }
+ Log.w(Constants.TAG, "Found negative time change.");
+ } else {
- // We don't have a speed and bearing in GPX, make something up from
- // the last two points.
- // TODO 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 = location.distanceTo(lastLocation) * 1000.0f / timeDifference;
- location.setSpeed(speed);
- location.setBearing(lastSegmentLocation.bearingTo(location));
+ // We don't have a speed and bearing in GPX, make something up from
+ // the last two points.
+ // TODO 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 = location.distanceTo(lastLocation) * 1000.0f / timeDifference;
+ location.setSpeed(speed);
+ location.setBearing(lastSegmentLocation.bearingTo(location));
+ }
}
// Fill in the time
@@ -472,9 +485,14 @@ public class GpxImporter extends DefaultHandler {
}
}
- private void onAltitudeElementEnd() {
+ private void onAltitudeElementEnd() throws SAXException {
if (location != null) {
- location.setAltitude(Double.parseDouble(content));
+ try {
+ location.setAltitude(Double.parseDouble(content));
+ } catch (NumberFormatException e) {
+ String msg = createErrorMessage("Unable to parse altitude: " + content);
+ throw new SAXException(msg, e);
+ }
}
}
diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/GpxTrackWriter.java b/MyTracks/src/com/google/android/apps/mytracks/io/file/GpxTrackWriter.java
similarity index 59%
rename from MyTracks/src/com/google/android/apps/mytracks/io/GpxTrackWriter.java
rename to MyTracks/src/com/google/android/apps/mytracks/io/file/GpxTrackWriter.java
index 6054dc9a5..6c3c3aef9 100644
--- a/MyTracks/src/com/google/android/apps/mytracks/io/GpxTrackWriter.java
+++ b/MyTracks/src/com/google/android/apps/mytracks/io/file/GpxTrackWriter.java
@@ -1,31 +1,35 @@
/*
* Copyright 2008 Google Inc.
- *
+ *
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
-package com.google.android.apps.mytracks.io;
+package com.google.android.apps.mytracks.io.file;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.content.Waypoint;
-import com.google.android.apps.mytracks.io.TrackWriterFactory.TrackFileFormat;
+import com.google.android.apps.mytracks.io.file.TrackWriterFactory.TrackFileFormat;
import com.google.android.apps.mytracks.util.StringUtils;
import android.location.Location;
+import android.os.Build;
import java.io.OutputStream;
import java.io.PrintWriter;
+import java.nio.charset.Charset;
+import java.text.NumberFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
+import java.util.Locale;
import java.util.TimeZone;
/**
@@ -34,16 +38,36 @@ import java.util.TimeZone;
* @author Sandor Dornbush
*/
public class GpxTrackWriter implements TrackFormatWriter {
+ private static final String TIMESTAMP_FORMAT = "yyyy-MM-dd'T'HH:mm:ss'Z'";
- static final SimpleDateFormat TIMESTAMP_FORMAT =
- new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
- static {
- TIMESTAMP_FORMAT.setTimeZone(TimeZone.getTimeZone("UTC"));
- }
-
+ private final NumberFormat elevationFormatter;
+ private final NumberFormat coordinateFormatter;
+ private final SimpleDateFormat timestampFormatter;
private PrintWriter pw = null;
private Track track;
+ public GpxTrackWriter() {
+ // GPX readers expect to see fractional numbers with US-style punctuation.
+ // That is, they want periods for decimal points, rather than commas.
+ elevationFormatter = NumberFormat.getInstance(Locale.US);
+ elevationFormatter.setMaximumFractionDigits(1);
+ elevationFormatter.setGroupingUsed(false);
+
+ coordinateFormatter = NumberFormat.getInstance(Locale.US);
+ coordinateFormatter.setMaximumFractionDigits(5);
+ coordinateFormatter.setMaximumIntegerDigits(3);
+ coordinateFormatter.setGroupingUsed(false);
+
+ timestampFormatter = new SimpleDateFormat(TIMESTAMP_FORMAT);
+ timestampFormatter.setTimeZone(TimeZone.getTimeZone("UTC"));
+ }
+
+ private String formatLocation(Location l) {
+ return "lat=\"" + coordinateFormatter.format(l.getLatitude())
+ + "\" lon=\"" + coordinateFormatter.format(l.getLongitude()) + "\"";
+ }
+
+ @SuppressWarnings("hiding")
@Override
public void prepare(Track track, OutputStream out) {
this.track = track;
@@ -58,18 +82,18 @@ public class GpxTrackWriter implements TrackFormatWriter {
@Override
public void writeHeader() {
if (pw != null) {
- pw.println("");
+ pw.format("\n",
+ Charset.defaultCharset().name());
pw.println("");
pw.println("");
@@ -93,7 +117,7 @@ public class GpxTrackWriter implements TrackFormatWriter {
pw.println("" + StringUtils.stringAsCData(track.getDescription())
+ "");
pw.println("" + track.getId() + "");
- pw.println("c0c0c0");
+ pw.println("c0c0c0");
}
}
@@ -117,11 +141,10 @@ public class GpxTrackWriter implements TrackFormatWriter {
@Override
public void writeLocation(Location l) {
if (pw != null) {
- pw.println("");
+ pw.println("");
Date d = new Date(l.getTime());
- pw.println("" + l.getAltitude() + "");
- pw.println("");
+ pw.println("" + elevationFormatter.format(l.getAltitude()) + "");
+ pw.println("");
pw.println("");
}
}
@@ -137,17 +160,15 @@ public class GpxTrackWriter implements TrackFormatWriter {
@Override
public void writeWaypoint(Waypoint waypoint) {
if (pw != null) {
- // TODO: The gpx spec says waypoints should come *before* tracks
Location l = waypoint.getLocation();
if (l != null) {
- pw.println("");
+ pw.println("");
+ pw.println("" + elevationFormatter.format(l.getAltitude()) + "");
+ pw.println("");
pw.println("" + StringUtils.stringAsCData(waypoint.getName())
+ "");
pw.println(""
+ StringUtils.stringAsCData(waypoint.getDescription()) + "");
- pw.println("");
- pw.println("" + l.getAltitude() + "");
pw.println("");
}
}
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
new file mode 100644
index 000000000..1ca2b8061
--- /dev/null
+++ b/MyTracks/src/com/google/android/apps/mytracks/io/file/ImportActivity.java
@@ -0,0 +1,216 @@
+/*
+ * Copyright 2011 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.android.apps.mytracks.io.file;
+
+import static com.google.android.apps.mytracks.Constants.TAG;
+
+import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
+import com.google.android.apps.mytracks.content.TracksColumns;
+import com.google.android.apps.mytracks.util.UriUtils;
+import com.google.android.maps.mytracks.R;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.AlertDialog.Builder;
+import android.app.Dialog;
+import android.app.ProgressDialog;
+import android.content.ContentUris;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.util.Log;
+
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+import javax.xml.parsers.ParserConfigurationException;
+
+import org.xml.sax.SAXException;
+
+/**
+ * Helper activity which imports tracks from a file.
+ *
+ * @author Rodrigo Damazio
+ */
+public class ImportActivity extends Activity {
+
+ private static final int PROGRESS_DIALOG = 1;
+ private static final int SUCCESS_DIALOG = 2;
+ private static final int FAILURE_DIALOG = 3;
+
+ private MyTracksProviderUtils providerUtils;
+
+ private ProgressDialog progressDialog;
+ private int resultMessage;
+ private long importedTrackIds[];
+
+ @Override
+ public void onCreate(Bundle savedState) {
+ super.onCreate(savedState);
+
+ providerUtils = MyTracksProviderUtils.Factory.get(this);
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+
+ Intent intent = getIntent();
+ String action = intent.getAction();
+ Uri data = intent.getData();
+ if (!(Intent.ACTION_VIEW.equals(action) || Intent.ACTION_ATTACH_DATA.equals(action))
+ || !UriUtils.isFileUri(data)) {
+ Log.e(TAG, "Received an intent with unsupported action or data: " + intent);
+ finish();
+ return;
+ }
+
+ Log.i(TAG, "Importing GPX file at " + data);
+ startTrackImport(data.getPath());
+ }
+
+ @Override
+ protected Dialog onCreateDialog(int id) {
+ switch (id) {
+ case PROGRESS_DIALOG:
+ progressDialog = new ProgressDialog(this);
+ progressDialog.setIcon(android.R.drawable.ic_dialog_info);
+ progressDialog.setTitle(R.string.progress_title);
+ progressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER);
+ progressDialog.setMessage(getString(R.string.import_progress_message));
+ return progressDialog;
+ case SUCCESS_DIALOG:
+ final Builder dialogBuilder = new AlertDialog.Builder(this);
+ dialogBuilder.setCancelable(true);
+ dialogBuilder.setPositiveButton(android.R.string.ok, new OnClickListener() {
+ @Override
+ public void onClick(DialogInterface arg0, int arg1) {
+ finish();
+ }
+ });
+ dialogBuilder.setNeutralButton(R.string.import_show_track, new OnClickListener() {
+ @Override
+ public void onClick(DialogInterface arg0, int arg1) {
+ showImportedTrack();
+ finish();
+ }
+ });
+ return dialogBuilder.create();
+ case FAILURE_DIALOG:
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setNeutralButton(R.string.ok, new OnClickListener() {
+ @Override
+ public void onClick(DialogInterface arg0, int arg1) {
+ finish();
+ }
+ });
+ builder.setIcon(android.R.drawable.ic_dialog_alert);
+ builder.setTitle(R.string.error);
+ return builder.create();
+
+ }
+
+ return null;
+ }
+
+ @Override
+ protected void onPrepareDialog(int id, Dialog dialog) {
+ super.onPrepareDialog(id, dialog);
+
+ switch (id) {
+ case SUCCESS_DIALOG:
+ AlertDialog successDialog = (AlertDialog) dialog;
+ successDialog.setMessage(getString(R.string.import_success, importedTrackIds.length));
+ break;
+ case FAILURE_DIALOG:
+ AlertDialog failureDialog = (AlertDialog) dialog;
+ failureDialog.setMessage(getString(resultMessage));
+ break;
+ }
+ }
+
+ protected void showImportedTrack() {
+ long lastTrackId = importedTrackIds[importedTrackIds.length - 1];
+ Uri trackUri = ContentUris.withAppendedId(TracksColumns.CONTENT_URI, lastTrackId);
+
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ intent.setDataAndType(trackUri, TracksColumns.CONTENT_ITEMTYPE);
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ startActivity(intent);
+ finish();
+ }
+
+ private void startTrackImport(final String fileName) {
+ showDialog(PROGRESS_DIALOG);
+
+ Thread t = new Thread() {
+ @Override
+ public void run() {
+ resultMessage = R.string.success;
+
+ importedTrackIds = null;
+
+ try {
+ try {
+ InputStream is = new FileInputStream(fileName);
+ importedTrackIds = GpxImporter.importGPXFile(is, providerUtils);
+ } catch (SAXException e) {
+ Log.e(TAG, "Caught an unexpected exception.", e);
+ resultMessage = R.string.error_generic;
+ } catch (ParserConfigurationException e) {
+ Log.e(TAG, "Caught an unexpected exception.", e);
+ resultMessage = R.string.error_generic;
+ } catch (IOException e) {
+ Log.e(TAG, "Caught an unexpected exception.", e);
+ resultMessage = R.string.error_unable_to_read_file;
+ } catch (NullPointerException e) {
+ Log.e(TAG, "Caught an unexpected exception.", e);
+ resultMessage = R.string.error_invalid_gpx_format;
+ } catch (OutOfMemoryError e) {
+ Log.e(TAG, "Caught an unexpected exception.", e);
+ resultMessage = R.string.error_out_of_memory;
+ }
+
+ boolean success = (importedTrackIds != null && importedTrackIds.length > 0);
+ showImportResult(success);
+ } finally {
+ runOnUiThread(new Runnable() {
+ public void run() {
+ dismissDialog(PROGRESS_DIALOG);
+ }
+ });
+ }
+ }
+ };
+ t.start();
+ }
+
+ private void showImportResult(boolean success) {
+ final int dialogToShow = success ? SUCCESS_DIALOG : FAILURE_DIALOG;
+
+ // Show a dialog telling the user about the import, and asking if he wishes
+ // to open the track right away.
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ showDialog(dialogToShow);
+ }
+ });
+ }
+}
diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/KmlTrackWriter.java b/MyTracks/src/com/google/android/apps/mytracks/io/file/KmlTrackWriter.java
similarity index 95%
rename from MyTracks/src/com/google/android/apps/mytracks/io/KmlTrackWriter.java
rename to MyTracks/src/com/google/android/apps/mytracks/io/file/KmlTrackWriter.java
index 8ee29c1ee..5e893920c 100644
--- a/MyTracks/src/com/google/android/apps/mytracks/io/KmlTrackWriter.java
+++ b/MyTracks/src/com/google/android/apps/mytracks/io/file/KmlTrackWriter.java
@@ -1,27 +1,28 @@
/*
* Copyright 2008 Google Inc.
- *
+ *
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
-package com.google.android.apps.mytracks.io;
+package com.google.android.apps.mytracks.io.file;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.content.Waypoint;
-import com.google.android.apps.mytracks.io.TrackWriterFactory.TrackFileFormat;
+import com.google.android.apps.mytracks.io.file.TrackWriterFactory.TrackFileFormat;
import com.google.android.apps.mytracks.util.StringUtils;
import android.content.Context;
import android.location.Location;
+import android.os.Build;
import java.io.OutputStream;
import java.io.PrintWriter;
@@ -51,6 +52,7 @@ public class KmlTrackWriter implements TrackFormatWriter {
this.stringUtils = stringUtils;
}
+ @SuppressWarnings("hiding")
@Override
public void prepare(Track track, OutputStream out) {
this.track = track;
@@ -70,8 +72,9 @@ public class KmlTrackWriter implements TrackFormatWriter {
pw.print(" xmlns=\"http://earth.google.com/kml/2.0\"");
pw.println(" xmlns:atom=\"http://www.w3.org/2005/Atom\">");
pw.println("");
- pw.println("My Tracks running on Android"
- + "");
+ pw.format("My Tracks running on %s"
+ + "\n", Build.MODEL);
+
pw.println("" + StringUtils.stringAsCData(track.getName())
+ "");
pw.println(""
diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/file/SaveActivity.java b/MyTracks/src/com/google/android/apps/mytracks/io/file/SaveActivity.java
new file mode 100644
index 000000000..818dcbdd2
--- /dev/null
+++ b/MyTracks/src/com/google/android/apps/mytracks/io/file/SaveActivity.java
@@ -0,0 +1,236 @@
+/*
+ * Copyright 2011 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.android.apps.mytracks.io.file;
+
+import static com.google.android.apps.mytracks.Constants.TAG;
+
+import com.google.android.apps.mytracks.Constants;
+import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
+import com.google.android.apps.mytracks.content.TracksColumns;
+import com.google.android.apps.mytracks.io.file.TrackWriterFactory.TrackFileFormat;
+import com.google.android.apps.mytracks.util.FileUtils;
+import com.google.android.apps.mytracks.util.UriUtils;
+import com.google.android.maps.mytracks.R;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.ContentUris;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnCancelListener;
+import android.content.DialogInterface.OnClickListener;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.util.Log;
+
+import java.io.File;
+
+/**
+ * Activity for saving a track to a file (and optionally sending that file).
+ *
+ * @author Rodrigo Damazio
+ */
+public class SaveActivity extends Activity {
+ public static final String EXTRA_SHARE_FILE = "share_file";
+ public static final String EXTRA_FILE_FORMAT = "file_format";
+ private static final int RESULT_DIALOG = 1;
+ /* VisibleForTesting */
+ static final int PROGRESS_DIALOG = 2;
+
+ private MyTracksProviderUtils providerUtils;
+ private long trackId;
+ private TrackWriter writer;
+ private boolean shareFile;
+ private TrackFileFormat format;
+ private WriteProgressController controller;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ providerUtils = MyTracksProviderUtils.Factory.get(this);
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+
+ Intent intent = getIntent();
+ String action = intent.getAction();
+ String type = intent.getType();
+ Uri data = intent.getData();
+ if (!getString(R.string.save_intent_action).equals(action) ||
+ !TracksColumns.CONTENT_ITEMTYPE.equals(type) ||
+ !UriUtils.matchesContentUri(data, TracksColumns.CONTENT_URI)) {
+ Log.e(TAG, "Got bad save intent: " + intent);
+ finish();
+ return;
+ }
+
+ trackId = ContentUris.parseId(data);
+
+ int formatIdx = intent.getIntExtra(EXTRA_FILE_FORMAT, -1);
+ format = TrackFileFormat.values()[formatIdx];
+ shareFile = intent.getBooleanExtra(EXTRA_SHARE_FILE, false);
+
+ writer = TrackWriterFactory.newWriter(this, providerUtils, trackId, format);
+ if (writer == null) {
+ Log.e(TAG, "Unable to build writer");
+ finish();
+ return;
+ }
+
+ if (shareFile) {
+ // If the file is for sending, save it to a temporary location instead.
+ FileUtils fileUtils = new FileUtils();
+ String extension = format.getExtension();
+ String dirName = fileUtils.buildExternalDirectoryPath(extension, "tmp");
+
+ File dir = new File(dirName);
+ writer.setDirectory(dir);
+ }
+
+ controller = new WriteProgressController(this, writer, PROGRESS_DIALOG);
+ controller.setOnCompletionListener(new WriteProgressController.OnCompletionListener() {
+ @Override
+ public void onComplete() {
+ onWriteComplete();
+ }
+ });
+ controller.startWrite();
+ }
+
+ private void onWriteComplete() {
+ if (shareFile) {
+ shareWrittenFile();
+ } else {
+ showResultDialog();
+ }
+ }
+
+ private void shareWrittenFile() {
+ if (!writer.wasSuccess()) {
+ showResultDialog();
+ return;
+ }
+
+ // Share the file.
+ Intent shareIntent = new Intent(Intent.ACTION_SEND);
+ shareIntent.putExtra(Intent.EXTRA_SUBJECT,
+ getResources().getText(R.string.send_track_subject).toString());
+ shareIntent.putExtra(Intent.EXTRA_TEXT,
+ getResources().getText(R.string.send_track_body_format)
+ .toString());
+ shareIntent.setType(format.getMimeType());
+ Uri u = Uri.fromFile(new File(writer.getAbsolutePath()));
+ shareIntent.putExtra(Intent.EXTRA_STREAM, u);
+ startActivity(Intent.createChooser(shareIntent,
+ getResources().getText(R.string.share_track).toString()));
+ }
+
+ private void showResultDialog() {
+ removeDialog(RESULT_DIALOG);
+ showDialog(RESULT_DIALOG);
+ }
+
+ @Override
+ protected Dialog onCreateDialog(int id) {
+ switch (id) {
+ case RESULT_DIALOG:
+ return createResultDialog();
+ case PROGRESS_DIALOG:
+ if (controller != null) {
+ return controller.createProgressDialog();
+ }
+ //$FALL-THROUGH$
+ default:
+ return super.onCreateDialog(id);
+ }
+ }
+
+ private Dialog createResultDialog() {
+ boolean success = writer.wasSuccess();
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setMessage(writer.getErrorMessage());
+ builder.setNeutralButton(R.string.ok, new OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int arg1) {
+ dialog.dismiss();
+ finish();
+ }
+ });
+ builder.setOnCancelListener(new OnCancelListener() {
+ @Override
+ public void onCancel(DialogInterface dialog) {
+ dialog.dismiss();
+ finish();
+ }
+ });
+ builder.setIcon(success ? android.R.drawable.ic_dialog_info :
+ android.R.drawable.ic_dialog_alert);
+ builder.setTitle(success ? R.string.success : R.string.error);
+ return builder.create();
+ }
+
+ public static void handleExportTrackAction(Context ctx, long trackId, int actionCode) {
+ if (trackId < 0) {
+ return;
+ }
+
+ TrackFileFormat exportFormat = null;
+ switch (actionCode) {
+ case Constants.SAVE_GPX_FILE:
+ case Constants.SHARE_GPX_FILE:
+ exportFormat = TrackFileFormat.GPX;
+ break;
+ case Constants.SAVE_KML_FILE:
+ case Constants.SHARE_KML_FILE:
+ exportFormat = TrackFileFormat.KML;
+ break;
+ case Constants.SAVE_CSV_FILE:
+ case Constants.SHARE_CSV_FILE:
+ exportFormat = TrackFileFormat.CSV;
+ break;
+ case Constants.SAVE_TCX_FILE:
+ case Constants.SHARE_TCX_FILE:
+ exportFormat = TrackFileFormat.TCX;
+ break;
+ default:
+ throw new IllegalArgumentException("Warning unhandled action code: " + actionCode);
+ }
+
+ boolean shareFile = false;
+ switch (actionCode) {
+ case Constants.SHARE_GPX_FILE:
+ case Constants.SHARE_KML_FILE:
+ case Constants.SHARE_CSV_FILE:
+ case Constants.SHARE_TCX_FILE:
+ shareFile = true;
+ }
+
+ Uri uri = ContentUris.withAppendedId(TracksColumns.CONTENT_URI, trackId);
+
+ Intent intent = new Intent(ctx, SaveActivity.class);
+ intent.setAction(ctx.getString(R.string.save_intent_action));
+ intent.setDataAndType(uri, TracksColumns.CONTENT_ITEMTYPE);
+ intent.putExtra(EXTRA_FILE_FORMAT, exportFormat.ordinal());
+ intent.putExtra(EXTRA_SHARE_FILE, shareFile);
+ ctx.startActivity(intent);
+ }
+}
diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/file/TcxTrackWriter.java b/MyTracks/src/com/google/android/apps/mytracks/io/file/TcxTrackWriter.java
new file mode 100644
index 000000000..49961fbae
--- /dev/null
+++ b/MyTracks/src/com/google/android/apps/mytracks/io/file/TcxTrackWriter.java
@@ -0,0 +1,319 @@
+/*
+ * Copyright 2010 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.android.apps.mytracks.io.file;
+
+import com.google.android.apps.mytracks.content.MyTracksLocation;
+import com.google.android.apps.mytracks.content.Sensor;
+import com.google.android.apps.mytracks.content.Sensor.SensorDataSet;
+import com.google.android.apps.mytracks.content.Track;
+import com.google.android.apps.mytracks.content.Waypoint;
+import com.google.android.apps.mytracks.io.file.TrackWriterFactory.TrackFileFormat;
+import com.google.android.apps.mytracks.util.SystemUtils;
+
+import android.content.Context;
+import android.location.Location;
+import android.os.Build;
+
+import java.io.OutputStream;
+import java.io.PrintWriter;
+import java.nio.charset.Charset;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+import java.util.TimeZone;
+
+/**
+ * Write out a a track in the Garmin training center database, tcx format.
+ * As defined by:
+ * http://www.garmin.com/xmlschemas/TrainingCenterDatabase/v2
+ *
+ * The TCX file written by this class has been verified as compatible with
+ * Garmin Training Center 3.5.3.
+ *
+ * @author Sandor Dornbush
+ * @author Dominik Ršttsches
+ */
+public class TcxTrackWriter implements TrackFormatWriter {
+ protected static final String TIMESTAMP_FORMAT = "yyyy-MM-dd'T'HH:mm:ss'Z'";
+
+ // These are the only sports allowed by the TCX v2 specification for fields
+ // of type Sport_t.
+ private static final String TCX_SPORT_BIKING = "Biking";
+ private static final String TCX_SPORT_RUNNING = "Running";
+ private static final String TCX_SPORT_OTHER = "Other";
+
+ // Values for fields of type Build_t/Type.
+ private static final String TCX_TYPE_RELEASE = "Release";
+ private static final String TCX_TYPE_INTERNAL = "Internal";
+
+ private final SimpleDateFormat timestampFormatter;
+ private final Context context;
+
+ private PrintWriter pw = null;
+ private Track track;
+
+ // Determines whether to encode cadence value as running or cycling cadence.
+ private boolean sportIsCycling;
+
+ public TcxTrackWriter(Context context) {
+ this.context = context;
+
+ timestampFormatter = new SimpleDateFormat(TIMESTAMP_FORMAT);
+ timestampFormatter.setTimeZone(TimeZone.getTimeZone("UTC"));
+ }
+
+ @SuppressWarnings("hiding")
+ @Override
+ public void prepare(Track track, OutputStream out) {
+ this.track = track;
+ this.pw = new PrintWriter(out);
+ this.sportIsCycling = categoryToTcxSport(track.getCategory()).equals(TCX_SPORT_BIKING);
+ }
+
+ @Override
+ public void close() {
+ if (pw != null) {
+ pw.close();
+ pw = null;
+ }
+ }
+
+ @Override
+ public String getExtension() {
+ return TrackFileFormat.TCX.getExtension();
+ }
+
+ @Override
+ public void writeHeader() {
+ if (pw == null) {
+ return;
+ }
+ pw.format("\n",
+ Charset.defaultCharset().name());
+ pw.print("");
+ pw.println();
+ }
+
+ @Override
+ public void writeBeginTrack(Location firstPoint) {
+ if (pw == null) {
+ return;
+ }
+
+ String startTime = timestampFormatter.format(track.getStatistics().getStartTime());
+
+ pw.println(" ");
+ pw.format(" \n", categoryToTcxSport(track.getCategory()));
+ pw.format(" %s\n", startTime);
+ pw.format(" \n", startTime);
+ pw.print(" ");
+ pw.print(track.getStatistics().getTotalTime() / 1000);
+ pw.println("");
+ pw.print(" ");
+ pw.print(track.getStatistics().getTotalDistance());
+ pw.println("");
+ // TODO max speed etc.
+ // Calories are a required element just put in 0.
+ pw.print("0");
+ pw.println("Active");
+ pw.println("Manual");
+ }
+
+ @Override
+ public void writeOpenSegment() {
+ if (pw != null) {
+ pw.println(" ");
+ }
+ }
+
+ @Override
+ public void writeEndTrack(Location lastPoint) {
+ if (pw == null) {
+ return;
+ }
+ pw.println(" ");
+ pw.print(" ");
+ pw.format("My Tracks running on %s\n", Build.MODEL);
+
+ // The following code is correct. ID is inconsistently capitalized in the
+ // TCX schema.
+ pw.println("0");
+ pw.println("0");
+
+ writeVersion();
+
+ pw.println("");
+ pw.println(" ");
+ pw.println(" ");
+ }
+
+ @Override
+ public void writeFooter() {
+ if (pw == null) {
+ return;
+ }
+ pw.println(" ");
+
+ // We put the version in the name because there isn't a better place for
+ // it. The TCX schema tightly defined the Version tag, so we can't put it
+ // there. They've similarly constrained the PartNumber tag, so it can't go
+ // there either.
+ pw.format("My Tracks %s by Google\n", SystemUtils.getMyTracksVersion(context));
+
+ pw.println("");
+
+ writeVersion();
+
+ pw.format("%s\n", SystemUtils.isRelease(context) ? TCX_TYPE_RELEASE
+ : TCX_TYPE_INTERNAL);
+ pw.println("");
+ pw.format("%s\n", Locale.getDefault().getLanguage());
+ pw.println("000-00000-00");
+ pw.println("");
+ pw.println("");
+ }
+
+ @Override
+ public void writeWaypoint(Waypoint waypoint) {
+ // TODO Write out the waypoints somewhere.
+ }
+
+ private void writeVersion() {
+ if (pw == null) {
+ return;
+ }
+
+ // Splitting the myTracks version code into VersionMajor, VersionMinor and BuildMajor
+ // to fit the integer type requirement for these fields in the TCX spec.
+ // Putting a string like "x.x.x" into VersionMajor breaks XML validation.
+ // We also set the BuildMinor version to 1 if this is a development build to
+ // signify that this build is newer than the one associated with the
+ // version code given in BuildMajor.
+
+ String[] myTracksVersionComponents = SystemUtils.getMyTracksVersion(context).split("\\.");
+
+ pw.println("");
+ pw.format("%d\n", Integer.valueOf(myTracksVersionComponents[0]));
+ pw.format("%d\n", Integer.valueOf(myTracksVersionComponents[1]));
+ // TCX schema says these are optional but http://connect.garmin.com only accepts
+ // the TCX file when they are present.
+ pw.format("%d\n", Integer.valueOf(myTracksVersionComponents[2]));
+ pw.format("%d\n", SystemUtils.isRelease(context) ? 0 : 1);
+ pw.println("");
+ }
+
+ private String categoryToTcxSport(String category) {
+ category = category.trim();
+ if (category.equalsIgnoreCase(TCX_SPORT_RUNNING)) {
+ return TCX_SPORT_RUNNING;
+ } else if (category.equalsIgnoreCase(TCX_SPORT_BIKING)) {
+ return TCX_SPORT_BIKING;
+ } else {
+ return TCX_SPORT_OTHER;
+ }
+ }
+}
diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/TempFileCleaner.java b/MyTracks/src/com/google/android/apps/mytracks/io/file/TempFileCleaner.java
similarity index 87%
rename from MyTracks/src/com/google/android/apps/mytracks/io/TempFileCleaner.java
rename to MyTracks/src/com/google/android/apps/mytracks/io/file/TempFileCleaner.java
index 21621225d..ec200f823 100644
--- a/MyTracks/src/com/google/android/apps/mytracks/io/TempFileCleaner.java
+++ b/MyTracks/src/com/google/android/apps/mytracks/io/file/TempFileCleaner.java
@@ -1,21 +1,24 @@
/*
* Copyright 2010 Google Inc.
- *
+ *
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
-package com.google.android.apps.mytracks.io;
+package com.google.android.apps.mytracks.io.file;
+
+import static com.google.android.apps.mytracks.Constants.TAG;
import android.os.Environment;
+import android.util.Log;
import java.io.File;
@@ -24,18 +27,18 @@ import java.io.File;
* @author Sandor Dornbush
*/
public class TempFileCleaner {
-
+
private long currentTimeMillis;
public static void clean() {
(new TempFileCleaner(System.currentTimeMillis())).cleanImpl();
}
-
+
// @VisibleForTesting
TempFileCleaner(long time) {
currentTimeMillis = time;
}
-
+
private void cleanImpl() {
if (!Environment.getExternalStorageState().equals(
Environment.MEDIA_MOUNTED)) {
@@ -64,7 +67,9 @@ public class TempFileCleaner {
long oldest = currentTimeMillis - 1000 * 3600;
for (File f : dir.listFiles()) {
if (f.lastModified() < oldest) {
- f.delete();
+ if (!f.delete()) {
+ Log.w(TAG, "Failed to delete file " + f.getAbsolutePath());
+ }
count++;
}
}
diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/TrackFormatWriter.java b/MyTracks/src/com/google/android/apps/mytracks/io/file/TrackFormatWriter.java
similarity index 93%
rename from MyTracks/src/com/google/android/apps/mytracks/io/TrackFormatWriter.java
rename to MyTracks/src/com/google/android/apps/mytracks/io/file/TrackFormatWriter.java
index c5ac02514..0b7101c6f 100644
--- a/MyTracks/src/com/google/android/apps/mytracks/io/TrackFormatWriter.java
+++ b/MyTracks/src/com/google/android/apps/mytracks/io/file/TrackFormatWriter.java
@@ -1,19 +1,19 @@
/*
* Copyright 2010 Google Inc.
- *
+ *
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
-package com.google.android.apps.mytracks.io;
+package com.google.android.apps.mytracks.io.file;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.content.Waypoint;
@@ -54,7 +54,7 @@ public interface TrackFormatWriter {
void prepare(Track track, OutputStream out);
/**
- * @return The file extentsion (i.e. gpx, kml, ...)
+ * @return The file extension (i.e. gpx, kml, ...)
*/
String getExtension();
@@ -77,7 +77,7 @@ public interface TrackFormatWriter {
*
* @param location the location to write
*/
- void writeLocation(Location location);
+ void writeLocation(Location location) throws InterruptedException;
/**
* Write a way point.
diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/file/TrackWriter.java b/MyTracks/src/com/google/android/apps/mytracks/io/file/TrackWriter.java
new file mode 100644
index 000000000..bace24d33
--- /dev/null
+++ b/MyTracks/src/com/google/android/apps/mytracks/io/file/TrackWriter.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2011 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.android.apps.mytracks.io.file;
+
+import java.io.File;
+
+/**
+ * Implementations of this class export tracks to the SD card. This class is
+ * intended to be format-neutral - it handles creating the output file and
+ * reading the track to be exported, but requires an instance of
+ * {@link TrackFormatWriter} to actually format the data.
+ *
+ * @author Sandor Dornbush
+ * @author Rodrigo Damazio
+ */
+public interface TrackWriter {
+ /** This listener is used to signal completion of track write */
+ public interface OnCompletionListener {
+ public void onComplete();
+ }
+
+ /** This listener is used to signal track writes. */
+ public interface OnWriteListener {
+ /**
+ * This method is invoked whenever a location within a track is written.
+ * @param number the location number
+ * @param max the maximum number of locations, for calculation of
+ * completion percentage
+ */
+ public void onWrite(int number, int max);
+ }
+
+ /**
+ * Sets listener to be invoked when the writer has finished.
+ */
+ void setOnCompletionListener(OnCompletionListener onCompletionListener);
+
+ /**
+ * Sets a listener to be invoked for each location writer.
+ */
+ void setOnWriteListener(OnWriteListener onWriteListener);
+
+ /**
+ * Sets a custom directory where the file will be written.
+ */
+ void setDirectory(File directory);
+
+ /**
+ * Returns the absolute path to the file which was created.
+ */
+ String getAbsolutePath();
+
+ /**
+ * Writes the given track id to the SD card.
+ * This is non-blocking.
+ */
+ void writeTrackAsync();
+
+ /**
+ * Writes the given track id to the SD card.
+ * This is blocking.
+ */
+ void writeTrack();
+
+ /**
+ * Stop any in-progress writes
+ */
+ void stopWriteTrack();
+
+ /**
+ * Returns true if the write completed successfully.
+ */
+ boolean wasSuccess();
+
+ /**
+ * Returns the error message (if any) generated by a writer failure.
+ */
+ int getErrorMessage();
+}
diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/TrackWriterFactory.java b/MyTracks/src/com/google/android/apps/mytracks/io/file/TrackWriterFactory.java
similarity index 88%
rename from MyTracks/src/com/google/android/apps/mytracks/io/TrackWriterFactory.java
rename to MyTracks/src/com/google/android/apps/mytracks/io/file/TrackWriterFactory.java
index 55261f8a5..49f1f9f01 100644
--- a/MyTracks/src/com/google/android/apps/mytracks/io/TrackWriterFactory.java
+++ b/MyTracks/src/com/google/android/apps/mytracks/io/file/TrackWriterFactory.java
@@ -1,12 +1,12 @@
/*
* Copyright 2010 Google Inc.
- *
+ *
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
@@ -14,12 +14,15 @@
* the License.
*/
-package com.google.android.apps.mytracks.io;
+package com.google.android.apps.mytracks.io.file;
+
+import static com.google.android.apps.mytracks.Constants.TAG;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.content.Track;
import android.content.Context;
+import android.util.Log;
/**
* A factory to produce track writers for any format.
@@ -53,7 +56,7 @@ public class TrackWriterFactory {
TCX {
@Override
public TrackFormatWriter newFormatWriter(Context context) {
- return new TcxTrackWriter();
+ return new TcxTrackWriter(context);
}
};
@@ -90,6 +93,11 @@ public class TrackWriterFactory {
MyTracksProviderUtils providerUtils,
long trackId, TrackFileFormat format) {
Track track = providerUtils.getTrack(trackId);
+ if (track == null) {
+ Log.w(TAG, "Trying to create a writer for an invalid track, id=" + trackId);
+ return null;
+ }
+
return newWriter(context, providerUtils, track, format);
}
@@ -106,7 +114,7 @@ public class TrackWriterFactory {
MyTracksProviderUtils providerUtils,
Track track, TrackFileFormat format) {
TrackFormatWriter writer = format.newFormatWriter(context);
- return new TrackWriter(context, providerUtils, track, writer);
+ return new TrackWriterImpl(context, providerUtils, track, writer);
}
private TrackWriterFactory() { }
diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/TrackWriter.java b/MyTracks/src/com/google/android/apps/mytracks/io/file/TrackWriterImpl.java
similarity index 50%
rename from MyTracks/src/com/google/android/apps/mytracks/io/TrackWriter.java
rename to MyTracks/src/com/google/android/apps/mytracks/io/file/TrackWriterImpl.java
index cb49c4a51..88478aa94 100644
--- a/MyTracks/src/com/google/android/apps/mytracks/io/TrackWriter.java
+++ b/MyTracks/src/com/google/android/apps/mytracks/io/file/TrackWriterImpl.java
@@ -1,26 +1,30 @@
/*
* Copyright 2008 Google Inc.
- *
+ *
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
-package com.google.android.apps.mytracks.io;
+package com.google.android.apps.mytracks.io.file;
-import com.google.android.apps.mytracks.MyTracksConstants;
+import static com.google.android.apps.mytracks.Constants.TAG;
+
+import com.google.android.apps.mytracks.Constants;
+import com.google.android.apps.mytracks.content.MyTracksLocation;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
+import com.google.android.apps.mytracks.content.MyTracksProviderUtils.LocationIterator;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.util.FileUtils;
-import com.google.android.apps.mytracks.util.MyTracksUtils;
+import com.google.android.apps.mytracks.util.LocationUtils;
import com.google.android.maps.mytracks.R;
import android.app.Activity;
@@ -35,28 +39,29 @@ import java.io.FileOutputStream;
import java.io.OutputStream;
/**
- * The class which exports tracks to the SD card.
- * This writer is format-neutral - it handles creating the output file
- * and reading the track to be exported, but requires an instance of
- * {@link TrackFormatWriter} to actually format the data.
+ * This class exports tracks to the SD card. It is intended to be format-
+ * neutral -- it handles creating the output file and reading the track to be
+ * exported, but requires an instance of {@link TrackFormatWriter} to actually
+ * format the data.
*
* @author Sandor Dornbush
* @author Rodrigo Damazio
*/
-public class TrackWriter {
-
+class TrackWriterImpl implements TrackWriter {
private final Context context;
private final MyTracksProviderUtils providerUtils;
private final Track track;
private final TrackFormatWriter writer;
private final FileUtils fileUtils;
- private Runnable onCompletion = null;
private boolean success = false;
private int errorMessage = -1;
private File directory = null;
private File file = null;
+ private OnCompletionListener onCompletionListener;
+ private OnWriteListener onWriteListener;
+ private Thread writeThread;
- public TrackWriter(Context context, MyTracksProviderUtils providerUtils,
+ TrackWriterImpl(Context context, MyTracksProviderUtils providerUtils,
Track track, TrackFormatWriter writer) {
this.context = context;
this.providerUtils = providerUtils;
@@ -65,60 +70,90 @@ public class TrackWriter {
this.fileUtils = new FileUtils();
}
- /**
- * Sets a completion callback.
- *
- * @param onCompletion Runnable that will be executed when finished
- */
- public void setOnCompletion(Runnable onCompletion) {
- this.onCompletion = onCompletion;
+ @Override
+ public void setOnCompletionListener(OnCompletionListener onCompletionListener) {
+ this.onCompletionListener = onCompletionListener;
}
- /**
- * Sets a custom directory where the file will be written.
- */
+ @Override
+ public void setOnWriteListener(OnWriteListener onWriteListener) {
+ this.onWriteListener = onWriteListener;
+ }
+
+ @Override
public void setDirectory(File directory) {
this.directory = directory;
}
+ @Override
public String getAbsolutePath() {
return file.getAbsolutePath();
}
- /**
- * Writes the given track id to the SD card.
- * This is non-blocking.
- */
+ @Override
public void writeTrackAsync() {
- Thread t = new Thread() {
+ writeThread = new Thread() {
@Override
public void run() {
- writeTrack();
+ doWriteTrack();
}
};
- t.start();
+ writeThread.start();
}
- /**
- * Writes the given track id to the SD card.
- * This is blocking.
- */
+ @Override
public void writeTrack() {
+ writeTrackAsync();
+ try {
+ writeThread.join();
+ } catch (InterruptedException e) {
+ Log.e(Constants.TAG, "Interrupted waiting for write to complete", e);
+ }
+ }
+
+ private void doWriteTrack() {
// Open the input and output
success = false;
errorMessage = R.string.error_track_does_not_exist;
if (track != null) {
if (openFile()) {
- writeDocument();
+ try {
+ writeDocument();
+ } catch (InterruptedException e) {
+ Log.i(Constants.TAG, "The track write was interrupted");
+ if (file != null) {
+ if (!file.delete()) {
+ Log.w(TAG, "Failed to delete file " + file.getAbsolutePath());
+ }
+ }
+ success = false;
+ errorMessage = R.string.error_operation_cancelled;
+ }
}
}
finished();
}
+ public void stopWriteTrack() {
+ if (writeThread != null && writeThread.isAlive()) {
+ Log.i(Constants.TAG, "Attempting to stop track write");
+ writeThread.interrupt();
+
+ try {
+ writeThread.join();
+ Log.i(Constants.TAG, "Track write stopped");
+ } catch (InterruptedException e) {
+ Log.e(Constants.TAG, "Failed to wait for writer to stop", e);
+ }
+ }
+ }
+
+ @Override
public int getErrorMessage() {
return errorMessage;
}
+ @Override
public boolean wasSuccess() {
return success;
}
@@ -129,8 +164,13 @@ public class TrackWriter {
*/
private void finished() {
- if (onCompletion != null) {
- runOnUiThread(onCompletion);
+ if (onCompletionListener != null) {
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ onCompletionListener.onComplete();
+ }
+ });
return;
}
}
@@ -145,8 +185,8 @@ public class TrackWriter {
}
/**
- * Opens the file and prepares the format writer for it.
- *
+ * Opens the file and prepares the format writer for it.
+ *
* @return true on success, false otherwise (and errorMessage is set)
*/
protected boolean openFile() {
@@ -158,16 +198,16 @@ public class TrackWriter {
String fileName = fileUtils.buildUniqueFileName(
directory, track.getName(), writer.getExtension());
if (fileName == null) {
- Log.e(MyTracksConstants.TAG,
- "Unable to get a unique filename for " + fileName);
+ Log.e(Constants.TAG,
+ "Unable to get a unique filename for " + track.getName());
return false;
}
- Log.i(MyTracksConstants.TAG, "Writing track to: " + fileName);
+ Log.i(Constants.TAG, "Writing track to: " + fileName);
try {
writer.prepare(track, newOutputStream(fileName));
} catch (FileNotFoundException e) {
- Log.e(MyTracksConstants.TAG, "Failed to open output file.", e);
+ Log.e(Constants.TAG, "Failed to open output file.", e);
errorMessage = R.string.io_write_failed;
return false;
}
@@ -185,12 +225,12 @@ public class TrackWriter {
}
if (!fileUtils.isSdCardAvailable()) {
- Log.i(MyTracksConstants.TAG, "Could not find SD card.");
+ Log.i(Constants.TAG, "Could not find SD card.");
errorMessage = R.string.io_no_external_storage_found;
return false;
}
if (!fileUtils.ensureDirectoryExists(directory)) {
- Log.i(MyTracksConstants.TAG, "Could not create export directory.");
+ Log.i(Constants.TAG, "Could not create export directory.");
errorMessage = R.string.io_create_dir_failed;
return false;
}
@@ -228,7 +268,7 @@ public class TrackWriter {
// same time.
Cursor cursor = null;
cursor = providerUtils.getWaypointsCursor(trackId, 0,
- MyTracksConstants.MAX_LOADED_WAYPOINTS_POINTS);
+ Constants.MAX_LOADED_WAYPOINTS_POINTS);
if (cursor != null) {
try {
if (cursor.moveToFirst()) {
@@ -240,9 +280,7 @@ public class TrackWriter {
}
}
} finally {
- if (cursor != null) {
- cursor.close();
- }
+ cursor.close();
}
}
}
@@ -250,78 +288,104 @@ public class TrackWriter {
/**
* Does the actual work of writing the track to the now open file.
*/
- void writeDocument() {
- Log.d(MyTracksConstants.TAG, "Started writing track.");
+ void writeDocument() throws InterruptedException {
+ Log.d(Constants.TAG, "Started writing track.");
writer.writeHeader();
- // TODO: Fix ordering (in GPX waypoints should come first)
- writeLocations();
writeWaypoints(track.getId());
+ writeLocations();
writer.writeFooter();
writer.close();
success = true;
- Log.d(MyTracksConstants.TAG, "Done writing track.");
+ Log.d(Constants.TAG, "Done writing track.");
errorMessage = R.string.io_write_finished;
}
- private void writeLocations() {
+ private void writeLocations() throws InterruptedException {
boolean wroteFirst = false;
boolean segmentOpen = false;
- Location lastLoc = null, loc = null;
boolean isLastValid = false;
- Cursor locationsCursor =
- providerUtils.getLocationsCursor(track.getId(), 0, -1, false);
-
- if (locationsCursor == null || !locationsCursor.moveToFirst()) {
- Log.w(MyTracksConstants.TAG, "Unable to get any points to write");
- return;
- }
- do {
- if (loc == null) loc = new Location("");
- providerUtils.fillLocation(locationsCursor, loc);
+ class TrackWriterLocationFactory implements MyTracksProviderUtils.LocationFactory {
+ Location currentLocation;
+ Location lastLocation;
- boolean isValid = MyTracksUtils.isValidLocation(loc);
- boolean validSegment = isValid && isLastValid;
- if (!wroteFirst && validSegment) {
- // Found the first two consecutive points which are valid
- writer.writeBeginTrack(lastLoc);
- wroteFirst = true;
+ @Override
+ public Location createLocation() {
+ if (currentLocation == null) {
+ currentLocation = new MyTracksLocation("");
+ }
+ return currentLocation;
}
- if (validSegment) {
- if (!segmentOpen) {
- // Start a segment for this point
- writer.writeOpenSegment();
- segmentOpen = true;
-
- // Write the previous point, which we had previously skipped
- writer.writeLocation(lastLoc);
- }
-
- // Write the current point
- writer.writeLocation(loc);
- } else {
- if (segmentOpen) {
- writer.writeCloseSegment();
- segmentOpen = false;
+ public void swapLocations() {
+ Location tmpLoc = lastLocation;
+ lastLocation = currentLocation;
+ currentLocation = tmpLoc;
+ if (currentLocation != null) {
+ currentLocation.reset();
}
}
+ };
- // Swap loc and lastLoc (so lastLoc is reused)
- Location tmp = lastLoc;
- lastLoc = loc;
- loc = tmp;
- if (loc != null) loc.reset();
+ TrackWriterLocationFactory locationFactory = new TrackWriterLocationFactory();
+ LocationIterator it = providerUtils.getLocationIterator(track.getId(), 0, false,
+ locationFactory);
+ try {
+ if (!it.hasNext()) {
+ Log.w(Constants.TAG, "Unable to get any points to write");
+ return;
+ }
+ int pointNumber = 0;
+ while (it.hasNext()) {
+ Location loc = it.next();
+ if (Thread.interrupted()) {
+ throw new InterruptedException();
+ }
- isLastValid = isValid;
- } while (locationsCursor.moveToNext());
+ pointNumber++;
- if (segmentOpen) {
- writer.writeCloseSegment();
- segmentOpen = false;
- }
- if (wroteFirst) {
- writer.writeEndTrack(lastLoc);
+ boolean isValid = LocationUtils.isValidLocation(loc);
+ boolean validSegment = isValid && isLastValid;
+ if (!wroteFirst && validSegment) {
+ // Found the first two consecutive points which are valid
+ writer.writeBeginTrack(locationFactory.lastLocation);
+ wroteFirst = true;
+ }
+
+ if (validSegment) {
+ if (!segmentOpen) {
+ // Start a segment for this point
+ writer.writeOpenSegment();
+ segmentOpen = true;
+
+ // Write the previous point, which we had previously skipped
+ writer.writeLocation(locationFactory.lastLocation);
+ }
+
+ // Write the current point
+ writer.writeLocation(loc);
+ if (onWriteListener != null) {
+ onWriteListener.onWrite(pointNumber, track.getNumberOfPoints());
+ }
+ } else {
+ if (segmentOpen) {
+ writer.writeCloseSegment();
+ segmentOpen = false;
+ }
+ }
+
+ locationFactory.swapLocations();
+ isLastValid = isValid;
+ }
+ if (segmentOpen) {
+ writer.writeCloseSegment();
+ segmentOpen = false;
+ }
+ if (wroteFirst) {
+ writer.writeEndTrack(locationFactory.lastLocation);
+ }
+ } finally {
+ it.close();
}
}
}
diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/file/WriteProgressController.java b/MyTracks/src/com/google/android/apps/mytracks/io/file/WriteProgressController.java
new file mode 100644
index 000000000..f41930fd8
--- /dev/null
+++ b/MyTracks/src/com/google/android/apps/mytracks/io/file/WriteProgressController.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright 2011 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.android.apps.mytracks.io.file;
+
+import com.google.android.maps.mytracks.R;
+
+import android.app.Activity;
+import android.app.ProgressDialog;
+import android.content.DialogInterface;
+
+/**
+ * Given a {@link TrackWriter}, this class manages the process of writing the
+ * data in the track represented by the writer. This includes the display of
+ * a progress dialog, updating the progress bar in said dialog, and notifying
+ * interested parties when the write completes.
+ *
+ * @author Matthew Simmons
+ */
+class WriteProgressController {
+ /**
+ * This listener is used to notify interested parties when the write has
+ * completed.
+ */
+ public interface OnCompletionListener {
+ /**
+ * When this method is invoked, the write has completed, and the progress
+ * dialog has been dismissed. Whether the write succeeded can be
+ * determined by examining the {@link TrackWriter}.
+ */
+ public void onComplete();
+ }
+
+ private final Activity activity;
+ private final TrackWriter writer;
+ private ProgressDialog dialog;
+
+ private OnCompletionListener onCompletionListener;
+ private final int progressDialogId;
+
+ /**
+ * @param activity the activity associated with this write
+ * @param writer the writer which writes the track to disk. Note that this
+ * class will use the writer's completion listener. If callers are
+ * interested in notification upon completion of the write, they should
+ * use {@link #setOnCompletionListener}.
+ */
+ public WriteProgressController(Activity activity, TrackWriter writer, int progressDialogId) {
+ this.activity = activity;
+ this.writer = writer;
+ this.progressDialogId = progressDialogId;
+
+ writer.setOnCompletionListener(writerCompleteListener);
+ writer.setOnWriteListener(writerWriteListener);
+ }
+
+ /** Set a listener to be invoked when the write completes. */
+ public void setOnCompletionListener(OnCompletionListener onCompletionListener) {
+ this.onCompletionListener = onCompletionListener;
+ }
+
+ public ProgressDialog createProgressDialog() {
+ dialog = new ProgressDialog(activity);
+ dialog.setIcon(android.R.drawable.ic_dialog_info);
+ dialog.setTitle(activity.getString(R.string.progress_title));
+ dialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
+ dialog.setMessage(activity.getString(R.string.write_progress_message));
+ dialog.setIndeterminate(true);
+ dialog.setOnCancelListener(dialogCancelListener);
+ return dialog;
+ }
+
+ /** Initiate an asynchronous write. */
+ public void startWrite() {
+ activity.showDialog(progressDialogId);
+ writer.writeTrackAsync();
+ }
+
+ /** VisibleForTesting */
+ ProgressDialog getDialog() {
+ return dialog;
+ }
+
+ private final DialogInterface.OnCancelListener dialogCancelListener =
+ new DialogInterface.OnCancelListener() {
+ @Override
+ public void onCancel(DialogInterface dialogInterface) {
+ writer.stopWriteTrack();
+ }
+ };
+
+ private final TrackWriter.OnCompletionListener writerCompleteListener =
+ new TrackWriter.OnCompletionListener() {
+ @Override
+ public void onComplete() {
+ activity.dismissDialog(progressDialogId);
+
+ if (onCompletionListener != null) {
+ onCompletionListener.onComplete();
+ }
+ }
+ };
+
+ private final TrackWriter.OnWriteListener writerWriteListener =
+ new TrackWriter.OnWriteListener() {
+ @Override
+ public void onWrite(int number, int max) {
+ if (number % 500 == 0) {
+ dialog.setIndeterminate(false);
+ dialog.setMax(max);
+ dialog.setProgress(Math.min(number, max));
+ }
+ }
+ };
+}
diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/gdata/AndroidGDataClient.java b/MyTracks/src/com/google/android/apps/mytracks/io/gdata/AndroidGDataClient.java
index 20c4498d7..449056870 100644
--- a/MyTracks/src/com/google/android/apps/mytracks/io/gdata/AndroidGDataClient.java
+++ b/MyTracks/src/com/google/android/apps/mytracks/io/gdata/AndroidGDataClient.java
@@ -1,12 +1,12 @@
/*
* Copyright 2010 Google Inc.
- *
+ *
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
@@ -137,7 +137,7 @@ public class AndroidGDataClient implements GDataClient {
/*
* (non-Javadoc)
- *
+ *
* @see GDataClient#encodeUri(java.lang.String)
*/
public String encodeUri(String uri) {
@@ -155,7 +155,7 @@ public class AndroidGDataClient implements GDataClient {
/*
* (non-Javadoc)
- *
+ *
* @see com.google.wireless.gdata.client.GDataClient#createQueryParams()
*/
public QueryParams createQueryParams() {
@@ -187,6 +187,7 @@ public class AndroidGDataClient implements GDataClient {
while (redirectsLeft > 0) {
HttpUriRequest request = creator.createRequest(uri);
+ request.addHeader("User-Agent", "Android-GData");
request.addHeader("Accept-Encoding", "gzip");
// only add the auth token if not null (to allow for GData feeds that do
@@ -272,7 +273,7 @@ public class AndroidGDataClient implements GDataClient {
String errorMessage = null;
HttpEntity entity = response.getEntity();
try {
- if (response != null && entity != null) {
+ if (entity != null) {
InputStream in = entity.getContent();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buf = new byte[8192];
@@ -328,7 +329,7 @@ public class AndroidGDataClient implements GDataClient {
/*
* (non-Javadoc)
- *
+ *
* @see GDataClient#getFeedAsStream(java.lang.String, java.lang.String)
*/
public InputStream getFeedAsStream(String feedUrl, String authToken)
@@ -357,7 +358,7 @@ public class AndroidGDataClient implements GDataClient {
/*
* (non-Javadoc)
- *
+ *
* @see GDataClient#createEntry
*/
public InputStream createEntry(String feedUrl, String authToken,
@@ -376,7 +377,7 @@ public class AndroidGDataClient implements GDataClient {
/*
* (non-Javadoc)
- *
+ *
* @see GDataClient#updateEntry
*/
public InputStream updateEntry(String editUri, String authToken,
@@ -394,7 +395,7 @@ public class AndroidGDataClient implements GDataClient {
/*
* (non-Javadoc)
- *
+ *
* @see GDataClient#deleteEntry
*/
public void deleteEntry(String editUri, String authToken)
diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/gdata/GDataClientFactory.java b/MyTracks/src/com/google/android/apps/mytracks/io/gdata/GDataClientFactory.java
index 15dd588d0..a0a717bb1 100644
--- a/MyTracks/src/com/google/android/apps/mytracks/io/gdata/GDataClientFactory.java
+++ b/MyTracks/src/com/google/android/apps/mytracks/io/gdata/GDataClientFactory.java
@@ -15,7 +15,7 @@
*/
package com.google.android.apps.mytracks.io.gdata;
-import com.google.android.apps.mytracks.MyTracksConstants;
+import com.google.android.apps.mytracks.Constants;
import com.google.wireless.gdata.client.GDataClient;
import android.content.Context;
@@ -43,7 +43,7 @@ public class GDataClientFactory {
} catch (LinkageError e) {
// On all other platforms use the client implementation packaged in the
// apk.
- Log.i(MyTracksConstants.TAG, "Using mytracks AndroidGDataClient.", e);
+ Log.i(Constants.TAG, "Using mytracks AndroidGDataClient.", e);
return new com.google.android.apps.mytracks.io.gdata.AndroidGDataClient();
}
}
diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/gdata/GDataWrapper.java b/MyTracks/src/com/google/android/apps/mytracks/io/gdata/GDataWrapper.java
index 97d7cc99e..2a6d63f06 100644
--- a/MyTracks/src/com/google/android/apps/mytracks/io/gdata/GDataWrapper.java
+++ b/MyTracks/src/com/google/android/apps/mytracks/io/gdata/GDataWrapper.java
@@ -1,12 +1,12 @@
/*
* Copyright 2008 Google Inc.
- *
+ *
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
@@ -15,13 +15,9 @@
*/
package com.google.android.apps.mytracks.io.gdata;
-import com.google.android.apps.mytracks.MyTracksConstants;
+import com.google.android.apps.mytracks.Constants;
import com.google.android.apps.mytracks.io.AuthManager;
-import com.google.wireless.gdata.client.GDataServiceClient;
-import com.google.wireless.gdata.client.HttpException;
-import com.google.wireless.gdata.parser.ParseException;
-import com.google.wireless.gdata2.ConflictDetectedException;
-import com.google.wireless.gdata2.client.AuthenticationException;
+import com.google.android.apps.mytracks.io.AuthManager.AuthCallback;
import android.util.Log;
@@ -31,6 +27,7 @@ import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicBoolean;
/**
@@ -39,9 +36,60 @@ import java.util.concurrent.TimeoutException;
* handling. After a query is run, the wrapper can be queried about the error
* that occurred.
*
+ * @param C the GData service client
* @author Sandor Dornbush
*/
-public class GDataWrapper {
+public class GDataWrapper {
+
+ public static class AuthenticationException extends Exception {
+ private Exception exception;
+ private static final long serialVersionUID = 1L;
+ public AuthenticationException(Exception caught) {
+ this.exception = caught;
+ }
+ public Exception getException() {
+ return exception;
+ }
+ };
+
+ public static class ParseException extends Exception {
+ private Exception exception;
+ private static final long serialVersionUID = 1L;
+ public ParseException(Exception caught) {
+ this.exception = caught;
+ }
+ public Exception getException() {
+ return exception;
+ }
+ };
+
+ public static class ConflictDetectedException extends Exception {
+ private Exception exception;
+ private static final long serialVersionUID = 1L;
+ public ConflictDetectedException(Exception caught) {
+ this.exception = caught;
+ }
+ public Exception getException() {
+ return exception;
+ }
+ };
+
+ public static class HttpException extends Exception {
+ private static final long serialVersionUID = 1L;
+ private int statusCode;
+ private String statusMessage;
+ public HttpException(int statusCode, String statusMessage) {
+ super();
+ this.statusCode = statusCode;
+ this.statusMessage = statusMessage;
+ }
+ public int getStatusCode() {
+ return statusCode;
+ }
+ public String getStatusMessage() {
+ return statusMessage;
+ }
+ };
/**
* A QueryFunction is passed in when executing a query. The query function of
@@ -49,8 +97,8 @@ public class GDataWrapper {
* should execute whatever operations it desires on the client without concern
* for whether the client will throw an error.
*/
- public interface QueryFunction {
- public abstract void query(GDataServiceClient client)
+ public interface QueryFunction {
+ public abstract void query(C client)
throws AuthenticationException, IOException, ParseException,
ConflictDetectedException, HttpException;
}
@@ -86,16 +134,16 @@ public class GDataWrapper {
public static final int ERROR_CLEANED_UP = 7;
// An unknown error occurred.
public static final int ERROR_UNKNOWN = 100;
-
+
private static final int AUTH_TOKEN_INVALIDATE_REFRESH_NUM_RETRIES = 1;
private static final int AUTH_TOKEN_INVALIDATE_REFRESH_TIMEOUT = 5000;
private String errorMessage;
private int errorType;
- private GDataServiceClient gdataServiceClient;
+ private C gdataServiceClient;
private AuthManager auth;
private boolean retryOnAuthFailure;
-
+
public GDataWrapper() {
errorType = ERROR_NO_ERROR;
errorMessage = null;
@@ -103,7 +151,7 @@ public class GDataWrapper {
retryOnAuthFailure = false;
}
- public void setClient(GDataServiceClient gdataServiceClient) {
+ public void setClient(C gdataServiceClient) {
this.gdataServiceClient = gdataServiceClient;
}
@@ -112,7 +160,7 @@ public class GDataWrapper {
return runCommon(function, null);
}
- public boolean runQuery(final QueryFunction query) {
+ public boolean runQuery(final QueryFunction query) {
return runCommon(null, query);
}
@@ -120,21 +168,21 @@ public class GDataWrapper {
* Runs an arbitrary piece of code.
*/
private boolean runCommon(final AuthenticatedFunction function,
- final QueryFunction query) {
+ final QueryFunction query) {
for (int i = 0; i <= AUTH_TOKEN_INVALIDATE_REFRESH_NUM_RETRIES; i++) {
runOne(function, query);
if (errorType == ERROR_NO_ERROR) {
return true;
}
- Log.d(MyTracksConstants.TAG, "GData error encountered: " + errorMessage);
+ Log.d(Constants.TAG, "GData error encountered: " + errorMessage);
if (errorType == ERROR_AUTH && auth != null) {
if (!retryOnAuthFailure || !invalidateAndRefreshAuthToken()) {
return false;
}
}
-
- Log.d(MyTracksConstants.TAG, "retrying function/query");
+
+ Log.d(Constants.TAG, "retrying function/query");
}
return false;
}
@@ -143,8 +191,8 @@ public class GDataWrapper {
* Execute a given function or query. If one is executed, errorType and
* errorMessage will contain the result/status of the function/query.
*/
- private void runOne(final AuthenticatedFunction function,
- final QueryFunction query) {
+ private void runOne(final AuthenticatedFunction function,
+ final QueryFunction query) {
try {
if (function != null) {
function.run(this.auth.getAuthToken());
@@ -160,74 +208,82 @@ public class GDataWrapper {
errorMessage = null;
} catch (AuthenticationException e) {
- Log.e(MyTracksConstants.TAG, "Exception", e);
+ Log.e(Constants.TAG, "AuthenticationException", e);
errorType = ERROR_AUTH;
errorMessage = e.getMessage();
} catch (HttpException e) {
- Log.e(MyTracksConstants.TAG, "HttpException", e);
+ Log.e(Constants.TAG,
+ "HttpException, code " + e.getStatusCode() + " message " + e.getMessage(), e);
errorMessage = e.getMessage();
- if (errorMessage.contains("401")) {
+ if (e.getStatusCode() == 401) {
errorType = ERROR_AUTH;
} else {
errorType = ERROR_CONNECTION;
}
} catch (FileNotFoundException e) {
- Log.e(MyTracksConstants.TAG, "Exception", e);
+ Log.e(Constants.TAG, "Exception", e);
errorType = ERROR_AUTH;
errorMessage = e.getMessage();
} catch (IOException e) {
- Log.e(MyTracksConstants.TAG, "Exception", e);
+ Log.e(Constants.TAG, "Exception", e);
errorMessage = e.getMessage();
- if (errorMessage.contains("503")) {
+ if (errorMessage != null && errorMessage.contains("503")) {
errorType = ERROR_INTERNAL;
} else {
errorType = ERROR_CONNECTION;
}
} catch (ParseException e) {
- Log.e(MyTracksConstants.TAG, "Exception", e);
+ Log.e(Constants.TAG, "Exception", e);
errorType = ERROR_LOCAL;
errorMessage = e.getMessage();
} catch (ConflictDetectedException e) {
- Log.e(MyTracksConstants.TAG, "Exception", e);
+ Log.e(Constants.TAG, "Exception", e);
errorType = ERROR_CONFLICT;
errorMessage = e.getMessage();
}
}
- /**
+ /**
* Invalidates and refreshes the auth token. Blocks until the refresh has
* completed or until we deem the refresh as having timed out.
- *
+ *
* @return true If the invalidate/refresh succeeds, false if it fails or
* times out.
*/
private boolean invalidateAndRefreshAuthToken() {
- Log.d(MyTracksConstants.TAG, "Retrying due to auth failure");
+ Log.d(Constants.TAG, "Retrying due to auth failure");
// This FutureTask doesn't do anything -- it exists simply to be
// blocked upon using get().
- FutureTask> whenFinishedFuture = new FutureTask