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
deleted file mode 100644
index 17833c28c..000000000
--- a/MyTracks/src/com/google/android/apps/mytracks/io/docs/DocsTagBuilder.java
+++ /dev/null
@@ -1,118 +0,0 @@
-/*
- * 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.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 {
-
- // Google Docs can only parse numbers in the English locale.
- private static final NumberFormat LARGE_UNIT_FORMAT = NumberFormat.getNumberInstance(
- Locale.ENGLISH);
- private static final NumberFormat SMALL_UNIT_FORMAT = NumberFormat.getIntegerInstance(
- Locale.ENGLISH);
-
- static {
- LARGE_UNIT_FORMAT.setMaximumFractionDigits(2);
- LARGE_UNIT_FORMAT.setMinimumFractionDigits(2);
- }
-
- 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/docs/SendDocsActivity.java b/MyTracks/src/com/google/android/apps/mytracks/io/docs/SendDocsActivity.java
new file mode 100644
index 000000000..61467a6f2
--- /dev/null
+++ b/MyTracks/src/com/google/android/apps/mytracks/io/docs/SendDocsActivity.java
@@ -0,0 +1,125 @@
+/*
+ * 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.docs;
+
+import com.google.android.apps.mytracks.io.sendtogoogle.SendRequest;
+import com.google.android.apps.mytracks.io.sendtogoogle.UploadResultActivity;
+import com.google.android.maps.mytracks.R;
+
+import android.app.Activity;
+import android.app.Dialog;
+import android.app.ProgressDialog;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.os.Bundle;
+
+/**
+ * An activity to send a track to Google Docs.
+ *
+ * @author jshih@google.com (Jimmy Shih)
+ */
+public class SendDocsActivity extends Activity {
+
+ private static final int PROGRESS_DIALOG = 1;
+
+ private SendRequest sendRequest;
+ private SendDocsAsyncTask asyncTask;
+ private ProgressDialog progressDialog;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ sendRequest = getIntent().getParcelableExtra(SendRequest.SEND_REQUEST_KEY);
+
+ Object retained = getLastNonConfigurationInstance();
+ if (retained instanceof SendDocsAsyncTask) {
+ asyncTask = (SendDocsAsyncTask) retained;
+ asyncTask.setActivity(this);
+ } else {
+ asyncTask = new SendDocsAsyncTask(this, sendRequest.getTrackId(), sendRequest.getAccount());
+ asyncTask.execute();
+ }
+ }
+
+ @Override
+ public Object onRetainNonConfigurationInstance() {
+ asyncTask.setActivity(null);
+ return asyncTask;
+ }
+
+ @Override
+ protected Dialog onCreateDialog(int id) {
+ switch (id) {
+ case PROGRESS_DIALOG:
+ progressDialog = new ProgressDialog(this);
+ progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
+ progressDialog.setIcon(android.R.drawable.ic_dialog_info);
+ progressDialog.setTitle(getString(
+ R.string.send_google_progress_title, getString(R.string.send_google_docs)));
+ progressDialog.setMax(100);
+ progressDialog.setProgress(0);
+ progressDialog.setCancelable(true);
+ progressDialog.setOnCancelListener(new DialogInterface.OnCancelListener() {
+ @Override
+ public void onCancel(DialogInterface dialog) {
+ asyncTask.cancel(true);
+ startNextActivity(false);
+ }
+ });
+ return progressDialog;
+ default:
+ return null;
+ }
+ }
+
+ /**
+ * Invokes when the associated AsyncTask completes.
+ *
+ * @param success true if success
+ */
+ public void onAsyncTaskCompleted(boolean success) {
+ startNextActivity(success);
+ }
+
+ /**
+ * Shows the progress dialog.
+ */
+ public void showProgressDialog() {
+ showDialog(PROGRESS_DIALOG);
+ }
+
+ /**
+ * Sets the progress dialog value.
+ */
+ public void setProgressDialogValue(int value) {
+ if (progressDialog != null) {
+ progressDialog.setProgress(value);
+ }
+ }
+
+ /**
+ * Starts the next activity.
+ *
+ * @param success true if sendDocs is success
+ */
+ private void startNextActivity(boolean success) {
+ sendRequest.setDocsSuccess(success);
+ Intent intent = new Intent(this, UploadResultActivity.class)
+ .putExtra(SendRequest.SEND_REQUEST_KEY, sendRequest);
+ startActivity(intent);
+ finish();
+ }
+}
diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/docs/SendDocsAsyncTask.java b/MyTracks/src/com/google/android/apps/mytracks/io/docs/SendDocsAsyncTask.java
new file mode 100644
index 000000000..7b4466d01
--- /dev/null
+++ b/MyTracks/src/com/google/android/apps/mytracks/io/docs/SendDocsAsyncTask.java
@@ -0,0 +1,389 @@
+/*
+ * 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.docs;
+
+import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
+import com.google.android.apps.mytracks.content.Track;
+import com.google.android.apps.mytracks.io.gdata.GDataClientFactory;
+import com.google.android.apps.mytracks.io.gdata.docs.DocumentsClient;
+import com.google.android.apps.mytracks.io.gdata.docs.SpreadsheetsClient;
+import com.google.android.apps.mytracks.io.gdata.docs.XmlDocsGDataParserFactory;
+import com.google.android.common.gdata.AndroidXmlParserFactory;
+import com.google.android.maps.mytracks.R;
+import com.google.wireless.gdata.client.GDataClient;
+import com.google.wireless.gdata.client.HttpException;
+import com.google.wireless.gdata.parser.ParseException;
+import com.google.wireless.gdata2.client.AuthenticationException;
+
+import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.accounts.AuthenticatorException;
+import android.accounts.OperationCanceledException;
+import android.content.Context;
+import android.os.AsyncTask;
+import android.util.Log;
+
+import java.io.IOException;
+
+/**
+ * AsyncTask to send a track to Google Docs.
+ *
+ * @author jshih@google.com (Jimmy Shih)
+ */
+public class SendDocsAsyncTask extends AsyncTask {
+ private static final int PROGRESS_GET_SPREADSHEET_ID = 0;
+ private static final int PROGRESS_CREATE_SPREADSHEET = 25;
+ private static final int PROGRESS_GET_WORKSHEET_ID = 50;
+ private static final int PROGRESS_ADD_TRACK_INFO = 75;
+ private static final int PROGRESS_COMPLETE = 100;
+
+ private static final String TAG = SendDocsAsyncTask.class.getSimpleName();
+
+ private SendDocsActivity activity;
+ private final long trackId;
+ private final Account account;
+ private final Context context;
+ private final MyTracksProviderUtils myTracksProviderUtils;
+ private final GDataClient gDataClient;
+ private final DocumentsClient documentsClient;
+ private final SpreadsheetsClient spreadsheetsClient;
+
+ /**
+ * True if can retry sending to Google Docs.
+ */
+ private boolean canRetry;
+
+ /**
+ * True if the AsyncTask has completed.
+ */
+ private boolean completed;
+
+ /**
+ * True if the result is success.
+ */
+ private boolean success;
+
+ // The following variables are for per upload states
+ private String documentsAuthToken;
+ private String spreadsheetsAuthToken;
+ private String spreadsheetId;
+ private String worksheetId;
+
+ public SendDocsAsyncTask(SendDocsActivity activity, long trackId, Account account) {
+ this.activity = activity;
+ this.trackId = trackId;
+ this.account = account;
+
+ context = activity.getApplicationContext();
+ myTracksProviderUtils = MyTracksProviderUtils.Factory.get(context);
+ gDataClient = GDataClientFactory.getGDataClient(context);
+ documentsClient = new DocumentsClient(
+ gDataClient, new XmlDocsGDataParserFactory(new AndroidXmlParserFactory()));
+ spreadsheetsClient = new SpreadsheetsClient(
+ gDataClient, new XmlDocsGDataParserFactory(new AndroidXmlParserFactory()));
+
+ canRetry = true;
+ completed = false;
+ success = false;
+ }
+
+ /**
+ * Sets the activity associated with this AyncTask.
+ *
+ * @param activity the activity.
+ */
+ public void setActivity(SendDocsActivity activity) {
+ this.activity = activity;
+ if (completed && activity != null) {
+ activity.onAsyncTaskCompleted(success);
+ }
+ }
+
+ @Override
+ protected void onPreExecute() {
+ activity.showProgressDialog();
+ }
+
+ @Override
+ protected Boolean doInBackground(Void... params) {
+ return doUpload();
+ }
+
+ @Override
+ protected void onProgressUpdate(Integer... values) {
+ if (activity != null) {
+ activity.setProgressDialogValue(values[0]);
+ }
+ }
+
+ @Override
+ protected void onPostExecute(Boolean result) {
+ closeClient();
+ success = result;
+ completed = true;
+ if (activity != null) {
+ activity.onAsyncTaskCompleted(success);
+ }
+ }
+
+ @Override
+ protected void onCancelled() {
+ closeClient();
+ }
+
+ private void closeClient() {
+ if (gDataClient != null) {
+ gDataClient.close();
+ }
+ }
+
+ /**
+ * Uploads a track to Google Docs.
+ *
+ * @return true if success.
+ */
+ private boolean doUpload() {
+ // Reset the per upload states
+ documentsAuthToken = null;
+ spreadsheetsAuthToken = null;
+ spreadsheetId = null;
+ worksheetId = null;
+
+ try {
+ documentsAuthToken = AccountManager.get(context).blockingGetAuthToken(
+ account, documentsClient.getServiceName(), false);
+ spreadsheetsAuthToken = AccountManager.get(context).blockingGetAuthToken(
+ account, spreadsheetsClient.getServiceName(), false);
+ } catch (OperationCanceledException e) {
+ Log.d(TAG, "Unable to get auth token", e);
+ return retryUpload();
+ } catch (AuthenticatorException e) {
+ Log.d(TAG, "Unable to get auth token", e);
+ return retryUpload();
+ } catch (IOException e) {
+ Log.d(TAG, "Unable to get auth token", e);
+ return retryUpload();
+ }
+
+ Track track = myTracksProviderUtils.getTrack(trackId);
+ if (track == null) {
+ Log.d(TAG, "Track is null");
+ return false;
+ }
+
+ String title = context.getString(R.string.my_tracks_app_name);
+ if (track.getCategory() != null && !track.getCategory().equals("")) {
+ title += "-" + track.getCategory();
+ }
+
+ // Get the spreadsheet ID
+ publishProgress(PROGRESS_GET_SPREADSHEET_ID);
+ if (!fetchSpreadSheetId(title, false)) {
+ return retryUpload();
+ }
+
+ // Create a new spreadsheet if necessary
+ publishProgress(PROGRESS_CREATE_SPREADSHEET);
+ if (spreadsheetId == null) {
+ if (!createSpreadSheet(title)) {
+ Log.d(TAG, "Unable to create a new spreadsheet");
+ return false;
+ }
+
+ // The previous creation might have succeeded even though GData
+ // reported an error. Seems to be a know bug.
+ // See http://code.google.com/p/gdata-issues/issues/detail?id=929
+ // Try to find the created spreadsheet.
+ if (spreadsheetId == null) {
+ if (!fetchSpreadSheetId(title, true)) {
+ Log.d(TAG, "Unable to check if the new spreadsheet is created");
+ return false;
+ }
+
+ if (spreadsheetId == null) {
+ Log.d(TAG, "Unable to create a new spreadsheet");
+ return false;
+ }
+ }
+ }
+
+ // Get the worksheet ID
+ publishProgress(PROGRESS_GET_WORKSHEET_ID);
+ if (!fetchWorksheetId()) {
+ return retryUpload();
+ }
+ if (worksheetId == null) {
+ Log.d(TAG, "Unable to get a worksheet ID");
+ return false;
+ }
+
+ // Add the track info
+ publishProgress(PROGRESS_ADD_TRACK_INFO);
+ if (!addTrackInfo(track)) {
+ Log.d(TAG, "Unable to add track info");
+ return false;
+ }
+
+ publishProgress(PROGRESS_COMPLETE);
+ return true;
+ }
+
+ /**
+ * Fetches the spreadsheet id. Sets the instance variable
+ * {@link SendDocsAsyncTask#spreadsheetId}.
+ *
+ * @param title the spreadsheet title
+ * @param waitFirst wait before checking
+ * @return true if completes.
+ */
+ private boolean fetchSpreadSheetId(String title, boolean waitFirst) {
+ if (isCancelled()) {
+ return false;
+ }
+
+ if (waitFirst) {
+ try {
+ Thread.sleep(5000);
+ } catch (InterruptedException e) {
+ Log.d(TAG, "Unable to wait", e);
+ return false;
+ }
+ }
+
+ try {
+ spreadsheetId = SendDocsUtils.getSpreadsheetId(title, documentsClient, documentsAuthToken);
+ } catch (ParseException e) {
+ Log.d(TAG, "Unable to fetch spreadsheet ID", e);
+ return false;
+ } catch (HttpException e) {
+ Log.d(TAG, "Unable to fetch spreadsheet ID", e);
+ return false;
+ } catch (IOException e) {
+ Log.d(TAG, "Unable to fetch spreadsheet ID", e);
+ return false;
+ }
+
+ if (spreadsheetId == null) {
+ // Waiting a few seconds and trying again. Maybe the server just had a
+ // hickup (unfortunately that happens quite a lot...).
+ try {
+ Thread.sleep(5000);
+ } catch (InterruptedException e) {
+ Log.d(TAG, "Unable to wait", e);
+ return false;
+ }
+
+ try {
+ spreadsheetId = SendDocsUtils.getSpreadsheetId(title, documentsClient, documentsAuthToken);
+ } catch (ParseException e) {
+ Log.d(TAG, "Unable to fetch spreadsheet ID", e);
+ return false;
+ } catch (HttpException e) {
+ Log.d(TAG, "Unable to fetch spreadsheet ID", e);
+ return false;
+ } catch (IOException e) {
+ Log.d(TAG, "Unable to fetch spreadsheet ID", e);
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Creates a spreadsheet. If successful, sets the instance variable
+ * {@link SendDocsAsyncTask#spreadsheetId}.
+ *
+ * @param spreadsheetTitle the spreadsheet title
+ * @return true if completes.
+ */
+ private boolean createSpreadSheet(String spreadsheetTitle) {
+ if (isCancelled()) {
+ return false;
+ }
+ try {
+ spreadsheetId = SendDocsUtils.createSpreadsheet(spreadsheetTitle, documentsAuthToken, context);
+ } catch (IOException e) {
+ Log.d(TAG, "Unable to create spreadsheet", e);
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Fetches the worksheet ID. Sets the instance variable
+ * {@link SendDocsAsyncTask#worksheetId}.
+ *
+ * @return true if completes.
+ */
+ private boolean fetchWorksheetId() {
+ if (isCancelled()) {
+ return false;
+ }
+ try {
+ worksheetId = SendDocsUtils.getWorksheetId(
+ spreadsheetId, spreadsheetsClient, spreadsheetsAuthToken);
+ } catch (IOException e) {
+ Log.d(TAG, "Unable to fetch worksheet ID", e);
+ return false;
+ } catch (AuthenticationException e) {
+ Log.d(TAG, "Unable to fetch worksheet ID", e);
+ return false;
+ } catch (ParseException e) {
+ Log.d(TAG, "Unable to fetch worksheet ID", e);
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Adds track info to a worksheet.
+ *
+ * @param track the track
+ * @return true if completes.
+ */
+ private boolean addTrackInfo(Track track) {
+ if (isCancelled()) {
+ return false;
+ }
+ try {
+ SendDocsUtils.addTrackInfo(track, spreadsheetId, worksheetId, spreadsheetsAuthToken, context);
+ } catch (IOException e) {
+ Log.d(TAG, "Unable to add track info", e);
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Retries upload. Invalidates the authToken. If can retry, invokes
+ * {@link SendDocsAsyncTask#doUpload()}. Returns false if cannot retry.
+ */
+ private boolean retryUpload() {
+ if (isCancelled()) {
+ return false;
+ }
+
+ AccountManager.get(context).invalidateAuthToken(
+ documentsClient.getServiceName(), documentsAuthToken);
+ AccountManager.get(context).invalidateAuthToken(
+ spreadsheetsClient.getServiceName(), spreadsheetsAuthToken);
+ if (canRetry) {
+ canRetry = false;
+ return doUpload();
+ }
+ return false;
+ }
+}
diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/docs/SendDocsUtils.java b/MyTracks/src/com/google/android/apps/mytracks/io/docs/SendDocsUtils.java
new file mode 100644
index 000000000..adee07e89
--- /dev/null
+++ b/MyTracks/src/com/google/android/apps/mytracks/io/docs/SendDocsUtils.java
@@ -0,0 +1,410 @@
+/*
+ * 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.docs;
+
+import com.google.android.apps.mytracks.Constants;
+import com.google.android.apps.mytracks.content.Track;
+import com.google.android.apps.mytracks.io.gdata.docs.DocumentsClient;
+import com.google.android.apps.mytracks.io.gdata.docs.SpreadsheetsClient;
+import com.google.android.apps.mytracks.io.gdata.docs.SpreadsheetsClient.WorksheetEntry;
+import com.google.android.apps.mytracks.stats.TripStatistics;
+import com.google.android.apps.mytracks.util.ResourceUtils;
+import com.google.android.apps.mytracks.util.StringUtils;
+import com.google.android.apps.mytracks.util.UnitConversions;
+import com.google.android.maps.mytracks.R;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.wireless.gdata.client.HttpException;
+import com.google.wireless.gdata.data.Entry;
+import com.google.wireless.gdata.parser.GDataParser;
+import com.google.wireless.gdata.parser.ParseException;
+import com.google.wireless.gdata2.client.AuthenticationException;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.util.Log;
+
+import java.io.BufferedReader;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.net.URL;
+import java.net.URLConnection;
+import java.net.URLEncoder;
+import java.text.NumberFormat;
+import java.util.Locale;
+
+/**
+ * Utilities for sending a track to Google Docs.
+ *
+ * @author Sandor Dornbush
+ * @author Matthew Simmons
+ */
+public class SendDocsUtils {
+ private static final String GET_SPREADSHEET_BY_TITLE_URI =
+ "https://docs.google.com/feeds/documents/private/full?"
+ + "category=mine,spreadsheet&title=%s&title-exact=true";
+ private static final String CREATE_SPREADSHEET_URI =
+ "https://docs.google.com/feeds/documents/private/full";
+ private static final String GET_WORKSHEETS_URI =
+ "https://spreadsheets.google.com/feeds/worksheets/%s/private/full";
+ private static final String GET_WORKSHEET_URI =
+ "https://spreadsheets.google.com/feeds/list/%s/%s/private/full";
+
+ private static final String SPREADSHEET_ID_PREFIX =
+ "https://docs.google.com/feeds/documents/private/full/spreadsheet%3A";
+
+ private static final String CONTENT_TYPE = "Content-Type";
+ private static final String ATOM_FEED_MIME_TYPE = "application/atom+xml";
+ private static final String OPENDOCUMENT_SPREADSHEET_MIME_TYPE =
+ "application/x-vnd.oasis.opendocument.spreadsheet";
+
+ private static final String AUTHORIZATION = "Authorization";
+ private static final String AUTHORIZATION_PREFIX = "GoogleLogin auth=";
+
+ private static final String SLUG = "Slug";
+
+ // Google Docs can only parse numbers in the English locale.
+ private static final NumberFormat NUMBER_FORMAT = NumberFormat.getNumberInstance(Locale.ENGLISH);
+ private static final NumberFormat INTEGER_FORMAT = NumberFormat.getIntegerInstance(
+ Locale.ENGLISH);
+
+ static {
+ NUMBER_FORMAT.setMaximumFractionDigits(2);
+ NUMBER_FORMAT.setMinimumFractionDigits(2);
+ }
+
+ private static final String TAG = SendDocsUtils.class.getSimpleName();
+
+ private SendDocsUtils() {}
+
+ /**
+ * Gets the spreadsheet id of a spreadsheet. Returns null if the spreadsheet
+ * doesn't exist.
+ *
+ * @param title the title of the spreadsheet
+ * @param documentsClient the documents client
+ * @param authToken the auth token
+ * @return spreadsheet id or null if it doesn't exist.
+ */
+ public static String getSpreadsheetId(
+ String title, DocumentsClient documentsClient, String authToken)
+ throws IOException, ParseException, HttpException {
+ GDataParser gDataParser = null;
+ try {
+ String uri = String.format(GET_SPREADSHEET_BY_TITLE_URI, URLEncoder.encode(title));
+ gDataParser = documentsClient.getParserForFeed(Entry.class, uri, authToken);
+ gDataParser.init();
+
+ while (gDataParser.hasMoreData()) {
+ Entry entry = gDataParser.readNextEntry(null);
+ String entryTitle = entry.getTitle();
+ if (entryTitle.equals(title)) {
+ return getEntryId(entry);
+ }
+ }
+ return null;
+ } finally {
+ if (gDataParser != null) {
+ gDataParser.close();
+ }
+ }
+ }
+
+ /**
+ * Gets the id from an entry. Returns null if not available.
+ *
+ * @param entry the entry
+ */
+ @VisibleForTesting
+ static String getEntryId(Entry entry) {
+ String entryId = entry.getId();
+ if (entryId.startsWith(SPREADSHEET_ID_PREFIX)) {
+ return entryId.substring(SPREADSHEET_ID_PREFIX.length());
+ }
+ return null;
+ }
+
+ /**
+ * Creates a new spreadsheet with the given title. Returns the spreadsheet ID
+ * if successful. Returns null otherwise. Note that it is possible that a new
+ * spreadsheet is created, but the returned ID is null.
+ *
+ * @param title the title
+ * @param authToken the auth token
+ * @param context the context
+ */
+ public static String createSpreadsheet(String title, String authToken, Context context)
+ throws IOException {
+ URL url = new URL(CREATE_SPREADSHEET_URI);
+ URLConnection conn = url.openConnection();
+ conn.addRequestProperty(CONTENT_TYPE, OPENDOCUMENT_SPREADSHEET_MIME_TYPE);
+ conn.addRequestProperty(SLUG, title);
+ conn.addRequestProperty(AUTHORIZATION, AUTHORIZATION_PREFIX + authToken);
+ conn.setDoOutput(true);
+ OutputStream outputStream = conn.getOutputStream();
+ ResourceUtils.readBinaryFileToOutputStream(
+ context, R.raw.mytracks_empty_spreadsheet, outputStream);
+
+ // Get the response
+ BufferedReader bufferedReader = null;
+ StringBuilder resultBuilder = new StringBuilder();
+ try {
+ bufferedReader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
+ String line;
+ while ((line = bufferedReader.readLine()) != null) {
+ resultBuilder.append(line);
+ }
+ } catch (FileNotFoundException e) {
+ // The GData API sometimes throws an error, even though creation of
+ // the document succeeded. In that case let's just return. The caller
+ // then needs to check if the doc actually exists.
+ Log.d(TAG, "Unable to read result after creating a spreadsheet", e);
+ return null;
+ } finally {
+ outputStream.close();
+ if (bufferedReader != null) {
+ bufferedReader.close();
+ }
+ }
+ return getNewSpreadsheetId(resultBuilder.toString());
+ }
+
+ /**
+ * Gets the spreadsheet id from a create spreadsheet result.
+ *
+ * @param result the create spreadsheet result
+ */
+ @VisibleForTesting
+ static String getNewSpreadsheetId(String result) {
+ int idTagIndex = result.indexOf("");
+ if (idTagIndex == -1) {
+ return null;
+ }
+
+ int idTagCloseIndex = result.indexOf("", idTagIndex);
+ if (idTagCloseIndex == -1) {
+ return null;
+ }
+
+ int idStringStart = result.indexOf(SPREADSHEET_ID_PREFIX, idTagIndex);
+ if (idStringStart == -1) {
+ return null;
+ }
+
+ return result.substring(idStringStart + SPREADSHEET_ID_PREFIX.length(), idTagCloseIndex);
+ }
+
+ /**
+ * Gets the first worksheet ID of a spreadsheet. Returns null if not
+ * available.
+ *
+ * @param spreadsheetId the spreadsheet ID
+ * @param spreadsheetClient the spreadsheet client
+ * @param authToken the auth token
+ */
+ public static String getWorksheetId(
+ String spreadsheetId, SpreadsheetsClient spreadsheetClient, String authToken)
+ throws IOException, AuthenticationException, ParseException {
+ GDataParser gDataParser = null;
+ try {
+ String uri = String.format(GET_WORKSHEETS_URI, spreadsheetId);
+ gDataParser = spreadsheetClient.getParserForWorksheetsFeed(uri, authToken);
+ gDataParser.init();
+ if (!gDataParser.hasMoreData()) {
+ Log.d(TAG, "No worksheet");
+ return null;
+ }
+
+ // Get the first worksheet
+ WorksheetEntry worksheetEntry =
+ (WorksheetEntry) gDataParser.readNextEntry(new WorksheetEntry());
+ return getWorksheetEntryId(worksheetEntry);
+ } finally {
+ if (gDataParser != null) {
+ gDataParser.close();
+ }
+ }
+ }
+
+ /**
+ * Gets the worksheet id from a worksheet entry. Returns null if not available.
+ *
+ * @param entry the worksheet entry
+ */
+ @VisibleForTesting
+ static String getWorksheetEntryId(WorksheetEntry entry) {
+ String id = entry.getId();
+ int lastSlash = id.lastIndexOf('/');
+ if (lastSlash == -1) {
+ Log.d(TAG, "No id");
+ return null;
+ }
+ return id.substring(lastSlash + 1);
+ }
+
+ /**
+ * Adds a track's info as a row in a worksheet.
+ *
+ * @param track the track
+ * @param spreadsheetId the spreadsheet ID
+ * @param worksheetId the worksheet ID
+ * @param authToken the auth token
+ * @param context the context
+ */
+ public static void addTrackInfo(
+ Track track, String spreadsheetId, String worksheetId, String authToken, Context context)
+ throws IOException {
+ String worksheetUri = String.format(GET_WORKSHEET_URI, spreadsheetId, worksheetId);
+ SharedPreferences prefs = context.getSharedPreferences(
+ Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
+ boolean metricUnits = prefs.getBoolean(context.getString(R.string.metric_units_key), true);
+
+ addRow(worksheetUri, getRowContent(track, metricUnits, context), authToken);
+ }
+
+ /**
+ * Gets the row content containing the track's info.
+ *
+ * @param track the track
+ * @param metricUnits true to use metric
+ * @param context the context
+ */
+ @VisibleForTesting
+ static String getRowContent(Track track, boolean metricUnits, Context context) {
+ TripStatistics stats = track.getStatistics();
+
+ String distanceUnit = context.getString(
+ metricUnits ? R.string.unit_kilometer : R.string.unit_mile);
+ String speedUnit = context.getString(
+ metricUnits ? R.string.unit_kilometer_per_hour : R.string.unit_mile_per_hour);
+ String elevationUnit = context.getString(
+ metricUnits ? R.string.unit_meter : R.string.unit_feet);
+
+ StringBuilder builder = new StringBuilder().append("");
+ appendTag(builder, "name", track.getName());
+ appendTag(builder, "description", track.getDescription());
+ appendTag(builder, "date", StringUtils.formatDateTime(context, stats.getStartTime()));
+ appendTag(builder, "totaltime", StringUtils.formatElapsedTime(stats.getTotalTime()));
+ appendTag(builder, "movingtime", StringUtils.formatElapsedTime(stats.getMovingTime()));
+ appendTag(builder, "distance", getDistance(stats.getTotalDistance(), metricUnits));
+ appendTag(builder, "distanceunit", distanceUnit);
+ appendTag(builder, "averagespeed", getSpeed(stats.getAverageSpeed(), metricUnits));
+ appendTag(builder, "averagemovingspeed", getSpeed(stats.getAverageMovingSpeed(), metricUnits));
+ appendTag(builder, "maxspeed", getSpeed(stats.getMaxSpeed(), metricUnits));
+ appendTag(builder, "speedunit", speedUnit);
+ appendTag(builder, "elevationgain", getElevation(stats.getTotalElevationGain(), metricUnits));
+ appendTag(builder, "minelevation", getElevation(stats.getMinElevation(), metricUnits));
+ appendTag(builder, "maxelevation", getElevation(stats.getMaxElevation(), metricUnits));
+ appendTag(builder, "elevationunit", elevationUnit);
+
+ if (track.getMapId().length() > 0) {
+ appendTag(builder, "map",
+ String.format("%s?msa=0&msid=%s", Constants.MAPSHOP_BASE_URL, track.getMapId()));
+ }
+
+ builder.append("");
+ return builder.toString();
+ }
+
+ /**
+ * Appends a name-value pair as a gsx tag to a string builder.
+ *
+ * @param stringBuilder the string builder
+ * @param name the name
+ * @param value the value
+ */
+ @VisibleForTesting
+ static void appendTag(StringBuilder stringBuilder, String name, String value) {
+ stringBuilder
+ .append("")
+ .append(StringUtils.stringAsCData(value))
+ .append("");
+ }
+
+ /**
+ * Gets the distance. Performs unit conversion and formatting.
+ *
+ * @param distanceInMeter the distance in meters
+ * @param metricUnits true to use metric
+ */
+ @VisibleForTesting
+ static final String getDistance(double distanceInMeter, boolean metricUnits) {
+ double distanceInKilometer = distanceInMeter / 1000.0;
+ double distance = metricUnits ? distanceInKilometer
+ : distanceInKilometer * UnitConversions.KM_TO_MI;
+ return NUMBER_FORMAT.format(distance);
+ }
+
+ /**
+ * Gets the speed. Performs unit conversion and formatting.
+ *
+ * @param speedInMeterPerSecond the speed in meters per second
+ * @param metricUnits true to use metric
+ */
+ @VisibleForTesting
+ static final String getSpeed(double speedInMeterPerSecond, boolean metricUnits) {
+ double speedInKilometerPerHour = speedInMeterPerSecond * 3.6;
+ double speed = metricUnits ? speedInKilometerPerHour
+ : speedInKilometerPerHour * UnitConversions.KMH_TO_MPH;
+ return NUMBER_FORMAT.format(speed);
+ }
+
+ /**
+ * Gets the elevation. Performs unit conversion and formatting.
+ *
+ * @param elevationInMeter the elevation value in meters
+ * @param metricUnits true to use metric
+ */
+ @VisibleForTesting
+ static final String getElevation(double elevationInMeter, boolean metricUnits) {
+ double elevation = metricUnits ? elevationInMeter : elevationInMeter * UnitConversions.M_TO_FT;
+ return INTEGER_FORMAT.format(elevation);
+ }
+
+ /**
+ * Adds a row to a Google Spreadsheet worksheet.
+ *
+ * @param worksheetUri the worksheet URI
+ * @param rowContent the row content
+ * @param authToken the auth token
+ */
+ private static final void addRow(String worksheetUri, String rowContent, String authToken)
+ throws IOException {
+ URL url = new URL(worksheetUri);
+ URLConnection conn = url.openConnection();
+ conn.addRequestProperty(CONTENT_TYPE, ATOM_FEED_MIME_TYPE);
+ conn.addRequestProperty(AUTHORIZATION, AUTHORIZATION_PREFIX + authToken);
+ conn.setDoOutput(true);
+ OutputStreamWriter writer = new OutputStreamWriter(conn.getOutputStream());
+ writer.write(rowContent);
+ writer.flush();
+
+ // Get the response
+ BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
+ while ((reader.readLine()) != null) {
+ // Just read till the end
+ }
+ writer.close();
+ reader.close();
+ }
+}
diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/fusiontables/SendFusionTablesActivity.java b/MyTracks/src/com/google/android/apps/mytracks/io/fusiontables/SendFusionTablesActivity.java
new file mode 100644
index 000000000..287f8e955
--- /dev/null
+++ b/MyTracks/src/com/google/android/apps/mytracks/io/fusiontables/SendFusionTablesActivity.java
@@ -0,0 +1,149 @@
+/*
+ * 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.fusiontables;
+
+import com.google.android.apps.mytracks.io.docs.SendDocsActivity;
+import com.google.android.apps.mytracks.io.sendtogoogle.SendRequest;
+import com.google.android.apps.mytracks.io.sendtogoogle.UploadResultActivity;
+import com.google.android.maps.mytracks.R;
+
+import android.app.Activity;
+import android.app.Dialog;
+import android.app.ProgressDialog;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.os.Bundle;
+
+/**
+ * An activity to send a track to Google Fusion Tables.
+ *
+ * The activity gets recreated when the screen rotates. To support the activity
+ * displaying a progress dialog, we do the following:
+ *
+ * - use one instance of an AyncTask to send the track
+ * - save that instance as the last non configuration instance of the activity
+ *
+ * - when a new activity is created, pass the activity to the AsyncTask so
+ * that the AsyncTask can update the progress dialog of the activity
+ *
+ *
+ * @author jshih@google.com (Jimmy Shih)
+ */
+public class SendFusionTablesActivity extends Activity {
+
+ private static final int PROGRESS_DIALOG = 1;
+
+ private SendRequest sendRequest;
+ private SendFusionTablesAsyncTask asyncTask;
+ private ProgressDialog progressDialog;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ sendRequest = getIntent().getParcelableExtra(SendRequest.SEND_REQUEST_KEY);
+
+ Object retained = getLastNonConfigurationInstance();
+ if (retained instanceof SendFusionTablesAsyncTask) {
+ asyncTask = (SendFusionTablesAsyncTask) retained;
+ asyncTask.setActivity(this);
+ } else {
+ asyncTask = new SendFusionTablesAsyncTask(
+ this, sendRequest.getTrackId(), sendRequest.getAccount());
+ asyncTask.execute();
+ }
+ }
+
+ @Override
+ public Object onRetainNonConfigurationInstance() {
+ asyncTask.setActivity(null);
+ return asyncTask;
+ }
+
+ @Override
+ protected Dialog onCreateDialog(int id) {
+ switch (id) {
+ case PROGRESS_DIALOG:
+ progressDialog = new ProgressDialog(this);
+ progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
+ progressDialog.setIcon(android.R.drawable.ic_dialog_info);
+ progressDialog.setTitle(getString(
+ R.string.send_google_progress_title, getString(R.string.send_google_fusion_tables)));
+ progressDialog.setMax(100);
+ progressDialog.setProgress(0);
+ progressDialog.setCancelable(true);
+ progressDialog.setOnCancelListener(new DialogInterface.OnCancelListener() {
+ @Override
+ public void onCancel(DialogInterface dialog) {
+ asyncTask.cancel(true);
+ startNextActivity(false, true);
+ }
+ });
+ return progressDialog;
+ default:
+ return null;
+ }
+ }
+
+ /**
+ * Invokes when the associated AsyncTask completes.
+ *
+ * @param success true if success
+ */
+ public void onAsyncTaskCompleted(boolean success) {
+ startNextActivity(success, false);
+ }
+
+ /**
+ * Shows the progress dialog.
+ */
+ public void showProgressDialog() {
+ showDialog(PROGRESS_DIALOG);
+ }
+
+ /**
+ * Sets the progress dialog value.
+ */
+ public void setProgressDialogValue(int value) {
+ if (progressDialog != null) {
+ progressDialog.setProgress(value);
+ }
+ }
+
+ /**
+ * Starts the next activity.
+ *
+ * @param success true if sendFusionTables is success
+ * @param isCancel true if it is a cancel request
+ */
+ private void startNextActivity(boolean success, boolean isCancel) {
+ sendRequest.setFusionTablesSuccess(success);
+
+ Class> next;
+ if (isCancel) {
+ next = UploadResultActivity.class;
+ } else {
+ if (sendRequest.isSendDocs()) {
+ next = SendDocsActivity.class;
+ } else {
+ next = UploadResultActivity.class;
+ }
+ }
+ Intent intent = new Intent(this, next)
+ .putExtra(SendRequest.SEND_REQUEST_KEY, sendRequest);
+ startActivity(intent);
+ finish();
+ }
+}
diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/fusiontables/SendFusionTablesAsyncTask.java b/MyTracks/src/com/google/android/apps/mytracks/io/fusiontables/SendFusionTablesAsyncTask.java
new file mode 100644
index 000000000..6d5e99f7b
--- /dev/null
+++ b/MyTracks/src/com/google/android/apps/mytracks/io/fusiontables/SendFusionTablesAsyncTask.java
@@ -0,0 +1,531 @@
+// Copyright 2012 Google Inc. All Rights Reserved.
+
+package com.google.android.apps.mytracks.io.fusiontables;
+
+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.content.Waypoint;
+import com.google.android.apps.mytracks.io.sendtogoogle.SendToGoogleUtils;
+import com.google.android.apps.mytracks.stats.DoubleBuffer;
+import com.google.android.apps.mytracks.util.ApiAdapterFactory;
+import com.google.android.apps.mytracks.util.LocationUtils;
+import com.google.android.apps.mytracks.util.StringUtils;
+import com.google.android.apps.mytracks.util.SystemUtils;
+import com.google.android.apps.mytracks.util.UnitConversions;
+import com.google.android.maps.mytracks.R;
+import com.google.api.client.googleapis.GoogleHeaders;
+import com.google.api.client.googleapis.MethodOverride;
+import com.google.api.client.http.GenericUrl;
+import com.google.api.client.http.HttpRequest;
+import com.google.api.client.http.HttpRequestFactory;
+import com.google.api.client.http.HttpResponse;
+import com.google.api.client.http.HttpTransport;
+import com.google.api.client.http.InputStreamContent;
+import com.google.api.client.util.Strings;
+
+import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.accounts.AuthenticatorException;
+import android.accounts.OperationCanceledException;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.database.Cursor;
+import android.location.Location;
+import android.os.AsyncTask;
+import android.util.Log;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URLEncoder;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Vector;
+
+/**
+ * AsyncTask to send a track to Google Fusion Tables.
+ *
+ * @author jshih@google.com (Jimmy Shih)
+ */
+public class SendFusionTablesAsyncTask extends AsyncTask {
+
+ private static final String APP_NAME_PREFIX = "Google-MyTracks-";
+ private static final String SQL_KEY = "sql=";
+ private static final String CONTENT_TYPE = "application/x-www-form-urlencoded";
+ private static final String SERVICE_ID = "fusiontables";
+ private static final String FUSION_TABLES_BASE_URL =
+ "https://www.google.com/fusiontables/api/query";
+ private static final int MAX_POINTS_PER_UPLOAD = 2048;
+ private static final String GDATA_VERSION = "2";
+
+ private static final int PROGRESS_CREATE_TABLE = 0;
+ private static final int PROGRESS_UNLIST_TABLE = 5;
+ private static final int PROGRESS_UPLOAD_DATA_MIN = 10;
+ private static final int PROGRESS_UPLOAD_DATA_MAX = 90;
+ private static final int PROGRESS_UPLOAD_WAYPOINTS = 95;
+ private static final int PROGRESS_COMPLETE = 100;
+
+ // See http://support.google.com/fusiontables/bin/answer.py?hl=en&answer=185991
+ private static final String MARKER_TYPE_START = "large_green";
+ private static final String MARKER_TYPE_END = "large_red";
+ private static final String MARKER_TYPE_WAYPOINT = "large_yellow";
+
+ private static final String TAG = SendFusionTablesAsyncTask.class.getSimpleName();
+
+ private SendFusionTablesActivity activity;
+
+ private final Context context;
+ private final long trackId;
+ private final Account account;
+ private final MyTracksProviderUtils myTracksProviderUtils;
+ private final HttpRequestFactory httpRequestFactory;
+
+ /**
+ * True if can retry sending to Google Fusion Tables.
+ */
+ private boolean canRetry;
+
+ /**
+ * True if the AsyncTask has completed.
+ */
+ private boolean completed;
+
+ /**
+ * True if the result is success.
+ */
+ private boolean success;
+
+ // The following variables are for per upload states
+ private String authToken;
+ private String tableId;
+ int currentSegment;
+
+ public SendFusionTablesAsyncTask(
+ SendFusionTablesActivity activity, long trackId, Account account) {
+ this.activity = activity;
+ this.trackId = trackId;
+ this.account = account;
+
+ context = activity.getApplicationContext();
+ myTracksProviderUtils = MyTracksProviderUtils.Factory.get(context);
+ HttpTransport transport = ApiAdapterFactory.getApiAdapter().getHttpTransport();
+ httpRequestFactory = transport.createRequestFactory(new MethodOverride());
+
+ canRetry = true;
+ completed = false;
+ success = false;
+ }
+
+ /**
+ * Sets the activity associated with this AyncTask.
+ *
+ * @param activity the activity.
+ */
+ public void setActivity(SendFusionTablesActivity activity) {
+ this.activity = activity;
+ if (completed && activity != null) {
+ activity.onAsyncTaskCompleted(success);
+ }
+ }
+
+ @Override
+ protected void onPreExecute() {
+ activity.showProgressDialog();
+ }
+
+ @Override
+ protected Boolean doInBackground(Void... params) {
+ return doUpload();
+ }
+
+ @Override
+ protected void onProgressUpdate(Integer... values) {
+ if (activity != null) {
+ activity.setProgressDialogValue(values[0]);
+ }
+ }
+
+ @Override
+ protected void onPostExecute(Boolean result) {
+ success = result;
+ completed = true;
+ if (success) {
+ Track track = myTracksProviderUtils.getTrack(trackId);
+ if (track != null) {
+ track.setTableId(tableId);
+ myTracksProviderUtils.updateTrack(track);
+ } else {
+ Log.d(TAG, "No track");
+ }
+ }
+ if (activity != null) {
+ activity.onAsyncTaskCompleted(success);
+ }
+ }
+
+ /**
+ * Uploads a track to Google Fusion Tables.
+ *
+ * @return true if success.
+ */
+ private boolean doUpload() {
+ // Reset the per upload states
+ authToken = null;
+ tableId = null;
+ currentSegment = 1;
+
+ try {
+ authToken = AccountManager.get(context).blockingGetAuthToken(account, SERVICE_ID, false);
+ } catch (OperationCanceledException e) {
+ Log.d(TAG, "Unable to get auth token", e);
+ return retryUpload();
+ } catch (AuthenticatorException e) {
+ Log.d(TAG, "Unable to get auth token", e);
+ return retryUpload();
+ } catch (IOException e) {
+ Log.d(TAG, "Unable to get auth token", e);
+ return retryUpload();
+ }
+
+ Track track = myTracksProviderUtils.getTrack(trackId);
+ if (track == null) {
+ Log.d(TAG, "Track is null");
+ return false;
+ }
+
+ // Create a new table
+ publishProgress(PROGRESS_CREATE_TABLE);
+ if (!createNewTable(track)) {
+ // Retry upload in case the auth token is invalid
+ return retryUpload();
+ }
+
+ // Unlist table
+ publishProgress(PROGRESS_UNLIST_TABLE);
+ if (!unlistTable()) {
+ return false;
+ }
+
+ // Upload all the track points plus the start and end markers
+ publishProgress(PROGRESS_UPLOAD_DATA_MIN);
+ if (!uploadAllTrackPoints(track)) {
+ return false;
+ }
+
+ // Upload all the waypoints
+ publishProgress(PROGRESS_UPLOAD_WAYPOINTS);
+ if (!uploadWaypoints()) {
+ return false;
+ }
+
+ publishProgress(PROGRESS_COMPLETE);
+ return true;
+ }
+
+ /**
+ * Retries upload. Invalidates the authToken. If can retry, invokes
+ * {@link SendFusionTablesAsyncTask#doUpload()}. Returns false if cannot
+ * retry.
+ */
+ private boolean retryUpload() {
+ if (isCancelled()) {
+ return false;
+ }
+
+ AccountManager.get(context).invalidateAuthToken(SERVICE_ID, authToken);
+ if (canRetry) {
+ canRetry = false;
+ return doUpload();
+ }
+ return false;
+ }
+
+ /**
+ * Creates a new table.
+ *
+ * @param track the track
+ * @return true if success.
+ */
+ private boolean createNewTable(Track track) {
+ String query = "CREATE TABLE '" + SendFusionTablesUtils.escapeSqlString(track.getName())
+ + "' (name:STRING,description:STRING,geometry:LOCATION,marker:STRING)";
+ return sendQuery(query, true);
+ }
+
+ /**
+ * Unlists a table.
+ *
+ * @return true if success.
+ */
+ private boolean unlistTable() {
+ String query = "UPDATE TABLE " + tableId + " SET VISIBILITY = UNLISTED";
+ return sendQuery(query, false);
+ }
+
+ /**
+ * Uploads all the points in a track.
+ *
+ * @param track the track
+ * @return true if success.
+ */
+ private boolean uploadAllTrackPoints(Track track) {
+ Cursor locationsCursor = null;
+ try {
+ SharedPreferences prefs = context.getSharedPreferences(
+ Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
+ boolean metricUnits = prefs.getBoolean(context.getString(R.string.metric_units_key), true);
+
+ locationsCursor = myTracksProviderUtils.getLocationsCursor(trackId, 0, -1, false);
+ if (locationsCursor == null) {
+ Log.d(TAG, "Location cursor is null");
+ return false;
+ }
+ int locationsCount = locationsCursor.getCount();
+ List locations = new ArrayList(MAX_POINTS_PER_UPLOAD);
+ Location lastLocation = null;
+
+ // Limit the number of elevation readings. Ideally we would want around
+ // 250.
+ int elevationSamplingFrequency = Math.max(1, (int) (locationsCount / 250.0));
+ double totalDistance = 0;
+ DoubleBuffer elevationBuffer = new DoubleBuffer(Constants.ELEVATION_SMOOTHING_FACTOR);
+ Vector distances = new Vector();
+ Vector elevations = new Vector();
+
+ for (int i = 0; i < locationsCount; i++) {
+ locationsCursor.moveToPosition(i);
+
+ Location location = myTracksProviderUtils.createLocation(locationsCursor);
+ locations.add(location);
+
+ int readCount = i + 1;
+
+ if (readCount == 1) {
+ // Create a start marker
+ String name = context.getString(R.string.marker_label_start, track.getName());
+ if (!createNewPoint(name, "", location, MARKER_TYPE_START)) {
+ Log.d(TAG, "Unable to create the start marker");
+ return false;
+ }
+ }
+
+ // Add to the distances and elevations vectors
+ if (LocationUtils.isValidLocation(location)) {
+ // All points go into the smoothing buffer
+ elevationBuffer.setNext(metricUnits ? location.getAltitude()
+ : location.getAltitude() * UnitConversions.M_TO_FT);
+ if (lastLocation != null) {
+ totalDistance += (double) lastLocation.distanceTo(location);
+ }
+ if (readCount % elevationSamplingFrequency == 0) {
+ distances.add(totalDistance);
+ elevations.add(elevationBuffer.getAverage());
+ }
+ }
+
+ // Upload periodically
+ if (readCount % MAX_POINTS_PER_UPLOAD == 0) {
+ if (!prepareAndUploadPoints(track, locations, false)) {
+ Log.d(TAG, "Unable to upload points");
+ return false;
+ }
+ updateProgress(readCount, locationsCount);
+ locations.clear();
+ }
+ lastLocation = location;
+ }
+
+ // Do a final upload with the remaining locations
+ if (!prepareAndUploadPoints(track, locations, true)) {
+ Log.d(TAG, "Unable to upload points");
+ return false;
+ }
+
+ // Create an end marker
+ if (lastLocation != null) {
+ StringUtils stringUtils = new StringUtils(context);
+ track.setDescription("" + track.getDescription() + "
"
+ + stringUtils.generateTrackDescription(track, distances, elevations) + "
");
+ String name = context.getString(R.string.marker_label_end, track.getName());
+ if (!createNewPoint(name, track.getDescription(), lastLocation, MARKER_TYPE_END)) {
+ Log.d(TAG, "Unable to create the end marker");
+ return false;
+ }
+ }
+
+ return true;
+ } finally {
+ if (locationsCursor != null) {
+ locationsCursor.close();
+ }
+ }
+ }
+
+ /**
+ * Prepares and uploads a list of locations from a track.
+ *
+ * @param track the track
+ * @param locations the locations from the track
+ * @param lastBatch true if it is the last batch of locations
+ */
+ private boolean prepareAndUploadPoints(Track track, List locations, boolean lastBatch) {
+ // Prepare locations
+ ArrayList