Change search_global_description to "Tracks and waypoints"

This commit is contained in:
Jimmy Shih
2012-02-14 13:12:57 -08:00
commit 1e63be4bae
566 changed files with 59929 additions and 0 deletions
@@ -0,0 +1,240 @@
/*
* 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.backup;
import com.google.android.apps.mytracks.Constants;
import com.google.android.apps.mytracks.MyTracks;
import com.google.android.apps.mytracks.util.FileUtils;
import com.google.android.apps.mytracks.util.StringUtils;
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.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.content.Intent;
import android.util.Log;
import android.widget.Toast;
import java.io.IOException;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Date;
/**
* Helper which shows a UI for writing or restoring a backup,
* and calls the appropriate handler for actually executing those
* operations.
*
* @author Rodrigo Damazio
*/
public class BackupActivityHelper {
private static final Comparator<Date> REVERSE_DATE_ORDER =
new Comparator<Date>() {
@Override
public int compare(Date s1, Date s2) {
return s2.compareTo(s1);
}
};
private final FileUtils fileUtils;
private final ExternalFileBackup backup;
private final Activity activity;
public BackupActivityHelper(Activity activity) {
this.activity = activity;
this.fileUtils = new FileUtils();
this.backup = new ExternalFileBackup(activity, fileUtils);
}
/**
* Writes a full backup to the default file.
* This shows the results to the user.
*/
public void writeBackup() {
if (!fileUtils.isSdCardAvailable()) {
showToast(R.string.sd_card_error_no_storage);
return;
}
if (!backup.isBackupsDirectoryAvailable(true)) {
showToast(R.string.sd_card_error_create_dir);
return;
}
final ProgressDialog progressDialog = ProgressDialog.show(
activity,
activity.getString(R.string.generic_progress_title),
activity.getString(R.string.settings_backup_now_progress_message),
true);
// Do the writing in another thread
new Thread() {
@Override
public void run() {
try {
backup.writeToDefaultFile();
showToast(R.string.sd_card_success_write_file);
} catch (IOException e) {
Log.e(Constants.TAG, "Failed to write backup", e);
showToast(R.string.sd_card_error_write_file);
} finally {
dismissDialog(progressDialog);
}
}
}.start();
}
/**
* Restores a full backup from the SD card.
* The user will be given a choice of which backup to restore as well as a
* confirmation dialog.
*/
public void restoreBackup() {
// Get the list of existing backups
if (!fileUtils.isSdCardAvailable()) {
showToast(R.string.sd_card_error_no_storage);
return;
}
if (!backup.isBackupsDirectoryAvailable(false)) {
showToast(R.string.settings_backup_restore_no_backup);
return;
}
final Date[] backupDates = backup.getAvailableBackups();
if (backupDates == null || backupDates.length == 0) {
showToast(R.string.settings_backup_restore_no_backup);
return;
}
Arrays.sort(backupDates, REVERSE_DATE_ORDER);
// Show a confirmation dialog
Builder confirmationDialogBuilder = new AlertDialog.Builder(activity);
confirmationDialogBuilder.setMessage(R.string.settings_backup_restore_confirm_message);
confirmationDialogBuilder.setCancelable(true);
confirmationDialogBuilder.setPositiveButton(android.R.string.yes,
new OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
pickBackupForRestore(backupDates);
}
});
confirmationDialogBuilder.setNegativeButton(android.R.string.no, null);
confirmationDialogBuilder.create().show();
}
/**
* Shows a backup list for the user to pick, then restores it.
*
* @param backupDates the list of available backup files
*/
private void pickBackupForRestore(final Date[] backupDates) {
if (backupDates.length == 1) {
// Only one choice, don't bother showing the list
restoreFromDateAsync(backupDates[0]);
return;
}
// Make a user-visible version of the backup filenames
final String backupDateStrs[] = new String[backupDates.length];
for (int i = 0; i < backupDates.length; i++) {
backupDateStrs[i] = StringUtils.formatDateTime(activity, backupDates[i].getTime());
}
// Show a dialog for the user to pick which backup to restore
Builder dialogBuilder = new AlertDialog.Builder(activity);
dialogBuilder.setCancelable(true);
dialogBuilder.setTitle(R.string.settings_backup_restore_select_title);
dialogBuilder.setItems(backupDateStrs, new OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
// User picked to restore this one
restoreFromDateAsync(backupDates[which]);
}
});
dialogBuilder.create().show();
}
/**
* Shows a progress dialog, then starts restoring the backup asynchronously.
*
* @param date the date
*/
private void restoreFromDateAsync(final Date date) {
// Show a progress dialog
ProgressDialog.show(
activity,
activity.getString(R.string.generic_progress_title),
activity.getString(R.string.settings_backup_restore_progress_message),
true);
// Do the actual importing in another thread (don't block the UI)
new Thread() {
@Override
public void run() {
try {
backup.restoreFromDate(date);
showToast(R.string.sd_card_success_read_file);
} catch (IOException e) {
Log.e(Constants.TAG, "Failed to restore backup", e);
showToast(R.string.sd_card_error_read_file);
} finally {
// Data may have been restored, "reboot" the app to catch it
restartApplication();
}
}
}.start();
}
/**
* Restarts My Tracks completely.
* This forces any modified data to be re-read.
*/
private void restartApplication() {
Intent intent = new Intent(activity, MyTracks.class);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
activity.startActivity(intent);
}
/**
* Shows a toast with the given contents.
*/
private void showToast(final int resId) {
activity.runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(activity, resId, Toast.LENGTH_LONG).show();
}
});
}
/**
* Safely dismisses the given dialog.
*/
private void dismissDialog(final Dialog dialog) {
activity.runOnUiThread(new Runnable() {
@Override
public void run() {
dialog.dismiss();
}
});
}
}
@@ -0,0 +1,70 @@
/*
* 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.backup;
import static com.google.android.apps.mytracks.content.ContentTypeIds.*;
import com.google.android.apps.mytracks.content.TrackPointsColumns;
import com.google.android.apps.mytracks.content.TracksColumns;
import com.google.android.apps.mytracks.content.WaypointsColumns;
public class BackupColumns {
/** Columns that go into the backup. */
public static final String[] POINTS_BACKUP_COLUMNS =
{ TrackPointsColumns._ID, TrackPointsColumns.TRACKID, TrackPointsColumns.LATITUDE,
TrackPointsColumns.LONGITUDE, TrackPointsColumns.ALTITUDE, TrackPointsColumns.BEARING,
TrackPointsColumns.TIME, TrackPointsColumns.ACCURACY, TrackPointsColumns.SPEED,
TrackPointsColumns.SENSOR };
public static final byte[] POINTS_BACKUP_COLUMN_TYPES =
{ LONG_TYPE_ID, LONG_TYPE_ID, INT_TYPE_ID, INT_TYPE_ID, FLOAT_TYPE_ID,
FLOAT_TYPE_ID, LONG_TYPE_ID, FLOAT_TYPE_ID, FLOAT_TYPE_ID, BLOB_TYPE_ID };
public static final String[] TRACKS_BACKUP_COLUMNS = {
TracksColumns._ID, TracksColumns.NAME, TracksColumns.DESCRIPTION, TracksColumns.CATEGORY,
TracksColumns.STARTID, TracksColumns.STOPID, TracksColumns.STARTTIME, TracksColumns.STOPTIME,
TracksColumns.NUMPOINTS, TracksColumns.TOTALDISTANCE, TracksColumns.TOTALTIME,
TracksColumns.MOVINGTIME, TracksColumns.AVGSPEED, TracksColumns.AVGMOVINGSPEED,
TracksColumns.MAXSPEED, TracksColumns.MINELEVATION, TracksColumns.MAXELEVATION,
TracksColumns.ELEVATIONGAIN, TracksColumns.MINGRADE, TracksColumns.MAXGRADE,
TracksColumns.MINLAT, TracksColumns.MAXLAT, TracksColumns.MINLON, TracksColumns.MAXLON,
TracksColumns.MAPID, TracksColumns.TABLEID};
public static final byte[] TRACKS_BACKUP_COLUMN_TYPES = {
LONG_TYPE_ID, STRING_TYPE_ID, STRING_TYPE_ID, STRING_TYPE_ID,
LONG_TYPE_ID, LONG_TYPE_ID, LONG_TYPE_ID, LONG_TYPE_ID, INT_TYPE_ID,
FLOAT_TYPE_ID, LONG_TYPE_ID, LONG_TYPE_ID, FLOAT_TYPE_ID, FLOAT_TYPE_ID,
FLOAT_TYPE_ID, FLOAT_TYPE_ID, FLOAT_TYPE_ID, FLOAT_TYPE_ID, FLOAT_TYPE_ID,
FLOAT_TYPE_ID, INT_TYPE_ID, INT_TYPE_ID, INT_TYPE_ID, INT_TYPE_ID, STRING_TYPE_ID,
STRING_TYPE_ID};
public static final String[] WAYPOINTS_BACKUP_COLUMNS = {
WaypointsColumns._ID, WaypointsColumns.TRACKID, WaypointsColumns.NAME,
WaypointsColumns.DESCRIPTION, WaypointsColumns.CATEGORY, WaypointsColumns.ICON,
WaypointsColumns.TYPE, WaypointsColumns.LENGTH, WaypointsColumns.DURATION,
WaypointsColumns.STARTTIME, WaypointsColumns.STARTID, WaypointsColumns.STOPID,
WaypointsColumns.LATITUDE, WaypointsColumns.LONGITUDE, WaypointsColumns.ALTITUDE,
WaypointsColumns.BEARING, WaypointsColumns.TIME, WaypointsColumns.ACCURACY,
WaypointsColumns.SPEED, WaypointsColumns.TOTALDISTANCE, WaypointsColumns.TOTALTIME,
WaypointsColumns.MOVINGTIME, WaypointsColumns.AVGSPEED, WaypointsColumns.AVGMOVINGSPEED,
WaypointsColumns.MAXSPEED, WaypointsColumns.MINELEVATION, WaypointsColumns.MAXELEVATION,
WaypointsColumns.ELEVATIONGAIN, WaypointsColumns.MINGRADE, WaypointsColumns.MAXGRADE };
public static final byte[] WAYPOINTS_BACKUP_COLUMN_TYPES = {
LONG_TYPE_ID, LONG_TYPE_ID, STRING_TYPE_ID, STRING_TYPE_ID,
STRING_TYPE_ID, STRING_TYPE_ID, INT_TYPE_ID, FLOAT_TYPE_ID, LONG_TYPE_ID,
LONG_TYPE_ID, LONG_TYPE_ID, LONG_TYPE_ID, INT_TYPE_ID, INT_TYPE_ID,
FLOAT_TYPE_ID, FLOAT_TYPE_ID, LONG_TYPE_ID, FLOAT_TYPE_ID, FLOAT_TYPE_ID,
FLOAT_TYPE_ID, LONG_TYPE_ID, LONG_TYPE_ID, FLOAT_TYPE_ID, FLOAT_TYPE_ID,
FLOAT_TYPE_ID, FLOAT_TYPE_ID, FLOAT_TYPE_ID, FLOAT_TYPE_ID, FLOAT_TYPE_ID,
FLOAT_TYPE_ID };
}
@@ -0,0 +1,27 @@
/*
* 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.backup;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
/**
* Shared preferences listener which notifies the backup system about new data
* being available for backup.
*
* @author Rodrigo Damazio
*/
public interface BackupPreferencesListener extends OnSharedPreferenceChangeListener {
}
@@ -0,0 +1,40 @@
/*
* 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.backup;
import android.app.backup.BackupManager;
import android.content.Context;
import android.content.SharedPreferences;
/**
* Implementation of {@link BackupPreferencesListener} that calls the
* {@link BackupManager}.
*
* @author Jimmy Shih
*/
public class BackupPreferencesListenerImpl implements BackupPreferencesListener {
private final BackupManager backupManager;
public BackupPreferencesListenerImpl(Context context) {
this.backupManager = new BackupManager(context);
}
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
backupManager.dataChanged();
}
}
@@ -0,0 +1,248 @@
/*
* 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.backup;
import com.google.android.apps.mytracks.content.ContentTypeIds;
import android.database.Cursor;
import android.database.MergeCursor;
import java.io.DataOutputStream;
import java.io.IOException;
/**
* Database dumper which is able to write only part of the database
* according to some query.
*
* This dumper is symmetrical to {@link DatabaseImporter}.
*
* @author Rodrigo Damazio
*/
class DatabaseDumper {
/** The names of the columns being dumped. */
private final String[] columnNames;
/** The types of the columns being dumped. */
private final byte[] columnTypes;
/** Whether to output null fields. */
private final boolean outputNullFields;
// Temporary state
private int[] columnIndices;
private boolean[] hasFields;
public DatabaseDumper(String[] columnNames, byte[] columnTypes,
boolean outputNullFields) {
if (columnNames.length != columnTypes.length) {
throw new IllegalArgumentException("Names don't match types");
}
this.columnNames = columnNames;
this.columnTypes = columnTypes;
this.outputNullFields = outputNullFields;
}
/**
* Writes the header plus all rows that can be read from the given cursor.
* This assumes the cursor will have the same column and column indices on
* every row (and thus may not work with a {@link MergeCursor}).
*/
public void writeAllRows(Cursor cursor, DataOutputStream writer)
throws IOException {
writeHeaders(cursor, cursor.getCount(), writer);
if (!cursor.moveToFirst()) {
return;
}
do {
writeOneRow(cursor, writer);
} while (cursor.moveToNext());
}
/**
* Writes just the headers for the data that will come from the given cursor.
* The headers include column information and the number of rows that will be
* written.
*
* @param cursor the cursor to get columns from
* @param numRows the number of rows that will be later written
* @param writer the output to write to
* @throws IOException if there are errors while writing
*/
public void writeHeaders(Cursor cursor, int numRows, DataOutputStream writer)
throws IOException {
initializeCachedValues(cursor);
writeQueryMetadata(numRows, writer);
}
/**
* Writes the current row from the cursor. The cursor is not advanced.
* This must be called after {@link #writeHeaders}.
*
* @param cursor the cursor to write data from
* @param writer the output to write to
* @throws IOException if there are any errors while writing
*/
public void writeOneRow(Cursor cursor, DataOutputStream writer)
throws IOException {
if (columnIndices == null) {
throw new IllegalStateException(
"Cannot write rows before writing the header");
}
if (columnIndices.length > Long.SIZE) {
throw new IllegalArgumentException("Too many fields");
}
// Build a bitmap of which fields are present
long fields = 0;
for (int i = 0; i < columnIndices.length; i++) {
hasFields[i] = !cursor.isNull(columnIndices[i]);
fields |= (hasFields[i] ? 1 : 0) << i;
}
writer.writeLong(fields);
// Actually write the present fields
for (int i = 0; i < columnIndices.length; i++) {
if (hasFields[i]) {
writeCell(columnIndices[i], columnTypes[i], cursor, writer);
} else if (outputNullFields) {
writeDummyCell(columnTypes[i], writer);
}
}
}
/**
* Initializes the column indices and other temporary state for reading from
* the given cursor.
*/
private void initializeCachedValues(Cursor cursor) {
// These indices are constant for every row (unless we're fed a MergeCursor)
if (cursor instanceof MergeCursor) {
throw new IllegalArgumentException("Cannot use a MergeCursor");
}
columnIndices = new int[columnNames.length];
for (int i = 0; i < columnNames.length; i++) {
String columnName = columnNames[i];
columnIndices[i] = cursor.getColumnIndexOrThrow(columnName);
}
hasFields = new boolean[columnIndices.length];
}
/**
* Writes metadata about the query to be dumped.
*
* @param numRows the number of rows that will be dumped
* @param writer the output to write to
* @throws IOException if there are any errors while writing
*/
private void writeQueryMetadata(
int numRows, DataOutputStream writer) throws IOException {
// Write column data
writer.writeInt(columnNames.length);
for (int i = 0; i < columnNames.length; i++) {
String columnName = columnNames[i];
byte columnType = columnTypes[i];
writer.writeUTF(columnName);
writer.writeByte(columnType);
}
// Write the number of rows
writer.writeInt(numRows);
}
/**
* Writes a single cell of the database to the output.
*
* @param columnIdx the column index to read from
* @param columnTypeId the type of the column to be read
* @param cursor the cursor to read from
* @param writer the output to write to
* @throws IOException if there are any errors while writing
*/
private void writeCell(
int columnIdx, byte columnTypeId, Cursor cursor, DataOutputStream writer)
throws IOException {
switch (columnTypeId) {
case ContentTypeIds.LONG_TYPE_ID:
writer.writeLong(cursor.getLong(columnIdx));
return;
case ContentTypeIds.DOUBLE_TYPE_ID:
writer.writeDouble(cursor.getDouble(columnIdx));
return;
case ContentTypeIds.FLOAT_TYPE_ID:
writer.writeFloat(cursor.getFloat(columnIdx));
return;
case ContentTypeIds.BOOLEAN_TYPE_ID:
writer.writeBoolean(cursor.getInt(columnIdx) != 0);
return;
case ContentTypeIds.INT_TYPE_ID:
writer.writeInt(cursor.getInt(columnIdx));
return;
case ContentTypeIds.STRING_TYPE_ID:
writer.writeUTF(cursor.getString(columnIdx));
return;
case ContentTypeIds.BLOB_TYPE_ID: {
byte[] blob = cursor.getBlob(columnIdx);
writer.writeInt(blob.length);
writer.write(blob);
return;
}
default:
throw new IllegalArgumentException(
"Type " + columnTypeId + " not supported");
}
}
/**
* Writes a dummy cell value to the output.
*
* @param columnTypeId the type of the value to write
* @throws IOException if there are any errors while writing
*/
private void writeDummyCell(byte columnTypeId, DataOutputStream writer)
throws IOException {
switch (columnTypeId) {
case ContentTypeIds.LONG_TYPE_ID:
writer.writeLong(0L);
return;
case ContentTypeIds.DOUBLE_TYPE_ID:
writer.writeDouble(0.0);
return;
case ContentTypeIds.FLOAT_TYPE_ID:
writer.writeFloat(0.0f);
return;
case ContentTypeIds.BOOLEAN_TYPE_ID:
writer.writeBoolean(false);
return;
case ContentTypeIds.INT_TYPE_ID:
writer.writeInt(0);
return;
case ContentTypeIds.STRING_TYPE_ID:
writer.writeUTF("");
return;
case ContentTypeIds.BLOB_TYPE_ID:
writer.writeInt(0);
return;
default:
throw new IllegalArgumentException(
"Type " + columnTypeId + " not supported");
}
}
}
@@ -0,0 +1,197 @@
/*
* 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.backup;
import com.google.android.apps.mytracks.content.ContentTypeIds;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.net.Uri;
import java.io.DataInputStream;
import java.io.IOException;
/**
* Database importer which reads values written by {@link DatabaseDumper}.
*
* @author Rodrigo Damazio
*/
public class DatabaseImporter {
/** Maximum number of entries in a bulk insertion */
private static final int DEFAULT_BULK_SIZE = 1024;
private final Uri destinationUri;
private final ContentResolver resolver;
private final boolean readNullFields;
private final int bulkSize;
// Metadata read from the reader
private String[] columnNames;
private byte[] columnTypes;
public DatabaseImporter(Uri destinationUri, ContentResolver resolver,
boolean readNullFields) {
this(destinationUri, resolver, readNullFields, DEFAULT_BULK_SIZE);
}
protected DatabaseImporter(Uri destinationUri, ContentResolver resolver,
boolean readNullFields, int bulkSize) {
this.destinationUri = destinationUri;
this.resolver = resolver;
this.readNullFields = readNullFields;
this.bulkSize = bulkSize;
}
/**
* Reads the header which includes metadata about the table being imported.
*
* @throws IOException if there are any problems while reading
*/
private void readHeaders(DataInputStream reader) throws IOException {
int numColumns = reader.readInt();
columnNames = new String[numColumns];
columnTypes = new byte[numColumns];
for (int i = 0; i < numColumns; i++) {
columnNames[i] = reader.readUTF();
columnTypes[i] = reader.readByte();
}
}
/**
* Imports all rows from the reader into the database.
* Insertion is done in bulks for efficiency.
*
* @throws IOException if there are any errors while reading
*/
public void importAllRows(DataInputStream reader) throws IOException {
readHeaders(reader);
ContentValues[] valueBulk = new ContentValues[bulkSize];
int numValues = 0;
int numRows = reader.readInt();
int numColumns = columnNames.length;
// For each row
for (int r = 0; r < numRows; r++) {
if (valueBulk[numValues] == null) {
valueBulk[numValues] = new ContentValues(numColumns);
} else {
// Reuse values objects
valueBulk[numValues].clear();
}
// Read the fields bitmap
long fields = reader.readLong();
for (int c = 0; c < numColumns; c++) {
if ((fields & 1) == 1) {
// Field is present, read into values
readOneCell(columnNames[c], columnTypes[c], valueBulk[numValues],
reader);
} else if (readNullFields) {
// Field not present but still written, read and discard
readOneCell(columnNames[c], columnTypes[c], null, reader);
}
fields >>= 1;
}
numValues++;
// If we have enough values, flush them as a bulk insertion
if (numValues >= bulkSize) {
doBulkInsert(valueBulk);
numValues = 0;
}
}
// Do a final bulk insert with the leftovers
if (numValues > 0) {
ContentValues[] leftovers = new ContentValues[numValues];
System.arraycopy(valueBulk, 0, leftovers, 0, numValues);
doBulkInsert(leftovers);
}
}
protected void doBulkInsert(ContentValues[] values) {
resolver.bulkInsert(destinationUri, values);
}
/**
* Reads a single cell from the reader.
*
* @param name the name of the column to be read
* @param typeId the type ID of the column to be read
* @param values the {@link ContentValues} object to put the read cell value
* in - if null, the value is just discarded
* @throws IOException if there are any problems while reading
*/
private void readOneCell(String name, byte typeId, ContentValues values,
DataInputStream reader) throws IOException {
switch (typeId) {
case ContentTypeIds.BOOLEAN_TYPE_ID: {
boolean value = reader.readBoolean();
if (values != null) { values.put(name, value); }
return;
}
case ContentTypeIds.LONG_TYPE_ID: {
long value = reader.readLong();
if (values != null) { values.put(name, value); }
return;
}
case ContentTypeIds.DOUBLE_TYPE_ID: {
double value = reader.readDouble();
if (values != null) { values.put(name, value); }
return;
}
case ContentTypeIds.FLOAT_TYPE_ID: {
Float value = reader.readFloat();
if (values != null) { values.put(name, value); }
return;
}
case ContentTypeIds.INT_TYPE_ID: {
int value = reader.readInt();
if (values != null) { values.put(name, value); }
return;
}
case ContentTypeIds.STRING_TYPE_ID: {
String value = reader.readUTF();
if (values != null) { values.put(name, value); }
return;
}
case ContentTypeIds.BLOB_TYPE_ID: {
int blobLength = reader.readInt();
if (blobLength != 0) {
byte[] blob = new byte[blobLength];
int readBytes = reader.read(blob, 0, blobLength);
if (readBytes != blobLength) {
throw new IOException(String.format(
"Short read on column %s; expected %d bytes, read %d",
name, blobLength, readBytes));
}
if (values != null) {
values.put(name, blob);
}
}
return;
}
default:
throw new IOException("Read unknown type " + typeId);
}
}
}
@@ -0,0 +1,266 @@
/*
* 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.backup;
import static com.google.android.apps.mytracks.Constants.TAG;
import com.google.android.apps.mytracks.Constants;
import com.google.android.apps.mytracks.content.TrackPointsColumns;
import com.google.android.apps.mytracks.content.TracksColumns;
import com.google.android.apps.mytracks.content.WaypointsColumns;
import com.google.android.apps.mytracks.util.FileUtils;
import android.content.ContentResolver;
import android.content.Context;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.util.Log;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.TimeZone;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipOutputStream;
/**
* Handler for writing or reading single-file backups.
*
* @author Rodrigo Damazio
*/
class ExternalFileBackup {
// Filename format - in UTC
private static final SimpleDateFormat BACKUP_FILENAME_FORMAT =
new SimpleDateFormat("'backup-'yyyy-MM-dd_HH-mm-ss'.zip'");
static {
BACKUP_FILENAME_FORMAT.setTimeZone(TimeZone.getTimeZone("UTC"));
}
private static final String BACKUPS_SUBDIR = "backups";
private static final int BACKUP_FORMAT_VERSION = 1;
private static final String ZIP_ENTRY_NAME =
"backup.mytracks.v" + BACKUP_FORMAT_VERSION;
private static final int COMPRESSION_LEVEL = 8;
private final Context context;
private final FileUtils fileUtils;
public ExternalFileBackup(Context context, FileUtils fileUtils) {
this.context = context;
this.fileUtils = fileUtils;
}
/**
* Returns whether the backups directory is (or can be made) available.
*
* @param create whether to try creating the directory if it doesn't exist
*/
public boolean isBackupsDirectoryAvailable(boolean create) {
return getBackupsDirectory(create) != null;
}
/**
* Returns the backup directory, or null if not available.
*
* @param create whether to try creating the directory if it doesn't exist
*/
private File getBackupsDirectory(boolean create) {
String dirName = fileUtils.buildExternalDirectoryPath(BACKUPS_SUBDIR);
final File dir = new File(dirName);
Log.d(Constants.TAG, "Dir: " + dir.getAbsolutePath());
if (create) {
// Try to create - if that fails, return null
return fileUtils.ensureDirectoryExists(dir) ? dir : null;
} else {
// Return it if it already exists, otherwise return null
return dir.isDirectory() ? dir : null;
}
}
/**
* Returns a list of available backups to be restored.
*/
public Date[] getAvailableBackups() {
File dir = getBackupsDirectory(false);
if (dir == null) { return null; }
String[] fileNames = dir.list();
List<Date> backupDates = new ArrayList<Date>(fileNames.length);
for (int i = 0; i < fileNames.length; i++) {
String fileName = fileNames[i];
try {
backupDates.add(BACKUP_FILENAME_FORMAT.parse(fileName));
} catch (ParseException e) {
// Not a backup file, ignore
}
}
return backupDates.toArray(new Date[backupDates.size()]);
}
/**
* Writes the backup to the default file.
*/
public void writeToDefaultFile() throws IOException {
writeToFile(getFileForDate(new Date()));
}
/**
* Restores the backup from the given date.
*/
public void restoreFromDate(Date when) throws IOException {
restoreFromFile(getFileForDate(when));
}
/**
* Produces the proper file descriptor for the given backup date.
*/
private File getFileForDate(Date when) {
File dir = getBackupsDirectory(false);
String fileName = BACKUP_FILENAME_FORMAT.format(when);
File file = new File(dir, fileName);
return file;
}
/**
* Synchronously writes a backup to the given file.
*/
private void writeToFile(File outputFile) throws IOException {
Log.d(Constants.TAG,
"Writing backup to file " + outputFile.getAbsolutePath());
// Create all the auxiliary classes that will do the writing
PreferenceBackupHelper preferencesHelper = new PreferenceBackupHelper();
DatabaseDumper trackDumper = new DatabaseDumper(
BackupColumns.TRACKS_BACKUP_COLUMNS,
BackupColumns.TRACKS_BACKUP_COLUMN_TYPES,
false);
DatabaseDumper waypointDumper = new DatabaseDumper(
BackupColumns.WAYPOINTS_BACKUP_COLUMNS,
BackupColumns.WAYPOINTS_BACKUP_COLUMN_TYPES,
false);
DatabaseDumper pointDumper = new DatabaseDumper(
BackupColumns.POINTS_BACKUP_COLUMNS,
BackupColumns.POINTS_BACKUP_COLUMN_TYPES,
false);
// Open the target for writing
FileOutputStream outputStream = new FileOutputStream(outputFile);
ZipOutputStream compressedStream = new ZipOutputStream(outputStream);
compressedStream.setLevel(COMPRESSION_LEVEL);
compressedStream.putNextEntry(new ZipEntry(ZIP_ENTRY_NAME));
DataOutputStream outWriter = new DataOutputStream(compressedStream);
try {
// Dump the entire contents of each table
ContentResolver contentResolver = context.getContentResolver();
Cursor tracksCursor = contentResolver.query(
TracksColumns.CONTENT_URI, null, null, null, null);
try {
trackDumper.writeAllRows(tracksCursor, outWriter);
} finally {
tracksCursor.close();
}
Cursor waypointsCursor = contentResolver.query(
WaypointsColumns.CONTENT_URI, null, null, null, null);
try {
waypointDumper.writeAllRows(waypointsCursor, outWriter);
} finally {
waypointsCursor.close();
}
Cursor pointsCursor = contentResolver.query(
TrackPointsColumns.CONTENT_URI, null, null, null, null);
try {
pointDumper.writeAllRows(pointsCursor, outWriter);
} finally {
pointsCursor.close();
}
// Dump preferences
SharedPreferences preferences = context.getSharedPreferences(
Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
preferencesHelper.exportPreferences(preferences, outWriter);
} catch (IOException e) {
// We tried to delete the partially created file, but do nothing
// if that also fails.
if (!outputFile.delete()) {
Log.w(TAG, "Failed to delete file " + outputFile.getAbsolutePath());
}
throw e;
} finally {
compressedStream.closeEntry();
compressedStream.close();
}
}
/**
* Synchronously restores the backup from the given file.
*/
private void restoreFromFile(File inputFile) throws IOException {
Log.d(Constants.TAG,
"Restoring from file " + inputFile.getAbsolutePath());
PreferenceBackupHelper preferencesHelper = new PreferenceBackupHelper();
ContentResolver resolver = context.getContentResolver();
DatabaseImporter trackImporter =
new DatabaseImporter(TracksColumns.CONTENT_URI, resolver, false);
DatabaseImporter waypointImporter =
new DatabaseImporter(WaypointsColumns.CONTENT_URI, resolver, false);
DatabaseImporter pointImporter =
new DatabaseImporter(TrackPointsColumns.CONTENT_URI, resolver, false);
ZipFile zipFile = new ZipFile(inputFile, ZipFile.OPEN_READ);
ZipEntry zipEntry = zipFile.getEntry(ZIP_ENTRY_NAME);
if (zipEntry == null) {
throw new IOException("Invalid backup ZIP file");
}
InputStream compressedStream = zipFile.getInputStream(zipEntry);
DataInputStream reader = new DataInputStream(compressedStream);
try {
// Delete all previous contents of the tables and preferences.
resolver.delete(TracksColumns.CONTENT_URI, null, null);
resolver.delete(TrackPointsColumns.CONTENT_URI, null, null);
resolver.delete(WaypointsColumns.CONTENT_URI, null, null);
// Import the new contents of each table
trackImporter.importAllRows(reader);
waypointImporter.importAllRows(reader);
pointImporter.importAllRows(reader);
// Restore preferences
SharedPreferences preferences = context.getSharedPreferences(
Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
preferencesHelper.importPreferences(reader, preferences);
} finally {
compressedStream.close();
zipFile.close();
}
}
}
@@ -0,0 +1,99 @@
/*
* 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.backup;
import com.google.android.apps.mytracks.Constants;
import android.app.backup.BackupAgent;
import android.app.backup.BackupDataInput;
import android.app.backup.BackupDataOutput;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.ParcelFileDescriptor;
import android.util.Log;
import java.io.IOException;
/**
* Backup agent used to backup and restore all preferences.
* We use a regular {@link BackupAgent} instead of the convenient helpers in
* order to be future-proof (assuming we'll want to back up tracks later).
*
* @author Rodrigo Damazio
*/
public class MyTracksBackupAgent extends BackupAgent {
private static final String PREFERENCES_ENTITY = "prefs";
@Override
public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data,
ParcelFileDescriptor newState) throws IOException {
Log.i(Constants.TAG, "Performing backup");
SharedPreferences preferences = this.getSharedPreferences(
Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
backupPreferences(data, preferences);
Log.i(Constants.TAG, "Backup complete");
}
private void backupPreferences(BackupDataOutput data,
SharedPreferences preferences) throws IOException {
PreferenceBackupHelper preferenceDumper = createPreferenceBackupHelper();
byte[] dumpedContents = preferenceDumper.exportPreferences(preferences);
data.writeEntityHeader(PREFERENCES_ENTITY, dumpedContents.length);
data.writeEntityData(dumpedContents, dumpedContents.length);
}
protected PreferenceBackupHelper createPreferenceBackupHelper() {
return new PreferenceBackupHelper();
}
@Override
public void onRestore(BackupDataInput data, int appVersionCode,
ParcelFileDescriptor newState) throws IOException {
Log.i(Constants.TAG, "Restoring from backup");
while (data.readNextHeader()) {
String key = data.getKey();
Log.d(Constants.TAG, "Restoring entity " + key);
if (key.equals(PREFERENCES_ENTITY)) {
restorePreferences(data);
} else {
Log.e(Constants.TAG, "Found unknown backup entity: " + key);
data.skipEntityData();
}
}
Log.i(Constants.TAG, "Done restoring from backup");
}
/**
* Restores all preferences from the backup.
*
* @param data the backup data to read from
* @throws IOException if there are any errors while reading
*/
private void restorePreferences(BackupDataInput data) throws IOException {
int dataSize = data.getDataSize();
byte[] dataBuffer = new byte[dataSize];
int read = data.readEntityData(dataBuffer, 0, dataSize);
if (read != dataSize) {
throw new IOException("Failed to read all the preferences data");
}
SharedPreferences preferences = this.getSharedPreferences(
Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
PreferenceBackupHelper importer = createPreferenceBackupHelper();
importer.importPreferences(dataBuffer, preferences);
}
}
@@ -0,0 +1,174 @@
/*
* 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.backup;
import com.google.android.apps.mytracks.content.ContentTypeIds;
import com.google.android.apps.mytracks.util.ApiAdapterFactory;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.util.Map;
/**
* Helper for backing up and restoring shared preferences.
*
* @author Rodrigo Damazio
*/
class PreferenceBackupHelper {
private static final int BUFFER_SIZE = 2048;
/**
* Exports all shared preferences from the given object as a byte array.
*
* @param preferences the preferences to export
* @return the corresponding byte array
* @throws IOException if there are any errors while writing to the byte array
*/
public byte[] exportPreferences(SharedPreferences preferences)
throws IOException {
ByteArrayOutputStream bufStream = new ByteArrayOutputStream(BUFFER_SIZE);
DataOutputStream outWriter = new DataOutputStream(bufStream);
exportPreferences(preferences, outWriter);
return bufStream.toByteArray();
}
/**
* Exports all shared preferences from the given object into the given output
* stream.
*
* @param preferences the preferences to export
* @param outWriter the stream to write them to
* @throws IOException if there are any errors while writing the output
*/
public void exportPreferences(
SharedPreferences preferences,
DataOutputStream outWriter) throws IOException {
Map<String, ?> values = preferences.getAll();
outWriter.writeInt(values.size());
for (Map.Entry<String, ?> entry : values.entrySet()) {
writePreference(entry.getKey(), entry.getValue(), outWriter);
}
outWriter.flush();
}
/**
* Imports all preferences from the given byte array.
*
* @param data the byte array to read preferences from
* @param preferences the shared preferences to edit
* @throws IOException if there are any errors while reading
*/
public void importPreferences(byte[] data, SharedPreferences preferences)
throws IOException {
ByteArrayInputStream bufStream = new ByteArrayInputStream(data);
DataInputStream reader = new DataInputStream(bufStream);
importPreferences(reader, preferences);
}
/**
* Imports all preferences from the given stream.
*
* @param reader the stream to read from
* @param preferences the shared preferences to edit
* @throws IOException if there are any errors while reading
*/
public void importPreferences(DataInputStream reader,
SharedPreferences preferences) throws IOException {
Editor editor = preferences.edit();
editor.clear();
int numPreferences = reader.readInt();
for (int i = 0; i < numPreferences; i++) {
String name = reader.readUTF();
byte typeId = reader.readByte();
readAndSetPreference(name, typeId, reader, editor);
}
ApiAdapterFactory.getApiAdapter().applyPreferenceChanges(editor);
}
/**
* Reads a single preference and sets it into the given editor.
*
* @param name the name of the preference to read
* @param typeId the type ID of the preference to read
* @param reader the reader to read from
* @param editor the editor to set the preference in
* @throws IOException if there are errors while reading
*/
private void readAndSetPreference(String name, byte typeId,
DataInputStream reader, Editor editor) throws IOException {
switch (typeId) {
case ContentTypeIds.BOOLEAN_TYPE_ID:
editor.putBoolean(name, reader.readBoolean());
return;
case ContentTypeIds.LONG_TYPE_ID:
editor.putLong(name, reader.readLong());
return;
case ContentTypeIds.FLOAT_TYPE_ID:
editor.putFloat(name, reader.readFloat());
return;
case ContentTypeIds.INT_TYPE_ID:
editor.putInt(name, reader.readInt());
return;
case ContentTypeIds.STRING_TYPE_ID:
editor.putString(name, reader.readUTF());
return;
}
}
/**
* Writes a single preference.
*
* @param name the name of the preference to write
* @param value the correctly-typed value of the preference
* @param writer the writer to write to
* @throws IOException if there are errors while writing
*/
private void writePreference(String name, Object value, DataOutputStream writer)
throws IOException {
writer.writeUTF(name);
if (value instanceof Boolean) {
writer.writeByte(ContentTypeIds.BOOLEAN_TYPE_ID);
writer.writeBoolean((Boolean) value);
} else if (value instanceof Integer) {
writer.writeByte(ContentTypeIds.INT_TYPE_ID);
writer.writeInt((Integer) value);
} else if (value instanceof Long) {
writer.writeByte(ContentTypeIds.LONG_TYPE_ID);
writer.writeLong((Long) value);
} else if (value instanceof Float) {
writer.writeByte(ContentTypeIds.FLOAT_TYPE_ID);
writer.writeFloat((Float) value);
} else if (value instanceof String) {
writer.writeByte(ContentTypeIds.STRING_TYPE_ID);
writer.writeUTF((String) value);
} else {
throw new IllegalArgumentException(
"Type " + value.getClass().getName() + " not supported");
}
}
}
@@ -0,0 +1,51 @@
/*
* 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.AbstractSendActivity;
import com.google.android.apps.mytracks.io.sendtogoogle.AbstractSendAsyncTask;
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.content.Intent;
/**
* An activity to send a track to Google Docs.
*
* @author Jimmy Shih
*/
public class SendDocsActivity extends AbstractSendActivity {
@Override
protected AbstractSendAsyncTask createAsyncTask() {
return new SendDocsAsyncTask(this, sendRequest.getTrackId(), sendRequest.getAccount());
}
@Override
protected String getServiceName() {
return getString(R.string.send_google_docs);
}
@Override
protected void startNextActivity(boolean success, boolean isCancel) {
sendRequest.setDocsSuccess(success);
Intent intent = new Intent(this, UploadResultActivity.class)
.putExtra(SendRequest.SEND_REQUEST_KEY, sendRequest);
startActivity(intent);
finish();
}
}
@@ -0,0 +1,314 @@
/*
* 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.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.apps.mytracks.io.sendtogoogle.AbstractSendAsyncTask;
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.util.Log;
import java.io.IOException;
/**
* AsyncTask to send a track to Google Docs.
*
* @author Jimmy Shih
*/
public class SendDocsAsyncTask extends AbstractSendAsyncTask {
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 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;
// 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) {
super(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()));
}
@Override
protected void closeConnection() {
if (gDataClient != null) {
gDataClient.close();
}
}
@Override
protected void saveResult() {
// No action for Google Docs
}
@Override
protected boolean performTask() {
// 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 retryTask();
} catch (AuthenticatorException e) {
Log.d(TAG, "Unable to get auth token", e);
return retryTask();
} catch (IOException e) {
Log.d(TAG, "Unable to get auth token", e);
return retryTask();
}
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 retryTask();
}
// 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 retryTask();
}
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;
}
@Override
protected void invalidateToken() {
AccountManager.get(context).invalidateAuthToken(Constants.ACCOUNT_TYPE, documentsAuthToken);
AccountManager.get(context).invalidateAuthToken(Constants.ACCOUNT_TYPE, spreadsheetsAuthToken);
}
/**
* 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;
}
}
@@ -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("<id>");
if (idTagIndex == -1) {
return null;
}
int idTagCloseIndex = result.indexOf("</id>", 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("<entry xmlns='http://www.w3.org/2005/Atom' "
+ "xmlns:gsx='http://schemas.google.com/spreadsheets/2006/extended'>");
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("</entry>");
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("<gsx:")
.append(name)
.append(">")
.append(StringUtils.stringAsCData(value))
.append("</gsx:")
.append(name)
.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();
}
}
@@ -0,0 +1,170 @@
/*
* 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.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.FileUtils;
import android.location.Location;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.text.NumberFormat;
import java.util.Date;
/**
* Exports a track as a CSV file, according to RFC 4180.
*
* The first field is a type:
* TRACK - track description
* P - point
* WAYPOINT - waypoint
*
* For each type, the fields are:
*
* TRACK,,,,,,,,name,description,
* P,time,lat,lon,alt,bearing,accurancy,speed,,,segmentIdx
* WAYPOINT,time,lat,lon,alt,bearing,accuracy,speed,name,description,
*
* @author Rodrigo Damazio
*/
public class CsvTrackWriter implements TrackFormatWriter {
private static final NumberFormat SHORT_FORMAT = NumberFormat.getInstance();
static {
SHORT_FORMAT.setMaximumFractionDigits(4);
}
private int segmentIdx = 0;
private int numFields = -1;
private PrintWriter pw;
private Track track;
@Override
public String getExtension() {
return TrackFileFormat.CSV.getExtension();
}
@SuppressWarnings("hiding")
@Override
public void prepare(Track track, OutputStream out) {
this.track = track;
this.pw = new PrintWriter(out);
}
@Override
public void writeHeader() {
writeCommaSeparatedLine("TYPE", "TIME", "LAT", "LON", "ALT", "BEARING",
"ACCURACY", "SPEED", "NAME", "DESCRIPTION", "SEGMENT");
}
@Override
public void writeBeginTrack(Location firstPoint) {
writeCommaSeparatedLine("TRACK",
null, null, null, null, null, null, null,
track.getName(), track.getDescription(),
null);
}
@Override
public void writeOpenSegment() {
// Do nothing
}
@Override
public void writeLocation(Location location) {
String timeStr = FileUtils.FILE_TIMESTAMP_FORMAT.format(new Date(location.getTime()));
writeCommaSeparatedLine("P",
timeStr,
Double.toString(location.getLatitude()),
Double.toString(location.getLongitude()),
Double.toString(location.getAltitude()),
Double.toString(location.getBearing()),
SHORT_FORMAT.format(location.getAccuracy()),
SHORT_FORMAT.format(location.getSpeed()),
null, null,
Integer.toString(segmentIdx));
}
@Override
public void writeWaypoint(Waypoint waypoint) {
Location location = waypoint.getLocation();
String timeStr = FileUtils.FILE_TIMESTAMP_FORMAT.format(new Date(location.getTime()));
writeCommaSeparatedLine("WAYPOINT",
timeStr,
Double.toString(location.getLatitude()),
Double.toString(location.getLongitude()),
Double.toString(location.getAltitude()),
Double.toString(location.getBearing()),
SHORT_FORMAT.format(location.getAccuracy()),
SHORT_FORMAT.format(location.getSpeed()),
waypoint.getName(),
waypoint.getDescription(),
null);
}
/**
* Writes a single line of a comma-separated-value file.
*
* @param strs the values to be written as comma-separated
*/
private void writeCommaSeparatedLine(String... strs) {
if (numFields == -1) {
numFields = strs.length;
} else if (strs.length != numFields) {
throw new IllegalArgumentException(
"CSV lines with different number of fields");
}
boolean isFirst = true;
for (String str : strs) {
if (!isFirst) {
pw.print(',');
}
isFirst = false;
if (str != null) {
pw.print('"');
pw.print(str.replaceAll("\"", "\"\""));
pw.print('"');
}
}
pw.println();
}
@Override
public void writeCloseSegment() {
segmentIdx++;
}
@Override
public void writeEndTrack(Location lastPoint) {
// Do nothing
}
@Override
public void writeFooter() {
// Do nothing
}
@Override
public void close() {
pw.close();
}
}
@@ -0,0 +1,548 @@
/*
* 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.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.LocationUtils;
import com.google.android.apps.mytracks.util.StringUtils;
import android.location.Location;
import android.location.LocationManager;
import android.net.Uri;
import android.util.Log;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import org.xml.sax.Attributes;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
/**
* Imports GPX XML files to the my tracks provider.
*
* TODO: Show progress indication to the user.
*
* @author Leif Hendrik Wilden
* @author Steffen Horlacher
* @author Rodrigo Damazio
*/
public class GpxImporter extends DefaultHandler {
/*
* GPX-XML tag names and attributes.
*/
private static final String TAG_TRACK = "trk";
private static final String TAG_TRACK_POINT = "trkpt";
private static final Object TAG_TRACK_SEGMENT = "trkseg";
private static final String TAG_NAME = "name";
private static final String TAG_DESCRIPTION = "desc";
private static final String TAG_ALTITUDE = "ele";
private static final String TAG_TIME = "time";
private static final String ATT_LAT = "lat";
private static final String ATT_LON = "lon";
/**
* The maximum number of locations to buffer for bulk-insertion into the database.
*/
private static final int MAX_BUFFERED_LOCATIONS = 512;
/**
* Utilities for accessing the contnet provider.
*/
private final MyTracksProviderUtils providerUtils;
/**
* List of track ids written in the database. Only contains successfully
* written tracks.
*/
private final List<Long> tracksWritten;
/**
* Contains the current elements content.
*/
private String content;
/**
* Currently reading location.
*/
private Location location;
/**
* Previous location, required for calculations.
*/
private Location lastLocation;
/**
* Currently reading track.
*/
private Track track;
/**
* Statistics builder for the current track.
*/
private TripStatisticsBuilder statsBuilder;
/**
* Buffer of locations to be bulk-inserted into the database.
*/
private Location[] bufferedPointInserts = new Location[MAX_BUFFERED_LOCATIONS];
/**
* Number of locations buffered to be inserted into the database.
*/
private int numBufferedPointInserts = 0;
/**
* Number of locations already processed.
*/
private int numberOfLocations;
/**
* Number of segments already processed.
*/
private int numberOfSegments;
/**
* Used to identify if a track was written to the database but not yet
* finished successfully.
*/
private boolean isCurrentTrackRollbackable;
/**
* Flag to indicate if we're inside a track's xml element.
* Some sub elements like name may be used in other parts of the gpx file,
* and we use this to ignore them.
*/
private boolean isInTrackElement;
/**
* Counter to find out which child level of track we are processing.
*/
private int trackChildDepth;
/**
* SAX-Locator to get current line information.
*/
private Locator locator;
private Location lastSegmentLocation;
/**
* Reads GPS tracks from a GPX file and writes tracks and their coordinates to
* the database.
*
* @param is a input steam with gpx-xml data
* @return long[] array of track ids written in the database
* @throws SAXException a parsing error
* @throws ParserConfigurationException internal error
* @throws IOException a file reading problem
*/
public static long[] importGPXFile(final InputStream is,
final MyTracksProviderUtils providerUtils)
throws ParserConfigurationException, SAXException, IOException {
SAXParserFactory factory = SAXParserFactory.newInstance();
GpxImporter handler = new GpxImporter(providerUtils);
SAXParser parser = factory.newSAXParser();
long[] trackIds = null;
try {
long start = System.currentTimeMillis();
parser.parse(is, handler);
long end = System.currentTimeMillis();
Log.d(Constants.TAG, "Total import time: " + (end - start) + "ms");
trackIds = handler.getImportedTrackIds();
} finally {
// delete track if not finished
handler.rollbackUnfinishedTracks();
}
return trackIds;
}
/**
* Constructor, requires providerUtils for writing tracks the database.
*/
public GpxImporter(MyTracksProviderUtils providerUtils) {
this.providerUtils = providerUtils;
tracksWritten = new ArrayList<Long>();
}
@Override
public void characters(char[] ch, int start, int length) throws SAXException {
String newContent = new String(ch, start, length);
if (content == null) {
content = newContent;
} else {
// In 99% of the cases, a single call to this method will be made for each
// sequence of characters we're interested in, so we'll rarely be
// concatenating strings, thus not justifying the use of a StringBuilder.
content += newContent;
}
}
@Override
public void startElement(String uri, String localName, String name,
Attributes attributes) throws SAXException {
if (isInTrackElement) {
trackChildDepth++;
if (localName.equals(TAG_TRACK_POINT)) {
onTrackPointElementStart(attributes);
} else if (localName.equals(TAG_TRACK_SEGMENT)) {
onTrackSegmentElementStart();
} else if (localName.equals(TAG_TRACK)) {
String msg = createErrorMessage("Invalid GPX-XML detected");
throw new SAXException(msg);
}
} else if (localName.equals(TAG_TRACK)) {
isInTrackElement = true;
trackChildDepth = 0;
onTrackElementStart();
}
}
@Override
public void endElement(String uri, String localName, String name)
throws SAXException {
if (!isInTrackElement) {
content = null;
return;
}
// process these elements only as sub-elements of track
if (localName.equals(TAG_TRACK_POINT)) {
onTrackPointElementEnd();
} else if (localName.equals(TAG_ALTITUDE)) {
onAltitudeElementEnd();
} else if (localName.equals(TAG_TIME)) {
onTimeElementEnd();
} else if (localName.equals(TAG_NAME)) {
// we are only interested in the first level name element
if (trackChildDepth == 1) {
onNameElementEnd();
}
} else if (localName.equals(TAG_DESCRIPTION)) {
// we are only interested in the first level description element
if (trackChildDepth == 1) {
onDescriptionElementEnd();
}
} else if (localName.equals(TAG_TRACK_SEGMENT)) {
onTrackSegmentElementEnd();
} else if (localName.equals(TAG_TRACK)) {
onTrackElementEnd();
isInTrackElement = false;
trackChildDepth = 0;
}
trackChildDepth--;
// reset element content
content = null;
}
@Override
public void setDocumentLocator(Locator locator) {
this.locator = locator;
}
/**
* Create a new Track object and insert empty track in database. Track will be
* updated with missing values later.
*/
private void onTrackElementStart() {
track = new Track();
numberOfLocations = 0;
Uri trackUri = providerUtils.insertTrack(track);
long trackId = Long.parseLong(trackUri.getLastPathSegment());
track.setId(trackId);
isCurrentTrackRollbackable = true;
}
private void onDescriptionElementEnd() {
track.setDescription(content.toString().trim());
}
private void onNameElementEnd() {
track.setName(content.toString().trim());
}
/**
* Track segment started.
*/
private void onTrackSegmentElementStart() {
if (numberOfSegments > 0) {
// Add a segment separator:
location = new Location(LocationManager.GPS_PROVIDER);
location.setLatitude(100.0);
location.setLongitude(100.0);
location.setAltitude(0);
if (lastLocation != null) {
location.setTime(lastLocation.getTime());
}
insertTrackPoint(location);
lastLocation = location;
lastSegmentLocation = null;
location = null;
}
numberOfSegments++;
}
/**
* Reads trackpoint attributes and assigns them to the current location.
*
* @param attributes xml attributes
*/
private void onTrackPointElementStart(Attributes attributes) throws SAXException {
if (location != null) {
String errorMsg = createErrorMessage("Found a track point inside another one.");
throw new SAXException(errorMsg);
}
location = createLocationFromAttributes(attributes);
}
/**
* Creates and returns a location with the position parsed from the given
* attributes.
*
* @param attributes the attributes to parse
* @return the created location
* @throws SAXException if the attributes cannot be parsed
*/
private Location createLocationFromAttributes(Attributes attributes) throws SAXException {
String latitude = attributes.getValue(ATT_LAT);
String longitude = attributes.getValue(ATT_LON);
if (latitude == null || longitude == null) {
throw new SAXException(createErrorMessage("Point with no longitude or latitude"));
}
// create new location and set attributes
Location loc = new Location(LocationManager.GPS_PROVIDER);
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;
}
/**
* Track point finished, write in database.
*
* @throws SAXException - thrown if track point is invalid
*/
private void onTrackPointElementEnd() throws SAXException {
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
insertTrackPoint(location);
// first track point?
if (lastLocation == null && numberOfSegments == 1) {
track.setStartId(getLastPointId());
}
lastLocation = location;
lastSegmentLocation = location;
location = null;
} else {
// invalid location - abort import
String msg = createErrorMessage("Invalid location detected: " + location);
throw new SAXException(msg);
}
}
private void insertTrackPoint(Location loc) {
bufferedPointInserts[numBufferedPointInserts] = loc;
numBufferedPointInserts++;
numberOfLocations++;
if (numBufferedPointInserts >= MAX_BUFFERED_LOCATIONS) {
flushPointInserts();
}
}
private void flushPointInserts() {
if (numBufferedPointInserts <= 0) { return; }
providerUtils.bulkInsertTrackPoints(bufferedPointInserts, numBufferedPointInserts, track.getId());
numBufferedPointInserts = 0;
}
/**
* Track segment finished.
*/
private void onTrackSegmentElementEnd() {
// Nothing to be done
}
/**
* Track finished - update in database.
*/
private void onTrackElementEnd() {
if (lastLocation != null) {
flushPointInserts();
// Calculate statistics for the imported track and update
statsBuilder.pauseAt(lastLocation.getTime());
track.setStopId(getLastPointId());
track.setNumberOfPoints(numberOfLocations);
track.setStatistics(statsBuilder.getStatistics());
providerUtils.updateTrack(track);
tracksWritten.add(track.getId());
isCurrentTrackRollbackable = false;
lastSegmentLocation = null;
lastLocation = null;
statsBuilder = null;
} else {
// track contains no track points makes no real
// sense to import it as we have no location
// information -> roll back
rollbackUnfinishedTracks();
}
}
/**
* Setting time and doing additional calculations as this is the last value
* required. Also sets the start time for track and statistics as there is no
* start time in the track root element.
*
* @throws SAXException on parsing errors
*/
private void onTimeElementEnd() throws SAXException {
if (location == null) { return; }
// Parse the time
long time;
try {
time = StringUtils.parseXmlDateTime(content.trim());
} catch (IllegalArgumentException e) {
String msg = createErrorMessage("Unable to parse time: " + content);
throw new SAXException(msg, e);
}
// Calculate derived attributes from previous point
if (lastSegmentLocation != null) {
long timeDifference = time - lastSegmentLocation.getTime();
// check for negative time change
if (timeDifference < 0) {
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));
}
}
// Fill in the time
location.setTime(time);
// initialize start time with time of first track point
if (statsBuilder == null) {
statsBuilder = new TripStatisticsBuilder(time);
}
}
private void onAltitudeElementEnd() throws SAXException {
if (location != null) {
try {
location.setAltitude(Double.parseDouble(content));
} catch (NumberFormatException e) {
String msg = createErrorMessage("Unable to parse altitude: " + content);
throw new SAXException(msg, e);
}
}
}
/**
* Deletes the last track if it was not completely imported.
*/
public void rollbackUnfinishedTracks() {
if (isCurrentTrackRollbackable) {
providerUtils.deleteTrack(track.getId());
isCurrentTrackRollbackable = false;
}
}
/**
* Get all track ids of the tracks created by this importer run.
*
* @return array of track ids
*/
private long[] getImportedTrackIds() {
// Convert from java.lang.Long for convenience
long[] result = new long[tracksWritten.size()];
for (int i = 0; i < result.length; i++) {
result[i] = tracksWritten.get(i);
}
return result;
}
/**
* Returns the ID of the last point inserted into the database.
*/
private long getLastPointId() {
flushPointInserts();
return providerUtils.getLastLocationId(track.getId());
}
/**
* Builds a parsing error message with current line information.
*
* @param details details about the error, will be appended
* @return error message string with current line information
*/
private String createErrorMessage(String details) {
StringBuffer msg = new StringBuffer();
msg.append("Parsing error at line: ");
msg.append(locator.getLineNumber());
msg.append(" column: ");
msg.append(locator.getColumnNumber());
msg.append(". ");
msg.append(details);
return msg.toString();
}
}
@@ -0,0 +1,169 @@
/*
* 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.file;
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.FileUtils;
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.util.Date;
import java.util.Locale;
/**
* Log of one track.
*
* @author Sandor Dornbush
*/
public class GpxTrackWriter implements TrackFormatWriter {
private final NumberFormat elevationFormatter;
private final NumberFormat coordinateFormatter;
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);
}
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;
this.pw = new PrintWriter(out);
}
@Override
public String getExtension() {
return TrackFileFormat.GPX.getExtension();
}
@Override
public void writeHeader() {
if (pw != null) {
pw.format("<?xml version=\"1.0\" encoding=\"%s\" standalone=\"yes\"?>\n",
Charset.defaultCharset().name());
pw.println("<?xml-stylesheet type=\"text/xsl\" href=\"details.xsl\"?>");
pw.println("<gpx");
pw.println(" version=\"1.1\"");
pw.format(" creator=\"My Tracks running on %s\"\n", Build.MODEL);
pw.println(" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"");
pw.println(" xmlns=\"http://www.topografix.com/GPX/1/1\"");
pw.print(" xmlns:topografix=\"http://www.topografix.com/GPX/Private/"
+ "TopoGrafix/0/1\"");
pw.print(" xsi:schemaLocation=\"http://www.topografix.com/GPX/1/1 ");
pw.print("http://www.topografix.com/GPX/1/1/gpx.xsd ");
pw.print("http://www.topografix.com/GPX/Private/TopoGrafix/0/1 ");
pw.println("http://www.topografix.com/GPX/Private/TopoGrafix/0/1/"
+ "topografix.xsd\">");
// TODO: Author etc.
}
}
@Override
public void writeFooter() {
if (pw != null) {
pw.println("</gpx>");
}
}
@Override
public void writeBeginTrack(Location firstPoint) {
if (pw != null) {
pw.println("<trk>");
pw.println("<name>" + StringUtils.stringAsCData(track.getName())
+ "</name>");
pw.println("<desc>" + StringUtils.stringAsCData(track.getDescription())
+ "</desc>");
pw.println("<number>" + track.getId() + "</number>");
pw.println("<extensions><topografix:color>c0c0c0</topografix:color></extensions>");
}
}
@Override
public void writeEndTrack(Location lastPoint) {
if (pw != null) {
pw.println("</trk>");
}
}
@Override
public void writeOpenSegment() {
pw.println("<trkseg>");
}
@Override
public void writeCloseSegment() {
pw.println("</trkseg>");
}
@Override
public void writeLocation(Location l) {
if (pw != null) {
pw.println("<trkpt " + formatLocation(l) + ">");
Date d = new Date(l.getTime());
pw.println("<ele>" + elevationFormatter.format(l.getAltitude()) + "</ele>");
pw.println("<time>" + FileUtils.FILE_TIMESTAMP_FORMAT.format(d) + "</time>");
pw.println("</trkpt>");
}
}
@Override
public void close() {
if (pw != null) {
pw.close();
pw = null;
}
}
@Override
public void writeWaypoint(Waypoint waypoint) {
if (pw != null) {
Location l = waypoint.getLocation();
if (l != null) {
pw.println("<wpt " + formatLocation(l) + ">");
pw.println("<ele>" + elevationFormatter.format(l.getAltitude()) + "</ele>");
pw.println("<time>" + FileUtils.FILE_TIMESTAMP_FORMAT.format(l.getTime()) + "</time>");
pw.println("<name>" + StringUtils.stringAsCData(waypoint.getName())
+ "</name>");
pw.println("<desc>"
+ StringUtils.stringAsCData(waypoint.getDescription()) + "</desc>");
pw.println("</wpt>");
}
}
}
}
@@ -0,0 +1,58 @@
/*
* 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.ImportAllTracks;
import com.google.android.apps.mytracks.util.UriUtils;
import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import android.util.Log;
/**
* An activity that imports a track from a file and displays the track in My Tracks.
*
* @author Rodrigo Damazio
*/
public class ImportActivity extends Activity {
@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))) {
Log.e(TAG, "Received an intent with unsupported action: " + intent);
finish();
return;
}
if (!UriUtils.isFileUri(data)) {
Log.e(TAG, "Received an intent with unsupported data: " + intent);
finish();
return;
}
String path = data.getPath();
Log.i(TAG, "Importing GPX file at " + path);
new ImportAllTracks(this, path);
}
}
@@ -0,0 +1,235 @@
/*
* 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.file;
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.StringUtils;
import android.content.Context;
import android.location.Location;
import android.os.Build;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.util.Vector;
/**
* Write track as KML to a file.
*
* @author Leif Hendrik Wilden
*/
public class KmlTrackWriter implements TrackFormatWriter {
private final Vector<Double> distances = new Vector<Double>();
private final Vector<Double> elevations = new Vector<Double>();
private final StringUtils stringUtils;
private PrintWriter pw = null;
private Track track;
public KmlTrackWriter(Context context) {
stringUtils = new StringUtils(context);
}
/**
* Testing constructor.
*/
KmlTrackWriter(StringUtils stringUtils) {
this.stringUtils = stringUtils;
}
@SuppressWarnings("hiding")
@Override
public void prepare(Track track, OutputStream out) {
this.track = track;
this.pw = new PrintWriter(out);
}
@Override
public String getExtension() {
return TrackFileFormat.KML.getExtension();
}
@Override
public void writeHeader() {
if (pw != null) {
pw.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
pw.print("<kml");
pw.print(" xmlns=\"http://earth.google.com/kml/2.0\"");
pw.println(" xmlns:atom=\"http://www.w3.org/2005/Atom\">");
pw.println("<Document>");
pw.format("<atom:author><atom:name>My Tracks running on %s"
+ "</atom:name></atom:author>\n", Build.MODEL);
pw.println("<name>" + StringUtils.stringAsCData(track.getName())
+ "</name>");
pw.println("<description>"
+ StringUtils.stringAsCData(track.getDescription())
+ "</description>");
writeStyles();
}
}
@Override
public void writeFooter() {
if (pw != null) {
pw.println("</Document>");
pw.println("</kml>");
}
}
@Override
public void writeBeginTrack(Location firstPoint) {
if (pw != null) {
writePlacemark("(Start)", track.getDescription(), "#sh_green-circle",
firstPoint);
pw.println("<Placemark>");
pw.println("<name>" + StringUtils.stringAsCData(track.getName())
+ "</name>");
pw.println("<description>"
+ StringUtils.stringAsCData(track.getDescription())
+ "</description>");
pw.println("<styleUrl>#track</styleUrl>");
pw.println("<MultiGeometry>");
}
}
@Override
public void writeEndTrack(Location lastPoint) {
if (pw != null) {
pw.println("</MultiGeometry>");
pw.println("</Placemark>");
String description = stringUtils.generateTrackDescription(
track, distances, elevations);
writePlacemark("(End)", description, "#sh_red-circle", lastPoint);
}
}
@Override
public void writeOpenSegment() {
if (pw != null) {
pw.print("<LineString><coordinates>");
}
}
@Override
public void writeCloseSegment() {
if (pw != null) {
pw.println("</coordinates></LineString>");
}
}
@Override
public void writeLocation(Location l) {
if (pw != null) {
pw.print(l.getLongitude() + "," + l.getLatitude() + ","
+ l.getAltitude() + " ");
}
}
private String getPinStyle(Waypoint waypoint) {
if (waypoint.getType() == Waypoint.TYPE_STATISTICS) {
return "#sh_ylw-pushpin";
}
// Try to find the icon color.
// The string should be of the form:
// "http://maps.google.com/mapfiles/ms/micons/XXX.png"
int slash = waypoint.getIcon().lastIndexOf('/');
int png = waypoint.getIcon().lastIndexOf('.');
if ((slash != -1) && (slash < png)) {
String color = waypoint.getIcon().substring(slash + 1, png);
return "#sh_" + color + "-pushpin";
}
return "#sh_blue-pushpin";
}
@Override
public void writeWaypoint(Waypoint waypoint) {
if (pw != null) {
writePlacemark(
waypoint.getName(),
waypoint.getDescription(),
getPinStyle(waypoint),
waypoint.getLocation());
}
}
@Override
public void close() {
if (pw != null) {
pw.close();
pw = null;
}
}
private void writeStyles() {
pw.println("<Style id=\"track\"><LineStyle><color>7f0000ff</color>"
+ "<width>4</width></LineStyle></Style>");
pw.print("<Style id=\"sh_green-circle\"><IconStyle><scale>1.3</scale>");
pw.print("<Icon><href>http://maps.google.com/mapfiles/kml/paddle/"
+ "grn-circle.png</href></Icon>");
pw.println("<hotSpot x=\"32\" y=\"1\" xunits=\"pixels\" yunits=\"pixels\"/>"
+ "</IconStyle></Style>");
pw.print("<Style id=\"sh_red-circle\"><IconStyle><scale>1.3</scale>");
pw.print("<Icon><href>http://maps.google.com/mapfiles/kml/paddle/"
+ "red-circle.png</href></Icon>");
pw.println("<hotSpot x=\"32\" y=\"1\" xunits=\"pixels\" yunits=\"pixels\"/>"
+ "</IconStyle></Style>");
pw.print("<Style id=\"sh_ylw-pushpin\"><IconStyle><scale>1.3</scale>");
pw.print("<Icon><href>http://maps.google.com/mapfiles/kml/pushpin/"
+ "ylw-pushpin.png</href></Icon>");
pw.println("<hotSpot x=\"20\" y=\"2\" xunits=\"pixels\" yunits=\"pixels\"/>"
+ "</IconStyle></Style>");
pw.print("<Style id=\"sh_blue-pushpin\"><IconStyle><scale>1.3</scale>");
pw.print("<Icon><href>http://maps.google.com/mapfiles/kml/pushpin/"
+ "blue-pushpin.png</href></Icon>");
pw.println("<hotSpot x=\"20\" y=\"2\" xunits=\"pixels\" yunits=\"pixels\"/>"
+ "</IconStyle></Style>");
pw.print("<Style id=\"sh_green-pushpin\"><IconStyle><scale>1.3</scale>");
pw.print("<Icon><href>http://maps.google.com/mapfiles/kml/pushpin/"
+ "grn-pushpin.png</href></Icon>");
pw.println("<hotSpot x=\"20\" y=\"2\" xunits=\"pixels\" yunits=\"pixels\"/>"
+ "</IconStyle></Style>");
pw.print("<Style id=\"sh_red-pushpin\"><IconStyle><scale>1.3</scale>");
pw.print("<Icon><href>http://maps.google.com/mapfiles/kml/pushpin/"
+ "red-pushpin.png</href></Icon>");
pw.println("<hotSpot x=\"20\" y=\"2\" xunits=\"pixels\" yunits=\"pixels\"/>"
+ "</IconStyle></Style>");
}
private void writePlacemark(String name, String description, String style,
Location location) {
if (location != null) {
pw.println("<Placemark>");
pw.println(" <name>" + StringUtils.stringAsCData(name) + "</name>");
pw.println(" <description>" + StringUtils.stringAsCData(description)
+ "</description>");
pw.println(" <styleUrl>" + style + "</styleUrl>");
pw.println(" <Point>");
pw.println(" <coordinates>" + location.getLongitude() + ","
+ location.getLatitude() + "</coordinates>");
pw.println(" </Point>");
pw.println("</Placemark>");
}
}
}
@@ -0,0 +1,258 @@
/*
* 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.PlayTrackUtils;
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_FILE_FORMAT = "file_format";
public static final String EXTRA_SHARE_FILE = "share_file";
public static final String EXTRA_PLAY_FILE = "play_file";
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 boolean playFile;
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.track_action_save).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);
playFile = intent.getBooleanExtra(EXTRA_PLAY_FILE, false);
writer = TrackWriterFactory.newWriter(this, providerUtils, trackId, format);
if (writer == null) {
Log.e(TAG, "Unable to build writer");
finish();
return;
}
if (shareFile || playFile) {
// 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();
} if (playFile) {
playWrittenFile();
} 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.share_track_subject).toString());
shareIntent.putExtra(Intent.EXTRA_TEXT,
getResources().getText(R.string.share_track_file_body_format)
.toString());
shareIntent.setType(format.getMimeType());
Uri u = Uri.fromFile(new File(writer.getAbsolutePath()));
shareIntent.putExtra(Intent.EXTRA_STREAM, u);
shareIntent.putExtra(getString(R.string.track_id_broadcast_extra), trackId);
startActivity(Intent.createChooser(shareIntent,
getResources().getText(R.string.share_track_picker_title).toString()));
}
private void playWrittenFile() {
if (!writer.wasSuccess()) {
showResultDialog();
return;
}
Uri uri = Uri.fromFile(new File(writer.getAbsolutePath()));
Intent intent = new Intent()
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
.setDataAndType(uri, PlayTrackUtils.KML_MIME_TYPE);
startActivity(intent);
}
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.setPositiveButton(R.string.generic_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.generic_success_title : R.string.generic_error_title);
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.track_action_save));
intent.setDataAndType(uri, TracksColumns.CONTENT_ITEMTYPE);
intent.putExtra(EXTRA_FILE_FORMAT, exportFormat.ordinal());
intent.putExtra(EXTRA_SHARE_FILE, shareFile);
ctx.startActivity(intent);
}
}
@@ -0,0 +1,313 @@
/*
* 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.FileUtils;
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.util.Date;
import java.util.Locale;
/**
* 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 {
// 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 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;
}
@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("<?xml version=\"1.0\" encoding=\"%s\" standalone=\"no\" ?>\n",
Charset.defaultCharset().name());
pw.print("<TrainingCenterDatabase ");
pw.print("xmlns=\"http://www.garmin.com/xmlschemas/TrainingCenterDatabase/v2\" ");
pw.print("xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" ");
pw.print("xsi:schemaLocation=\"http://www.garmin.com/xmlschemas/TrainingCenterDatabase/v2 ");
pw.println("http://www.garmin.com/xmlschemas/TrainingCenterDatabasev2.xsd\">");
pw.println();
}
@Override
public void writeBeginTrack(Location firstPoint) {
if (pw == null) {
return;
}
String startTime = FileUtils.FILE_TIMESTAMP_FORMAT.format(track.getStatistics().getStartTime());
pw.println(" <Activities>");
pw.format(" <Activity Sport=\"%s\">\n", categoryToTcxSport(track.getCategory()));
pw.format(" <Id>%s</Id>\n", startTime);
pw.format(" <Lap StartTime=\"%s\">\n", startTime);
pw.print(" <TotalTimeSeconds>");
pw.print(track.getStatistics().getTotalTime() / 1000);
pw.println("</TotalTimeSeconds>");
pw.print(" <DistanceMeters>");
pw.print(track.getStatistics().getTotalDistance());
pw.println("</DistanceMeters>");
// TODO max speed etc.
// Calories are a required element just put in 0.
pw.print("<Calories>0</Calories>");
pw.println("<Intensity>Active</Intensity>");
pw.println("<TriggerMethod>Manual</TriggerMethod>");
}
@Override
public void writeOpenSegment() {
if (pw != null) {
pw.println(" <Track>");
}
}
@Override
public void writeLocation(Location location) {
if (pw == null) {
return;
}
pw.println(" <Trackpoint>");
Date d = new Date(location.getTime());
pw.println(" <Time>" + FileUtils.FILE_TIMESTAMP_FORMAT.format(d) + "</Time>");
pw.println(" <Position>");
pw.print(" <LatitudeDegrees>");
pw.print(location.getLatitude());
pw.println("</LatitudeDegrees>");
pw.print(" <LongitudeDegrees>");
pw.print(location.getLongitude());
pw.println("</LongitudeDegrees>");
pw.println(" </Position>");
pw.print(" <AltitudeMeters>");
pw.print(location.getAltitude());
pw.println("</AltitudeMeters>");
if (location instanceof MyTracksLocation) {
SensorDataSet sensorData = ((MyTracksLocation) location).getSensorDataSet();
if (sensorData != null) {
if (sensorData.hasHeartRate()
&& sensorData.getHeartRate().getState() == Sensor.SensorState.SENDING
&& sensorData.getHeartRate().hasValue()) {
pw.print(" <HeartRateBpm>");
pw.print("<Value>");
pw.print(sensorData.getHeartRate().getValue());
pw.print("</Value>");
pw.println("</HeartRateBpm>");
}
boolean cadenceAvailable = sensorData.hasCadence()
&& sensorData.getCadence().getState() == Sensor.SensorState.SENDING
&& sensorData.getCadence().hasValue();
// TCX Trackpoint_t contains a sequence. Thus, the legacy XML element
// <Cadence> needs to be put before <Extensions>.
// This field should only be used for the case that activity was marked as biking.
// Otherwise cadence is interpreted as running cadence data which
// is written in the <Extensions> as <RunCadence>.
if (sportIsCycling && cadenceAvailable) {
pw.print(" <Cadence>");
pw.print(Math.min(254, sensorData.getCadence().getValue()));
pw.println("</Cadence>");
}
boolean powerAvailable = sensorData.hasPower()
&& sensorData.getPower().getState() == Sensor.SensorState.SENDING
&& sensorData.getPower().hasValue();
if(powerAvailable || (!sportIsCycling && cadenceAvailable)) {
pw.print(" <Extensions>");
pw.print("<TPX xmlns=\"http://www.garmin.com/xmlschemas/ActivityExtension/v2\">");
// RunCadence needs to be put before power in order to be understood
// by Garmin Training Center.
if (!sportIsCycling && cadenceAvailable) {
pw.print("<RunCadence>");
pw.print(Math.min(254, sensorData.getCadence().getValue()));
pw.print("</RunCadence>");
}
if (powerAvailable) {
pw.print("<Watts>");
pw.print(sensorData.getPower().getValue());
pw.print("</Watts>");
}
pw.println("</TPX></Extensions>");
}
}
}
pw.println(" </Trackpoint>");
}
@Override
public void writeCloseSegment() {
if (pw != null) {
pw.println(" </Track>");
}
}
@Override
public void writeEndTrack(Location lastPoint) {
if (pw == null) {
return;
}
pw.println(" </Lap>");
pw.print(" <Creator xsi:type=\"Device_t\">");
pw.format("<Name>My Tracks running on %s</Name>\n", Build.MODEL);
// The following code is correct. ID is inconsistently capitalized in the
// TCX schema.
pw.println("<UnitId>0</UnitId>");
pw.println("<ProductID>0</ProductID>");
writeVersion();
pw.println("</Creator>");
pw.println(" </Activity>");
pw.println(" </Activities>");
}
@Override
public void writeFooter() {
if (pw == null) {
return;
}
pw.println(" <Author xsi:type=\"Application_t\">");
// 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("<Name>My Tracks %s by Google</Name>\n", SystemUtils.getMyTracksVersion(context));
pw.println("<Build>");
writeVersion();
pw.format("<Type>%s</Type>\n", SystemUtils.isRelease(context) ? TCX_TYPE_RELEASE
: TCX_TYPE_INTERNAL);
pw.println("</Build>");
pw.format("<LangID>%s</LangID>\n", Locale.getDefault().getLanguage());
pw.println("<PartNumber>000-00000-00</PartNumber>");
pw.println("</Author>");
pw.println("</TrainingCenterDatabase>");
}
@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("<Version>");
pw.format("<VersionMajor>%d</VersionMajor>\n", Integer.valueOf(myTracksVersionComponents[0]));
pw.format("<VersionMinor>%d</VersionMinor>\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("<BuildMajor>%d</BuildMajor>\n", Integer.valueOf(myTracksVersionComponents[2]));
pw.format("<BuildMinor>%d</BuildMinor>\n", SystemUtils.isRelease(context) ? 0 : 1);
pw.println("</Version>");
}
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;
}
}
}
@@ -0,0 +1,78 @@
/*
* 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 static com.google.android.apps.mytracks.Constants.TAG;
import com.google.android.apps.mytracks.util.FileUtils;
import android.os.Environment;
import android.util.Log;
import java.io.File;
/**
* A class to clean up old temporary files.
* @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)) {
return; // Can't do anything now.
}
cleanTmpDirectory("csv");
cleanTmpDirectory("gpx");
cleanTmpDirectory("kml");
cleanTmpDirectory("tcx");
}
private void cleanTmpDirectory(String name) {
FileUtils fileUtils = new FileUtils();
String dirName = fileUtils.buildExternalDirectoryPath(name, "tmp");
cleanTmpDirectory(new File(dirName));
}
// @VisibleForTesting
int cleanTmpDirectory(File dir) {
if (!dir.exists()) {
return 0;
}
int count = 0;
long oldest = currentTimeMillis - 1000 * 3600;
for (File f : dir.listFiles()) {
if (f.lastModified() < oldest) {
if (!f.delete()) {
Log.w(TAG, "Failed to delete file " + f.getAbsolutePath());
}
count++;
}
}
return count;
}
}
@@ -0,0 +1,113 @@
/*
* 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.Track;
import com.google.android.apps.mytracks.content.Waypoint;
import android.location.Location;
import java.io.OutputStream;
/**
* Interface for writing data to a specific track file format.
*
* The expected sequence of calls is:
* <ol>
* <li>{@link #prepare}
* <li>{@link #writeHeader}
* <li>{@link #writeBeginTrack}
* <li>For each segment:
* <ol>
* <li>{@link #writeOpenSegment}
* <li>For each location in the segment: {@link #writeLocation}
* <li>{@link #writeCloseSegment}
* </ol>
* <li>{@link #writeEndTrack}
* <li>For each waypoint: {@link #writeWaypoint}
* <li>{@link #writeFooter}
* </ol>
*
* @author Rodrigo Damazio
*/
public interface TrackFormatWriter {
/**
* Sets up the writer to write the given track to the given output.
*
* @param track the track to write
* @param out the stream to write the track contents to
*/
void prepare(Track track, OutputStream out);
/**
* @return The file extension (i.e. gpx, kml, ...)
*/
String getExtension();
/**
* Writes the header.
* This is chance for classes to write out opening information.
*/
void writeHeader();
/**
* Writes the footer.
* This is chance for classes to write out closing information.
*/
void writeFooter();
/**
* Write the given location object.
*
* TODO Add some flexible handling of other sensor data.
*
* @param location the location to write
*/
void writeLocation(Location location) throws InterruptedException;
/**
* Write a way point.
*
* @param waypoint
*/
void writeWaypoint(Waypoint waypoint);
/**
* Write the beginning of a track.
*/
void writeBeginTrack(Location firstPoint);
/**
* Write the end of a track.
*/
void writeEndTrack(Location lastPoint);
/**
* Write the statements necessary to open a new segment.
*/
void writeOpenSegment();
/**
* Write the statements necessary to close a segment.
*/
void writeCloseSegment();
/**
* Close the underlying file handle.
*/
void close();
}
@@ -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();
}
@@ -0,0 +1,121 @@
/*
* 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 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.
*
* @author Rodrigo Damazio
*/
public class TrackWriterFactory {
/**
* Definition of all possible track formats.
*/
public enum TrackFileFormat {
GPX {
@Override
TrackFormatWriter newFormatWriter(Context context) {
return new GpxTrackWriter();
}
},
KML {
@Override
TrackFormatWriter newFormatWriter(Context context) {
return new KmlTrackWriter(context);
}
},
CSV {
@Override
public TrackFormatWriter newFormatWriter(Context context) {
return new CsvTrackWriter();
}
},
TCX {
@Override
public TrackFormatWriter newFormatWriter(Context context) {
return new TcxTrackWriter(context);
}
};
/**
* Creates and returns a new format writer for each format.
*/
abstract TrackFormatWriter newFormatWriter(Context context);
/**
* Returns the mime type for each format.
*/
public String getMimeType() {
return "application/" + getExtension() + "+xml";
}
/**
* Returns the file extension for each format.
*/
public String getExtension() {
return this.name().toLowerCase();
}
}
/**
* Creates a new track writer to write the track with the given ID.
*
* @param context the context in which the track will be read
* @param providerUtils the data provider utils to read the track with
* @param trackId the ID of the track to be written
* @param format the output format to write in
* @return the new track writer
*/
public static TrackWriter newWriter(Context context,
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);
}
/**
* Creates a new track writer to write the given track.
*
* @param context the context in which the track will be read
* @param providerUtils the data provider utils to read the track with
* @param track the track to be written
* @param format the output format to write in
* @return the new track writer
*/
private static TrackWriter newWriter(Context context,
MyTracksProviderUtils providerUtils,
Track track, TrackFileFormat format) {
TrackFormatWriter writer = format.newFormatWriter(context);
return new TrackWriterImpl(context, providerUtils, track, writer);
}
private TrackWriterFactory() { }
}
@@ -0,0 +1,391 @@
/*
* 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.file;
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.LocationUtils;
import com.google.android.maps.mytracks.R;
import android.app.Activity;
import android.content.Context;
import android.database.Cursor;
import android.location.Location;
import android.util.Log;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.OutputStream;
/**
* 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
*/
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 boolean success = false;
private int errorMessage = -1;
private File directory = null;
private File file = null;
private OnCompletionListener onCompletionListener;
private OnWriteListener onWriteListener;
private Thread writeThread;
TrackWriterImpl(Context context, MyTracksProviderUtils providerUtils,
Track track, TrackFormatWriter writer) {
this.context = context;
this.providerUtils = providerUtils;
this.track = track;
this.writer = writer;
this.fileUtils = new FileUtils();
}
@Override
public void setOnCompletionListener(OnCompletionListener onCompletionListener) {
this.onCompletionListener = onCompletionListener;
}
@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();
}
@Override
public void writeTrackAsync() {
writeThread = new Thread() {
@Override
public void run() {
doWriteTrack();
}
};
writeThread.start();
}
@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.sd_card_error_write_file;
if (track != null) {
if (openFile()) {
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.sd_card_canceled;
}
}
}
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;
}
/*
* Helper methods:
* ===============
*/
private void finished() {
if (onCompletionListener != null) {
runOnUiThread(new Runnable() {
@Override
public void run() {
onCompletionListener.onComplete();
}
});
return;
}
}
/**
* Runs the given runnable in the UI thread.
*/
protected void runOnUiThread(Runnable runnable) {
if (context instanceof Activity) {
((Activity) context).runOnUiThread(runnable);
}
}
/**
* Opens the file and prepares the format writer for it.
*
* @return true on success, false otherwise (and errorMessage is set)
*/
protected boolean openFile() {
if (!canWriteFile()) {
return false;
}
// Make sure the file doesn't exist yet (possibly by changing the filename)
String fileName = fileUtils.buildUniqueFileName(
directory, track.getName(), writer.getExtension());
if (fileName == null) {
Log.e(Constants.TAG,
"Unable to get a unique filename for " + track.getName());
return false;
}
Log.i(Constants.TAG, "Writing track to: " + fileName);
try {
writer.prepare(track, newOutputStream(fileName));
} catch (FileNotFoundException e) {
Log.e(Constants.TAG, "Failed to open output file.", e);
errorMessage = R.string.sd_card_error_write_file;
return false;
}
return true;
}
/**
* Checks and returns whether we're ready to create the output file.
*/
protected boolean canWriteFile() {
if (directory == null) {
String dirName =
fileUtils.buildExternalDirectoryPath(writer.getExtension());
directory = newFile(dirName);
}
if (!fileUtils.isSdCardAvailable()) {
Log.i(Constants.TAG, "Could not find SD card.");
errorMessage = R.string.sd_card_error_no_storage;
return false;
}
if (!fileUtils.ensureDirectoryExists(directory)) {
Log.i(Constants.TAG, "Could not create export directory.");
errorMessage = R.string.sd_card_error_create_dir;
return false;
}
return true;
}
/**
* Creates a new output stream to write to the given filename.
*
* @throws FileNotFoundException if the file could't be created
*/
protected OutputStream newOutputStream(String fileName)
throws FileNotFoundException {
file = new File(directory, fileName);
return new FileOutputStream(file);
}
/**
* Creates a new file object for the given path.
*/
protected File newFile(String path) {
return new File(path);
}
/**
* Writes the waypoints for the given track.
*
* @param trackId the ID of the track to write waypoints for
*/
private void writeWaypoints(long trackId) {
// TODO: Stream through he waypoints in chunks.
// I am leaving the number of waypoints very high which should not be a
// problem because we don't try to load them into objects all at the
// same time.
Cursor cursor = null;
cursor = providerUtils.getWaypointsCursor(trackId, 0,
Constants.MAX_LOADED_WAYPOINTS_POINTS);
if (cursor != null) {
try {
if (cursor.moveToFirst()) {
// Yes, this will skip the 1st way point and that is intentional
// as the 1st points holds the stats for the current/last segment.
while (cursor.moveToNext()) {
Waypoint wpt = providerUtils.createWaypoint(cursor);
writer.writeWaypoint(wpt);
}
}
} finally {
cursor.close();
}
}
}
/**
* Does the actual work of writing the track to the now open file.
*/
void writeDocument() throws InterruptedException {
Log.d(Constants.TAG, "Started writing track.");
writer.writeHeader();
writeWaypoints(track.getId());
writeLocations();
writer.writeFooter();
writer.close();
success = true;
Log.d(Constants.TAG, "Done writing track.");
errorMessage = R.string.sd_card_success_write_file;
}
private void writeLocations() throws InterruptedException {
boolean wroteFirst = false;
boolean segmentOpen = false;
boolean isLastValid = false;
class TrackWriterLocationFactory implements MyTracksProviderUtils.LocationFactory {
Location currentLocation;
Location lastLocation;
@Override
public Location createLocation() {
if (currentLocation == null) {
currentLocation = new MyTracksLocation("");
}
return currentLocation;
}
public void swapLocations() {
Location tmpLoc = lastLocation;
lastLocation = currentLocation;
currentLocation = tmpLoc;
if (currentLocation != null) {
currentLocation.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();
}
pointNumber++;
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();
}
}
}
@@ -0,0 +1,132 @@
/*
* 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;
}
// For testing purpose
OnCompletionListener getOnCompletionListener() {
return onCompletionListener;
}
public ProgressDialog createProgressDialog() {
dialog = new ProgressDialog(activity);
dialog.setIcon(android.R.drawable.ic_dialog_info);
dialog.setTitle(activity.getString(R.string.generic_progress_title));
dialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
dialog.setMessage(activity.getString(R.string.sd_card_progress_write_file));
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));
}
}
};
}
@@ -0,0 +1,62 @@
/*
* 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.AbstractSendActivity;
import com.google.android.apps.mytracks.io.sendtogoogle.AbstractSendAsyncTask;
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.content.Intent;
/**
* An activity to send a track to Google Fusion Tables.
*
* @author Jimmy Shih
*/
public class SendFusionTablesActivity extends AbstractSendActivity {
@Override
protected AbstractSendAsyncTask createAsyncTask() {
return new SendFusionTablesAsyncTask(this, sendRequest.getTrackId(), sendRequest.getAccount());
}
@Override
protected String getServiceName() {
return getString(R.string.send_google_fusion_tables);
}
@Override
protected 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();
}
}
@@ -0,0 +1,462 @@
// 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.AbstractSendAsyncTask;
import com.google.android.apps.mytracks.io.sendtogoogle.SendToGoogleUtils;
import com.google.android.apps.mytracks.stats.DoubleBuffer;
import com.google.android.apps.mytracks.stats.TripStatisticsBuilder;
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.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 Jimmy Shih
*/
public class SendFusionTablesAsyncTask extends AbstractSendAsyncTask {
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 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 final Context context;
private final long trackId;
private final Account account;
private final MyTracksProviderUtils myTracksProviderUtils;
private final HttpRequestFactory httpRequestFactory;
// The following variables are for per upload states
private String authToken;
private String tableId;
int currentSegment;
public SendFusionTablesAsyncTask(
SendFusionTablesActivity activity, long trackId, Account account) {
super(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());
}
@Override
protected void closeConnection() {
// No action needed for Google Fusion Tables
}
@Override
protected void saveResult() {
Track track = myTracksProviderUtils.getTrack(trackId);
if (track != null) {
track.setTableId(tableId);
myTracksProviderUtils.updateTrack(track);
} else {
Log.d(TAG, "No track");
}
}
@Override
protected boolean performTask() {
// Reset the per upload states
authToken = null;
tableId = null;
currentSegment = 1;
try {
authToken = AccountManager.get(context).blockingGetAuthToken(
account, SendFusionTablesUtils.SERVICE, false);
} catch (OperationCanceledException e) {
Log.d(TAG, "Unable to get auth token", e);
return retryTask();
} catch (AuthenticatorException e) {
Log.d(TAG, "Unable to get auth token", e);
return retryTask();
} catch (IOException e) {
Log.d(TAG, "Unable to get auth token", e);
return retryTask();
}
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 retryTask();
}
// 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;
}
@Override
protected void invalidateToken() {
AccountManager.get(context).invalidateAuthToken(Constants.ACCOUNT_TYPE, authToken);
}
/**
* 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<Location> locations = new ArrayList<Location>(MAX_POINTS_PER_UPLOAD);
Location lastLocation = null;
// For chart server, limit the number of elevation readings to 250.
int elevationSamplingFrequency = Math.max(1, (int) (locationsCount / 250.0));
TripStatisticsBuilder tripStatisticsBuilder = new TripStatisticsBuilder(
track.getStatistics().getStartTime());
DoubleBuffer elevationBuffer = new DoubleBuffer(Constants.ELEVATION_SMOOTHING_FACTOR);
Vector<Double> distances = new Vector<Double>();
Vector<Double> elevations = new Vector<Double>();
for (int i = 0; i < locationsCount; i++) {
locationsCursor.moveToPosition(i);
Location location = myTracksProviderUtils.createLocation(locationsCursor);
locations.add(location);
if (i == 0) {
// 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)) {
tripStatisticsBuilder.addLocation(location, location.getTime());
// All points go into the smoothing buffer
elevationBuffer.setNext(metricUnits ? location.getAltitude()
: location.getAltitude() * UnitConversions.M_TO_FT);
if (i % elevationSamplingFrequency == 0) {
distances.add(tripStatisticsBuilder.getStatistics().getTotalDistance());
elevations.add(elevationBuffer.getAverage());
}
lastLocation = location;
}
// Upload periodically
int readCount = i + 1;
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();
}
}
// 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) {
distances.add(tripStatisticsBuilder.getStatistics().getTotalDistance());
elevations.add(elevationBuffer.getAverage());
StringUtils stringUtils = new StringUtils(context);
track.setDescription("<p>" + track.getDescription() + "</p><p>"
+ stringUtils.generateTrackDescription(track, distances, elevations) + "</p>");
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<Location> locations, boolean lastBatch) {
// Prepare locations
ArrayList<Track> splitTracks = SendToGoogleUtils.prepareLocations(track, locations);
// Upload segments
boolean onlyOneSegment = lastBatch && currentSegment == 1 && splitTracks.size() == 1;
for (Track splitTrack : splitTracks) {
if (!onlyOneSegment) {
splitTrack.setName(context.getString(
R.string.send_google_track_part_label, splitTrack.getName(), currentSegment));
}
if (!createNewLineString(splitTrack)) {
Log.d(TAG, "Upload points failed");
return false;
}
currentSegment++;
}
return true;
}
/**
* Uploads all the waypoints.
*
* @return true if success.
*/
private boolean uploadWaypoints() {
Cursor cursor = null;
try {
cursor = myTracksProviderUtils.getWaypointsCursor(
trackId, 0, Constants.MAX_LOADED_WAYPOINTS_POINTS);
if (cursor != null && cursor.moveToFirst()) {
// This will skip the first waypoint (it carries the stats for the
// track).
while (cursor.moveToNext()) {
Waypoint wpt = myTracksProviderUtils.createWaypoint(cursor);
if (!createNewPoint(
wpt.getName(), wpt.getDescription(), wpt.getLocation(), MARKER_TYPE_WAYPOINT)) {
Log.d(TAG, "Upload waypoints failed");
return false;
}
}
}
return true;
} finally {
if (cursor != null) {
cursor.close();
}
}
}
/**
* Creates a new row in Google Fusion Tables representing a marker as a
* point.
*
* @param name the marker name
* @param description the marker description
* @param location the marker location
* @param type the marker type
* @return true if success.
*/
private boolean createNewPoint(
String name, String description, Location location, String type) {
String query = "INSERT INTO " + tableId + " (name,description,geometry,marker) VALUES "
+ SendFusionTablesUtils.formatSqlValues(
name, description, SendFusionTablesUtils.getKmlPoint(location), type);
return sendQuery(query, false);
}
/**
* Creates a new row in Google Fusion Tables representing the track as a
* line segment.
*
* @param track the track
* @return true if success.
*/
private boolean createNewLineString(Track track) {
String query = "INSERT INTO " + tableId + " (name,description,geometry) VALUES "
+ SendFusionTablesUtils.formatSqlValues(track.getName(), track.getDescription(),
SendFusionTablesUtils.getKmlLineString(track.getLocations()));
return sendQuery(query, false);
}
/**
* Sends a query to Google Fusion Tables.
*
* @param query the Fusion Tables SQL query
* @param setTableId true to set the table id
* @return true if success.
*/
private boolean sendQuery(String query, boolean setTableId) {
Log.d(TAG, "SendQuery: " + query);
if (isCancelled()) {
return false;
}
GenericUrl url = new GenericUrl(FUSION_TABLES_BASE_URL);
String sql = SQL_KEY + URLEncoder.encode(query);
ByteArrayInputStream inputStream = new ByteArrayInputStream(Strings.toBytesUtf8(sql));
InputStreamContent inputStreamContent = new InputStreamContent(null, inputStream);
HttpRequest request;
try {
request = httpRequestFactory.buildPostRequest(url, inputStreamContent);
} catch (IOException e) {
Log.d(TAG, "Unable to build request", e);
return false;
}
GoogleHeaders headers = new GoogleHeaders();
headers.setApplicationName(APP_NAME_PREFIX + SystemUtils.getMyTracksVersion(context));
headers.gdataVersion = GDATA_VERSION;
headers.setGoogleLogin(authToken);
headers.setContentType(CONTENT_TYPE);
request.setHeaders(headers);
HttpResponse response;
try {
response = request.execute();
} catch (IOException e) {
Log.d(TAG, "Unable to execute request", e);
return false;
}
boolean isSuccess = response.isSuccessStatusCode();
if (isSuccess) {
InputStream content;
try {
content = response.getContent();
} catch (IOException e) {
Log.d(TAG, "Unable to get response", e);
return false;
}
if (setTableId) {
tableId = SendFusionTablesUtils.getTableId(content);
if (tableId == null) {
Log.d(TAG, "tableId is null");
return false;
}
}
} else {
Log.d(TAG,
"sendQuery failed: " + response.getStatusMessage() + ": " + response.getStatusCode());
return false;
}
return true;
}
/**
* Updates the progress based on the number of locations uploaded.
*
* @param uploaded the number of uploaded locations
* @param total the number of total locations
*/
private void updateProgress(int uploaded, int total) {
double totalPercentage = uploaded / total;
double scaledPercentage = totalPercentage
* (PROGRESS_UPLOAD_DATA_MAX - PROGRESS_UPLOAD_DATA_MIN) + PROGRESS_UPLOAD_DATA_MIN;
publishProgress((int) scaledPercentage);
}
}
@@ -0,0 +1,196 @@
/*
* 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.content.Track;
import com.google.android.apps.mytracks.stats.TripStatistics;
import com.google.api.client.util.Strings;
import com.google.common.annotations.VisibleForTesting;
import android.location.Location;
import android.util.Log;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Locale;
/**
* Utilities for sending a track to Google Fusion Tables.
*
* @author Jimmy Shih
*/
public class SendFusionTablesUtils {
public static final String SERVICE = "fusiontables";
private static final String UTF8 = "UTF8";
private static final String TABLE_ID = "tableid";
private static final String MAP_URL = "https://www.google.com/fusiontables/embedviz?"
+ "viz=MAP&q=select+col0,+col1,+col2,+col3+from+%s+&h=false&lat=%f&lng=%f&z=%d&t=1&l=col2";
private static final String TAG = SendFusionTablesUtils.class.getSimpleName();
private SendFusionTablesUtils() {}
/**
* Gets the url to visualize a fusion table on a map.
*
* @param track the track
* @return the url.
*/
public static String getMapUrl(Track track) {
if (track == null || track.getStatistics() == null || track.getTableId() == null) {
Log.e(TAG, "Invalid track");
return null;
}
// TODO(jshih): Determine the correct bounding box and zoom level that
// will show the entire track.
TripStatistics stats = track.getStatistics();
double latE6 = stats.getBottom() + (stats.getTop() - stats.getBottom()) / 2;
double lonE6 = stats.getLeft() + (stats.getRight() - stats.getLeft()) / 2;
int z = 15;
// We explicitly format with Locale.US because we need the latitude and
// longitude to be formatted in a locale-independent manner. Specifically,
// we need the decimal separator to be a period rather than a comma.
return String.format(
Locale.US, MAP_URL, track.getTableId(), latE6 / 1.E6, lonE6 / 1.E6, z);
}
/**
* Formats an array of values as a SQL VALUES like
* ('value1','value2',...,'value_n'). Escapes single quotes with two single
* quotes.
*
* @param values an array of values to format
* @return the formated SQL VALUES.
*/
public static String formatSqlValues(String... values) {
StringBuilder builder = new StringBuilder("(");
for (int i = 0; i < values.length; i++) {
if (i > 0) {
builder.append(',');
}
builder.append('\'');
builder.append(escapeSqlString(values[i]));
builder.append('\'');
}
builder.append(")");
return builder.toString();
}
/**
* Escapes a SQL string. Escapes single quotes with two single quotes.
*
* @param string the string
* @return the escaped string.
*/
public static String escapeSqlString(String string) {
return string.replaceAll("'", "''");
}
/**
* Gets a KML Point value representing a location.
*
* @param location the location
* @return the KML Point value.
*/
public static String getKmlPoint(Location location) {
StringBuilder builder = new StringBuilder("<Point><coordinates>");
if (location != null) {
appendLocation(location, builder);
}
builder.append("</coordinates></Point>");
return builder.toString();
}
/**
* Gets a KML LineString value representing an array of locations.
*
* @param locations the locations.
* @return the KML LineString value.
*/
public static String getKmlLineString(ArrayList<Location> locations) {
StringBuilder builder = new StringBuilder("<LineString><coordinates>");
if (locations != null) {
for (int i = 0; i < locations.size(); i++) {
if (i != 0) {
builder.append(' ');
}
appendLocation(locations.get(i), builder);
}
}
builder.append("</coordinates></LineString>");
return builder.toString();
}
/**
* Appends a location to a string builder using "longitude,latitude[,altitude]" format.
*
* @param location the location
* @param builder the string builder
*/
@VisibleForTesting
static void appendLocation(Location location, StringBuilder builder) {
builder.append(location.getLongitude()).append(",").append(location.getLatitude());
if (location.hasAltitude()) {
builder.append(",");
builder.append(location.getAltitude());
}
}
/**
* Gets the table id from an input streawm.
*
* @param inputStream input stream
* @return table id or null if not available.
*/
public static String getTableId(InputStream inputStream) {
if (inputStream == null) {
Log.d(TAG, "inputStream is null");
return null;
}
byte[] result = new byte[1024];
int read;
try {
read = inputStream.read(result);
} catch (IOException e) {
Log.d(TAG, "Unable to read result", e);
return null;
}
if (read == -1) {
Log.d(TAG, "no data read");
return null;
}
String s;
try {
s = new String(result, 0, read, UTF8);
} catch (UnsupportedEncodingException e) {
Log.d(TAG, "Unable to parse result", e);
return null;
}
String[] lines = s.split(Strings.LINE_SEPARATOR);
if (lines.length > 1 && lines[0].equals(TABLE_ID)) {
// returns the next line
return lines[1];
} else {
Log.d(TAG, "Response is not valid: " + s);
return null;
}
}
}
@@ -0,0 +1,459 @@
/*
* 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.gdata;
import com.google.wireless.gdata.client.GDataClient;
import com.google.wireless.gdata.client.HttpException;
import com.google.wireless.gdata.client.QueryParams;
import com.google.wireless.gdata.data.StringUtils;
import com.google.wireless.gdata.parser.ParseException;
import com.google.wireless.gdata.serializer.GDataSerializer;
import android.text.TextUtils;
import android.util.Log;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLEncoder;
import java.util.zip.GZIPInputStream;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.StatusLine;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.entity.AbstractHttpEntity;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.entity.InputStreamEntity;
import org.apache.http.impl.client.DefaultHttpClient;
/**
* Implementation of a GDataClient using GoogleHttpClient to make HTTP requests.
* Always issues GETs and POSTs, using the X-HTTP-Method-Override header when a
* PUT or DELETE is desired, to avoid issues with firewalls, etc., that do not
* allow methods other than GET or POST.
*/
public class AndroidGDataClient implements GDataClient {
private static final String TAG = "GDataClient";
private static final boolean DEBUG = false;
private static final String X_HTTP_METHOD_OVERRIDE = "X-HTTP-Method-Override";
private static final int MAX_REDIRECTS = 10;
private final HttpClient httpClient;
/**
* Interface for creating HTTP requests. Used by
* {@link AndroidGDataClient#createAndExecuteMethod}, since HttpUriRequest
* does not allow for changing the URI after creation, e.g., when you want to
* follow a redirect.
*/
private interface HttpRequestCreator {
HttpUriRequest createRequest(URI uri);
}
private static class GetRequestCreator implements HttpRequestCreator {
public HttpUriRequest createRequest(URI uri) {
return new HttpGet(uri);
}
}
private static class PostRequestCreator implements HttpRequestCreator {
private final String mMethodOverride;
private final HttpEntity mEntity;
public PostRequestCreator(String methodOverride, HttpEntity entity) {
mMethodOverride = methodOverride;
mEntity = entity;
}
public HttpUriRequest createRequest(URI uri) {
HttpPost post = new HttpPost(uri);
if (mMethodOverride != null) {
post.addHeader(X_HTTP_METHOD_OVERRIDE, mMethodOverride);
}
post.setEntity(mEntity);
return post;
}
}
// MAJOR TODO: make this work across redirects (if we can reset the
// InputStream).
// OR, read the bits into a local buffer (yuck, the media could be large).
private static class MediaPutRequestCreator implements HttpRequestCreator {
private final InputStream mMediaInputStream;
private final String mContentType;
public MediaPutRequestCreator(InputStream mediaInputStream,
String contentType) {
mMediaInputStream = mediaInputStream;
mContentType = contentType;
}
public HttpUriRequest createRequest(URI uri) {
HttpPost post = new HttpPost(uri);
post.addHeader(X_HTTP_METHOD_OVERRIDE, "PUT");
InputStreamEntity entity =
new InputStreamEntity(mMediaInputStream, -1 /* read until EOF */);
entity.setContentType(mContentType);
post.setEntity(entity);
return post;
}
}
/**
* Creates a new AndroidGDataClient.
*/
public AndroidGDataClient() {
httpClient = new DefaultHttpClient();
}
public void close() {
}
/*
* (non-Javadoc)
*
* @see GDataClient#encodeUri(java.lang.String)
*/
public String encodeUri(String uri) {
String encodedUri;
try {
encodedUri = URLEncoder.encode(uri, "UTF-8");
} catch (UnsupportedEncodingException uee) {
// should not happen.
Log.e("JakartaGDataClient", "UTF-8 not supported -- should not happen. "
+ "Using default encoding.", uee);
encodedUri = URLEncoder.encode(uri);
}
return encodedUri;
}
/*
* (non-Javadoc)
*
* @see com.google.wireless.gdata.client.GDataClient#createQueryParams()
*/
public QueryParams createQueryParams() {
return new QueryParamsImpl();
}
// follows redirects
private InputStream createAndExecuteMethod(HttpRequestCreator creator,
String uriString, String authToken) throws HttpException, IOException {
HttpResponse response = null;
int status = 500;
int redirectsLeft = MAX_REDIRECTS;
URI uri;
try {
uri = new URI(uriString);
} catch (URISyntaxException use) {
Log.w(TAG, "Unable to parse " + uriString + " as URI.", use);
throw new IOException("Unable to parse " + uriString + " as URI: "
+ use.getMessage());
}
// we follow redirects ourselves, since we want to follow redirects even on
// POSTs, which
// the HTTP library does not do. following redirects ourselves also allows
// us to log
// the redirects using our own logging.
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
// not require
// authentication.)
if (!TextUtils.isEmpty(authToken)) {
request.addHeader("Authorization", "GoogleLogin auth=" + authToken);
}
if (DEBUG) {
for (Header h : request.getAllHeaders()) {
Log.v(TAG, h.getName() + ": " + h.getValue());
}
Log.d(TAG, "Executing " + request.getRequestLine().toString());
}
response = null;
try {
response = httpClient.execute(request);
} catch (IOException ioe) {
Log.w(TAG, "Unable to execute HTTP request." + ioe);
throw ioe;
}
StatusLine statusLine = response.getStatusLine();
if (statusLine == null) {
Log.w(TAG, "StatusLine is null.");
throw new NullPointerException(
"StatusLine is null -- should not happen.");
}
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, response.getStatusLine().toString());
for (Header h : response.getAllHeaders()) {
Log.d(TAG, h.getName() + ": " + h.getValue());
}
}
status = statusLine.getStatusCode();
HttpEntity entity = response.getEntity();
if ((status >= 200) && (status < 300) && entity != null) {
return getUngzippedContent(entity);
}
// TODO: handle 301, 307?
// TODO: let the http client handle the redirects, if we can be sure we'll
// never get a
// redirect on POST.
if (status == 302) {
// consume the content, so the connection can be closed.
entity.consumeContent();
Header location = response.getFirstHeader("Location");
if (location == null) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Redirect requested but no Location " + "specified.");
}
break;
}
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Following redirect to " + location.getValue());
}
try {
uri = new URI(location.getValue());
} catch (URISyntaxException use) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Unable to parse " + location.getValue() + " as URI.",
use);
throw new IOException("Unable to parse " + location.getValue()
+ " as URI.");
}
break;
}
--redirectsLeft;
} else {
break;
}
}
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "Received " + status + " status code.");
}
String errorMessage = null;
HttpEntity entity = response.getEntity();
try {
if (entity != null) {
InputStream in = entity.getContent();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buf = new byte[8192];
int bytesRead = -1;
while ((bytesRead = in.read(buf)) != -1) {
baos.write(buf, 0, bytesRead);
}
// TODO: use appropriate encoding, picked up from Content-Type.
errorMessage = new String(baos.toByteArray());
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, errorMessage);
}
}
} finally {
if (entity != null) {
entity.consumeContent();
}
}
String exceptionMessage = "Received " + status + " status code";
if (errorMessage != null) {
exceptionMessage += (": " + errorMessage);
}
throw new HttpException(exceptionMessage, status, null /* InputStream */);
}
/**
* Gets the input stream from a response entity. If the entity is gzipped
* then this will get a stream over the uncompressed data.
*
* @param entity the entity whose content should be read
* @return the input stream to read from
* @throws IOException
*/
private static InputStream getUngzippedContent(HttpEntity entity)
throws IOException {
InputStream responseStream = entity.getContent();
if (responseStream == null) {
return responseStream;
}
Header header = entity.getContentEncoding();
if (header == null) {
return responseStream;
}
String contentEncoding = header.getValue();
if (contentEncoding == null) {
return responseStream;
}
if (contentEncoding.contains("gzip")){
responseStream = new GZIPInputStream(responseStream);
}
return responseStream;
}
/*
* (non-Javadoc)
*
* @see GDataClient#getFeedAsStream(java.lang.String, java.lang.String)
*/
public InputStream getFeedAsStream(String feedUrl, String authToken)
throws HttpException, IOException {
InputStream in =
createAndExecuteMethod(new GetRequestCreator(), feedUrl, authToken);
if (in != null) {
return in;
}
throw new IOException("Unable to access feed.");
}
public InputStream getMediaEntryAsStream(String mediaEntryUrl,
String authToken) throws HttpException, IOException {
InputStream in =
createAndExecuteMethod(new GetRequestCreator(), mediaEntryUrl,
authToken);
if (in != null) {
return in;
}
throw new IOException("Unable to access media entry.");
}
/*
* (non-Javadoc)
*
* @see GDataClient#createEntry
*/
public InputStream createEntry(String feedUrl, String authToken,
GDataSerializer entry) throws HttpException, IOException {
HttpEntity entity =
createEntityForEntry(entry, GDataSerializer.FORMAT_CREATE);
InputStream in =
createAndExecuteMethod(new PostRequestCreator(null /* override */,
entity), feedUrl, authToken);
if (in != null) {
return in;
}
throw new IOException("Unable to create entry.");
}
/*
* (non-Javadoc)
*
* @see GDataClient#updateEntry
*/
public InputStream updateEntry(String editUri, String authToken,
GDataSerializer entry) throws HttpException, IOException {
HttpEntity entity =
createEntityForEntry(entry, GDataSerializer.FORMAT_UPDATE);
InputStream in =
createAndExecuteMethod(new PostRequestCreator("PUT", entity), editUri,
authToken);
if (in != null) {
return in;
}
throw new IOException("Unable to update entry.");
}
/*
* (non-Javadoc)
*
* @see GDataClient#deleteEntry
*/
public void deleteEntry(String editUri, String authToken)
throws HttpException, IOException {
if (StringUtils.isEmpty(editUri)) {
throw new IllegalArgumentException(
"you must specify an non-empty edit url");
}
InputStream in =
createAndExecuteMethod(
new PostRequestCreator("DELETE", null /* entity */), editUri,
authToken);
if (in == null) {
throw new IOException("Unable to delete entry.");
}
try {
in.close();
} catch (IOException ioe) {
// ignore
}
}
public InputStream updateMediaEntry(String editUri, String authToken,
InputStream mediaEntryInputStream, String contentType)
throws HttpException, IOException {
InputStream in =
createAndExecuteMethod(new MediaPutRequestCreator(
mediaEntryInputStream, contentType), editUri, authToken);
if (in != null) {
return in;
}
throw new IOException("Unable to write media entry.");
}
private HttpEntity createEntityForEntry(GDataSerializer entry, int format)
throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try {
entry.serialize(baos, format);
} catch (IOException ioe) {
Log.e(TAG, "Unable to serialize entry.", ioe);
throw ioe;
} catch (ParseException pe) {
Log.e(TAG, "Unable to serialize entry.", pe);
throw new IOException("Unable to serialize entry: " + pe.getMessage());
}
byte[] entryBytes = baos.toByteArray();
if (entryBytes != null && Log.isLoggable(TAG, Log.DEBUG)) {
try {
Log.d(TAG, "Serialized entry: " + new String(entryBytes, "UTF-8"));
} catch (UnsupportedEncodingException uee) {
// should not happen
throw new IllegalStateException("UTF-8 should be supported!", uee);
}
}
AbstractHttpEntity entity = new ByteArrayEntity(entryBytes);
entity.setContentType(entry.getContentType());
return entity;
}
}
@@ -0,0 +1,51 @@
/*
* 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.gdata;
import com.google.android.apps.mytracks.Constants;
import com.google.wireless.gdata.client.GDataClient;
import android.content.Context;
import android.util.Log;
/**
* This factory will fetch the right class for the platform.
*
* @author Sandor Dornbush
*/
public class GDataClientFactory {
private GDataClientFactory() { }
/**
* Creates a new GData client.
* This factory will fetch the right class for the platform.
* @return A GDataClient appropriate for this platform
*/
public static GDataClient getGDataClient(Context context) {
// TODO This should be moved into ApiAdapter
try {
// Try to use the official unbundled gdata client implementation.
// This should work on Froyo and beyond.
return new com.google.android.common.gdata.AndroidGDataClient(context);
} catch (LinkageError e) {
// On all other platforms use the client implementation packaged in the
// apk.
Log.i(Constants.TAG, "Using mytracks AndroidGDataClient.", e);
return new com.google.android.apps.mytracks.io.gdata.AndroidGDataClient();
}
}
}
@@ -0,0 +1,106 @@
/*
* 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.gdata;
import com.google.wireless.gdata.client.QueryParams;
import android.text.TextUtils;
import android.util.Log;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
/**
* Simple implementation of the QueryParams interface.
*/
// TODO: deal with categories
public class QueryParamsImpl extends QueryParams {
private final Map<String, String> mParams = new HashMap<String, String>();
@Override
public void clear() {
setEntryId(null);
mParams.clear();
}
@Override
public String generateQueryUrl(String feedUrl) {
if (TextUtils.isEmpty(getEntryId()) && mParams.isEmpty()) {
// nothing to do
return feedUrl;
}
// handle entry IDs
if (!TextUtils.isEmpty(getEntryId())) {
if (!mParams.isEmpty()) {
throw new IllegalStateException("Cannot set both an entry ID "
+ "and other query paramters.");
}
return feedUrl + '/' + getEntryId();
}
// otherwise, append the querystring params.
StringBuilder sb = new StringBuilder();
sb.append(feedUrl);
Set<String> params = mParams.keySet();
boolean first = true;
if (feedUrl.contains("?")) {
first = false;
} else {
sb.append('?');
}
for (String param : params) {
if (first) {
first = false;
} else {
sb.append('&');
}
sb.append(param);
sb.append('=');
String value = mParams.get(param);
String encodedValue = null;
try {
encodedValue = URLEncoder.encode(value, "UTF-8");
} catch (UnsupportedEncodingException uee) {
// should not happen.
Log.w("QueryParamsImpl", "UTF-8 not supported -- should not happen. "
+ "Using default encoding.", uee);
encodedValue = URLEncoder.encode(value);
}
sb.append(encodedValue);
}
return sb.toString();
}
@Override
public String getParamValue(String param) {
if (!(mParams.containsKey(param))) {
return null;
}
return mParams.get(param);
}
@Override
public void setParamValue(String param, String value) {
mParams.put(param, value);
}
}
@@ -0,0 +1,46 @@
/*
* Copyright 2009 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.gdata.docs;
import com.google.wireless.gdata.client.GDataClient;
import com.google.wireless.gdata.client.GDataParserFactory;
import com.google.wireless.gdata.client.GDataServiceClient;
/**
* GDataServiceClient for accessing Google Documents. This is not a full
* implementation.
*/
public class DocumentsClient extends GDataServiceClient {
/** The name of the service, dictated to be 'wise' by the protocol. */
public static final String SERVICE = "writely";
/**
* Creates a new DocumentsClient.
*
* @param client The GDataClient that should be used to authenticate requests,
* retrieve feeds, etc
* @param parserFactory The GDataParserFactory that should be used to obtain
* GDataParsers used by this client
*/
public DocumentsClient(GDataClient client, GDataParserFactory parserFactory) {
super(client, parserFactory);
}
@Override
public String getServiceName() {
return SERVICE;
}
}
@@ -0,0 +1,269 @@
/*
* Copyright 2007 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.gdata.docs;
import com.google.wireless.gdata.client.GDataClient;
import com.google.wireless.gdata.client.GDataParserFactory;
import com.google.wireless.gdata.client.GDataServiceClient;
import com.google.wireless.gdata.client.HttpException;
import com.google.wireless.gdata.data.Entry;
import com.google.wireless.gdata.data.StringUtils;
import com.google.wireless.gdata.parser.GDataParser;
import com.google.wireless.gdata.parser.ParseException;
import com.google.wireless.gdata.serializer.GDataSerializer;
import com.google.wireless.gdata2.client.AuthenticationException;
import java.io.IOException;
import java.io.InputStream;
/**
* GDataServiceClient for accessing Google Spreadsheets. This client can access
* and parse all of the Spreadsheets feed types: Spreadsheets feed, Worksheets
* feed, List feed, and Cells feed. Read operations are supported on all feed
* types, but only the List and Cells feeds support write operations. (This is a
* limitation of the protocol, not this API. Such write access may be added to
* the protocol in the future, requiring changes to this implementation.)
*
* Only 'private' visibility and 'full' projections are currently supported.
*/
public class SpreadsheetsClient extends GDataServiceClient {
/** The name of the service, dictated to be 'wise' by the protocol. */
public static final String SERVICE = "wise";
/** Standard base feed url for spreadsheets. */
public static final String SPREADSHEETS_BASE_FEED_URL =
"http://spreadsheets.google.com/feeds/spreadsheets/private/full";
/**
* Represents an entry in a GData Spreadsheets meta-feed.
*/
public static class SpreadsheetEntry extends Entry { }
/**
* Represents an entry in a GData Worksheets meta-feed.
*/
public static class WorksheetEntry extends Entry { }
/**
* Creates a new SpreadsheetsClient. Uses the standard base URL for
* spreadsheets feeds.
*
* @param client The GDataClient that should be used to authenticate requests,
* retrieve feeds, etc
*/
public SpreadsheetsClient(GDataClient client,
GDataParserFactory spreadsheetFactory) {
super(client, spreadsheetFactory);
}
@Override
public String getServiceName() {
return SERVICE;
}
/**
* Returns a parser for the specified feed type.
*
* @param feedEntryClass the Class of entry type that will be parsed, which
* lets this method figure out which parser to create
* @param feedUri the URI of the feed to be fetched and parsed
* @param authToken the current authToken to use for the request
* @return a parser for the indicated feed
* @throws AuthenticationException if the authToken is not valid
* @throws ParseException if the response from the server could not be parsed
*/
private GDataParser getParserForTypedFeed(
Class<? extends Entry> feedEntryClass, String feedUri, String authToken)
throws AuthenticationException, ParseException, IOException {
GDataClient gDataClient = getGDataClient();
GDataParserFactory gDataParserFactory = getGDataParserFactory();
try {
InputStream is = gDataClient.getFeedAsStream(feedUri, authToken);
return gDataParserFactory.createParser(feedEntryClass, is);
} catch (HttpException e) {
convertHttpExceptionForReads("Could not fetch parser feed.", e);
return null; // never reached
}
}
/**
* Converts an HTTP exception that happened while reading into the equivalent
* local exception.
*/
public void convertHttpExceptionForReads(String message, HttpException cause)
throws AuthenticationException, IOException {
switch (cause.getStatusCode()) {
case HttpException.SC_FORBIDDEN:
case HttpException.SC_UNAUTHORIZED:
throw new AuthenticationException(message, cause);
case HttpException.SC_GONE:
default:
throw new IOException(message + ": " + cause.getMessage());
}
}
@Override
public Entry createEntry(String feedUri, String authToken, Entry entry)
throws ParseException, IOException {
GDataParserFactory factory = getGDataParserFactory();
GDataSerializer serializer = factory.createSerializer(entry);
InputStream is;
try {
is = getGDataClient().createEntry(feedUri, authToken, serializer);
} catch (HttpException e) {
convertHttpExceptionForWrites(entry.getClass(),
"Could not update entry.", e);
return null; // never reached.
}
GDataParser parser = factory.createParser(entry.getClass(), is);
try {
return parser.parseStandaloneEntry();
} finally {
parser.close();
}
}
/**
* Fetches a GDataParser for the indicated feed. The parser can be used to
* access the contents of URI. WARNING: because we cannot reliably infer the
* feed type from the URI alone, this method assumes the default feed type!
* This is probably NOT what you want. Please use the getParserFor[Type]Feed
* methods.
*
* @param feedEntryClass
* @param feedUri the URI of the feed to be fetched and parsed
* @param authToken the current authToken to use for the request
* @return a parser for the indicated feed
* @throws ParseException if the response from the server could not be parsed
*/
@SuppressWarnings("unchecked")
@Override
public GDataParser getParserForFeed(
Class feedEntryClass, String feedUri, String authToken)
throws ParseException, IOException {
try {
return getParserForTypedFeed(SpreadsheetEntry.class, feedUri, authToken);
} catch (AuthenticationException e) {
throw new IOException("Authentication Failure: " + e.getMessage());
}
}
/**
* Returns a parser for a Worksheets meta-feed.
*
* @param feedUri the URI of the feed to be fetched and parsed
* @param authToken the current authToken to use for the request
* @return a parser for the indicated feed
* @throws AuthenticationException if the authToken is not valid
* @throws ParseException if the response from the server could not be parsed
*/
public GDataParser getParserForWorksheetsFeed(
String feedUri, String authToken)
throws AuthenticationException, ParseException, IOException {
return getParserForTypedFeed(WorksheetEntry.class, feedUri, authToken);
}
/**
* Updates an entry. The URI to be updated is taken from <code>entry</code>.
* Note that only entries in List and Cells feeds can be updated, so
* <code>entry</code> must be of the corresponding type; other types will
* result in an exception.
*
* @param entry the entry to be updated; must include its URI
* @param authToken the current authToken to be used for the operation
* @return An Entry containing the re-parsed version of the entry returned by
* the server in response to the update
* @throws ParseException if the server returned an error, if the server's
* response was unparseable (unlikely), or if <code>entry</code> is of
* a read-only type
* @throws IOException on network error
*/
@Override
public Entry updateEntry(Entry entry, String authToken)
throws ParseException, IOException {
GDataParserFactory factory = getGDataParserFactory();
GDataSerializer serializer = factory.createSerializer(entry);
String editUri = entry.getEditUri();
if (StringUtils.isEmpty(editUri)) {
throw new ParseException("No edit URI -- cannot update.");
}
InputStream is;
try {
is = getGDataClient().updateEntry(editUri, authToken, serializer);
} catch (HttpException e) {
convertHttpExceptionForWrites(entry.getClass(),
"Could not update entry.", e);
return null; // never reached
}
GDataParser parser = factory.createParser(entry.getClass(), is);
try {
return parser.parseStandaloneEntry();
} finally {
parser.close();
}
}
/**
* Converts an HTTP exception which happened while writing to the equivalent
* local exception.
*/
@SuppressWarnings("unchecked")
private void convertHttpExceptionForWrites(
Class entryClass, String message, HttpException cause)
throws ParseException, IOException {
switch (cause.getStatusCode()) {
case HttpException.SC_CONFLICT:
if (entryClass != null) {
InputStream is = cause.getResponseStream();
if (is != null) {
parseEntry(entryClass, cause.getResponseStream());
}
}
throw new IOException(message);
case HttpException.SC_BAD_REQUEST:
throw new ParseException(message + ": " + cause);
case HttpException.SC_FORBIDDEN:
case HttpException.SC_UNAUTHORIZED:
throw new IOException(message);
default:
throw new IOException(message + ": " + cause.getMessage());
}
}
/**
* Parses one entry from the input stream.
*/
@SuppressWarnings("unchecked")
private Entry parseEntry(Class entryClass, InputStream is)
throws ParseException, IOException {
GDataParser parser = null;
try {
parser = getGDataParserFactory().createParser(entryClass, is);
return parser.parseStandaloneEntry();
} finally {
if (parser != null) {
parser.close();
}
}
}
}
@@ -0,0 +1,72 @@
/*
* 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.gdata.docs;
import com.google.wireless.gdata.client.GDataParserFactory;
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.gdata.parser.xml.XmlGDataParser;
import com.google.wireless.gdata.parser.xml.XmlParserFactory;
import com.google.wireless.gdata.serializer.GDataSerializer;
import com.google.wireless.gdata.serializer.xml.XmlEntryGDataSerializer;
import java.io.InputStream;
import org.xmlpull.v1.XmlPullParserException;
/**
* Factory of Xml parsers for gdata maps data.
*/
public class XmlDocsGDataParserFactory implements GDataParserFactory {
private XmlParserFactory xmlFactory;
public XmlDocsGDataParserFactory(XmlParserFactory xmlFactory) {
this.xmlFactory = xmlFactory;
}
@Override
public GDataParser createParser(InputStream is) throws ParseException {
try {
return new XmlGDataParser(is, xmlFactory.createParser());
} catch (XmlPullParserException e) {
e.printStackTrace();
return null;
}
}
@SuppressWarnings("unchecked")
@Override
public GDataParser createParser(Class cls, InputStream is)
throws ParseException {
try {
return createParserForClass(is);
} catch (XmlPullParserException e) {
e.printStackTrace();
return null;
}
}
private GDataParser createParserForClass(InputStream is)
throws ParseException, XmlPullParserException {
return new XmlGDataParser(is, xmlFactory.createParser());
}
@Override
public GDataSerializer createSerializer(Entry en) {
return new XmlEntryGDataSerializer(xmlFactory, en);
}
}
@@ -0,0 +1,36 @@
// Copyright 2010 Google Inc. All Rights Reserved.
package com.google.android.apps.mytracks.io.gdata.maps;
import com.google.wireless.gdata.data.Entry;
import java.util.HashMap;
import java.util.Map;
/**
* GData entry for a map feature.
*/
public class MapFeatureEntry extends Entry {
private String mPrivacy = null;
private Map<String, String> mAttributes = new HashMap<String, String>();
public void setPrivacy(String privacy) {
mPrivacy = privacy;
}
public String getPrivacy() {
return mPrivacy;
}
public void setAttribute(String name, String value) {
mAttributes.put(name, value);
}
public void removeAttribute(String name) {
mAttributes.remove(name);
}
public Map<String, String> getAllAttributes() {
return mAttributes;
}
}
@@ -0,0 +1,101 @@
// Copyright 2010 Google Inc. All Rights Reserved.
package com.google.android.apps.mytracks.io.gdata.maps;
import com.google.wireless.gdata.client.GDataClient;
import com.google.wireless.gdata.client.GDataParserFactory;
import com.google.wireless.gdata.client.GDataServiceClient;
import android.util.Log;
/**
* Client to talk to Google Maps via GData.
*/
public class MapsClient extends GDataServiceClient {
private static final boolean DEBUG = false;
public static final boolean LOG_COMMUNICATION = false;
private static final String MAPS_BASE_FEED_URL =
"http://maps.google.com/maps/feeds/";
private static final String MAPS_MAP_FEED_PATH = "maps/default/full";
private static final String MAPS_FEATURE_FEED_PATH_BEFORE_MAPID = "features/";
private static final String MAPS_FEATURE_FEED_PATH_AFTER_MAPID = "/full";
private static final String MAPS_VERSION_FEED_PATH_FORMAT =
"%smaps/%s/versions/%s/full/%s";
private static final String MAP_ENTRY_ID_BEFORE_USER_ID = "maps/feeds/maps/";
private static final String MAP_ENTRY_ID_BETWEEN_USER_ID_AND_MAP_ID = "/";
private static final String V2_ONLY_PARAM = "?v=2.0";
public MapsClient(GDataClient dataClient,
GDataParserFactory dataParserFactory) {
super(dataClient, dataParserFactory);
}
@Override
public String getServiceName() {
return MapsConstants.SERVICE_NAME;
}
public static String buildMapUrl(String mapId) {
return MapsConstants.MAPSHOP_BASE_URL + "?msa=0&msid=" + mapId;
}
public static String getMapsFeed() {
if (DEBUG) {
Log.d("Maps Client", "Requesting map feed:");
}
return MAPS_BASE_FEED_URL + MAPS_MAP_FEED_PATH + V2_ONLY_PARAM;
}
public static String getFeaturesFeed(String mapid) {
StringBuilder feed = new StringBuilder();
feed.append(MAPS_BASE_FEED_URL);
feed.append(MAPS_FEATURE_FEED_PATH_BEFORE_MAPID);
feed.append(mapid);
feed.append(MAPS_FEATURE_FEED_PATH_AFTER_MAPID);
feed.append(V2_ONLY_PARAM);
return feed.toString();
}
public static String getMapIdFromMapEntryId(String entryId) {
String userId = null;
String mapId = null;
if (DEBUG) {
Log.d("Maps GData Client", "Getting mapid from entry id: " + entryId);
}
int userIdStart =
entryId.indexOf(MAP_ENTRY_ID_BEFORE_USER_ID)
+ MAP_ENTRY_ID_BEFORE_USER_ID.length();
int userIdEnd =
entryId.indexOf(MAP_ENTRY_ID_BETWEEN_USER_ID_AND_MAP_ID, userIdStart);
if (userIdStart >= 0 && userIdEnd < entryId.length()
&& userIdStart <= userIdEnd) {
userId = entryId.substring(userIdStart, userIdEnd);
}
int mapIdStart =
entryId.indexOf(MAP_ENTRY_ID_BETWEEN_USER_ID_AND_MAP_ID, userIdEnd)
+ MAP_ENTRY_ID_BETWEEN_USER_ID_AND_MAP_ID.length();
if (mapIdStart >= 0 && mapIdStart < entryId.length()) {
mapId = entryId.substring(mapIdStart);
}
if (userId == null) {
userId = "";
}
if (mapId == null) {
mapId = "";
}
if (DEBUG) {
Log.d("Maps GData Client", "Got user id: " + userId);
Log.d("Maps GData Client", "Got map id: " + mapId);
}
return userId + "." + mapId;
}
public static String getVersionFeed(String versionUserId,
String versionClient, String currentVersion) {
return String.format(MAPS_VERSION_FEED_PATH_FORMAT,
MAPS_BASE_FEED_URL, versionUserId,
versionClient, currentVersion);
}
}
@@ -0,0 +1,16 @@
// Copyright 2009 Google Inc. All Rights Reserved.
package com.google.android.apps.mytracks.io.gdata.maps;
/**
* Constants for Google Maps.
*/
public class MapsConstants {
static final String MAPSHOP_BASE_URL =
"https://maps.google.com/maps/ms";
public static final String SERVICE_NAME = "local";
/**
* Private constructor to prevent instantiation.
*/
private MapsConstants() { }
}
@@ -0,0 +1,235 @@
// Copyright 2009 Google Inc. All Rights Reserved.
package com.google.android.apps.mytracks.io.gdata.maps;
import com.google.android.maps.GeoPoint;
import java.util.Random;
import java.util.Vector;
/**
* MapsFeature contains all of the data associated with a feature in Google
* Maps, where a feature is a marker, line, or shape. Some of the data is stored
* in a {@link MapsFeatureMetadata} object so that it can be more efficiently
* transmitted to other activities.
*/
public class MapsFeature {
private static final long serialVersionUID = 8439035544430497236L;
/** A marker feature displays an icon at a single point on the map. */
public static final int MARKER = 0;
/**
* A line feature displays a line connecting a set of points on the map.
*/
public static final int LINE = 1;
/**
* A shape feature displays a border defined by connecting a set of points,
* including connecting the last to the first, and displays the area
* confined by this border.
*/
public static final int SHAPE = 2;
/** The local feature id for this feature, if needed. */
private String androidId;
/**
* The latitudes of the points of this feature in order, specified in
* millionths of a degree north.
*/
private final Vector<Integer> latitudeE6 = new Vector<Integer>();
/**
* The longitudes of the points of this feature in order, specified in
* millionths of a degree east.
*/
private final Vector<Integer> longitudeE6 = new Vector<Integer>();
/** The metadata of this feature in a format efficient for transmission. */
private MapsFeatureMetadata featureInfo = new MapsFeatureMetadata();
private final Random random = new Random();
/**
* Initializes a valid but empty feature. It will default to a
* {@link #MARKER} with a blue placemark with a dot as an icon at the
* location (0, 0).
*/
public MapsFeature() {
}
/**
* Adds a new point to the end of this feature.
*
* @param point The new point to add
*/
public void addPoint(GeoPoint point) {
latitudeE6.add(point.getLatitudeE6());
longitudeE6.add(point.getLongitudeE6());
}
/**
* Generates a new local id for this feature based on the current time and
* a random number.
*/
public void generateAndroidId() {
long time = System.currentTimeMillis();
int rand = random.nextInt(10000);
androidId = time + "." + rand;
}
/**
* Retrieves the current local id for this feature if one is available.
*
* @return The local id for this feature
*/
public String getAndroidId() {
return androidId;
}
/**
* Retrieves the current (html) description of this feature. The description
* is stored in the feature metadata.
*
* @return The description of this feature
*/
public String getDescription() {
return featureInfo.getDescription();
}
/**
* Sets the description of this feature. That description is stored in the
* feature metadata.
*
* @param description The new description of this feature
*/
public void setDescription(String description) {
featureInfo.setDescription(description);
}
/**
* Retrieves the point at the given index for this feature.
*
* @param index The index of the point desired
* @return A {@link GeoPoint} representing the point or null if that point
* doesn't exist
*/
public GeoPoint getPoint(int index) {
if (latitudeE6.size() <= index) {
return null;
}
return new GeoPoint(latitudeE6.get(index), longitudeE6.get(index));
}
/**
* Counts the number of points in this feature and return that count.
*
* @return The number of points in this feature
*/
public int getPointCount() {
return latitudeE6.size();
}
/**
* Retrieves the title of this feature. That title is stored in the feature
* metadata.
*
* @return the current title of this feature
*/
public String getTitle() {
return featureInfo.getTitle();
}
/**
* Retrieves the type of this feature. That type is stored in the feature
* metadata.
*
* @return One of {@link #MARKER}, {@link #LINE}, or {@link #SHAPE}
* identifying the type of this feature
*/
public int getType() {
return featureInfo.getType();
}
/**
* Retrieves the current color of this feature as an ARGB color integer.
* That color is stored in the feature metadata.
*
* @return The ARGB color of this feature
*/
public int getColor() {
return featureInfo.getColor();
}
/**
* Retrieves the current line width of this feature. That line width is
* stored in the feature metadata.
*
* @return The line width of this feature
*/
public int getLineWidth() {
return featureInfo.getLineWidth();
}
/**
* Retrieves the current fill color of this feature as an ARGB color
* integer. That color is stored in the feature metadata.
*
* @return The ARGB fill color of this feature
*/
public int getFillColor() {
return featureInfo.getFillColor();
}
/**
* Retrieves the current icon url of this feature. That icon url is stored
* in the feature metadata.
*
* @return The icon url for this feature
*/
public String getIconUrl() {
return featureInfo.getIconUrl();
}
/**
* Sets the title of this feature. That title is stored in the feature
* metadata.
*
* @param title The new title of this feature
*/
public void setTitle(String title) {
featureInfo.setTitle(title);
}
/**
* Sets the type of this feature. That type is stored in the feature
* metadata.
*
* @param type The new type of the feature. That type must be one of
* {@link #MARKER}, {@link #LINE}, or {@link #SHAPE}
*/
public void setType(int type) {
featureInfo.setType(type);
}
/**
* Sets the ARGB color of this feature. That color is stored in the feature
* metadata.
*
* @param color The new ARGB color of this feature
*/
public void setColor(int color) {
featureInfo.setColor(color);
}
/**
* Sets the icon url of this feature. That icon url is stored in the feature
* metadata.
*
* @param url The new icon url of the feature
*/
public void setIconUrl(String url) {
featureInfo.setIconUrl(url);
}
}
@@ -0,0 +1,88 @@
// Copyright 2009 Google Inc. All Rights Reserved.
package com.google.android.apps.mytracks.io.gdata.maps;
/**
* Metadata about a maps feature.
*/
class MapsFeatureMetadata {
private static final String BLUE_DOT_URL =
"http://maps.google.com/mapfiles/ms/micons/blue-dot.png";
private static final int DEFAULT_COLOR = 0x800000FF;
private static final int DEFAULT_FILL_COLOR = 0xC00000FF;
private String title;
private String description;
private int type;
private int color;
private int lineWidth;
private int fillColor;
private String iconUrl;
public MapsFeatureMetadata() {
title = "";
description = "";
type = MapsFeature.MARKER;
color = DEFAULT_COLOR;
lineWidth = 5;
fillColor = DEFAULT_FILL_COLOR;
iconUrl = BLUE_DOT_URL;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public int getType() {
return type;
}
public void setType(int type) {
this.type = type;
}
public int getColor() {
return color;
}
public void setColor(int color) {
this.color = color;
}
public int getLineWidth() {
return lineWidth;
}
public void setLineWidth(int width) {
lineWidth = width;
}
public int getFillColor() {
return fillColor;
}
public void setFillColor(int color) {
fillColor = color;
}
public String getIconUrl() {
return iconUrl;
}
public void setIconUrl(String url) {
iconUrl = url;
}
}
@@ -0,0 +1,179 @@
// Copyright 2009 Google Inc. All Rights Reserved.
package com.google.android.apps.mytracks.io.gdata.maps;
import com.google.wireless.gdata.data.Entry;
import com.google.wireless.gdata.data.StringUtils;
import android.graphics.Color;
import android.util.Log;
import java.io.IOException;
import java.io.StringWriter;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlPullParserFactory;
import org.xmlpull.v1.XmlSerializer;
/**
* Converter from GData objects to Maps objects.
*/
public class MapsGDataConverter {
private final XmlSerializer xmlSerializer;
public MapsGDataConverter() throws XmlPullParserException {
xmlSerializer = XmlPullParserFactory.newInstance().newSerializer();
}
public static MapsMapMetadata getMapMetadataForEntry(
MapFeatureEntry entry) {
MapsMapMetadata metadata = new MapsMapMetadata();
if ("public".equals(entry.getPrivacy())) {
metadata.setSearchable(true);
} else {
metadata.setSearchable(false);
}
metadata.setTitle(entry.getTitle());
metadata.setDescription(entry.getSummary());
String editUri = entry.getEditUri();
if (editUri != null) {
metadata.setGDataEditUri(editUri);
}
return metadata;
}
public static String getMapidForEntry(Entry entry) {
return MapsClient.getMapIdFromMapEntryId(entry.getId());
}
public static Entry getMapEntryForMetadata(MapsMapMetadata metadata) {
MapFeatureEntry entry = new MapFeatureEntry();
entry.setEditUri(metadata.getGDataEditUri());
entry.setTitle(metadata.getTitle());
entry.setSummary(metadata.getDescription());
entry.setPrivacy(metadata.getSearchable() ? "public" : "unlisted");
entry.setAuthor("android");
entry.setEmail("nobody@google.com");
return entry;
}
public MapFeatureEntry getEntryForFeature(MapsFeature feature) {
MapFeatureEntry entry = new MapFeatureEntry();
entry.setTitle(feature.getTitle());
entry.setAuthor("android");
entry.setEmail("nobody@google.com");
entry.setCategoryScheme("http://schemas.google.com/g/2005#kind");
entry.setCategory("http://schemas.google.com/g/2008#mapfeature");
entry.setEditUri("");
if (!StringUtils.isEmpty(feature.getAndroidId())) {
entry.setAttribute("_androidId", feature.getAndroidId());
}
try {
StringWriter writer = new StringWriter();
xmlSerializer.setOutput(writer);
xmlSerializer.startTag(null, "Placemark");
xmlSerializer.attribute(null, "xmlns", "http://earth.google.com/kml/2.2");
xmlSerializer.startTag(null, "Style");
if (feature.getType() == MapsFeature.MARKER) {
xmlSerializer.startTag(null, "IconStyle");
xmlSerializer.startTag(null, "Icon");
xmlSerializer.startTag(null, "href");
xmlSerializer.text(feature.getIconUrl());
xmlSerializer.endTag(null, "href");
xmlSerializer.endTag(null, "Icon");
xmlSerializer.endTag(null, "IconStyle");
} else {
xmlSerializer.startTag(null, "LineStyle");
xmlSerializer.startTag(null, "color");
int color = feature.getColor();
// Reverse the color because KML is ABGR and Android is ARGB
xmlSerializer.text(Integer.toHexString(
Color.argb(Color.alpha(color), Color.blue(color),
Color.green(color), Color.red(color))));
xmlSerializer.endTag(null, "color");
xmlSerializer.startTag(null, "width");
xmlSerializer.text(Integer.toString(feature.getLineWidth()));
xmlSerializer.endTag(null, "width");
xmlSerializer.endTag(null, "LineStyle");
if (feature.getType() == MapsFeature.SHAPE) {
xmlSerializer.startTag(null, "PolyStyle");
xmlSerializer.startTag(null, "color");
int fcolor = feature.getFillColor();
// Reverse the color because KML is ABGR and Android is ARGB
xmlSerializer.text(Integer.toHexString(Color.argb(Color.alpha(fcolor),
Color.blue(fcolor), Color.green(fcolor), Color.red(fcolor))));
xmlSerializer.endTag(null, "color");
xmlSerializer.startTag(null, "fill");
xmlSerializer.text("1");
xmlSerializer.endTag(null, "fill");
xmlSerializer.startTag(null, "outline");
xmlSerializer.text("1");
xmlSerializer.endTag(null, "outline");
xmlSerializer.endTag(null, "PolyStyle");
}
}
xmlSerializer.endTag(null, "Style");
xmlSerializer.startTag(null, "name");
xmlSerializer.text(feature.getTitle());
xmlSerializer.endTag(null, "name");
xmlSerializer.startTag(null, "description");
xmlSerializer.cdsect(feature.getDescription());
xmlSerializer.endTag(null, "description");
StringBuilder pointBuilder = new StringBuilder();
for (int i = 0; i < feature.getPointCount(); ++i) {
if (i > 0) {
pointBuilder.append('\n');
}
pointBuilder.append(feature.getPoint(i).getLongitudeE6() / 1e6);
pointBuilder.append(',');
pointBuilder.append(feature.getPoint(i).getLatitudeE6() / 1e6);
pointBuilder.append(",0.000000");
}
String pointString = pointBuilder.toString();
if (feature.getType() == MapsFeature.MARKER) {
xmlSerializer.startTag(null, "Point");
xmlSerializer.startTag(null, "coordinates");
xmlSerializer.text(pointString);
xmlSerializer.endTag(null, "coordinates");
xmlSerializer.endTag(null, "Point");
} else if (feature.getType() == MapsFeature.LINE) {
xmlSerializer.startTag(null, "LineString");
xmlSerializer.startTag(null, "tessellate");
xmlSerializer.text("1");
xmlSerializer.endTag(null, "tessellate");
xmlSerializer.startTag(null, "coordinates");
xmlSerializer.text(pointString);
xmlSerializer.endTag(null, "coordinates");
xmlSerializer.endTag(null, "LineString");
} else {
xmlSerializer.startTag(null, "Polygon");
xmlSerializer.startTag(null, "outerBoundaryIs");
xmlSerializer.startTag(null, "LinearRing");
xmlSerializer.startTag(null, "tessellate");
xmlSerializer.text("1");
xmlSerializer.endTag(null, "tessellate");
xmlSerializer.startTag(null, "coordinates");
xmlSerializer.text(pointString + "\n"
+ Double.toString(feature.getPoint(0).getLongitudeE6() / 1e6)
+ ","
+ Double.toString(feature.getPoint(0).getLatitudeE6() / 1e6)
+ ",0.000000");
xmlSerializer.endTag(null, "coordinates");
xmlSerializer.endTag(null, "LinearRing");
xmlSerializer.endTag(null, "outerBoundaryIs");
xmlSerializer.endTag(null, "Polygon");
}
xmlSerializer.endTag(null, "Placemark");
xmlSerializer.flush();
entry.setContent(writer.toString());
Log.d("My Google Maps", "Generated kml:\n" + entry.getContent());
Log.d("My Google Maps", "Edit URI: " + entry.getEditUri());
} catch (IOException e) {
e.printStackTrace();
}
return entry;
}
}
@@ -0,0 +1,52 @@
// Copyright 2009 Google Inc. All Rights Reserved.
package com.google.android.apps.mytracks.io.gdata.maps;
/**
* Metadata about a Google Maps map.
*/
public class MapsMapMetadata {
private String title;
private String description;
private String gdataEditUri;
private boolean searchable;
public MapsMapMetadata() {
title = "";
description = "";
gdataEditUri = "";
searchable = false;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public boolean getSearchable() {
return searchable;
}
public void setSearchable(boolean searchable) {
this.searchable = searchable;
}
public String getGDataEditUri() {
return gdataEditUri;
}
public void setGDataEditUri(String editUri) {
this.gdataEditUri = editUri;
}
}
@@ -0,0 +1,163 @@
// Copyright 2009 Google Inc. All Rights Reserved.
package com.google.android.apps.mytracks.io.gdata.maps;
import com.google.wireless.gdata.data.Entry;
import com.google.wireless.gdata.data.Feed;
import com.google.wireless.gdata.data.XmlUtils;
import com.google.wireless.gdata.parser.ParseException;
import com.google.wireless.gdata.parser.xml.XmlGDataParser;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException;
import java.io.InputStream;
/**
* Parser for XML gdata maps data.
*/
class XmlMapsGDataParser extends XmlGDataParser {
public XmlMapsGDataParser(InputStream is, XmlPullParser xpp)
throws ParseException {
super(is, xpp);
}
@Override
protected Feed createFeed() {
return new Feed();
}
@Override
protected Entry createEntry() {
return new MapFeatureEntry();
}
@Override
protected void handleExtraElementInFeed(Feed feed) {
// Do nothing
}
@Override
protected void handleExtraLinkInEntry(
String rel, String type, String href, Entry entry)
throws XmlPullParserException, IOException {
if (!(entry instanceof MapFeatureEntry)) {
throw new IllegalArgumentException("Expected MapFeatureEntry!");
}
if (rel.endsWith("#view")) {
return;
}
super.handleExtraLinkInEntry(rel, type, href, entry);
}
/**
* Parses the current entry in the XML document. Assumes that the parser is
* currently pointing just after an &lt;entry&gt;.
*
* @param plainEntry The entry that will be filled.
* @throws XmlPullParserException Thrown if the XML cannot be parsed.
* @throws IOException Thrown if the underlying inputstream cannot be read.
*/
@Override
protected void handleEntry(Entry plainEntry)
throws XmlPullParserException, IOException, ParseException {
XmlPullParser parser = getParser();
if (!(plainEntry instanceof MapFeatureEntry)) {
throw new IllegalArgumentException("Expected MapFeatureEntry!");
}
MapFeatureEntry entry = (MapFeatureEntry) plainEntry;
int eventType = parser.getEventType();
entry.setPrivacy("public");
while (eventType != XmlPullParser.END_DOCUMENT) {
switch (eventType) {
case XmlPullParser.START_TAG:
String name = parser.getName();
if ("entry".equals(name)) {
// stop parsing here.
return;
} else if ("id".equals(name)) {
entry.setId(XmlUtils.extractChildText(parser));
} else if ("title".equals(name)) {
entry.setTitle(XmlUtils.extractChildText(parser));
} else if ("link".equals(name)) {
String rel = parser.getAttributeValue(null /* ns */, "rel");
String type = parser.getAttributeValue(null /* ns */, "type");
String href = parser.getAttributeValue(null /* ns */, "href");
if ("edit".equals(rel)) {
entry.setEditUri(href);
} else if ("alternate".equals(rel) && "text/html".equals(type)) {
entry.setHtmlUri(href);
} else {
handleExtraLinkInEntry(rel, type, href, entry);
}
} else if ("summary".equals(name)) {
entry.setSummary(XmlUtils.extractChildText(parser));
} else if ("content".equals(name)) {
StringBuilder contentBuilder = new StringBuilder();
int parentDepth = parser.getDepth();
while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {
int etype = parser.next();
switch (etype) {
case XmlPullParser.START_TAG:
contentBuilder.append('<');
contentBuilder.append(parser.getName());
contentBuilder.append('>');
break;
case XmlPullParser.TEXT:
contentBuilder.append("<![CDATA[");
contentBuilder.append(parser.getText());
contentBuilder.append("]]>");
break;
case XmlPullParser.END_TAG:
if (parser.getDepth() > parentDepth) {
contentBuilder.append("</");
contentBuilder.append(parser.getName());
contentBuilder.append('>');
}
break;
}
if (etype == XmlPullParser.END_TAG
&& parser.getDepth() == parentDepth) {
break;
}
}
entry.setContent(contentBuilder.toString());
} else if ("category".equals(name)) {
String category = parser.getAttributeValue(null /* ns */, "term");
if (category != null && category.length() > 0) {
entry.setCategory(category);
}
String categoryScheme =
parser.getAttributeValue(null /* ns */, "scheme");
if (categoryScheme != null && category.length() > 0) {
entry.setCategoryScheme(categoryScheme);
}
} else if ("published".equals(name)) {
entry.setPublicationDate(XmlUtils.extractChildText(parser));
} else if ("updated".equals(name)) {
entry.setUpdateDate(XmlUtils.extractChildText(parser));
} else if ("deleted".equals(name)) {
entry.setDeleted(true);
} else if ("draft".equals(name)) {
String draft = XmlUtils.extractChildText(parser);
entry.setPrivacy("yes".equals(draft) ? "unlisted" : "public");
} else if ("customProperty".equals(name)) {
String attrName = parser.getAttributeValue(null, "name");
String attrValue = XmlUtils.extractChildText(parser);
entry.setAttribute(attrName, attrValue);
} else if ("deleted".equals(name)) {
entry.setDeleted(true);
} else {
handleExtraElementInEntry(entry);
}
break;
default:
break;
}
eventType = parser.next();
}
}
}
@@ -0,0 +1,94 @@
// Copyright 2010 Google Inc. All Rights Reserved.
package com.google.android.apps.mytracks.io.gdata.maps;
import com.google.wireless.gdata.client.GDataParserFactory;
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.gdata.parser.xml.XmlGDataParser;
import com.google.wireless.gdata.parser.xml.XmlParserFactory;
import com.google.wireless.gdata.serializer.GDataSerializer;
import com.google.wireless.gdata.serializer.xml.XmlEntryGDataSerializer;
import android.util.Log;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import org.xmlpull.v1.XmlPullParserException;
/**
* Factory of Xml parsers for gdata maps data.
*/
public class XmlMapsGDataParserFactory implements GDataParserFactory {
private XmlParserFactory xmlFactory;
public XmlMapsGDataParserFactory(XmlParserFactory xmlFactory) {
this.xmlFactory = xmlFactory;
}
@Override
public GDataParser createParser(InputStream is) throws ParseException {
is = maybeLogCommunication(is);
try {
return new XmlGDataParser(is, xmlFactory.createParser());
} catch (XmlPullParserException e) {
e.printStackTrace();
return null;
}
}
@SuppressWarnings("unchecked")
@Override
public GDataParser createParser(Class cls, InputStream is)
throws ParseException {
is = maybeLogCommunication(is);
try {
return createParserForClass(cls, is);
} catch (XmlPullParserException e) {
e.printStackTrace();
return null;
}
}
private InputStream maybeLogCommunication(InputStream is)
throws ParseException {
if (MapsClient.LOG_COMMUNICATION) {
StringBuilder builder = new StringBuilder();
byte[] buffer = new byte[2048];
try {
for (int n = is.read(buffer); n >= 0; n = is.read(buffer)) {
String part = new String(buffer, 0, n);
builder.append(part);
Log.d("Response part", part);
}
} catch (IOException e) {
throw new ParseException("Could not read stream", e);
}
String whole = builder.toString();
Log.d("Response", whole);
is = new ByteArrayInputStream(whole.getBytes());
}
return is;
}
private GDataParser createParserForClass(
Class<? extends Entry> cls, InputStream is)
throws ParseException, XmlPullParserException {
if (cls == MapFeatureEntry.class) {
return new XmlMapsGDataParser(is, xmlFactory.createParser());
} else {
return new XmlGDataParser(is, xmlFactory.createParser());
}
}
@Override
public GDataSerializer createSerializer(Entry en) {
if (en instanceof MapFeatureEntry) {
return new XmlMapsGDataSerializer(xmlFactory, (MapFeatureEntry) en);
} else {
return new XmlEntryGDataSerializer(xmlFactory, en);
}
}
}
@@ -0,0 +1,262 @@
// Copyright 2010 Google Inc. All Rights Reserved.
package com.google.android.apps.mytracks.io.gdata.maps;
import com.google.wireless.gdata.data.StringUtils;
import com.google.wireless.gdata.parser.ParseException;
import com.google.wireless.gdata.parser.xml.XmlGDataParser;
import com.google.wireless.gdata.parser.xml.XmlParserFactory;
import com.google.wireless.gdata.serializer.xml.XmlEntryGDataSerializer;
import android.util.Log;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Map;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlSerializer;
/**
* Serializer of maps data for GData.
*/
class XmlMapsGDataSerializer extends XmlEntryGDataSerializer {
private static final String APP_NAMESPACE = "http://www.w3.org/2007/app";
private MapFeatureEntry entry;
private XmlParserFactory factory;
private OutputStream stream;
public XmlMapsGDataSerializer(XmlParserFactory factory, MapFeatureEntry entry) {
super(factory, entry);
this.factory = factory;
this.entry = entry;
}
@Override
public void serialize(OutputStream out, int format)
throws IOException, ParseException {
XmlSerializer serializer = null;
try {
serializer = factory.createSerializer();
} catch (XmlPullParserException e) {
throw new ParseException("Unable to create XmlSerializer.", e);
}
ByteArrayOutputStream printStream;
if (MapsClient.LOG_COMMUNICATION) {
printStream = new ByteArrayOutputStream();
serializer.setOutput(printStream, "UTF-8");
} else {
serializer.setOutput(out, "UTF-8");
}
serializer.startDocument("UTF-8", Boolean.FALSE);
declareEntryNamespaces(serializer);
serializer.startTag(XmlGDataParser.NAMESPACE_ATOM_URI, "entry");
if (MapsClient.LOG_COMMUNICATION) {
stream = printStream;
} else {
stream = out;
}
serializeEntryContents(serializer, format);
serializer.endTag(XmlGDataParser.NAMESPACE_ATOM_URI, "entry");
serializer.endDocument();
serializer.flush();
if (MapsClient.LOG_COMMUNICATION) {
Log.d("Request", printStream.toString());
out.write(printStream.toByteArray());
stream = out;
}
}
private final void declareEntryNamespaces(XmlSerializer serializer)
throws IOException {
serializer.setPrefix(
"" /* default ns */, XmlGDataParser.NAMESPACE_ATOM_URI);
serializer.setPrefix(
XmlGDataParser.NAMESPACE_GD, XmlGDataParser.NAMESPACE_GD_URI);
declareExtraEntryNamespaces(serializer);
}
private final void serializeEntryContents(XmlSerializer serializer,
int format) throws IOException {
if (format != FORMAT_CREATE) {
serializeId(serializer, entry.getId());
}
serializeTitle(serializer, entry.getTitle());
if (format != FORMAT_CREATE) {
serializeLink(serializer,
"edit" /* rel */, entry.getEditUri(), null /* type */);
serializeLink(serializer,
"alternate" /* rel */, entry.getHtmlUri(), "text/html" /* type */);
}
serializeSummary(serializer, entry.getSummary());
serializeContent(serializer, entry.getContent());
serializeAuthor(serializer, entry.getAuthor(), entry.getEmail());
serializeCategory(serializer,
entry.getCategory(), entry.getCategoryScheme());
if (format == FORMAT_FULL) {
serializePublicationDate(serializer, entry.getPublicationDate());
}
if (format != FORMAT_CREATE) {
serializeUpdateDate(serializer, entry.getUpdateDate());
}
serializeExtraEntryContents(serializer, format);
}
private static void serializeId(XmlSerializer serializer, String id)
throws IOException {
if (StringUtils.isEmpty(id)) {
return;
}
serializer.startTag(null /* ns */, "id");
serializer.text(id);
serializer.endTag(null /* ns */, "id");
}
private static void serializeTitle(XmlSerializer serializer, String title)
throws IOException {
if (StringUtils.isEmpty(title)) {
return;
}
serializer.startTag(null /* ns */, "title");
serializer.text(title);
serializer.endTag(null /* ns */, "title");
}
public static void serializeLink(XmlSerializer serializer, String rel,
String href, String type) throws IOException {
if (StringUtils.isEmpty(href)) {
return;
}
serializer.startTag(null /* ns */, "link");
serializer.attribute(null /* ns */, "rel", rel);
serializer.attribute(null /* ns */, "href", href);
if (!StringUtils.isEmpty(type)) {
serializer.attribute(null /* ns */, "type", type);
}
serializer.endTag(null /* ns */, "link");
}
private static void serializeSummary(XmlSerializer serializer, String summary)
throws IOException {
if (StringUtils.isEmpty(summary)) {
return;
}
serializer.startTag(null /* ns */, "summary");
serializer.text(summary);
serializer.endTag(null /* ns */, "summary");
}
private void serializeContent(XmlSerializer serializer, String content)
throws IOException {
if (content == null) {
return;
}
serializer.startTag(null /* ns */, "content");
if (content.contains("</Placemark>")) {
serializer.attribute(
null /* ns */, "type", "application/vnd.google-earth.kml+xml");
serializer.flush();
stream.write(content.getBytes());
} else {
serializer.text(content);
}
serializer.endTag(null /* ns */, "content");
}
private static void serializeAuthor(XmlSerializer serializer, String author,
String email) throws IOException {
if (StringUtils.isEmpty(author) || StringUtils.isEmpty(email)) {
return;
}
serializer.startTag(null /* ns */, "author");
serializer.startTag(null /* ns */, "name");
serializer.text(author);
serializer.endTag(null /* ns */, "name");
serializer.startTag(null /* ns */, "email");
serializer.text(email);
serializer.endTag(null /* ns */, "email");
serializer.endTag(null /* ns */, "author");
}
private static void serializeCategory(XmlSerializer serializer,
String category, String categoryScheme) throws IOException {
if (StringUtils.isEmpty(category) && StringUtils.isEmpty(categoryScheme)) {
return;
}
serializer.startTag(null /* ns */, "category");
if (!StringUtils.isEmpty(category)) {
serializer.attribute(null /* ns */, "term", category);
}
if (!StringUtils.isEmpty(categoryScheme)) {
serializer.attribute(null /* ns */, "scheme", categoryScheme);
}
serializer.endTag(null /* ns */, "category");
}
private static void serializePublicationDate(XmlSerializer serializer,
String publicationDate) throws IOException {
if (StringUtils.isEmpty(publicationDate)) {
return;
}
serializer.startTag(null /* ns */, "published");
serializer.text(publicationDate);
serializer.endTag(null /* ns */, "published");
}
private static void serializeUpdateDate(XmlSerializer serializer,
String updateDate) throws IOException {
if (StringUtils.isEmpty(updateDate)) {
return;
}
serializer.startTag(null /* ns */, "updated");
serializer.text(updateDate);
serializer.endTag(null /* ns */, "updated");
}
@Override
protected void serializeExtraEntryContents(XmlSerializer serializer,
int format) throws IOException {
Map<String, String> attrs = entry.getAllAttributes();
for (Map.Entry<String, String> attr : attrs.entrySet()) {
serializer.startTag("http://schemas.google.com/g/2005", "customProperty");
serializer.attribute(null, "name", attr.getKey());
serializer.text(attr.getValue());
serializer.endTag("http://schemas.google.com/g/2005", "customProperty");
}
String privacy = entry.getPrivacy();
if (!StringUtils.isEmpty(privacy)) {
serializer.setPrefix("app", APP_NAMESPACE);
if ("public".equals(privacy)) {
serializer.startTag(APP_NAMESPACE, "control");
serializer.startTag(APP_NAMESPACE, "draft");
serializer.text("no");
serializer.endTag(APP_NAMESPACE, "draft");
serializer.endTag(APP_NAMESPACE, "control");
}
if ("unlisted".equals(privacy)) {
serializer.startTag(APP_NAMESPACE, "control");
serializer.startTag(APP_NAMESPACE, "draft");
serializer.text("yes");
serializer.endTag(APP_NAMESPACE, "draft");
serializer.endTag(APP_NAMESPACE, "control");
}
}
}
}
@@ -0,0 +1,236 @@
/*
* 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.maps;
import com.google.android.apps.mytracks.io.gdata.maps.MapsMapMetadata;
import com.google.android.apps.mytracks.io.sendtogoogle.SendRequest;
import com.google.android.maps.mytracks.R;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.ProgressDialog;
import android.content.DialogInterface;
import android.content.DialogInterface.OnCancelListener;
import android.content.DialogInterface.OnClickListener;
import android.content.Intent;
import android.graphics.Color;
import android.os.Bundle;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.TextView;
import java.util.ArrayList;
/**
* An activity to choose a Google Map.
*
* @author Jimmy Shih
*/
public class ChooseMapActivity extends Activity {
private static final int PROGRESS_DIALOG = 1;
private static final int ERROR_DIALOG = 2;
private SendRequest sendRequest;
private ChooseMapAsyncTask asyncTask;
private ProgressDialog progressDialog;
private ArrayAdapter<ListItem> arrayAdapter;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
sendRequest = getIntent().getParcelableExtra(SendRequest.SEND_REQUEST_KEY);
setContentView(R.layout.choose_map);
arrayAdapter = new ArrayAdapter<ListItem>(this, R.layout.choose_map_item, new ArrayList<
ListItem>()) {
@Override
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null) {
convertView = getLayoutInflater().inflate(R.layout.choose_map_item, parent, false);
}
MapsMapMetadata mapData = getItem(position).getMapData();
TextView title = (TextView) convertView.findViewById(R.id.choose_map_list_item_title);
title.setText(mapData.getTitle());
TextView description = (TextView) convertView.findViewById(
R.id.choose_map_list_item_description);
String descriptionText = mapData.getDescription();
if (descriptionText == null || descriptionText.equals("")) {
description.setVisibility(View.GONE);
} else {
description.setVisibility(View.VISIBLE);
description.setText(descriptionText);
}
TextView searchStatus = (TextView) convertView.findViewById(
R.id.choose_map_list_item_search_status);
searchStatus.setTextColor(mapData.getSearchable() ? Color.RED : Color.GREEN);
searchStatus.setText(mapData.getSearchable() ? R.string.maps_list_public_label
: R.string.maps_list_unlisted_label);
return convertView;
}
};
ListView list = (ListView) findViewById(R.id.choose_map_list_view);
list.setEmptyView(findViewById(R.id.choose_map_empty_view));
list.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
startNextActivity(arrayAdapter.getItem(position).getMapId());
}
});
list.setAdapter(arrayAdapter);
Object retained = getLastNonConfigurationInstance();
if (retained instanceof ChooseMapAsyncTask) {
asyncTask = (ChooseMapAsyncTask) retained;
asyncTask.setActivity(this);
} else {
asyncTask = new ChooseMapAsyncTask(this, 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_SPINNER);
progressDialog.setMessage(getString(R.string.maps_list_loading));
progressDialog.setCancelable(true);
progressDialog.setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
asyncTask.cancel(true);
finish();
}
});
progressDialog.setIcon(android.R.drawable.ic_dialog_info);
progressDialog.setTitle(R.string.generic_progress_title);
return progressDialog;
case ERROR_DIALOG:
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setIcon(android.R.drawable.ic_dialog_alert);
builder.setTitle(R.string.generic_error_title);
builder.setMessage(R.string.maps_list_error);
builder.setPositiveButton(R.string.generic_ok, new OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int arg1) {
finish();
}
});
builder.setOnCancelListener(new OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
finish();
}
});
return builder.create();
default:
return null;
}
}
/**
* Invokes when the associated AsyncTask completes.
*
* @param success true if success
* @param mapIds an array of map ids
* @param mapData an array of map data
*/
public void onAsyncTaskCompleted(
boolean success, ArrayList<String> mapIds, ArrayList<MapsMapMetadata> mapData) {
removeDialog(PROGRESS_DIALOG);
if (success) {
arrayAdapter.clear();
// To prevent displaying the emptyView message momentarily before the
// arrayAdapter is set, don't set the emptyView message in the xml layout.
// Instead, set it only when needed.
if (mapIds.size() == 0) {
TextView emptyView = (TextView) findViewById(R.id.choose_map_empty_view);
emptyView.setText(R.string.maps_list_no_maps);
} else {
for (int i = 0; i < mapIds.size(); i++) {
arrayAdapter.add(new ListItem(mapIds.get(i), mapData.get(i)));
}
}
} else {
showDialog(ERROR_DIALOG);
}
}
/**
* Shows the progress dialog.
*/
public void showProgressDialog() {
showDialog(PROGRESS_DIALOG);
}
/**
* Starts the next activity, {@link SendMapsActivity}.
*
* @param mapId the chosen map id
*/
private void startNextActivity(String mapId) {
sendRequest.setMapId(mapId);
Intent intent = new Intent(this, SendMapsActivity.class)
.putExtra(SendRequest.SEND_REQUEST_KEY, sendRequest);
startActivity(intent);
finish();
}
/**
* A class containing {@link ChooseMapActivity} list item.
*
* @author Jimmy Shih
*/
private class ListItem {
private String mapId;
private MapsMapMetadata mapData;
private ListItem(String mapId, MapsMapMetadata mapData) {
this.mapId = mapId;
this.mapData = mapData;
}
/**
* Gets the map id.
*/
public String getMapId() {
return mapId;
}
/**
* Gets the map data.
*/
public MapsMapMetadata getMapData() {
return mapData;
}
}
}
@@ -0,0 +1,209 @@
/*
* 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.maps;
import com.google.android.apps.mytracks.Constants;
import com.google.android.apps.mytracks.io.gdata.GDataClientFactory;
import com.google.android.apps.mytracks.io.gdata.maps.MapFeatureEntry;
import com.google.android.apps.mytracks.io.gdata.maps.MapsClient;
import com.google.android.apps.mytracks.io.gdata.maps.MapsConstants;
import com.google.android.apps.mytracks.io.gdata.maps.MapsGDataConverter;
import com.google.android.apps.mytracks.io.gdata.maps.MapsMapMetadata;
import com.google.android.apps.mytracks.io.gdata.maps.XmlMapsGDataParserFactory;
import com.google.android.common.gdata.AndroidXmlParserFactory;
import com.google.wireless.gdata.client.GDataClient;
import com.google.wireless.gdata.client.HttpException;
import com.google.wireless.gdata.parser.GDataParser;
import com.google.wireless.gdata.parser.ParseException;
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;
import java.util.ArrayList;
/**
* AsyncTask for {@link ChooseMapActivity} to get all the maps from Google Maps.
*
* @author Jimmy Shih
*/
public class ChooseMapAsyncTask extends AsyncTask<Void, Integer, Boolean> {
private static final String TAG = ChooseMapAsyncTask.class.getSimpleName();
private ChooseMapActivity activity;
private final Account account;
private final Context context;
private final GDataClient gDataClient;
private final MapsClient mapsClient;
/**
* 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 request states
private String authToken;
private ArrayList<String> mapIds;
private ArrayList<MapsMapMetadata> mapData;
public ChooseMapAsyncTask(ChooseMapActivity activity, Account account) {
this.activity = activity;
this.account = account;
context = activity.getApplicationContext();
gDataClient = GDataClientFactory.getGDataClient(context);
mapsClient = new MapsClient(
gDataClient, new XmlMapsGDataParserFactory(new AndroidXmlParserFactory()));
canRetry = true;
completed = false;
success = false;
}
/**
* Sets the activity associated with this AyncTask.
*
* @param activity the activity.
*/
public void setActivity(ChooseMapActivity activity) {
this.activity = activity;
if (completed && activity != null) {
activity.onAsyncTaskCompleted(success, mapIds, mapData);
}
}
@Override
protected void onPreExecute() {
activity.showProgressDialog();
}
@Override
protected Boolean doInBackground(Void... params) {
return getMaps();
}
@Override
protected void onCancelled() {
closeClient();
}
@Override
protected void onPostExecute(Boolean result) {
closeClient();
success = result;
completed = true;
if (activity != null) {
activity.onAsyncTaskCompleted(success, mapIds, mapData);
}
}
/**
* Closes the gdata client.
*/
private void closeClient() {
if (gDataClient != null) {
gDataClient.close();
}
}
/**
* Gets all the maps from Google Maps.
*
* @return true if success.
*/
private boolean getMaps() {
// Reset the per request states
authToken = null;
mapIds = new ArrayList<String>();
mapData = new ArrayList<MapsMapMetadata>();
try {
authToken = AccountManager.get(context).blockingGetAuthToken(
account, MapsConstants.SERVICE_NAME, 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();
}
if (isCancelled()) {
return false;
}
GDataParser gDataParser = null;
try {
gDataParser = mapsClient.getParserForFeed(
MapFeatureEntry.class, MapsClient.getMapsFeed(), authToken);
gDataParser.init();
while (gDataParser.hasMoreData()) {
MapFeatureEntry entry = (MapFeatureEntry) gDataParser.readNextEntry(null);
mapIds.add(MapsGDataConverter.getMapidForEntry(entry));
mapData.add(MapsGDataConverter.getMapMetadataForEntry(entry));
}
} catch (ParseException e) {
Log.d(TAG, "Unable to get maps", e);
return retryUpload();
} catch (IOException e) {
Log.d(TAG, "Unable to get maps", e);
return retryUpload();
} catch (HttpException e) {
Log.d(TAG, "Unable to get maps", e);
return retryUpload();
} finally {
if (gDataParser != null) {
gDataParser.close();
}
}
return true;
}
/**
* Retries upload. Invalidates the authToken. If can retry, invokes
* {@link ChooseMapAsyncTask#getMaps()}. Returns false if cannot retry.
*/
private boolean retryUpload() {
if (isCancelled()) {
return false;
}
AccountManager.get(context).invalidateAuthToken(Constants.ACCOUNT_TYPE, authToken);
if (canRetry) {
canRetry = false;
return getMaps();
}
return false;
}
}
@@ -0,0 +1,66 @@
/*
* 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.maps;
import com.google.android.apps.mytracks.io.docs.SendDocsActivity;
import com.google.android.apps.mytracks.io.fusiontables.SendFusionTablesActivity;
import com.google.android.apps.mytracks.io.sendtogoogle.AbstractSendActivity;
import com.google.android.apps.mytracks.io.sendtogoogle.AbstractSendAsyncTask;
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.content.Intent;
/**
* An activity to send a track to Google Maps.
*
* @author Jimmy Shih
*/
public class SendMapsActivity extends AbstractSendActivity {
@Override
protected AbstractSendAsyncTask createAsyncTask() {
return new SendMapsAsyncTask(
this, sendRequest.getTrackId(), sendRequest.getAccount(), sendRequest.getMapId());
}
@Override
protected String getServiceName() {
return getString(R.string.send_google_maps);
}
@Override
protected void startNextActivity(boolean success, boolean isCancel) {
sendRequest.setMapsSuccess(success);
Class<?> next;
if (isCancel) {
next = UploadResultActivity.class;
} else {
if (sendRequest.isSendFusionTables()) {
next = SendFusionTablesActivity.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();
}
}
@@ -0,0 +1,479 @@
/*
* 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.maps;
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.gdata.GDataClientFactory;
import com.google.android.apps.mytracks.io.gdata.maps.MapsClient;
import com.google.android.apps.mytracks.io.gdata.maps.MapsConstants;
import com.google.android.apps.mytracks.io.gdata.maps.MapsGDataConverter;
import com.google.android.apps.mytracks.io.gdata.maps.XmlMapsGDataParserFactory;
import com.google.android.apps.mytracks.io.sendtogoogle.AbstractSendAsyncTask;
import com.google.android.apps.mytracks.io.sendtogoogle.SendToGoogleUtils;
import com.google.android.apps.mytracks.stats.DoubleBuffer;
import com.google.android.apps.mytracks.stats.TripStatisticsBuilder;
import com.google.android.apps.mytracks.util.LocationUtils;
import com.google.android.apps.mytracks.util.StringUtils;
import com.google.android.apps.mytracks.util.UnitConversions;
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 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.util.Log;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Vector;
import org.xmlpull.v1.XmlPullParserException;
/**
* AsyncTask to send a track to Google Maps.
* <p>
* IMPORTANT: While this code is Apache-licensed, please notice that usage of
* the Google Maps servers through this API is only allowed for the My Tracks
* application. Other applications looking to upload maps data should look into
* using the Google Fusion Tables API.
*
* @author Jimmy Shih
*/
public class SendMapsAsyncTask extends AbstractSendAsyncTask {
private static final String START_ICON_URL =
"http://maps.google.com/mapfiles/ms/micons/green-dot.png";
private static final String END_ICON_URL =
"http://maps.google.com/mapfiles/ms/micons/red-dot.png";
private static final int MAX_POINTS_PER_UPLOAD = 500;
private static final int PROGRESS_FETCH_MAP_ID = 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;
private static final String TAG = SendMapsAsyncTask.class.getSimpleName();
private final long trackId;
private final Account account;
private final String chooseMapId;
private final Context context;
private final MyTracksProviderUtils myTracksProviderUtils;
private final GDataClient gDataClient;
private final MapsClient mapsClient;
// The following variables are for per upload states
private MapsGDataConverter mapsGDataConverter;
private String authToken;
private String mapId;
int currentSegment;
public SendMapsAsyncTask (
SendMapsActivity activity, long trackId, Account account, String chooseMapId) {
super(activity);
this.trackId = trackId;
this.account = account;
this.chooseMapId = chooseMapId;
context = activity.getApplicationContext();
myTracksProviderUtils = MyTracksProviderUtils.Factory.get(context);
gDataClient = GDataClientFactory.getGDataClient(context);
mapsClient = new MapsClient(
gDataClient, new XmlMapsGDataParserFactory(new AndroidXmlParserFactory()));
}
@Override
protected void closeConnection() {
if (gDataClient != null) {
gDataClient.close();
}
}
@Override
protected void saveResult() {
Track track = myTracksProviderUtils.getTrack(trackId);
if (track != null) {
track.setMapId(mapId);
myTracksProviderUtils.updateTrack(track);
} else {
Log.d(TAG, "No track");
}
}
@Override
protected boolean performTask() {
// Reset the per upload states
mapsGDataConverter = null;
authToken = null;
mapId = null;
currentSegment = 1;
// Create a maps gdata converter
try {
mapsGDataConverter = new MapsGDataConverter();
} catch (XmlPullParserException e) {
Log.d(TAG, "Unable to create a maps gdata converter", e);
return false;
}
// Get auth token
try {
authToken = AccountManager.get(context).blockingGetAuthToken(
account, MapsConstants.SERVICE_NAME, false);
} catch (OperationCanceledException e) {
Log.d(TAG, "Unable to get auth token", e);
return retryTask();
} catch (AuthenticatorException e) {
Log.d(TAG, "Unable to get auth token", e);
return retryTask();
} catch (IOException e) {
Log.d(TAG, "Unable to get auth token", e);
return retryTask();
}
// Get the track
Track track = myTracksProviderUtils.getTrack(trackId);
if (track == null) {
Log.d(TAG, "Track is null");
return false;
}
// Fetch the mapId, create a new map if necessary
publishProgress(PROGRESS_FETCH_MAP_ID);
if (!fetchSendMapId(track)) {
Log.d("TAG", "Unable to upload all track points");
return retryTask();
}
// Upload all the track points plus the start and end markers
publishProgress(PROGRESS_UPLOAD_DATA_MIN);
if (!uploadAllTrackPoints(track)) {
Log.d("TAG", "Unable to upload all track points");
return retryTask();
}
// Upload all the waypoints
publishProgress(PROGRESS_UPLOAD_WAYPOINTS);
if (!uploadWaypoints()) {
Log.d("TAG", "Unable to upload waypoints");
return false;
}
publishProgress(PROGRESS_COMPLETE);
return true;
}
@Override
protected void invalidateToken() {
AccountManager.get(context).invalidateAuthToken(Constants.ACCOUNT_TYPE, authToken);
}
/**
* Fetches the {@link SendMapsAsyncTask#mapId} instance variable for
* sending a track to Google Maps.
*
* @param track the Track
* @return true if able to fetch the mapId variable.
*/
private boolean fetchSendMapId(Track track) {
if (isCancelled()) {
return false;
}
if (chooseMapId != null) {
mapId = chooseMapId;
return true;
} else {
SharedPreferences sharedPreferences = context.getSharedPreferences(
Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
boolean mapPublic = sharedPreferences.getBoolean(
context.getString(R.string.default_map_public_key), true);
try {
String description = track.getCategory() + "\n" + track.getDescription() + "\n"
+ StringUtils.getCreatedByMyTracks(context, false);
mapId = SendMapsUtils.createNewMap(
track.getName(), description, mapPublic, mapsClient, authToken);
} catch (ParseException e) {
Log.d(TAG, "Unable to create a new map", e);
return false;
} catch (HttpException e) {
Log.d(TAG, "Unable to create a new map", e);
return false;
} catch (IOException e) {
Log.d(TAG, "Unable to create a new map", e);
return false;
}
return mapId != null;
}
}
/**
* 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<Location> locations = new ArrayList<Location>(MAX_POINTS_PER_UPLOAD);
Location lastLocation = null;
// For chart server, limit the number of elevation readings to 250.
int elevationSamplingFrequency = Math.max(1, (int) (locationsCount / 250.0));
TripStatisticsBuilder tripStatisticsBuilder = new TripStatisticsBuilder(
track.getStatistics().getStartTime());
DoubleBuffer elevationBuffer = new DoubleBuffer(Constants.ELEVATION_SMOOTHING_FACTOR);
Vector<Double> distances = new Vector<Double>();
Vector<Double> elevations = new Vector<Double>();
for (int i = 0; i < locationsCount; i++) {
locationsCursor.moveToPosition(i);
Location location = myTracksProviderUtils.createLocation(locationsCursor);
locations.add(location);
if (i == 0) {
// Create a start marker
if (!uploadMarker(context.getString(R.string.marker_label_start, track.getName()), "",
START_ICON_URL, location)) {
Log.d(TAG, "Unable to create a start marker");
return false;
}
}
// Add to the distances and elevations vectors
if (LocationUtils.isValidLocation(location)) {
tripStatisticsBuilder.addLocation(location, location.getTime());
// All points go into the smoothing buffer
elevationBuffer.setNext(metricUnits ? location.getAltitude()
: location.getAltitude() * UnitConversions.M_TO_FT);
if (i % elevationSamplingFrequency == 0) {
distances.add(tripStatisticsBuilder.getStatistics().getTotalDistance());
elevations.add(elevationBuffer.getAverage());
}
lastLocation = location;
}
// Upload periodically
int readCount = i + 1;
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();
}
}
// 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) {
distances.add(tripStatisticsBuilder.getStatistics().getTotalDistance());
elevations.add(elevationBuffer.getAverage());
StringUtils stringUtils = new StringUtils(context);
track.setDescription("<p>" + track.getDescription() + "</p><p>"
+ stringUtils.generateTrackDescription(track, distances, elevations) + "</p>");
if (!uploadMarker(context.getString(R.string.marker_label_end, track.getName()),
track.getDescription(), END_ICON_URL, lastLocation)) {
Log.d(TAG, "Unable to create an 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
* @return true if success.
*/
private boolean prepareAndUploadPoints(Track track, List<Location> locations, boolean lastBatch) {
// Prepare locations
ArrayList<Track> splitTracks = SendToGoogleUtils.prepareLocations(track, locations);
// Upload segments
boolean onlyOneSegment = lastBatch && currentSegment == 1 && splitTracks.size() == 1;
for (Track segment : splitTracks) {
if (!onlyOneSegment) {
segment.setName(context.getString(
R.string.send_google_track_part_label, segment.getName(), currentSegment));
}
if (!uploadSegment(segment.getName(), segment.getLocations())) {
Log.d(TAG, "Unable to upload segment");
return false;
}
currentSegment++;
}
return true;
}
/**
* Uploads a marker.
*
* @param title marker title
* @param description marker description
* @param iconUrl marker marker icon
* @param location marker location
* @return true if success.
*/
private boolean uploadMarker(
String title, String description, String iconUrl, Location location) {
if (isCancelled()) {
return false;
}
try {
if (!SendMapsUtils.uploadMarker(mapId, title, description, iconUrl, location, mapsClient,
authToken, mapsGDataConverter)) {
Log.d(TAG, "Unable to upload marker");
return false;
}
} catch (ParseException e) {
Log.d(TAG, "Unable to upload marker", e);
return false;
} catch (HttpException e) {
Log.d(TAG, "Unable to upload marker", e);
return false;
} catch (IOException e) {
Log.d(TAG, "Unable to upload marker", e);
return false;
}
return true;
}
/**
* Uploads a segment
*
* @param title segment title
* @param locations segment locations
* @return true if success
*/
private boolean uploadSegment(String title, ArrayList<Location> locations) {
if (isCancelled()) {
return false;
}
try {
if (!SendMapsUtils.uploadSegment(
mapId, title, locations, mapsClient, authToken, mapsGDataConverter)) {
Log.d(TAG, "Unable to upload track points");
return false;
}
} catch (ParseException e) {
Log.d(TAG, "Unable to upload track points", e);
return false;
} catch (HttpException e) {
Log.d(TAG, "Unable to upload track points", e);
return false;
} catch (IOException e) {
Log.d(TAG, "Unable to upload track points", e);
return false;
}
return true;
}
/**
* Uploads all the waypoints.
*
* @return true if success.
*/
private boolean uploadWaypoints() {
Cursor cursor = null;
try {
cursor = myTracksProviderUtils.getWaypointsCursor(
trackId, 0, Constants.MAX_LOADED_WAYPOINTS_POINTS);
if (cursor != null && cursor.moveToFirst()) {
// This will skip the first waypoint (it carries the stats for the
// track).
while (cursor.moveToNext()) {
if (isCancelled()) {
return false;
}
Waypoint waypoint = myTracksProviderUtils.createWaypoint(cursor);
try {
if (!SendMapsUtils.uploadWaypoint(
mapId, waypoint, mapsClient, authToken, mapsGDataConverter)) {
Log.d(TAG, "Unable to upload waypoint");
return false;
}
} catch (ParseException e) {
Log.d(TAG, "Unable to upload waypoint", e);
return false;
} catch (HttpException e) {
Log.d(TAG, "Unable to upload waypoint", e);
return false;
} catch (IOException e) {
Log.d(TAG, "Unable to upload waypoint", e);
return false;
}
}
}
return true;
} finally {
if (cursor != null) {
cursor.close();
}
}
}
/**
* Updates the progress based on the number of locations uploaded.
*
* @param uploaded the number of uploaded locations
* @param total the number of total locations
*/
private void updateProgress(int uploaded, int total) {
double totalPercentage = uploaded / total;
double scaledPercentage = totalPercentage
* (PROGRESS_UPLOAD_DATA_MAX - PROGRESS_UPLOAD_DATA_MIN) + PROGRESS_UPLOAD_DATA_MIN;
publishProgress((int) scaledPercentage);
}
}
@@ -0,0 +1,227 @@
/*
* 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.maps;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.io.gdata.maps.MapsClient;
import com.google.android.apps.mytracks.io.gdata.maps.MapsFeature;
import com.google.android.apps.mytracks.io.gdata.maps.MapsGDataConverter;
import com.google.android.apps.mytracks.io.gdata.maps.MapsMapMetadata;
import com.google.android.maps.GeoPoint;
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.ParseException;
import android.location.Location;
import android.text.TextUtils;
import android.util.Log;
import java.io.IOException;
import java.util.ArrayList;
/**
* Utilities for sending a track to Google Maps.
*
* @author Jimmy Shih
*/
public class SendMapsUtils {
private static final String EMPTY_TITLE = "-";
private static final int LINE_COLOR = 0x80FF0000;
private static final String TAG = SendMapsUtils.class.getSimpleName();
private SendMapsUtils() {}
/**
* Gets the Google Maps url for a track.
*
* @param track the track
* @return the url if available.
*/
public static String getMapUrl(Track track) {
if (track == null || track.getMapId() == null) {
Log.e(TAG, "Invalid track");
return null;
}
return MapsClient.buildMapUrl(track.getMapId());
}
/**
* Creates a new Google Map.
*
* @param title title of the map
* @param description description of the map
* @param isPublic true if the map can be public
* @param mapsClient the maps client
* @param authToken the auth token
* @return map id of the created map if successful.
*/
public static String createNewMap(
String title, String description, boolean isPublic, MapsClient mapsClient, String authToken)
throws ParseException, HttpException, IOException {
String mapFeed = MapsClient.getMapsFeed();
MapsMapMetadata metaData = new MapsMapMetadata();
metaData.setTitle(title);
metaData.setDescription(description);
metaData.setSearchable(isPublic);
Entry entry = MapsGDataConverter.getMapEntryForMetadata(metaData);
Entry result = mapsClient.createEntry(mapFeed, authToken, entry);
if (result == null) {
Log.d(TAG, "No result when creating a new map");
return null;
}
return MapsClient.getMapIdFromMapEntryId(result.getId());
}
/**
* Uploads a start/end marker to Google Maps.
*
* @param mapId the map id
* @param title the marker title
* @param description the marker description
* @param iconUrl the marker icon URL
* @param location the marker location
* @param mapsClient the maps client
* @param authToken the auth token
* @param mapsGDataConverter the maps gdata converter
* @return true if success.
*/
public static boolean uploadMarker(String mapId, String title, String description, String iconUrl,
Location location, MapsClient mapsClient, String authToken,
MapsGDataConverter mapsGDataConverter) throws ParseException, HttpException, IOException {
String featuresFeed = MapsClient.getFeaturesFeed(mapId);
MapsFeature mapsFeature = buildMapsMarkerFeature(
title, description, iconUrl, getGeoPoint(location));
Entry entry = mapsGDataConverter.getEntryForFeature(mapsFeature);
try {
mapsClient.createEntry(featuresFeed, authToken, entry);
} catch (IOException e) {
// Retry once (often IOException is thrown on a timeout)
Log.d(TAG, "Retry upload marker", e);
mapsClient.createEntry(featuresFeed, authToken, entry);
}
return true;
}
/**
* Uploads a waypoint as a marker feature to Google Maps.
*
* @param mapId the map id
* @param waypoint the waypoint
* @param mapsClient the maps client
* @param authToken the auth token
* @param mapsGDataConverter the maps gdata converter
* @return true if success.
*/
public static boolean uploadWaypoint(String mapId, Waypoint waypoint, MapsClient mapsClient,
String authToken, MapsGDataConverter mapsGDataConverter)
throws ParseException, HttpException, IOException {
String featuresFeed = MapsClient.getFeaturesFeed(mapId);
MapsFeature feature = buildMapsMarkerFeature(waypoint.getName(), waypoint.getDescription(),
waypoint.getIcon(), getGeoPoint(waypoint.getLocation()));
Entry entry = mapsGDataConverter.getEntryForFeature(feature);
try {
mapsClient.createEntry(featuresFeed, authToken, entry);
} catch (IOException e) {
// Retry once (often IOException is thrown on a timeout)
Log.d(TAG, "Retry upload waypoint", e);
mapsClient.createEntry(featuresFeed, authToken, entry);
}
return true;
}
/**
* Uploads a segment as a line feature to Google Maps.
*
* @param mapId the map id
* @param title the segment title
* @param locations the segment locations
* @param mapsClient the maps client
* @param authToken the auth token
* @param mapsGDataConverter the maps gdata converter
* @return true if success.
*/
public static boolean uploadSegment(String mapId, String title, ArrayList<Location> locations,
MapsClient mapsClient, String authToken, MapsGDataConverter mapsGDataConverter)
throws ParseException, HttpException, IOException {
String featuresFeed = MapsClient.getFeaturesFeed(mapId);
Entry entry = mapsGDataConverter.getEntryForFeature(buildMapsLineFeature(title, locations));
try {
mapsClient.createEntry(featuresFeed, authToken, entry);
} catch (IOException e) {
// Retry once (often IOException is thrown on a timeout)
Log.d(TAG, "Retry upload track points", e);
mapsClient.createEntry(featuresFeed, authToken, entry);
}
return true;
}
/**
* Builds a map marker feature.
*
* @param title feature title
* @param description the feature description
* @param iconUrl the feature icon URL
* @param geoPoint the marker
*/
@VisibleForTesting
static MapsFeature buildMapsMarkerFeature(
String title, String description, String iconUrl, GeoPoint geoPoint) {
MapsFeature mapsFeature = new MapsFeature();
mapsFeature.setType(MapsFeature.MARKER);
mapsFeature.generateAndroidId();
// Feature must have a name (otherwise GData upload may fail)
mapsFeature.setTitle(TextUtils.isEmpty(title) ? EMPTY_TITLE : title);
mapsFeature.setDescription(description.replaceAll("\n", "<br>"));
mapsFeature.setIconUrl(iconUrl);
mapsFeature.addPoint(geoPoint);
return mapsFeature;
}
/**
* Builds a maps line feature from a set of locations.
*
* @param title the feature title
* @param locations set of locations
*/
@VisibleForTesting
static MapsFeature buildMapsLineFeature(String title, ArrayList<Location> locations) {
MapsFeature mapsFeature = new MapsFeature();
mapsFeature.setType(MapsFeature.LINE);
mapsFeature.generateAndroidId();
// Feature must have a name (otherwise GData upload may fail)
mapsFeature.setTitle(TextUtils.isEmpty(title) ? EMPTY_TITLE : title);
mapsFeature.setColor(LINE_COLOR);
for (Location location : locations) {
mapsFeature.addPoint(getGeoPoint(location));
}
return mapsFeature;
}
/**
* Gets a {@link GeoPoint} from a {@link Location}.
*
* @param location the location
*/
@VisibleForTesting
static GeoPoint getGeoPoint(Location location) {
return new GeoPoint(
(int) (location.getLatitude() * 1E6), (int) (location.getLongitude() * 1E6));
}
}
@@ -0,0 +1,139 @@
/*
* 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.sendtogoogle;
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.os.Bundle;
/**
* The abstract class for activities sending a track to Google.
* <p>
* The activity gets recreated when the screen rotates. To support the activity
* displaying a progress dialog, we do the following:
* <ul>
* <li>use one instance of an AyncTask to send the track</li>
* <li>save that instance as the last non configuration instance of the activity
* </li>
* <li>when a new activity is created, pass the activity to the AsyncTask so
* that the AsyncTask can update the progress dialog of the activity</li>
* </ul>
*
* @author Jimmy Shih
*/
public abstract class AbstractSendActivity extends Activity {
private static final int PROGRESS_DIALOG = 1;
protected SendRequest sendRequest;
private AbstractSendAsyncTask 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 AbstractSendAsyncTask) {
asyncTask = (AbstractSendAsyncTask) retained;
asyncTask.setActivity(this);
} else {
asyncTask = createAsyncTask();
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, getServiceName()));
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 the AsyncTask is successful
*/
public void onAsyncTaskCompleted(boolean success) {
startNextActivity(success, false);
}
/**
* Shows the progress dialog.
*/
public void showProgressDialog() {
showDialog(PROGRESS_DIALOG);
}
/**
* Sets the progress dialog value.
*
* @param value the dialog value
*/
public void setProgressDialogValue(int value) {
if (progressDialog != null) {
progressDialog.setProgress(value);
}
}
/**
* Creates the AsyncTask.
*/
protected abstract AbstractSendAsyncTask createAsyncTask();
/**
* Gets the service name.
*/
protected abstract String getServiceName();
/**
* Starts the next activity.
*
* @param success true if this activity is successful
* @param isCancel true if it is a cancel request
*/
protected abstract void startNextActivity(boolean success, boolean isCancel);
}
@@ -0,0 +1,147 @@
/*
* 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.sendtogoogle;
import android.os.AsyncTask;
/**
* The abstract class for AsyncTasks sending a track to Google.
*
* @author Jimmy Shih
*/
public abstract class AbstractSendAsyncTask extends AsyncTask<Void, Integer, Boolean> {
/**
* The activity associated with this AsyncTask.
*/
private AbstractSendActivity activity;
/**
* True if the AsyncTask result is success.
*/
private boolean success;
/**
* True if the AsyncTask has completed.
*/
private boolean completed;
/**
* True if can retry the AsyncTask.
*/
private boolean canRetry;
/**
* Creates an AsyncTask.
*
* @param activity the activity currently associated with this AsyncTask
*/
public AbstractSendAsyncTask(AbstractSendActivity activity) {
this.activity = activity;
success = false;
completed = false;
canRetry = true;
}
/**
* Sets the current activity associated with this AyncTask.
*
* @param activity the current activity, can be null
*/
public void setActivity(AbstractSendActivity activity) {
this.activity = activity;
if (completed && activity != null) {
activity.onAsyncTaskCompleted(success);
}
}
@Override
protected void onPreExecute() {
activity.showProgressDialog();
}
@Override
protected Boolean doInBackground(Void... params) {
return performTask();
}
@Override
protected void onProgressUpdate(Integer... values) {
if (activity != null) {
activity.setProgressDialogValue(values[0]);
}
}
@Override
protected void onPostExecute(Boolean result) {
success = result;
if (success) {
saveResult();
}
completed = true;
closeConnection();
if (activity != null) {
activity.onAsyncTaskCompleted(success);
}
}
@Override
protected void onCancelled() {
closeConnection();
}
/**
* Retries the task. First, invalidates the auth token. If can retry, invokes
* {@link #performTask()}. Returns false if cannot retry.
*
* @return the result of the retry.
*/
protected boolean retryTask() {
if (isCancelled()) {
return false;
}
invalidateToken();
if (canRetry) {
canRetry = false;
return performTask();
}
return false;
}
/**
* Closes any AsyncTask connection.
*/
protected abstract void closeConnection();
/**
* Saves any AsyncTask result.
*/
protected abstract void saveResult();
/**
* Performs the AsyncTask.
*
* @return true if success
*/
protected abstract boolean performTask();
/**
* Invalidates the auth token.
*/
protected abstract void invalidateToken();
}
@@ -0,0 +1,297 @@
/*
* 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.sendtogoogle;
import com.google.android.apps.mytracks.Constants;
import com.google.android.apps.mytracks.io.docs.SendDocsActivity;
import com.google.android.apps.mytracks.io.fusiontables.SendFusionTablesActivity;
import com.google.android.apps.mytracks.io.fusiontables.SendFusionTablesUtils;
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.maps.MapsConstants;
import com.google.android.apps.mytracks.io.maps.ChooseMapActivity;
import com.google.android.apps.mytracks.io.maps.SendMapsActivity;
import com.google.android.apps.mytracks.util.ApiAdapterFactory;
import com.google.android.maps.mytracks.R;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.accounts.AccountManagerCallback;
import android.accounts.AccountManagerFuture;
import android.accounts.AuthenticatorException;
import android.accounts.OperationCanceledException;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.os.Bundle;
import android.util.Log;
import java.io.IOException;
/**
* A chooser to select an account.
*
* @author Jimmy Shih
*/
public class AccountChooserActivity extends Activity {
private static final String TAG = AccountChooserActivity.class.getSimpleName();
private static final int NO_ACCOUNT_DIALOG = 1;
private static final int CHOOSE_ACCOUNT_DIALOG = 2;
/**
* A callback after getting the permission to access a Google service.
*
* @author Jimmy Shih
*/
private interface PermissionCallback {
/**
* To be invoked when the permission is granted.
*/
public void onSuccess();
/**
* To be invoked when the permission is not granted.
*/
public void onFailure();
}
private SendRequest sendRequest;
private Account[] accounts;
private int selectedAccountIndex;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
sendRequest = getIntent().getParcelableExtra(SendRequest.SEND_REQUEST_KEY);
accounts = AccountManager.get(this).getAccountsByType(Constants.ACCOUNT_TYPE);
if (accounts.length == 1) {
sendRequest.setAccount(accounts[0]);
getPermission(MapsConstants.SERVICE_NAME, sendRequest.isSendMaps(), mapsCallback);
return;
}
SharedPreferences prefs = getSharedPreferences(Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
String preferredAccount = prefs.getString(getString(R.string.preferred_account_key), "");
selectedAccountIndex = 0;
for (int i = 0; i < accounts.length; i++) {
if (accounts[i].name.equals(preferredAccount)) {
selectedAccountIndex = i;
break;
}
}
}
@Override
protected void onResume() {
super.onResume();
if (accounts.length == 0) {
showDialog(NO_ACCOUNT_DIALOG);
} else {
showDialog(CHOOSE_ACCOUNT_DIALOG);
}
}
@Override
protected Dialog onCreateDialog(int id) {
AlertDialog.Builder builder;
switch (id) {
case NO_ACCOUNT_DIALOG:
builder = new AlertDialog.Builder(this);
builder.setTitle(R.string.send_google_no_account_title);
builder.setMessage(R.string.send_google_no_account_message);
builder.setCancelable(true);
builder.setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
finish();
}
});
builder.setPositiveButton(R.string.generic_ok, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
finish();
}
});
return builder.create();
case CHOOSE_ACCOUNT_DIALOG:
builder = new AlertDialog.Builder(this);
builder.setTitle(R.string.send_google_choose_account_title);
String[] choices = new String[accounts.length];
for (int i = 0; i < accounts.length; i++) {
choices[i] = accounts[i].name;
}
builder.setSingleChoiceItems(choices, selectedAccountIndex, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
selectedAccountIndex = which;
}
});
builder.setCancelable(true);
builder.setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
finish();
}
});
builder.setNegativeButton(R.string.generic_cancel, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
finish();
}
});
builder.setPositiveButton(R.string.generic_ok, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
Account account = accounts[selectedAccountIndex];
SharedPreferences prefs = getSharedPreferences(
Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
Editor editor = prefs.edit();
editor.putString(getString(R.string.preferred_account_key), account.name);
ApiAdapterFactory.getApiAdapter().applyPreferenceChanges(editor);
sendRequest.setAccount(account);
getPermission(MapsConstants.SERVICE_NAME, sendRequest.isSendMaps(), mapsCallback);
}
});
return builder.create();
default:
return null;
}
}
private PermissionCallback spreadsheetsCallback = new PermissionCallback() {
@Override
public void onSuccess() {
startNextActivity();
}
@Override
public void onFailure() {
finish();
}
};
private PermissionCallback docsCallback = new PermissionCallback() {
@Override
public void onSuccess() {
getPermission(SpreadsheetsClient.SERVICE, sendRequest.isSendDocs(), spreadsheetsCallback);
}
@Override
public void onFailure() {
finish();
}
};
private PermissionCallback fusionTablesCallback = new PermissionCallback() {
@Override
public void onSuccess() {
getPermission(DocumentsClient.SERVICE, sendRequest.isSendDocs(), docsCallback);
}
@Override
public void onFailure() {
finish();
}
};
private PermissionCallback mapsCallback = new PermissionCallback() {
@Override
public void onSuccess() {
getPermission(
SendFusionTablesUtils.SERVICE, sendRequest.isSendFusionTables(), fusionTablesCallback);
}
@Override
public void onFailure() {
finish();
}
};
/**
* Gets the user permission to access a service.
*
* @param authTokenType the auth token type of the service
* @param needPermission true if need the permission
* @param callback callback after getting the permission
*/
private void getPermission(
String authTokenType, boolean needPermission, final PermissionCallback callback) {
if (needPermission) {
AccountManager.get(this).getAuthToken(sendRequest.getAccount(), authTokenType, null, this,
new AccountManagerCallback<Bundle>() {
@Override
public void run(AccountManagerFuture<Bundle> future) {
try {
if (future.getResult().getString(AccountManager.KEY_AUTHTOKEN) != null) {
callback.onSuccess();
} else {
Log.d(TAG, "auth token is null");
callback.onFailure();
}
} catch (OperationCanceledException e) {
Log.d(TAG, "Unable to get auth token", e);
callback.onFailure();
} catch (AuthenticatorException e) {
Log.d(TAG, "Unable to get auth token", e);
callback.onFailure();
} catch (IOException e) {
Log.d(TAG, "Unable to get auth token", e);
callback.onFailure();
}
}
}, null);
} else {
callback.onSuccess();
}
}
/**
* Starts the next activity. If
* <p>
* sendMaps and newMap -> {@link SendMapsActivity}
* <p>
* sendMaps and !newMap -> {@link ChooseMapActivity}
* <p>
* !sendMaps && sendFusionTables -> {@link SendFusionTablesActivity}
* <p>
* !sendMaps && !sendFusionTables && sendDocs -> {@link SendDocsActivity}
* <p>
* !sendMaps && !sendFusionTables && !sendDocs -> {@link UploadResultActivity}
*
*/
private void startNextActivity() {
Class<?> next;
if (sendRequest.isSendMaps()) {
next = sendRequest.isNewMap() ? SendMapsActivity.class : ChooseMapActivity.class;
} else if (sendRequest.isSendFusionTables()) {
next = SendFusionTablesActivity.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();
}
}
@@ -0,0 +1,294 @@
/*
* 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.sendtogoogle;
import android.accounts.Account;
import android.os.Parcel;
import android.os.Parcelable;
/**
* Send request states for sending a track to Google Maps, Google Fusion Tables,
* and Google Docs.
*
* @author Jimmy Shih
*/
public class SendRequest implements Parcelable {
public static final String SEND_REQUEST_KEY = "sendRequest";
private long trackId = -1L;
private boolean showMaps = false;
private boolean showFusionTables = false;
private boolean showDocs = false;
private boolean sendMaps = false;
private boolean sendFusionTables = false;
private boolean sendDocs = false;
private boolean newMap = false;
private Account account = null;
private String mapId = null;
private boolean mapsSuccess = false;
private boolean docsSuccess = false;
private boolean fusionTablesSuccess = false;
/**
* Creates a new send request.
*
* @param trackId the track id
* @param showMaps true to show the Google Maps option
* @param showFusionTables true to show the Google Fusion Tables option
* @param showDocs true to show the Google Docs option
*/
public SendRequest(long trackId, boolean showMaps, boolean showFusionTables, boolean showDocs) {
this.trackId = trackId;
this.showMaps = showMaps;
this.showFusionTables = showFusionTables;
this.showDocs = showDocs;
}
/**
* Get the track id.
*/
public long getTrackId() {
return trackId;
}
/**
* True if showing the send to Google Maps option.
*/
public boolean isShowMaps() {
return showMaps;
}
/**
* True if showing the send to Google Fusion Tables option.
*/
public boolean isShowFusionTables() {
return showFusionTables;
}
/**
* True if showing the send to Google Docs option.
*/
public boolean isShowDocs() {
return showDocs;
}
/**
* True if showing all the send options.
*/
public boolean isShowAll() {
return showMaps && showFusionTables && showDocs;
}
/**
* True if the user has selected the send to Google Maps option.
*/
public boolean isSendMaps() {
return sendMaps;
}
/**
* Sets the send to Google Maps option.
*
* @param sendMaps true if the user has selected the send to Google Maps
* option
*/
public void setSendMaps(boolean sendMaps) {
this.sendMaps = sendMaps;
}
/**
* True if the user has selected the send to Google Fusion Tables option.
*/
public boolean isSendFusionTables() {
return sendFusionTables;
}
/**
* Sets the send to Google Fusion Tables option.
*
* @param sendFusionTables true if the user has selected the send to Google
* Fusion Tables option
*/
public void setSendFusionTables(boolean sendFusionTables) {
this.sendFusionTables = sendFusionTables;
}
/**
* True if the user has selected the send to Google Docs option.
*/
public boolean isSendDocs() {
return sendDocs;
}
/**
* Sets the send to Google Docs option.
*
* @param sendDocs true if the user has selected the send to Google Docs
* option
*/
public void setSendDocs(boolean sendDocs) {
this.sendDocs = sendDocs;
}
/**
* True if the user has selected to create a new Google Maps.
*/
public boolean isNewMap() {
return newMap;
}
/**
* Sets the new map option.
*
* @param newMap true if the user has selected to create a new Google Maps.
*/
public void setNewMap(boolean newMap) {
this.newMap = newMap;
}
/**
* Gets the account.
*/
public Account getAccount() {
return account;
}
/**
* Sets the account.
*
* @param account the account
*/
public void setAccount(Account account) {
this.account = account;
}
/**
* Gets the selected map id if the user has selected to send a track to an
* existing Google Maps.
*/
public String getMapId() {
return mapId;
}
/**
* Sets the map id.
*
* @param mapId the map id
*/
public void setMapId(String mapId) {
this.mapId = mapId;
}
/**
* True if sending to Google Maps is success.
*/
public boolean isMapsSuccess() {
return mapsSuccess;
}
/**
* Sets the Google Maps result.
*
* @param mapsSuccess true if sending to Google Maps is success
*/
public void setMapsSuccess(boolean mapsSuccess) {
this.mapsSuccess = mapsSuccess;
}
/**
* True if sending to Google Fusion Tables is success.
*/
public boolean isFusionTablesSuccess() {
return fusionTablesSuccess;
}
/**
* Sets the Google Fusion Tables result.
*
* @param fusionTablesSuccess true if sending to Google Fusion Tables is
* success
*/
public void setFusionTablesSuccess(boolean fusionTablesSuccess) {
this.fusionTablesSuccess = fusionTablesSuccess;
}
/**
* True if sending to Google Docs is success.
*/
public boolean isDocsSuccess() {
return docsSuccess;
}
/**
* Sets the Google Docs result.
*
* @param docsSuccess true if sending to Google Docs is success
*/
public void setDocsSuccess(boolean docsSuccess) {
this.docsSuccess = docsSuccess;
}
private SendRequest(Parcel in) {
trackId = in.readLong();
showMaps = in.readByte() == 1;
showFusionTables = in.readByte() == 1;
showDocs = in.readByte() == 1;
sendMaps = in.readByte() == 1;
sendFusionTables = in.readByte() == 1;
sendDocs = in.readByte() == 1;
newMap = in.readByte() == 1;
account = in.readParcelable(null);
mapId = in.readString();
mapsSuccess = in.readByte() == 1;
fusionTablesSuccess = in.readByte() == 1;
docsSuccess = in.readByte() == 1;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel out, int flags) {
out.writeLong(trackId);
out.writeByte((byte) (showMaps ? 1 : 0));
out.writeByte((byte) (showFusionTables ? 1 : 0));
out.writeByte((byte) (showDocs ? 1 : 0));
out.writeByte((byte) (sendMaps ? 1 : 0));
out.writeByte((byte) (sendFusionTables ? 1 : 0));
out.writeByte((byte) (sendDocs ? 1 : 0));
out.writeByte((byte) (newMap ? 1 : 0));
out.writeParcelable(account, 0);
out.writeString(mapId);
out.writeByte((byte) (mapsSuccess ? 1 : 0));
out.writeByte((byte) (fusionTablesSuccess ? 1 : 0));
out.writeByte((byte) (docsSuccess ? 1 : 0));
}
public static final Parcelable.Creator<SendRequest> CREATOR = new Parcelable.Creator<
SendRequest>() {
public SendRequest createFromParcel(Parcel in) {
return new SendRequest(in);
}
public SendRequest[] newArray(int size) {
return new SendRequest[size];
}
};
}
@@ -0,0 +1,132 @@
/*
* 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.sendtogoogle;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.stats.TripStatistics;
import com.google.android.apps.mytracks.util.LocationUtils;
import android.location.Location;
import android.util.Log;
import java.util.ArrayList;
import java.util.List;
/**
* Commons utilities for sending a track to Google.
*
* @author Jimmy Shih
*/
public class SendToGoogleUtils {
private static final String TAG = SendToGoogleUtils.class.getSimpleName();
private SendToGoogleUtils() {}
/**
* Prepares a list of locations to send to Google Maps or Google Fusion
* Tables. Splits the locations into segments if necessary.
*
* @param track the track
* @param locations the list of locations
* @return an array of split segments.
*/
public static ArrayList<Track> prepareLocations(Track track, List<Location> locations) {
ArrayList<Track> splitTracks = new ArrayList<Track>();
// Create a new segment
Track segment = new Track();
segment.setId(track.getId());
segment.setName(track.getName());
segment.setDescription("");
segment.setCategory(track.getCategory());
TripStatistics segmentStats = segment.getStatistics();
TripStatistics trackStats = track.getStatistics();
segmentStats.setStartTime(trackStats.getStartTime());
segmentStats.setStopTime(trackStats.getStopTime());
boolean startNewTrackSegment = false;
for (Location loc : locations) {
// Latitude is greater than 90 if the location is invalid. Do not add to
// the segment.
if (loc.getLatitude() > 90) {
startNewTrackSegment = true;
}
if (startNewTrackSegment) {
// Close the last segment
prepareTrackSegment(segment, splitTracks);
startNewTrackSegment = false;
segment = new Track();
segment.setId(track.getId());
segment.setName(track.getName());
segment.setDescription("");
segment.setCategory(track.getCategory());
segmentStats = segment.getStatistics();
}
if (loc.getLatitude() <= 90) {
segment.addLocation(loc);
// For a new segment, sets its start time using the first available
// location time.
if (segmentStats.getStartTime() < 0) {
segmentStats.setStartTime(loc.getTime());
}
}
}
prepareTrackSegment(segment, splitTracks);
return splitTracks;
}
/**
* Prepares a track segment for sending to Google Maps or Google Fusion
* Tables. The main steps are:
* <ul>
* <li>make sure the segment has at least 2 points</li>
* <li>set the segment stop time if necessary</li>
* <li>decimate locations precision</li>
* </ul>
* The prepared track will be added to the splitTracks.
*
* @param segment the track segment
* @param splitTracks an array of track segments
*/
private static void prepareTrackSegment(Track segment, ArrayList<Track> splitTracks) {
// Make sure the segment has at least 2 points
if (segment.getLocations().size() < 2) {
Log.d(TAG, "segment has less than 2 points");
return;
}
// For a new segment, sets it stop time
TripStatistics segmentStats = segment.getStatistics();
if (segmentStats.getStopTime() < 0) {
Location lastLocation = segment.getLocations().get(segment.getLocations().size() - 1);
segmentStats.setStopTime(lastLocation.getTime());
}
// Decimate to 2 meter precision. Google Maps and Google Fusion Tables do
// not like the locations to be too precise.
LocationUtils.decimate(segment, 2.0);
splitTracks.add(segment);
}
}
@@ -0,0 +1,194 @@
/*
* 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.sendtogoogle;
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.io.fusiontables.SendFusionTablesUtils;
import com.google.android.apps.mytracks.io.maps.SendMapsUtils;
import com.google.android.maps.mytracks.R;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.view.View;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
/**
* A dialog to show the result of uploading to Google services.
*
* @author Jimmy Shih
*/
public class UploadResultActivity extends Activity {
private static final String TEXT_PLAIN_TYPE = "text/plain";
private static final int RESULT_DIALOG = 1;
private SendRequest sendRequest;
private Track track;
private String shareUrl;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
sendRequest = getIntent().getParcelableExtra(SendRequest.SEND_REQUEST_KEY);
track = null;
shareUrl = null;
if (sendRequest.isSendMaps() && sendRequest.isMapsSuccess()) {
shareUrl = SendMapsUtils.getMapUrl(getTrack());
}
if (shareUrl == null && sendRequest.isSendFusionTables()
&& sendRequest.isFusionTablesSuccess()) {
shareUrl = SendFusionTablesUtils.getMapUrl(getTrack());
}
}
private Track getTrack() {
if (track == null) {
track = MyTracksProviderUtils.Factory.get(this).getTrack(sendRequest.getTrackId());
}
return track;
}
@Override
protected void onResume() {
super.onResume();
showDialog(RESULT_DIALOG);
}
@Override
protected Dialog onCreateDialog(int id) {
switch (id) {
case RESULT_DIALOG:
AlertDialog.Builder builder = new AlertDialog.Builder(this);
View view = getLayoutInflater().inflate(R.layout.upload_result, null);
builder.setView(view);
LinearLayout mapsResult = (LinearLayout) view.findViewById(R.id.upload_result_maps_result);
LinearLayout fusionTablesResult = (LinearLayout) view.findViewById(
R.id.upload_result_fusion_tables_result);
LinearLayout docsResult = (LinearLayout) view.findViewById(R.id.upload_result_docs_result);
ImageView mapsResultIcon = (ImageView) view.findViewById(
R.id.upload_result_maps_result_icon);
ImageView fusionTablesResultIcon = (ImageView) view.findViewById(
R.id.upload_result_fusion_tables_result_icon);
ImageView docsResultIcon = (ImageView) view.findViewById(
R.id.upload_result_docs_result_icon);
TextView successFooter = (TextView) view.findViewById(R.id.upload_result_success_footer);
TextView errorFooter = (TextView) view.findViewById(R.id.upload_result_error_footer);
boolean hasError = false;
if (!sendRequest.isSendMaps()) {
mapsResult.setVisibility(View.GONE);
} else {
if (!sendRequest.isMapsSuccess()) {
mapsResultIcon.setImageResource(R.drawable.failure);
hasError = true;
}
}
if (!sendRequest.isSendFusionTables()) {
fusionTablesResult.setVisibility(View.GONE);
} else {
if (!sendRequest.isFusionTablesSuccess()) {
fusionTablesResultIcon.setImageResource(R.drawable.failure);
hasError = true;
}
}
if (!sendRequest.isSendDocs()) {
docsResult.setVisibility(View.GONE);
} else {
if (!sendRequest.isDocsSuccess()) {
docsResultIcon.setImageResource(R.drawable.failure);
hasError = true;
}
}
if (hasError) {
builder.setTitle(R.string.generic_error_title);
builder.setIcon(android.R.drawable.ic_dialog_alert);
successFooter.setVisibility(View.GONE);
} else {
builder.setTitle(R.string.generic_success_title);
builder.setIcon(android.R.drawable.ic_dialog_info);
errorFooter.setVisibility(View.GONE);
}
builder.setCancelable(true);
builder.setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
finish();
}
});
builder.setPositiveButton(
getString(R.string.generic_ok), new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
if (!sendRequest.isShowAll() && shareUrl != null) {
startShareUrlActivity(shareUrl);
}
finish();
}
});
// Add a Share URL button if showing all the options and a shareUrl
// exists
if (sendRequest.isShowAll() && shareUrl != null) {
builder.setNegativeButton(getString(R.string.send_google_result_share_url),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
startShareUrlActivity(shareUrl);
finish();
}
});
}
return builder.create();
default:
return null;
}
}
/**
* Starts an activity to share the url.
*
* @param url the url
*/
private void startShareUrlActivity(String url) {
SharedPreferences prefs = getSharedPreferences(Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
boolean shareUrlOnly = prefs.getBoolean(getString(R.string.share_url_only_key), false);
Intent intent = new Intent(Intent.ACTION_SEND);
intent.setType(TEXT_PLAIN_TYPE);
intent.putExtra(Intent.EXTRA_SUBJECT, getString(R.string.share_track_subject));
intent.putExtra(Intent.EXTRA_TEXT,
shareUrlOnly ? url : getString(R.string.share_track_url_body_format, url));
startActivity(Intent.createChooser(intent, getString(R.string.share_track_picker_title)));
}
}
@@ -0,0 +1,259 @@
/*
* 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.sendtogoogle;
import com.google.android.apps.analytics.GoogleAnalyticsTracker;
import com.google.android.apps.mytracks.Constants;
import com.google.android.apps.mytracks.util.ApiAdapterFactory;
import com.google.android.apps.mytracks.util.SystemUtils;
import com.google.android.maps.mytracks.R;
import android.app.Activity;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.os.Bundle;
import android.view.View;
import android.view.Window;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.CompoundButton.OnCheckedChangeListener;
import android.widget.RadioButton;
import android.widget.TableRow;
/**
* A chooser to select the Google services to upload a track to.
*
* @author Jimmy Shih
*/
public class UploadServiceChooserActivity extends Activity {
private static final int SERVICE_PICKER_DIALOG = 1;
private SendRequest sendRequest;
private TableRow mapsTableRow;
private TableRow fusionTablesTableRow;
private TableRow docsTableRow;
private CheckBox mapsCheckBox;
private CheckBox fusionTablesCheckBox;
private CheckBox docsCheckBox;
private TableRow mapsOptionTableRow;
private RadioButton newMapRadioButton;
private RadioButton existingMapRadioButton;
private Button cancel;
private Button send;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
sendRequest = getIntent().getParcelableExtra(SendRequest.SEND_REQUEST_KEY);
}
@Override
protected void onResume() {
super.onResume();
showDialog(SERVICE_PICKER_DIALOG);
}
@Override
protected Dialog onCreateDialog(int id) {
switch (id) {
case SERVICE_PICKER_DIALOG:
final Dialog dialog = new Dialog(this);
dialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
dialog.setContentView(R.layout.upload_service_chooser);
dialog.setCancelable(true);
dialog.setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface d) {
finish();
}
});
mapsTableRow = (TableRow) dialog.findViewById(R.id.send_google_maps_row);
fusionTablesTableRow = (TableRow) dialog.findViewById(R.id.send_google_fusion_tables_row);
docsTableRow = (TableRow) dialog.findViewById(R.id.send_google_docs_row);
mapsCheckBox = (CheckBox) dialog.findViewById(R.id.send_google_maps);
fusionTablesCheckBox = (CheckBox) dialog.findViewById(R.id.send_google_fusion_tables);
docsCheckBox = (CheckBox) dialog.findViewById(R.id.send_google_docs);
mapsOptionTableRow = (TableRow) dialog.findViewById(R.id.send_google_maps_option_row);
newMapRadioButton = (RadioButton) dialog.findViewById(R.id.send_google_new_map);
existingMapRadioButton = (RadioButton) dialog.findViewById(R.id.send_google_existing_map);
// Setup checkboxes
OnCheckedChangeListener checkBoxListener = new OnCheckedChangeListener() {
public void onCheckedChanged(CompoundButton button, boolean checked) {
updateStateBySelection();
}
};
mapsCheckBox.setOnCheckedChangeListener(checkBoxListener);
fusionTablesCheckBox.setOnCheckedChangeListener(checkBoxListener);
docsCheckBox.setOnCheckedChangeListener(checkBoxListener);
// Setup buttons
cancel = (Button) dialog.findViewById(R.id.send_google_cancel);
cancel.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
finish();
}
});
send = (Button) dialog.findViewById(R.id.send_google_send_now);
send.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
saveState();
startNextActivity();
}
});
// Setup initial state
initState();
// Update state based on sendRequest
updateStateBySendRequest();
// Update state based on current user selection
updateStateBySelection();
return dialog;
default:
return null;
}
}
/**
* Initializes the UI state based on the shared preferences.
*/
private void initState() {
SharedPreferences prefs = getSharedPreferences(Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
boolean pickExistingMap = prefs.getBoolean(getString(R.string.pick_existing_map_key), false);
newMapRadioButton.setChecked(!pickExistingMap);
existingMapRadioButton.setChecked(pickExistingMap);
mapsCheckBox.setChecked(prefs.getBoolean(getString(R.string.send_to_maps_key), true));
fusionTablesCheckBox.setChecked(
prefs.getBoolean(getString(R.string.send_to_fusion_tables_key), true));
docsCheckBox.setChecked(prefs.getBoolean(getString(R.string.send_to_docs_key), true));
}
/**
* Updates the UI state based on sendRequest.
*/
private void updateStateBySendRequest() {
if (!sendRequest.isShowAll()) {
if (sendRequest.isShowMaps()) {
mapsCheckBox.setChecked(true);
} else if (sendRequest.isShowFusionTables()) {
fusionTablesCheckBox.setChecked(true);
} else if (sendRequest.isShowDocs()) {
docsCheckBox.setChecked(true);
}
}
mapsTableRow.setVisibility(sendRequest.isShowMaps() ? View.VISIBLE : View.GONE);
fusionTablesTableRow.setVisibility(sendRequest.isShowFusionTables() ? View.VISIBLE : View.GONE);
docsTableRow.setVisibility(sendRequest.isShowDocs() ? View.VISIBLE : View.GONE);
}
/**
* Updates the UI state based on the current selection.
*/
private void updateStateBySelection() {
mapsOptionTableRow.setVisibility(sendMaps() ? View.VISIBLE : View.GONE);
send.setEnabled(sendMaps() || sendFusionTables() || sendDocs());
}
/**
* Saves the UI state to the shared preferences.
*/
private void saveState() {
SharedPreferences prefs = getSharedPreferences(Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
Editor editor = prefs.edit();
editor.putBoolean(
getString(R.string.pick_existing_map_key), existingMapRadioButton.isChecked());
if (sendRequest.isShowAll()) {
editor.putBoolean(getString(R.string.send_to_maps_key), sendMaps());
editor.putBoolean(getString(R.string.send_to_fusion_tables_key), sendFusionTables());
editor.putBoolean(getString(R.string.send_to_docs_key), sendDocs());
}
ApiAdapterFactory.getApiAdapter().applyPreferenceChanges(editor);
}
/**
* Returns true to send to Google Maps.
*/
private boolean sendMaps() {
return sendRequest.isShowMaps() && mapsCheckBox.isChecked();
}
/**
* Returns true to send to Google Fusion Tables.
*/
private boolean sendFusionTables() {
return sendRequest.isShowFusionTables() && fusionTablesCheckBox.isChecked();
}
/**
* Returns true to send to Google Docs.
*/
private boolean sendDocs() {
return sendRequest.isShowDocs() && docsCheckBox.isChecked();
}
/**
* Starts the next activity, {@link AccountChooserActivity}.
*/
private void startNextActivity() {
sendStats();
sendRequest.setSendMaps(sendMaps());
sendRequest.setSendFusionTables(sendFusionTables());
sendRequest.setSendDocs(sendDocs());
sendRequest.setNewMap(!existingMapRadioButton.isChecked());
Intent intent = new Intent(this, AccountChooserActivity.class)
.putExtra(SendRequest.SEND_REQUEST_KEY, sendRequest);
startActivity(intent);
finish();
}
/**
* Sends stats to Google Analytics.
*/
private void sendStats() {
GoogleAnalyticsTracker tracker = GoogleAnalyticsTracker.getInstance();
tracker.start(getString(R.string.my_tracks_analytics_id), getApplicationContext());
tracker.setProductVersion("android-mytracks", SystemUtils.getMyTracksVersion(this));
if (sendRequest.isSendMaps()) {
tracker.trackPageView("/send/maps");
}
if (sendRequest.isSendFusionTables()) {
tracker.trackPageView("/send/fusion_tables");
}
if (sendRequest.isSendDocs()) {
tracker.trackPageView("/send/docs");
}
tracker.dispatch();
tracker.stop();
}
}