Handles interrupt and error cleanup during import

This commit is contained in:
Jimmy Shih
2013-09-05 13:02:03 -07:00
commit 429ebdbf3a
1119 changed files with 75233 additions and 0 deletions
@@ -0,0 +1,91 @@
/*
* 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.backup;
import com.google.android.apps.mytracks.util.DialogUtils;
import com.google.android.maps.mytracks.R;
import android.app.Activity;
import android.app.Dialog;
import android.content.DialogInterface;
import android.os.Bundle;
import android.widget.Toast;
/**
* Activity to backup data to the SD card.
*
* @author Jimmy Shih
*/
public class BackupActivity extends Activity {
private static final int DIALOG_PROGRESS_ID = 0;
private BackupAsyncTask backupAsyncTask;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Object retained = getLastNonConfigurationInstance();
if (retained instanceof BackupAsyncTask) {
backupAsyncTask = (BackupAsyncTask) retained;
backupAsyncTask.setActivity(this);
} else {
backupAsyncTask = new BackupAsyncTask(this);
backupAsyncTask.execute();
}
}
@Override
public Object onRetainNonConfigurationInstance() {
backupAsyncTask.setActivity(null);
return backupAsyncTask;
}
@Override
protected Dialog onCreateDialog(int id) {
if (id != DIALOG_PROGRESS_ID) {
return null;
}
return DialogUtils.createSpinnerProgressDialog(
this, R.string.settings_backup_now_progress_message, new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
finish();
}
});
}
/**
* Invokes when the associated AsyncTask completes.
*
* @param success true if the AsyncTask is successful
* @param messageId message id to display to user
*/
public void onAsyncTaskCompleted(boolean success, int messageId) {
removeDialog(DIALOG_PROGRESS_ID);
Toast.makeText(this, messageId, success ? Toast.LENGTH_SHORT : Toast.LENGTH_LONG).show();
finish();
}
/**
* Shows the progress dialog.
*/
public void showProgressDialog() {
showDialog(DIALOG_PROGRESS_ID);
}
}
@@ -0,0 +1,111 @@
/*
* 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.backup;
import com.google.android.apps.mytracks.util.FileUtils;
import com.google.android.maps.mytracks.R;
import android.os.AsyncTask;
import android.util.Log;
import java.io.IOException;
/**
* AsyncTask to backup data to the SD card.
*
* @author Jimmy Shih
*/
public class BackupAsyncTask extends AsyncTask<Void, Integer, Boolean> {
private static final String TAG = BackupAsyncTask.class.getSimpleName();
private BackupActivity backupActivity;
private final ExternalFileBackup externalFileBackup;
// true if the AsyncTask result is success
private boolean success;
// true if the AsyncTask has completed
private boolean completed;
// message id to return to the activity
private int messageId;
/**
* Creates an AsyncTask.
*
* @param backupActivity the activity currently associated with this
* AsyncTask
*/
public BackupAsyncTask(BackupActivity backupActivity) {
this.backupActivity = backupActivity;
this.externalFileBackup = new ExternalFileBackup(backupActivity);
success = false;
completed = false;
messageId = R.string.settings_backup_now_error;
}
/**
* Sets the current {@link BackupActivity} associated with this AyncTask.
*
* @param activity the current {@link BackupActivity}, can be null
*/
public void setActivity(BackupActivity activity) {
this.backupActivity = activity;
if (completed && backupActivity != null) {
backupActivity.onAsyncTaskCompleted(success, messageId);
}
}
@Override
protected void onPreExecute() {
if (backupActivity != null) {
backupActivity.showProgressDialog();
}
}
@Override
protected Boolean doInBackground(Void... params) {
if (!FileUtils.isExternalStorageWriteable()) {
messageId = R.string.external_storage_not_writable;
return false;
}
if (!externalFileBackup.isBackupsDirectoryAvailable(true)) {
messageId = R.string.external_storage_not_writable;
return false;
}
try {
externalFileBackup.writeToDefaultFile();
messageId = R.string.settings_backup_now_success;
return true;
} catch (IOException e) {
Log.d(TAG, "IO exception", e);
return false;
}
}
@Override
protected void onPostExecute(Boolean result) {
success = result;
completed = true;
if (backupActivity != null) {
backupActivity.onAsyncTaskCompleted(success, messageId);
}
}
}
@@ -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;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
/**
* Backup preference listener.
*
* @author Jimmy Shih
*/
public class BackupPreferencesListener implements OnSharedPreferenceChangeListener {
private final BackupManager backupManager;
public BackupPreferencesListener(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,198 @@
/*
* 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;
import java.util.Locale;
/**
* 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(Locale.US,
"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,278 @@
/*
* 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.Locale;
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'", Locale.US);
static {
BACKUP_FILENAME_FORMAT.setTimeZone(TimeZone.getTimeZone("UTC"));
}
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;
public ExternalFileBackup(Context context) {
this.context = context;
}
/**
* 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 directoryPath = FileUtils.getDirectoryPath(FileUtils.BACKUPS_DIR);
final File dir = new File(directoryPath);
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 FileUtils.isDirectory(dir) ? 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(context);
DatabaseDumper trackDumper = new DatabaseDumper(
TracksColumns.COLUMNS,
TracksColumns.COLUMN_TYPES,
false);
DatabaseDumper waypointDumper = new DatabaseDumper(
WaypointsColumns.COLUMNS,
WaypointsColumns.COLUMN_TYPES,
false);
DatabaseDumper pointDumper = new DatabaseDumper(
TrackPointsColumns.COLUMNS,
TrackPointsColumns.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 = null;
try {
tracksCursor = contentResolver.query(TracksColumns.CONTENT_URI, null, null, null, null);
if (tracksCursor != null) {
trackDumper.writeAllRows(tracksCursor, outWriter);
}
} finally {
if (tracksCursor != null) {
tracksCursor.close();
}
}
Cursor waypointsCursor = null;
try {
waypointsCursor = contentResolver.query(
WaypointsColumns.CONTENT_URI, null, null, null, null);
if (waypointsCursor != null) {
waypointDumper.writeAllRows(waypointsCursor, outWriter);
}
} finally {
if (waypointsCursor != null) {
waypointsCursor.close();
}
}
Cursor pointsCursor = null;
try {
pointsCursor = contentResolver.query(
TrackPointsColumns.CONTENT_URI, null, null, null, null);
if (pointsCursor != null) {
pointDumper.writeAllRows(pointsCursor, outWriter);
}
} finally {
if (pointsCursor != null) {
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(context);
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(this);
}
@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,216 @@
/*
* 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 com.google.android.maps.mytracks.R;
import android.content.Context;
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.HashSet;
import java.util.Map;
/**
* Helper for backing up and restoring shared preferences.
*
* @author Rodrigo Damazio
*/
public class PreferenceBackupHelper {
private static final int BUFFER_SIZE = 2048;
private final HashSet<String> doNotBackup;
public PreferenceBackupHelper(Context context) {
doNotBackup = new HashSet<String>();
doNotBackup.add(context.getString(R.string.activity_recognition_type_key));
doNotBackup.add(context.getString(R.string.ant_bike_cadence_sensor_id_key));
doNotBackup.add(context.getString(R.string.ant_combined_bike_sensor_id_key));
doNotBackup.add(context.getString(R.string.ant_heart_rate_monitor_id_key));
doNotBackup.add(context.getString(R.string.ant_speed_distance_monitor_id_key));
doNotBackup.add(context.getString(R.string.auto_resume_track_current_retry_key));
doNotBackup.add(context.getString(R.string.bluetooth_sensor_key));
doNotBackup.add(context.getString(R.string.confirm_play_earth_key));
doNotBackup.add(context.getString(R.string.drive_deleted_list_key));
doNotBackup.add(context.getString(R.string.drive_largest_change_id_key));
doNotBackup.add(context.getString(R.string.drive_sync_key));
doNotBackup.add(context.getString(R.string.google_account_key));
doNotBackup.add(context.getString(R.string.recording_track_id_key));
doNotBackup.add(context.getString(R.string.recording_track_paused_key));
}
/**
* 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();
for (String name : doNotBackup) {
values.remove(name);
}
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 {
boolean save = true;
if (doNotBackup.contains(name)) {
save = false;
}
switch (typeId) {
case ContentTypeIds.BOOLEAN_TYPE_ID:
boolean booleanValue = reader.readBoolean();
if (save) {
editor.putBoolean(name, booleanValue);
}
return;
case ContentTypeIds.LONG_TYPE_ID:
long longValue = reader.readLong();
if (save) {
editor.putLong(name, longValue);
}
return;
case ContentTypeIds.FLOAT_TYPE_ID:
float floatValue = reader.readFloat();
if (save) {
editor.putFloat(name, floatValue);
}
return;
case ContentTypeIds.INT_TYPE_ID:
int intValue = reader.readInt();
if (save) {
editor.putInt(name, intValue);
}
return;
case ContentTypeIds.STRING_TYPE_ID:
String utfValue = reader.readUTF();
if (save) {
editor.putString(name, utfValue);
}
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,107 @@
/*
* 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.backup;
import com.google.android.apps.mytracks.TrackListActivity;
import com.google.android.apps.mytracks.util.DialogUtils;
import com.google.android.apps.mytracks.util.IntentUtils;
import com.google.android.maps.mytracks.R;
import android.app.Activity;
import android.app.Dialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.widget.Toast;
import java.util.Date;
/**
* An activity to restore data from the SD card.
*
* @author Jimmy Shih
*/
public class RestoreActivity extends Activity {
public static final String EXTRA_DATE = "date";
private static final String TAG = RestoreActivity.class.getSimpleName();
private static final int DIALOG_PROGRESS_ID = 0;
private RestoreAsyncTask restoreAsyncTask;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Object retained = getLastNonConfigurationInstance();
if (retained instanceof RestoreAsyncTask) {
restoreAsyncTask = (RestoreAsyncTask) retained;
restoreAsyncTask.setActivity(this);
} else {
long date = getIntent().getLongExtra(EXTRA_DATE, -1L);
if (date == -1L) {
Log.d(TAG, "Invalid date");
finish();
return;
}
restoreAsyncTask = new RestoreAsyncTask(this, new Date(date));
restoreAsyncTask.execute();
}
}
@Override
public Object onRetainNonConfigurationInstance() {
restoreAsyncTask.setActivity(null);
return restoreAsyncTask;
}
@Override
protected Dialog onCreateDialog(int id) {
if (id != DIALOG_PROGRESS_ID) {
return null;
}
return DialogUtils.createSpinnerProgressDialog(this,
R.string.settings_backup_restore_progress_message, new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
finish();
}
});
}
/**
* Invokes when the associated AsyncTask completes.
*
* @param success true if the AsyncTask is successful
* @param messageId message id to display to user
*/
public void onAsyncTaskCompleted(boolean success, int messageId) {
removeDialog(DIALOG_PROGRESS_ID);
Toast.makeText(this, messageId, success ? Toast.LENGTH_SHORT : Toast.LENGTH_LONG).show();
Intent intent = IntentUtils.newIntent(this, TrackListActivity.class);
startActivity(intent);
}
/**
* Shows the progress dialog.
*/
public void showProgressDialog() {
showDialog(DIALOG_PROGRESS_ID);
}
}
@@ -0,0 +1,110 @@
/*
* 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.backup;
import com.google.android.apps.mytracks.io.sync.SyncUtils;
import com.google.android.apps.mytracks.util.PreferencesUtils;
import com.google.android.maps.mytracks.R;
import android.os.AsyncTask;
import android.util.Log;
import java.io.IOException;
import java.util.Date;
/**
* AsyncTask to restore data from the SD card.
*
* @author Jimmy Shih
*/
public class RestoreAsyncTask extends AsyncTask<Void, Integer, Boolean> {
private static final String TAG = RestoreAsyncTask.class.getSimpleName();
private RestoreActivity restoreActivity;
private final Date date;
private final ExternalFileBackup externalFileBackup;
// true if the AsyncTask result is success
private boolean success;
// true if the AsyncTask has completed
private boolean completed;
// message id to return to the activity
private int messageId;
/**
* Creates an AsyncTask.
*
* @param restoreActivity the activity currently associated with this
* AsyncTask
* @param date the date to retore from
*/
public RestoreAsyncTask(RestoreActivity restoreActivity, Date date) {
this.restoreActivity = restoreActivity;
this.date = date;
this.externalFileBackup = new ExternalFileBackup(restoreActivity);
success = false;
completed = false;
messageId = R.string.settings_backup_restore_error;
}
/**
* Sets the current {@link RestoreActivity} associated with this AyncTask.
*
* @param activity the current {@link RestoreActivity}, can be null
*/
public void setActivity(RestoreActivity activity) {
this.restoreActivity = activity;
if (completed && restoreActivity != null) {
restoreActivity.onAsyncTaskCompleted(success, messageId);
}
}
@Override
protected void onPreExecute() {
if (restoreActivity != null) {
restoreActivity.showProgressDialog();
}
}
@Override
protected Boolean doInBackground(Void... params) {
try {
PreferencesUtils.setBoolean(
restoreActivity, R.string.drive_sync_key, PreferencesUtils.DRIVE_SYNC_DEFAULT);
SyncUtils.disableSync(restoreActivity);
externalFileBackup.restoreFromDate(date);
SyncUtils.clearSyncState(restoreActivity);
messageId = R.string.settings_backup_restore_success;
return true;
} catch (IOException e) {
Log.d(TAG, "IO exception", e);
return false;
}
}
@Override
protected void onPostExecute(Boolean result) {
success = result;
completed = true;
if (restoreActivity != null) {
restoreActivity.onAsyncTaskCompleted(success, messageId);
}
}
}
@@ -0,0 +1,130 @@
/*
* 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.backup;
import com.google.android.apps.mytracks.util.FileUtils;
import com.google.android.apps.mytracks.util.IntentUtils;
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.Dialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
import android.widget.Toast;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Date;
/**
* An activity to choose a date to restore from.
*
* @author Jimmy Shih
*/
public class RestoreChooserActivity extends Activity {
private static final Comparator<Date> REVERSE_DATE_ORDER = new Comparator<Date>() {
@Override
public int compare(Date s1, Date s2) {
return s2.compareTo(s1);
}
};
private static final int DIALOG_CHOOSER_ID = 0;
private Date[] backupDates;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ExternalFileBackup externalFileBackup = new ExternalFileBackup(this);
// Get the list of existing backups
if (!FileUtils.isExternalStorageAvailable()) {
Toast.makeText(this, R.string.external_storage_not_available, Toast.LENGTH_LONG).show();
finish();
return;
}
if (!externalFileBackup.isBackupsDirectoryAvailable(false)) {
showNoBackupMessage();
finish();
return;
}
backupDates = externalFileBackup.getAvailableBackups();
if (backupDates == null || backupDates.length == 0) {
showNoBackupMessage();
finish();
return;
}
if (backupDates.length == 1) {
Intent intent = IntentUtils.newIntent(this, RestoreActivity.class)
.putExtra(RestoreActivity.EXTRA_DATE, backupDates[0].getTime());
startActivity(intent);
finish();
return;
}
Arrays.sort(backupDates, REVERSE_DATE_ORDER);
showDialog(DIALOG_CHOOSER_ID);
}
@Override
protected Dialog onCreateDialog(int id) {
if (id != DIALOG_CHOOSER_ID) {
return null;
}
String items[] = new String[backupDates.length];
for (int i = 0; i < backupDates.length; i++) {
items[i] = StringUtils.formatDateTime(this, backupDates[i].getTime());
}
return new AlertDialog.Builder(this)
.setCancelable(true)
.setItems(items, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
Intent intent = IntentUtils.newIntent(
RestoreChooserActivity.this, RestoreActivity.class)
.putExtra(RestoreActivity.EXTRA_DATE, backupDates[which].getTime());
startActivity(intent);
finish();
}
})
.setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
finish();
}
})
.setTitle(R.string.settings_backup_restore_select_title)
.create();
}
/**
* Shows a no backup message.
*/
private void showNoBackupMessage() {
String message = getString(R.string.settings_backup_restore_no_backups,
FileUtils.getDirectoryDisplayName(FileUtils.BACKUPS_DIR));
Toast.makeText(this, message, Toast.LENGTH_LONG).show();
}
}
@@ -0,0 +1,74 @@
/*
* Copyright 2013 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.drive;
import com.google.android.apps.mytracks.io.fusiontables.SendFusionTablesActivity;
import com.google.android.apps.mytracks.io.maps.SendMapsActivity;
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.apps.mytracks.io.spreadsheets.SendSpreadsheetsActivity;
import com.google.android.apps.mytracks.util.IntentUtils;
import com.google.android.maps.mytracks.R;
import android.content.Intent;
/**
* An activity to send a track to Google Drive.
*
* @author Jimmy Shih
*/
public class SendDriveActivity extends AbstractSendActivity {
@Override
protected AbstractSendAsyncTask createAsyncTask() {
return new SendDriveAsyncTask(this, sendRequest.getTrackId(), sendRequest.getAccount(),
sendRequest.getDriveShareEmails(), sendRequest.isDriveSharePublic());
}
@Override
protected String getServiceName() {
return getString(R.string.export_google_drive);
}
@Override
protected void startNextActivity(boolean success, boolean isCancel) {
sendRequest.setDriveSuccess(success);
Class<?> next = getNextClass(isCancel);
Intent intent = IntentUtils.newIntent(this, next)
.putExtra(SendRequest.SEND_REQUEST_KEY, sendRequest);
startActivity(intent);
finish();
}
private Class<?> getNextClass(boolean isCancel) {
if (isCancel) {
return UploadResultActivity.class;
} else {
if (sendRequest.isSendMaps()) {
return SendMapsActivity.class;
} else if (sendRequest.isSendFusionTables()) {
return SendFusionTablesActivity.class;
} else if (sendRequest.isSendSpreadsheets()) {
return SendSpreadsheetsActivity.class;
} else {
return UploadResultActivity.class;
}
}
}
}
@@ -0,0 +1,168 @@
/*
* Copyright 2013 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.drive;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.io.sendtogoogle.AbstractSendAsyncTask;
import com.google.android.apps.mytracks.io.sendtogoogle.SendToGoogleUtils;
import com.google.android.apps.mytracks.io.sync.SyncUtils;
import com.google.android.gms.auth.GoogleAuthException;
import com.google.android.gms.auth.UserRecoverableAuthException;
import com.google.api.client.googleapis.extensions.android.gms.auth.GoogleAccountCredential;
import com.google.api.client.googleapis.extensions.android.gms.auth.UserRecoverableAuthIOException;
import com.google.api.services.drive.Drive;
import com.google.api.services.drive.model.File;
import com.google.api.services.drive.model.Permission;
import com.google.api.services.drive.model.PermissionList;
import android.accounts.Account;
import android.content.Context;
import java.io.IOException;
/**
* AsyncTask to send a track to Google Drive.
*
* @author Jimmy Shih
*/
public class SendDriveAsyncTask extends AbstractSendAsyncTask {
private final long trackId;
private final Account account;
private final String[] acl;
private final boolean isPublic;
private final Context context;
private final MyTracksProviderUtils myTracksProviderUtils;
public SendDriveAsyncTask(
SendDriveActivity activity, long trackId, Account account, String acl, boolean isPublic) {
super(activity);
this.trackId = trackId;
this.account = account;
if (acl != null) {
this.acl = acl.split(",");
} else {
this.acl = null;
}
this.isPublic = isPublic;
context = activity.getApplicationContext();
myTracksProviderUtils = MyTracksProviderUtils.Factory.get(context);
}
@Override
protected void closeConnection() {}
@Override
protected boolean performTask() {
try {
GoogleAccountCredential credential = SendToGoogleUtils.getGoogleAccountCredential(
context, account.name, SendToGoogleUtils.DRIVE_SCOPE);
if (credential == null) {
return false;
}
Drive drive = SyncUtils.getDriveService(credential);
File folder = SyncUtils.getMyTracksFolder(context, drive);
if (folder == null) {
return false;
}
String folderId = folder.getId();
if (folderId == null) {
return false;
}
Track track = myTracksProviderUtils.getTrack(trackId);
String driveId = track.getDriveId();
if (driveId != null && !driveId.equals("")) {
File driveFile = drive.files().get(driveId).execute();
if (SyncUtils.isValid(driveFile, folderId) && SyncUtils.updateDriveFile(
drive, driveFile, context, myTracksProviderUtils, track, false)) {
addPermission(drive, driveFile);
return true;
}
SyncUtils.updateTrackWithDriveFileInfo(myTracksProviderUtils, track, null);
}
File file = SyncUtils.insertDriveFile(
drive, folderId, context, myTracksProviderUtils, track, false);
if (file == null) {
return false;
}
addPermission(drive, file);
return true;
} catch (UserRecoverableAuthException e) {
SendToGoogleUtils.sendNotification(
context, account.name, e.getIntent(), SendToGoogleUtils.DRIVE_NOTIFICATION_ID);
return false;
} catch (GoogleAuthException e) {
return retryTask();
} catch (UserRecoverableAuthIOException e) {
SendToGoogleUtils.sendNotification(
context, account.name, e.getIntent(), SendToGoogleUtils.DRIVE_NOTIFICATION_ID);
return false;
} catch (IOException e) {
return retryTask();
}
}
@Override
protected void invalidateToken() {}
/**
* Adds permission.
*
* @param drive the drive
* @param file the drive file
*/
private void addPermission(Drive drive, File file) throws IOException {
if (isPublic) {
boolean hasPublic = false;
PermissionList permissionList = drive.permissions().list(file.getId()).execute();
for (Permission permission : permissionList.getItems()) {
String role = permission.getRole();
if (role.equals("reader") || role.equals("writer")) {
if (permission.getType().equals("anyone")) {
hasPublic = true;
break;
}
}
}
if (!hasPublic) {
Permission permission = new Permission();
permission.setRole("reader");
permission.setType("anyone");
permission.setValue("");
drive.permissions().insert(file.getId(), permission).execute();
}
shareUrl = file.getAlternateLink();
}
if (acl != null) {
for (String email : acl) {
email = email.trim();
if (!email.equals("")) {
Permission permission = new Permission();
permission.setRole("reader");
permission.setType("user");
permission.setValue(email);
drive.permissions().insert(file.getId(), permission).execute();
}
}
}
}
}
@@ -0,0 +1,603 @@
/*
* Copyright 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.io.file;
import com.google.android.apps.mytracks.Constants;
import com.google.android.apps.mytracks.content.DescriptionGeneratorImpl;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.content.Waypoint.WaypointType;
import com.google.android.apps.mytracks.services.TrackRecordingService;
import com.google.android.apps.mytracks.stats.TripStatistics;
import com.google.android.apps.mytracks.stats.TripStatisticsUpdater;
import com.google.android.apps.mytracks.util.LocationUtils;
import com.google.android.apps.mytracks.util.PreferencesUtils;
import com.google.android.apps.mytracks.util.StringUtils;
import com.google.android.apps.mytracks.util.TrackIconUtils;
import com.google.android.maps.mytracks.R;
import android.content.Context;
import android.database.Cursor;
import android.location.Location;
import android.location.LocationManager;
import android.net.Uri;
import android.util.Log;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
/**
* Abstract class for various file track importers like {@link GpxFileTrackImporter} and
* {@link KmlFileTrackImporter}.
*
* @author Jimmy Shih
*/
abstract class AbstractFileTrackImporter extends DefaultHandler implements TrackImporter {
/**
* Data for the current track.
*
* @author Jimmy Shih
*/
private class TrackData {
// The current track
Track track = new Track();
// The number of segments processed for the current track
int numberOfSegments = 0;
/*
* The last location in the current segment. Null if the current segment
* doesn't have a last location.
*/
Location lastLocationInCurrentSegment;
// The number of locations processed for the current track
int numberOfLocations = 0;
// The trip statistics updater for the current track
TripStatisticsUpdater tripStatisticsUpdater;
// The import time of the track.
long importTime = System.currentTimeMillis();
// The buffered locations
Location[] bufferedLocations = new Location[MAX_BUFFERED_LOCATIONS];
// The number of buffered locations
int numBufferedLocations = 0;
}
private static final String TAG = AbstractFileTrackImporter.class.getSimpleName();
// The maximum number of buffered locations for bulk-insertion
private static final int MAX_BUFFERED_LOCATIONS = 512;
private final Context context;
private final long importTrackId;
private final MyTracksProviderUtils myTracksProviderUtils;
private final int recordingDistanceInterval;
private final List<Long> trackIds;
private final List<Waypoint> waypoints;
// The current track data
private TrackData trackData;
// The SAX locator to get the current line information
private Locator locator;
// The current element content
protected String content;
protected String name;
protected String description;
protected String category;
protected String latitude;
protected String longitude;
protected String altitude;
protected String time;
protected String waypointType;
protected String photoUrl;
/**
* Constructor.
*
* @param context the context
* @param importTrackId the track id to import to. -1L to import to a new
* track.
*/
AbstractFileTrackImporter(
Context context, long importTrackId, MyTracksProviderUtils myTracksProviderUtils) {
this.context = context;
this.importTrackId = importTrackId;
this.myTracksProviderUtils = myTracksProviderUtils;
this.recordingDistanceInterval = PreferencesUtils.getInt(context,
R.string.recording_distance_interval_key,
PreferencesUtils.RECORDING_DISTANCE_INTERVAL_DEFAULT);
trackIds = new ArrayList<Long>();
waypoints = new ArrayList<Waypoint>();
}
@Override
public void setDocumentLocator(Locator locator) {
this.locator = locator;
}
@Override
public void characters(char[] ch, int start, int length) throws SAXException {
String newContent = new String(ch, start, length);
if (content == null) {
content = newContent;
} else {
/*
* In 99% of the cases, a single call to this method will be made for each
* sequence of characters we're interested in, so we'll rarely be
* concatenating strings, thus not justifying the use of a StringBuilder.
*/
content += newContent;
}
}
@Override
public long importFile(InputStream inputStream) {
try {
SAXParser saxParser = SAXParserFactory.newInstance().newSAXParser();
long start = System.currentTimeMillis();
saxParser.parse(inputStream, this);
Log.d(Constants.TAG, "Total import time: " + (System.currentTimeMillis() - start) + "ms");
if (trackIds.size() != 1) {
Log.d(TAG, trackIds.size() + " tracks imported");
cleanImport();
return -1L;
}
return trackIds.get(0);
} catch (IOException e) {
Log.e(TAG, "Unable to import file", e);
cleanImport();
return -1L;
} catch (ParserConfigurationException e) {
Log.e(TAG, "Unable to import file", e);
cleanImport();
return -1L;
} catch (SAXException e) {
Log.e(TAG, "Unable to import file", e);
cleanImport();
return -1L;
}
}
/**
* On file end.
*/
protected void onFileEnd() {
// Add waypoints to the last imported track
int size = trackIds.size();
if (size == 0) {
return;
}
long trackId = trackIds.get(size - 1);
Track track = myTracksProviderUtils.getTrack(trackId);
if (track == null) {
return;
}
Cursor trackPointCursor = null;
try {
trackPointCursor = myTracksProviderUtils.getTrackPointCursor(track.getId(), -1L, -1, false);
if (trackPointCursor == null) {
return;
}
int waypointPosition = -1;
Waypoint waypoint = null;
int trackPointPosition = -1;
Location trackPoint = null;
TripStatisticsUpdater trackTripStatisticstrackUpdater = new TripStatisticsUpdater(
track.getTripStatistics().getStartTime());
TripStatisticsUpdater markerTripStatisticsUpdater = new TripStatisticsUpdater(
track.getTripStatistics().getStartTime());
while (true) {
if (waypoint == null) {
waypointPosition++;
waypoint = waypointPosition < waypoints.size() ? waypoints.get(waypointPosition) : null;
if (waypoint == null) {
// No more waypoints
return;
}
}
if (trackPoint == null) {
trackPointPosition++;
trackPoint = trackPointCursor.moveToPosition(trackPointPosition) ? myTracksProviderUtils
.createTrackPoint(trackPointCursor)
: null;
if (trackPoint == null) {
// No more track points. Ignore the rest of the waypoints.
return;
}
trackTripStatisticstrackUpdater.addLocation(trackPoint, recordingDistanceInterval,
PreferencesUtils.getInt(context, R.string.stats_weight_key,
PreferencesUtils.STATS_WEIGHT_DEFAULT));
markerTripStatisticsUpdater.addLocation(trackPoint, recordingDistanceInterval,
PreferencesUtils.getInt(context, R.string.stats_weight_key,
PreferencesUtils.STATS_WEIGHT_DEFAULT));
}
if (waypoint.getLocation().getTime() > trackPoint.getTime()) {
trackPoint = null;
} else if (waypoint.getLocation().getTime() < trackPoint.getTime()) {
waypoint = null;
} else {
// The waypoint location time matches the track point time
if (trackPoint.getLatitude() == waypoint.getLocation().getLatitude()
&& trackPoint.getLongitude() == waypoint.getLocation().getLongitude()) {
// Get tripStatistics, description, and icon
TripStatistics tripStatistics;
String waypointDescription;
String icon;
if (waypoint.getType() == WaypointType.STATISTICS) {
tripStatistics = markerTripStatisticsUpdater.getTripStatistics();
markerTripStatisticsUpdater = new TripStatisticsUpdater(trackPoint.getTime());
waypointDescription = new DescriptionGeneratorImpl(context)
.generateWaypointDescription(tripStatistics);
icon = context.getString(R.string.marker_statistics_icon_url);
} else {
tripStatistics = null;
waypointDescription = waypoint.getDescription();
icon = context.getString(R.string.marker_waypoint_icon_url);
}
// Get length and duration
double length = trackTripStatisticstrackUpdater.getTripStatistics().getTotalDistance();
long duration = trackTripStatisticstrackUpdater.getTripStatistics().getTotalTime();
// Insert waypoint
Waypoint newWaypoint = new Waypoint(waypoint.getName(), waypointDescription,
waypoint.getCategory(), icon, track.getId(), waypoint.getType(), length, duration,
-1L, -1L, trackPoint, tripStatistics, waypoint.getPhotoUrl());
myTracksProviderUtils.insertWaypoint(newWaypoint);
}
waypoint = null;
}
}
} finally {
if (trackPointCursor != null) {
trackPointCursor.close();
}
}
}
/**
* On track start.
*/
protected void onTrackStart() throws SAXException {
trackData = new TrackData();
long trackId;
if (importTrackId == -1L) {
Uri uri = myTracksProviderUtils.insertTrack(trackData.track);
trackId = Long.parseLong(uri.getLastPathSegment());
} else {
if (trackIds.size() > 0) {
throw new SAXException(createErrorMessage(
"Cannot import more than one track to an existing track " + importTrackId));
}
trackId = importTrackId;
myTracksProviderUtils.clearTrack(trackId);
}
trackIds.add(trackId);
trackData.track.setId(trackId);
}
/**
* On track end.
*/
protected void onTrackEnd() {
flushLocations(trackData);
if (name != null) {
trackData.track.setName(name);
}
if (description != null) {
trackData.track.setDescription(description);
}
if (category != null) {
trackData.track.setCategory(category);
trackData.track.setIcon(TrackIconUtils.getIconValue(context, category));
}
if (trackData.tripStatisticsUpdater == null) {
trackData.tripStatisticsUpdater = new TripStatisticsUpdater(trackData.importTime);
trackData.tripStatisticsUpdater.updateTime(trackData.importTime);
}
trackData.track.setTripStatistics(trackData.tripStatisticsUpdater.getTripStatistics());
trackData.track.setNumberOfPoints(trackData.numberOfLocations);
myTracksProviderUtils.updateTrack(trackData.track);
insertFirstWaypoint(trackData.track);
}
/**
* On track segment start.
*/
protected void onTrackSegmentStart() {
trackData.numberOfSegments++;
/*
* If not the first segment, add a pause separator if there is at least one
* location in the last segment.
*/
if (trackData.numberOfSegments > 1 && trackData.lastLocationInCurrentSegment != null) {
insertLocation(createLocation(TrackRecordingService.PAUSE_LATITUDE, 0.0, 0.0,
trackData.lastLocationInCurrentSegment.getTime()));
}
trackData.lastLocationInCurrentSegment = null;
}
/**
* Adds a waypoint.
*
* @param type the waypoint type
*/
protected void addWaypoint(WaypointType type) throws SAXException {
// Waypoint must have a time, else cannot match to the track points
if (time == null) {
return;
}
Waypoint waypoint = new Waypoint();
Location location = createLocation();
if (!LocationUtils.isValidLocation(location)) {
throw new SAXException(createErrorMessage("Invalid location detected: " + location));
}
waypoint.setLocation(location);
if (name != null) {
waypoint.setName(name);
}
if (description != null) {
waypoint.setDescription(description);
}
if (category != null) {
waypoint.setCategory(category);
}
waypoint.setType(type);
if (photoUrl != null) {
waypoint.setPhotoUrl(photoUrl);
}
waypoints.add(waypoint);
}
/**
* Gets a track point.
*/
protected Location getTrackPoint() throws SAXException {
Location location = createLocation();
// Calculate derived attributes from the previous point
if (trackData.lastLocationInCurrentSegment != null
&& trackData.lastLocationInCurrentSegment.getTime() != 0) {
long timeDifference = location.getTime() - trackData.lastLocationInCurrentSegment.getTime();
// Check for negative time change
if (timeDifference <= 0) {
Log.w(Constants.TAG, "Time difference not postive.");
} else {
/*
* We don't have a speed and bearing in GPX, make something up from the
* last two points. GPS points tend to have some inherent imprecision,
* speed and bearing will likely be off, so the statistics for things
* like max speed will also be off.
*/
float speed = trackData.lastLocationInCurrentSegment.distanceTo(location) * 1000.0f
/ timeDifference;
location.setSpeed(speed);
}
location.setBearing(trackData.lastLocationInCurrentSegment.bearingTo(location));
}
if (!LocationUtils.isValidLocation(location)) {
throw new SAXException(createErrorMessage("Invalid location detected: " + location));
}
if (trackData.numberOfSegments > 1 && trackData.lastLocationInCurrentSegment == null) {
/*
* If not the first segment, add a resume separator before adding the
* first location.
*/
insertLocation(
createLocation(TrackRecordingService.RESUME_LATITUDE, 0.0, 0.0, location.getTime()));
}
trackData.lastLocationInCurrentSegment = location;
return location;
}
/**
* Inserts a track point.
*
* @param location the location
*/
protected void insertTrackPoint(Location location) {
insertLocation(location);
if (trackData.track.getStartId() == -1L) {
// Flush the location to set the track start id and the track end id
flushLocations(trackData);
}
}
/**
* Creates an error message.
*
* @param message the message
*/
protected String createErrorMessage(String message) {
return String.format(Locale.US, "Parsing error at line: %d column: %d. %s",
locator.getLineNumber(), locator.getColumnNumber(), message);
}
/**
* Creates a location.
*/
private Location createLocation() throws SAXException {
if (latitude == null || longitude == null) {
return null;
}
double latitudeValue;
double longitudeValue;
try {
latitudeValue = Double.parseDouble(latitude);
longitudeValue = Double.parseDouble(longitude);
} catch (NumberFormatException e) {
throw new SAXException(createErrorMessage(
String.format("Unable to parse latitude longitude: %s %s", latitude, longitude)), e);
}
Double altitudeValue = null;
if (altitude != null) {
try {
altitudeValue = Double.parseDouble(altitude);
} catch (NumberFormatException e) {
throw new SAXException(
createErrorMessage(String.format("Unable to parse altitude: %s", altitude)), e);
}
}
long timeValue;
if (time == null) {
timeValue = trackData.importTime;
} else {
try {
timeValue = StringUtils.getTime(time);
} catch (IllegalArgumentException e) {
throw new SAXException(
createErrorMessage(String.format("Unable to parse time: %s", time)), e);
}
}
return createLocation(latitudeValue, longitudeValue, altitudeValue, timeValue);
}
/**
* Creates a location.
*
* @param latitudeValue the latitude value
* @param longitudeValue the longitude value
* @param altitudeValue the altitude value
* @param timeValue the time value
*/
private Location createLocation(
double latitudeValue, double longitudeValue, Double altitudeValue, long timeValue) {
Location location = new Location(LocationManager.GPS_PROVIDER);
location.setLatitude(latitudeValue);
location.setLongitude(longitudeValue);
if (altitudeValue != null) {
location.setAltitude(altitudeValue);
} else {
location.removeAltitude();
}
location.setTime(timeValue);
location.removeAccuracy();
location.removeBearing();
location.removeSpeed();
return location;
}
/**
* Inserts a location.
*
* @param location the location
*/
private void insertLocation(Location location) {
if (trackData.tripStatisticsUpdater == null) {
trackData.tripStatisticsUpdater = new TripStatisticsUpdater(
location.getTime() != -1L ? location.getTime() : trackData.importTime);
}
trackData.tripStatisticsUpdater.addLocation(location, recordingDistanceInterval,
PreferencesUtils.getInt(context, R.string.stats_weight_key,
PreferencesUtils.STATS_WEIGHT_DEFAULT));
trackData.bufferedLocations[trackData.numBufferedLocations] = location;
trackData.numBufferedLocations++;
trackData.numberOfLocations++;
if (trackData.numBufferedLocations >= MAX_BUFFERED_LOCATIONS) {
flushLocations(trackData);
}
}
/**
* Flushes the locations to the database.
*
* @param data the track data
*/
private void flushLocations(TrackData data) {
if (data.numBufferedLocations <= 0) {
return;
}
myTracksProviderUtils.bulkInsertTrackPoint(
data.bufferedLocations, data.numBufferedLocations, data.track.getId());
data.numBufferedLocations = 0;
if (data.track.getStartId() == -1L) {
data.track.setStartId(myTracksProviderUtils.getFirstTrackPointId(data.track.getId()));
}
data.track.setStopId(myTracksProviderUtils.getLastTrackPointId(data.track.getId()));
}
/**
* Inserts the first waypoint, the track statistics waypoint.
*
* @param track the track
*/
private void insertFirstWaypoint(Track track) {
String waypointName = context.getString(R.string.marker_split_name_format, 0);
String waypointCategory = "";
TripStatisticsUpdater updater = new TripStatisticsUpdater(
track.getTripStatistics().getStartTime());
TripStatistics tripStatistics = updater.getTripStatistics();
String waypointDescription = new DescriptionGeneratorImpl(context).generateWaypointDescription(
tripStatistics);
String icon = context.getString(R.string.marker_statistics_icon_url);
double length = 0.0;
long duration = 0L;
Location waypointLocation = new Location("");
waypointLocation.setLatitude(100);
waypointLocation.setLongitude(180);
Waypoint waypoint = new Waypoint(waypointName, waypointDescription, waypointCategory, icon,
track.getId(), WaypointType.STATISTICS, length, duration, -1L, -1L, waypointLocation,
tripStatistics, "");
myTracksProviderUtils.insertWaypoint(waypoint);
}
/**
* Cleans up import.
*/
private void cleanImport() {
for (long trackId : trackIds) {
myTracksProviderUtils.deleteTrack(trackId);
}
}
}
@@ -0,0 +1,173 @@
/*
* 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.MyTracksProviderUtils;
import com.google.android.apps.mytracks.content.Waypoint.WaypointType;
import com.google.common.annotations.VisibleForTesting;
import android.content.Context;
import android.location.Location;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
/**
* Imports a GPX file.
*
* @author Jimmy Shih
*/
public class GpxFileTrackImporter extends AbstractFileTrackImporter {
private static final String TAG_DESCRIPTION = "desc";
private static final String TAG_COMMENT = "cmt";
private static final String TAG_ELEVATION = "ele";
private static final String TAG_GPX = "gpx";
private static final String TAG_NAME = "name";
private static final String TAG_TIME = "time";
private static final String TAG_TRACK = "trk";
private static final String TAG_TRACK_POINT = "trkpt";
private static final String TAG_TRACK_SEGMENT = "trkseg";
private static final String TAG_TYPE = "type";
private static final String TAG_WAYPOINT = "wpt";
private static final String ATTRIBUTE_LAT = "lat";
private static final String ATTRIBUTE_LON = "lon";
/**
* Constructor.
*
* @param context the context
*/
public GpxFileTrackImporter(Context context) {
this(context, MyTracksProviderUtils.Factory.get(context));
}
@VisibleForTesting
GpxFileTrackImporter(Context context, MyTracksProviderUtils myTracksProviderUtils) {
super(context, -1L, myTracksProviderUtils);
}
@Override
public void startElement(String uri, String localName, String tag, Attributes attributes)
throws SAXException {
if (tag.equals(TAG_WAYPOINT)) {
onWaypointStart(attributes);
} else if (tag.equals(TAG_TRACK)) {
onTrackStart();
} else if (tag.equals(TAG_TRACK_SEGMENT)) {
onTrackSegmentStart();
} else if (tag.equals(TAG_TRACK_POINT)) {
onTrackPointStart(attributes);
}
}
@Override
public void endElement(String uri, String localName, String tag) throws SAXException {
if (tag.equals(TAG_GPX)) {
onFileEnd();
} else if (tag.equals(TAG_WAYPOINT)) {
onWaypointEnd();
} else if (tag.equals(TAG_TRACK)) {
onTrackEnd();
} else if (tag.equals(TAG_TRACK_POINT)) {
onTrackPointEnd();
} else if (tag.equals(TAG_NAME)) {
if (content != null) {
name = content.trim();
}
} else if (tag.equals(TAG_DESCRIPTION)) {
if (content != null) {
description = content.trim();
}
} else if (tag.equals(TAG_TYPE)) {
if (content != null) {
category = content.trim();
}
} else if (tag.equals(TAG_TIME)) {
if (content != null) {
time = content.trim();
}
} else if (tag.equals(TAG_ELEVATION)) {
if (content != null) {
altitude = content.trim();
}
} else if (tag.equals(TAG_COMMENT)) {
if (content != null) {
waypointType = content.trim();
}
}
// Reset element content
content = null;
}
@Override
protected void onTrackStart() throws SAXException {
super.onTrackStart();
name = null;
description = null;
category = null;
}
/**
* On track point start.
*
* @param attributes the attributes
*/
private void onTrackPointStart(Attributes attributes) {
latitude = attributes.getValue(ATTRIBUTE_LAT);
longitude = attributes.getValue(ATTRIBUTE_LON);
altitude = null;
time = null;
}
/**
* On track point end.
*/
private void onTrackPointEnd() throws SAXException {
Location location = getTrackPoint();
if (location == null) {
return;
}
insertTrackPoint(location);
}
/**
* On waypoint start.
*
* @param attributes the attributes
*/
private void onWaypointStart(Attributes attributes) {
name = null;
description = null;
category = null;
latitude = attributes.getValue(ATTRIBUTE_LAT);
longitude = attributes.getValue(ATTRIBUTE_LON);
altitude = null;
time = null;
waypointType = null;
}
/**
* On waypoint end.
*/
private void onWaypointEnd() throws SAXException {
addWaypoint(WaypointType.STATISTICS.name().equals(waypointType) ? WaypointType.STATISTICS
: WaypointType.WAYPOINT);
}
}
@@ -0,0 +1,232 @@
/*
* 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.apps.mytracks.TrackDetailActivity;
import com.google.android.apps.mytracks.util.DialogUtils;
import com.google.android.apps.mytracks.util.FileUtils;
import com.google.android.apps.mytracks.util.IntentUtils;
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.app.ProgressDialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.support.v4.app.TaskStackBuilder;
import android.util.Log;
import android.widget.Toast;
import java.io.File;
/**
* An activity to import files from the external storage. Optionally to import
* one specific file.
*
* @author Rodrigo Damazio
*/
public class ImportActivity extends Activity {
public static final String EXTRA_IMPORT_ALL = "import_all";
public static final String EXTRA_TRACK_FILE_FORMAT = "track_file_format";
private static final String TAG = ImportActivity.class.getSimpleName();
private static final int DIALOG_PROGRESS_ID = 0;
private static final int DIALOG_RESULT_ID = 1;
private ImportAsyncTask importAsyncTask;
private ProgressDialog progressDialog;
private boolean importAll;
private TrackFileFormat trackFileFormat;
// the path on the external storage to import
private String directoryDisplayName;
// the number of files successfully imported
private int successCount;
// the number of files to import
private int totalCount;
// the last successfully imported track id
private long trackId;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Intent intent = getIntent();
importAll = intent.getBooleanExtra(EXTRA_IMPORT_ALL, false);
trackFileFormat = intent.getParcelableExtra(EXTRA_TRACK_FILE_FORMAT);
if (trackFileFormat == null) {
trackFileFormat = TrackFileFormat.GPX;
}
if (!FileUtils.isExternalStorageAvailable()) {
Toast.makeText(this, R.string.external_storage_not_available, Toast.LENGTH_LONG).show();
finish();
return;
}
String directoryPath;
if (importAll) {
directoryDisplayName = FileUtils.getDirectoryDisplayName(
trackFileFormat.getExtension());
directoryPath = FileUtils.getDirectoryPath(trackFileFormat.getExtension());
if (!FileUtils.isDirectory(new File(directoryPath))) {
Toast.makeText(
this, getString(R.string.import_no_directory, directoryDisplayName), Toast.LENGTH_LONG)
.show();
finish();
return;
}
} else {
String action = intent.getAction();
if (!(Intent.ACTION_ATTACH_DATA.equals(action) || Intent.ACTION_VIEW.equals(action))) {
Log.d(TAG, "Invalid action: " + intent);
finish();
return;
}
Uri data = intent.getData();
if (!UriUtils.isFileUri(data)) {
Log.d(TAG, "Invalid data: " + intent);
finish();
return;
}
directoryDisplayName = data.getPath();
directoryPath = data.getPath();
}
Object retained = getLastNonConfigurationInstance();
if (retained instanceof ImportAsyncTask) {
importAsyncTask = (ImportAsyncTask) retained;
importAsyncTask.setActivity(this);
} else {
importAsyncTask = new ImportAsyncTask(this, importAll, trackFileFormat, directoryPath);
importAsyncTask.execute();
}
}
@Override
public Object onRetainNonConfigurationInstance() {
importAsyncTask.setActivity(null);
return importAsyncTask;
}
@Override
protected Dialog onCreateDialog(int id) {
switch (id) {
case DIALOG_PROGRESS_ID:
progressDialog = DialogUtils.createHorizontalProgressDialog(
this, R.string.import_progress_message, new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
importAsyncTask.cancel(true);
dialog.dismiss();
}
}, directoryDisplayName);
return progressDialog;
case DIALOG_RESULT_ID:
int iconId;
int titleId;
String message;
String totalFiles = getResources()
.getQuantityString(R.plurals.files, totalCount, totalCount);
if (successCount == totalCount) {
if (totalCount == 0) {
iconId = android.R.drawable.ic_dialog_info;
titleId = R.string.import_no_file_title;
message = getString(R.string.import_no_file, directoryDisplayName);
} else {
iconId = R.drawable.ic_dialog_success;
titleId = R.string.generic_success_title;
message = getString(R.string.import_success, totalFiles, directoryDisplayName);
}
} else {
iconId = android.R.drawable.ic_dialog_alert;
titleId = R.string.generic_error_title;
message = getString(
R.string.import_error, successCount, totalFiles, directoryDisplayName);
}
return new AlertDialog.Builder(this).setCancelable(true).setIcon(iconId).setMessage(message)
.setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
dialog.dismiss();
finish();
}
}).setPositiveButton(R.string.generic_ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
if (successCount == totalCount && !importAll && trackId != -1L) {
Intent intent = IntentUtils.newIntent(
ImportActivity.this, TrackDetailActivity.class)
.putExtra(TrackDetailActivity.EXTRA_TRACK_ID, trackId);
TaskStackBuilder.create(ImportActivity.this)
.addParentStack(TrackDetailActivity.class).addNextIntent(intent)
.startActivities();
}
dialog.dismiss();
finish();
}
}).setTitle(titleId).create();
default:
return null;
}
}
/**
* Invokes when the associated AsyncTask completes.
*
* @param aSuccessCount the number of files successfully imported
* @param aTotalCount the number of files to import
* @param aTrackId the last successfully imported track id
*/
public void onAsyncTaskCompleted(int aSuccessCount, int aTotalCount, long aTrackId) {
successCount = aSuccessCount;
totalCount = aTotalCount;
trackId = aTrackId;
removeDialog(DIALOG_PROGRESS_ID);
showDialog(DIALOG_RESULT_ID);
}
/**
* Shows the progress dialog.
*/
public void showProgressDialog() {
showDialog(DIALOG_PROGRESS_ID);
}
/**
* Sets the progress dialog value.
*
* @param number the number of files imported
* @param max the maximum number of files
*/
public void setProgressDialogValue(int number, int max) {
if (progressDialog != null) {
progressDialog.setIndeterminate(false);
progressDialog.setMax(max);
progressDialog.setProgress(Math.min(number, max));
}
}
}
@@ -0,0 +1,239 @@
/*
* Copyright 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.io.file;
import com.google.android.apps.mytracks.io.file.export.KmzTrackExporter;
import com.google.android.apps.mytracks.util.FileUtils;
import com.google.android.apps.mytracks.util.PreferencesUtils;
import com.google.android.apps.mytracks.util.SystemUtils;
import com.google.android.maps.mytracks.R;
import android.content.Context;
import android.os.AsyncTask;
import android.os.Environment;
import android.os.PowerManager.WakeLock;
import android.util.Log;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/**
* AsyncTask to import files from the external storage.
*
* @author Jimmy Shih
*/
public class ImportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
private static final String TAG = ImportAsyncTask.class.getSimpleName();
private ImportActivity importActivity;
private final boolean importAll;
private final TrackFileFormat trackFileFormat;
private final String path;
private final Context context;
private WakeLock wakeLock;
// true if the AsyncTask has completed
private boolean completed;
// the number of files successfully imported
private int successCount;
// the number of files to import
private int totalCount;
// the last successfully imported track id
private long trackId;
/**
* Creates an AsyncTask.
*
* @param importActivity the activity currently associated with this AsyncTask
* @param importAll true to import all GPX files
* @param trackFileFormat the track file format
* @param path path to import GPX files
*/
public ImportAsyncTask(ImportActivity importActivity, boolean importAll,
TrackFileFormat trackFileFormat, String path) {
this.importActivity = importActivity;
this.importAll = importAll;
this.trackFileFormat = trackFileFormat;
this.path = path;
context = importActivity.getApplicationContext();
completed = false;
successCount = 0;
totalCount = 0;
trackId = -1L;
}
/**
* Sets the current {@link ImportActivity} associated with this AyncTask.
*
* @param importActivity the current {@link ImportActivity}, can be null
*/
public void setActivity(ImportActivity importActivity) {
this.importActivity = importActivity;
if (completed && importActivity != null) {
importActivity.onAsyncTaskCompleted(successCount, totalCount, trackId);
}
}
@Override
protected void onPreExecute() {
if (importActivity != null) {
importActivity.showProgressDialog();
}
}
@Override
protected Boolean doInBackground(Void... params) {
try {
// Get the wake lock if not recording or paused
boolean isRecording =
PreferencesUtils.getLong(importActivity, R.string.recording_track_id_key)
!= PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
boolean isPaused = PreferencesUtils.getBoolean(importActivity,
R.string.recording_track_paused_key, PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT);
if (!isRecording || isPaused) {
wakeLock = SystemUtils.acquireWakeLock(importActivity, wakeLock);
}
List<File> files = getFiles();
totalCount = files.size();
if (totalCount == 0) {
return true;
}
for (int i = 0; i < totalCount; i++) {
if (isCancelled()) {
// If cancelled, return true to show the number of files imported
return true;
}
if (importFile(files.get(i))) {
successCount++;
}
publishProgress(i + 1, totalCount);
}
return true;
} finally {
if (wakeLock != null && wakeLock.isHeld()) {
wakeLock.release();
}
}
}
@Override
protected void onProgressUpdate(Integer... values) {
if (importActivity != null) {
importActivity.setProgressDialogValue(values[0], values[1]);
}
}
@Override
protected void onPostExecute(Boolean result) {
completed = true;
if (importActivity != null) {
importActivity.onAsyncTaskCompleted(successCount, totalCount, trackId);
}
}
@Override
protected void onCancelled() {
completed = true;
if (importActivity != null) {
importActivity.onAsyncTaskCompleted(successCount, totalCount, trackId);
}
}
/**
* Imports a file.
*
* @param file the file
*/
private boolean importFile(final File file) {
FileInputStream fileInputStream = null;
try {
TrackImporter trackImporter;
if (trackFileFormat == TrackFileFormat.KML) {
String name = file.getName();
if (name.endsWith("." + TrackFileFormat.KML.getExtension())) {
trackImporter = new KmlFileTrackImporter(context, -1L, null);
} else {
File dir = new File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES),
FileUtils.SDCARD_TOP_DIR);
FileUtils.ensureDirectoryExists(dir);
dir = new File(dir, name.substring(0, name.lastIndexOf('.')));
FileUtils.ensureDirectoryExists(dir);
trackImporter = new KmzTrackImporter(context, dir.getPath());
}
} else {
trackImporter = new GpxFileTrackImporter(context);
}
fileInputStream = new FileInputStream(file);
trackId = trackImporter.importFile(fileInputStream);
return trackId != -1L;
} catch (FileNotFoundException e) {
Log.e(TAG, "Unable to import file", e);
return false;
} finally {
if (fileInputStream != null) {
try {
fileInputStream.close();
} catch (IOException e) {
Log.e(TAG, "Unable to close file input stream", e);
}
}
}
}
/**
* Gets a list of files. If importAll is true, returns a list of the files
* under the path directory. If importAll is false, returns a list containing
* just the path file.
*/
private List<File> getFiles() {
List<File> files = new ArrayList<File>();
File file = new File(path);
if (importAll) {
File[] candidates = file.listFiles();
if (candidates != null) {
for (File candidate : candidates) {
if (!candidate.isDirectory()) {
String name = candidate.getName();
if (trackFileFormat == TrackFileFormat.KML && (
name.endsWith("." + TrackFileFormat.KML.getExtension())
|| name.endsWith("." + KmzTrackExporter.KMZ_EXTENSION))) {
files.add(candidate);
} else if (trackFileFormat == TrackFileFormat.GPX
&& name.endsWith("." + TrackFileFormat.GPX.getExtension())) {
files.add(candidate);
}
}
}
}
} else {
files.add(file);
}
return files;
}
}
@@ -0,0 +1,306 @@
/*
* Copyright 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.io.file;
import com.google.android.apps.mytracks.content.MyTracksLocation;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.content.Sensor;
import com.google.android.apps.mytracks.content.Sensor.SensorDataSet;
import com.google.android.apps.mytracks.content.Waypoint.WaypointType;
import com.google.common.annotations.VisibleForTesting;
import android.content.Context;
import android.location.Location;
import android.net.Uri;
import java.io.File;
import java.util.ArrayList;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
/**
* Imports a KML file.
*
* @author Jimmy Shih
*/
public class KmlFileTrackImporter extends AbstractFileTrackImporter {
private static final String CADENCE = "cadence";
private static final String HEART_RATE = "heart_rate";
private static final String POWER = "power";
private static final String STATISTICS_STYLE = "#statistics";
private static final String WAYPOINT_STYLE = "#waypoint";
private static final String TAG_COORDINATES = "coordinates";
private static final String TAG_DESCRIPTION = "description";
private static final String TAG_GX_COORD = "gx:coord";
private static final String TAG_GX_MULTI_TRACK = "gx:MultiTrack";
private static final String TAG_GX_SIMPLE_ARRAY_DATA = "gx:SimpleArrayData";
private static final String TAG_GX_TRACK = "gx:Track";
private static final String TAG_GX_VALUE = "gx:value";
private static final String TAG_HREF = "href";
private static final String TAG_KML = "kml";
private static final String TAG_NAME = "name";
private static final String TAG_PHOTO_OVERLAY = "PhotoOverlay";
private static final String TAG_PLACEMARK = "Placemark";
private static final String TAG_STYLE_URL = "styleUrl";
private static final String TAG_VALUE = "value";
private static final String TAG_WHEN = "when";
private static final String ATTRIBUTE_NAME = "name";
private final String photoPath;
private boolean trackStarted = false;
private String sensorName;
private ArrayList<Location> locationList;
private ArrayList<Integer> cadenceList;
private ArrayList<Integer> heartRateList;
private ArrayList<Integer> powerList;
/**
* Constructor.
*
* @param context the context
* @param importTrackId track id to import to. -1L to import to a new track.
*/
public KmlFileTrackImporter(Context context, long importTrackId, String photoPath) {
this(context, importTrackId, photoPath, MyTracksProviderUtils.Factory.get(context));
}
@VisibleForTesting
public KmlFileTrackImporter(Context context, long importTrackId, String photoPath,
MyTracksProviderUtils myTracksProviderUtils) {
super(context, importTrackId, myTracksProviderUtils);
this.photoPath = photoPath;
}
@Override
public void startElement(String uri, String localName, String tag, Attributes attributes)
throws SAXException {
if (tag.equals(TAG_PLACEMARK) || tag.equals(TAG_PHOTO_OVERLAY)) {
onWaypointStart();
} else if (tag.equals(TAG_GX_MULTI_TRACK)) {
trackStarted = true;
onTrackStart();
} else if (tag.equals(TAG_GX_TRACK)) {
if (!trackStarted) {
throw new SAXException("No " + TAG_GX_MULTI_TRACK);
}
onTrackSegmentStart();
} else if (tag.equals(TAG_GX_SIMPLE_ARRAY_DATA)) {
onSensorDataStart(attributes);
}
}
@Override
public void endElement(String uri, String localName, String tag) throws SAXException {
if (tag.equals(TAG_KML)) {
onFileEnd();
} else if (tag.equals(TAG_PLACEMARK) || tag.equals(TAG_PHOTO_OVERLAY)) {
onWaypointEnd();
} else if (localName.equals(TAG_COORDINATES)) {
onWaypointLocationEnd();
} else if (tag.equals(TAG_GX_MULTI_TRACK)) {
onTrackEnd();
} else if (tag.equals(TAG_GX_TRACK)) {
onTrackSegmentEnd();
} else if (tag.equals(TAG_GX_COORD)) {
onTrackPointEnd();
} else if (tag.equals(TAG_GX_VALUE)) {
onSensorValueEnd();
} else if (tag.equals(TAG_NAME)) {
if (content != null) {
name = content.trim();
}
} else if (localName.equals(TAG_DESCRIPTION)) {
if (content != null) {
description = content.trim();
}
} else if (localName.equals(TAG_VALUE)) {
if (content != null) {
category = content.trim();
}
} else if (localName.equals(TAG_WHEN)) {
if (content != null) {
time = content.trim();
}
} else if (localName.equals(TAG_STYLE_URL)) {
if (content != null) {
waypointType = content.trim();
}
} else if (localName.equals(TAG_HREF)) {
if (content != null) {
photoUrl = content.trim();
}
}
// Reset element content
content = null;
}
/**
* On waypoint start.
*/
private void onWaypointStart() {
// Reset all Placemark variables
name = null;
description = null;
category = null;
latitude = null;
longitude = null;
altitude = null;
time = null;
waypointType = null;
photoUrl = null;
}
/**
* On waypoint end.
*/
private void onWaypointEnd() throws SAXException {
// Add a waypoint if the waypointType matches
WaypointType type = null;
if (WAYPOINT_STYLE.equals(waypointType)) {
type = WaypointType.WAYPOINT;
} else if (STATISTICS_STYLE.equals(waypointType)) {
type = WaypointType.STATISTICS;
}
if (type == null) {
return;
}
if (photoUrl != null && photoPath != null) {
Uri uri = Uri.parse(photoUrl);
photoUrl = photoPath + File.separatorChar + uri.getLastPathSegment();
}
addWaypoint(type);
}
/**
* On waypoint location end.
*/
private void onWaypointLocationEnd() {
if (content != null) {
String parts[] = content.trim().split(",");
if (parts.length != 2 && parts.length != 3) {
return;
}
longitude = parts[0];
latitude = parts[1];
altitude = parts.length == 3 ? parts[2] : null;
}
}
@Override
protected void onTrackSegmentStart() {
super.onTrackSegmentStart();
locationList = new ArrayList<Location>();
powerList = new ArrayList<Integer>();
cadenceList = new ArrayList<Integer>();
heartRateList = new ArrayList<Integer>();
}
/**
* On track segment end.
*/
private void onTrackSegmentEnd() {
// Close a track segment by inserting the segment locations
boolean hasPower = powerList.size() == locationList.size();
boolean hasCadence = cadenceList.size() == locationList.size();
boolean hasHeartRate = heartRateList.size() == locationList.size();
for (int i = 0; i < locationList.size(); i++) {
Location location = locationList.get(i);
if (!hasPower && !hasCadence && !hasHeartRate) {
insertTrackPoint(location);
} else {
SensorDataSet.Builder builder = Sensor.SensorDataSet.newBuilder();
if (hasPower) {
builder.setPower(Sensor.SensorData.newBuilder()
.setValue(powerList.get(i)).setState(Sensor.SensorState.SENDING));
}
if (hasCadence) {
builder.setCadence(Sensor.SensorData.newBuilder()
.setValue(cadenceList.get(i)).setState(Sensor.SensorState.SENDING));
}
if (hasHeartRate) {
builder.setHeartRate(Sensor.SensorData.newBuilder()
.setValue(heartRateList.get(i)).setState(Sensor.SensorState.SENDING));
}
SensorDataSet sensorDataSet = builder.setCreationTime(location.getTime()).build();
MyTracksLocation myTracksLocation = new MyTracksLocation(location, sensorDataSet);
insertTrackPoint(myTracksLocation);
}
}
}
/**
* On track point end. gx:coord end tag.
*/
private void onTrackPointEnd() throws SAXException {
// Add location to locationList
if (content == null) {
return;
}
String parts[] = content.trim().split(" ");
if (parts.length != 2 && parts.length != 3) {
return;
}
longitude = parts[0];
latitude = parts[1];
altitude = parts.length == 3 ? parts[2] : null;
Location location = getTrackPoint();
if (location == null) {
return;
}
locationList.add(location);
time = null;
}
/**
* On sensor data start. gx:SimpleArrayData start tag.
*
* @param attributes
*/
private void onSensorDataStart(Attributes attributes) {
sensorName = attributes.getValue(ATTRIBUTE_NAME);
}
/**
* On sensor value end. gx:value end tag.
*/
private void onSensorValueEnd() throws SAXException {
if (content != null) {
int value;
try {
value = Integer.parseInt(content.trim());
} catch (NumberFormatException e) {
throw new SAXException(createErrorMessage("Unable to parse gx:value:" + content), e);
}
if (POWER.equals(sensorName)) {
powerList.add(value);
} else if (HEART_RATE.equals(sensorName)) {
heartRateList.add(value);
} else if (CADENCE.equals(sensorName)) {
cadenceList.add(value);
}
}
}
}
@@ -0,0 +1,181 @@
/*
* Copyright 2013 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.MyTracksProviderUtils;
import com.google.android.apps.mytracks.io.file.export.KmzTrackExporter;
import android.content.Context;
import android.util.Log;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
/**
* Imports a KMZ file.
*
* @author Jimmy Shih
*/
public class KmzTrackImporter implements TrackImporter {
private static final String TAG = KmzTrackImporter.class.getSimpleName();
private static final int BUFFER_SIZE = 4096;
private final Context context;
private final String photoPath;
public KmzTrackImporter(Context context, String photoPath) {
this.context = context;
this.photoPath = photoPath;
}
@Override
public long importFile(InputStream inputStream) {
ZipInputStream zipInputStream = null;
long importedTrackId = -1L;
try {
ZipEntry zipEntry;
zipInputStream = new ZipInputStream(inputStream);
while ((zipEntry = zipInputStream.getNextEntry()) != null) {
if (Thread.interrupted()) {
Log.d(TAG, "Thread interrupted");
cleanImport(importedTrackId);
return -1L;
}
String fileName = zipEntry.getName();
if (fileName.equals(KmzTrackExporter.KMZ_KML_FILE)) {
importedTrackId = parseKml(zipInputStream);
if (importedTrackId == -1L) {
Log.d(TAG, "Unable to parse kml in kmz");
cleanImport(importedTrackId);
return -1L;
}
} else {
String prefix = KmzTrackExporter.KMZ_IMAGES_DIR + File.separatorChar;
if (fileName.startsWith(prefix)) {
readImageFile(zipInputStream, fileName.substring(prefix.length()));
}
}
zipInputStream.closeEntry();
}
return importedTrackId;
} catch (IOException e) {
Log.e(TAG, "Unable to import file", e);
cleanImport(importedTrackId);
return -1L;
} finally {
if (zipInputStream != null) {
try {
zipInputStream.close();
} catch (IOException e) {
Log.e(TAG, "Unable to close zip input stream", e);
}
}
}
}
/**
* Cleans up import.
*
* @param importedTrackId the imported track id
*/
private void cleanImport(long importedTrackId) {
if (importedTrackId != -1L) {
MyTracksProviderUtils myTracksProviderUtils = MyTracksProviderUtils.Factory.get(context);
myTracksProviderUtils.deleteTrack(importedTrackId);
}
if (photoPath != null) {
File dir = new File(photoPath);
if (dir.exists() && dir.isDirectory()) {
for (File file : dir.listFiles()) {
file.delete();
}
dir.delete();
}
}
}
/**
* Parses kml
*
* @param zipInputStream the zip input stream
* @return the imported track id or -1L
*/
private long parseKml(ZipInputStream zipInputStream) throws IOException {
ByteArrayInputStream byteArrayInputStream = null;
try {
KmlFileTrackImporter kmlFileTrackImporter = new KmlFileTrackImporter(context, -1L, photoPath);
byteArrayInputStream = new ByteArrayInputStream(getKml(zipInputStream));
return kmlFileTrackImporter.importFile(byteArrayInputStream);
} finally {
if (byteArrayInputStream != null) {
byteArrayInputStream.close();
}
}
}
/**
* Gets the kml as byte array.
*
* @param zipInputStream the zip input stream
*/
private byte[] getKml(ZipInputStream zipInputStream) throws IOException {
ByteArrayOutputStream byteArrayOutputStream = null;
try {
byteArrayOutputStream = new ByteArrayOutputStream();
byte[] buffer = new byte[BUFFER_SIZE];
int count;
while ((count = zipInputStream.read(buffer)) != -1) {
byteArrayOutputStream.write(buffer, 0, count);
}
return byteArrayOutputStream.toByteArray();
} finally {
if (byteArrayOutputStream != null) {
byteArrayOutputStream.close();
}
}
}
/**
* Reads an image file.
*
* @param zipInputStream the zip input stream
* @param fileName the file name
*/
private void readImageFile(ZipInputStream zipInputStream, String fileName) throws IOException {
FileOutputStream fileOutputStream = null;
try {
fileOutputStream = new FileOutputStream(photoPath + File.separatorChar + fileName);
byte[] buffer = new byte[BUFFER_SIZE];
int count;
while ((count = zipInputStream.read(buffer)) != -1) {
fileOutputStream.write(buffer, 0, count);
}
} finally {
if (fileOutputStream != null) {
fileOutputStream.close();
}
}
}
}
@@ -0,0 +1,84 @@
package com.google.android.apps.mytracks.io.file;
import com.google.android.apps.mytracks.io.file.export.CsvTrackWriter;
import com.google.android.apps.mytracks.io.file.export.GpxTrackWriter;
import com.google.android.apps.mytracks.io.file.export.KmlTrackWriter;
import com.google.android.apps.mytracks.io.file.export.TcxTrackWriter;
import com.google.android.apps.mytracks.io.file.export.TrackWriter;
import android.content.Context;
import android.os.Parcel;
import android.os.Parcelable;
import java.util.Locale;
/**
* Definition of all possible track formats.
*/
public enum TrackFileFormat implements Parcelable {
KML {
@Override
public TrackWriter newTrackWriter(Context context, boolean inZip) {
return new KmlTrackWriter(context, inZip);
}
},
GPX {
@Override
public TrackWriter newTrackWriter(Context context, boolean inZip) {
return new GpxTrackWriter(context);
}
},
CSV {
@Override
public TrackWriter newTrackWriter(Context context, boolean inZip) {
return new CsvTrackWriter(context);
}
},
TCX {
@Override
public TrackWriter newTrackWriter(Context context, boolean inZip) {
return new TcxTrackWriter(context);
}
};
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(final Parcel dest, final int flags) {
dest.writeInt(ordinal());
}
public static final Creator<TrackFileFormat> CREATOR = new Creator<TrackFileFormat>() {
@Override
public TrackFileFormat createFromParcel(final Parcel source) {
return TrackFileFormat.values()[source.readInt()];
}
@Override
public TrackFileFormat[] newArray(final int size) {
return new TrackFileFormat[size];
}
};
/**
* Creates a new track writer for the format.
*/
public abstract TrackWriter newTrackWriter(Context context, boolean inZip);
/**
* 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(Locale.US);
}
}
@@ -0,0 +1,34 @@
/*
* Copyright 2013 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.InputStream;
/**
* Interface for a track importer.
*
* @author Jimmy Shih
*/
public interface TrackImporter {
/**
* Import a file.
*
* @param inputStream the file's input stream
* @return the imported track id or -1L.
*/
public long importFile(InputStream inputStream);
}
@@ -0,0 +1,68 @@
/*
* Copyright 2013 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.export;
import android.util.Log;
import java.io.IOException;
import java.io.OutputStream;
/**
* Abstract class for {@link TrackExporter}.
*
* @author Jimmy Shih
*/
abstract class AbstractTrackExporter implements TrackExporter {
private static final String TAG = AbstractTrackExporter.class.getSimpleName();
private Thread writeThread;
private boolean success = false;
@Override
public void writeTrack(final OutputStream outputStream) {
writeThread = new Thread() {
@Override
public void run() {
try {
performWrite(outputStream);
success = true;
} catch (InterruptedException e) {
Log.e(TAG, "Unable to perform write", e);
success = false;
} catch (IOException e) {
Log.e(TAG, "Unable to perform write", e);
success = false;
}
}
};
writeThread.start();
try {
writeThread.join();
} catch (InterruptedException e) {
Log.e(TAG, "Interrupted while waiting for write thread to finish", e);
success = false;
}
}
@Override
public boolean isSuccess() {
return success;
}
abstract void performWrite(OutputStream outputStream) throws InterruptedException, IOException;
}
@@ -0,0 +1,251 @@
/*
* 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.export;
import com.google.android.apps.mytracks.content.MyTracksLocation;
import com.google.android.apps.mytracks.content.Sensor;
import com.google.android.apps.mytracks.content.Sensor.SensorData;
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.TrackFileFormat;
import com.google.android.apps.mytracks.util.StringUtils;
import com.google.android.maps.mytracks.R;
import android.content.Context;
import android.location.Location;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.text.NumberFormat;
/**
* Write track as CSV to a file. See RFC 4180 for info on CSV. Output three
* tables.<br>
* The first table contains the track info. Its columns are:<br>
* "Track name","Activity type","Track description" <br>
* <br>
* The second table contains the markers. Its columns are:<br>
* "Marker name","Marker type","Marker description","Latitude (deg)","Longitude
* (deg)","Altitude (m)","Bearing (deg)","Accuracy (m)","Speed (m/s)","Time"<br>
* <br>
* The thrid table contains the points. Its columns are:<br>
* "Segment","Point","Latitude (deg)","Longitude (deg)","Altitude (m)","Bearing
* (deg)","Accuracy (m)","Speed (m/s)","Time","Power (W)","Cadence (rpm)","Heart
* rate (bpm)","Battery level (%)"<br>
*
* @author Rodrigo Damazio
*/
public class CsvTrackWriter implements TrackWriter {
private static final NumberFormat SHORT_FORMAT = NumberFormat.getInstance();
static {
SHORT_FORMAT.setMaximumFractionDigits(4);
}
private final Context context;
private PrintWriter printWriter;
private int segmentIndex;
private int pointIndex;
public CsvTrackWriter(Context context) {
this.context = context;
}
@Override
public String getExtension() {
return TrackFileFormat.CSV.getExtension();
}
@Override
public void prepare(OutputStream outputStream) {
printWriter = new PrintWriter(outputStream);
segmentIndex = 0;
pointIndex = 0;
}
@Override
public void close() {
if (printWriter != null) {
printWriter.flush();
printWriter = null;
}
}
@Override
public void writeHeader(Track track) {
writeCommaSeparatedLine(context.getString(R.string.generic_name),
context.getString(R.string.track_edit_activity_type_hint),
context.getString(R.string.generic_description));
writeCommaSeparatedLine(track.getName(), track.getCategory(), track.getDescription());
writeCommaSeparatedLine();
}
@Override
public void writeFooter() {
// Do nothing
}
@Override
public void writeBeginWaypoints() {
writeCommaSeparatedLine(context.getString(R.string.generic_name),
context.getString(R.string.marker_edit_marker_type_hint),
context.getString(R.string.generic_description),
context.getString(R.string.description_location_latitude),
context.getString(R.string.description_location_longitude),
context.getString(R.string.description_location_altitude),
context.getString(R.string.description_location_bearing),
context.getString(R.string.description_location_accuracy),
context.getString(R.string.description_location_speed),
context.getString(R.string.description_time));
}
@Override
public void writeEndWaypoints() {
writeCommaSeparatedLine();
}
@Override
public void writeWaypoint(Waypoint waypoint) {
Location location = waypoint.getLocation();
writeCommaSeparatedLine(waypoint.getName(),
waypoint.getCategory(),
waypoint.getDescription(),
Double.toString(location.getLatitude()),
Double.toString(location.getLongitude()),
getAltitude(location),
getBearing(location),
getAccuracy(location),
getSpeed(location),
StringUtils.formatDateTimeIso8601(location.getTime()));
}
@Override
public void writeBeginTrack(Track track, Location firstPoint) {
writeCommaSeparatedLine(context.getString(R.string.description_track_segment),
context.getString(R.string.description_track_point),
context.getString(R.string.description_location_latitude),
context.getString(R.string.description_location_longitude),
context.getString(R.string.description_location_altitude),
context.getString(R.string.description_location_bearing),
context.getString(R.string.description_location_accuracy),
context.getString(R.string.description_location_speed),
context.getString(R.string.description_time),
context.getString(R.string.description_sensor_power),
context.getString(R.string.description_sensor_cadence),
context.getString(R.string.description_sensor_heart_rate));
}
@Override
public void writeEndTrack(Track track, Location lastPoint) {
// Do nothing
}
@Override
public void writeOpenSegment() {
segmentIndex++;
pointIndex = 0;
}
@Override
public void writeCloseSegment() {
// Do nothing
}
@Override
public void writeLocation(Location location) {
String power = null;
String cadence = null;
String heartRate = null;
if (location instanceof MyTracksLocation) {
SensorDataSet sensorDataSet = ((MyTracksLocation) location).getSensorDataSet();
if (sensorDataSet != null) {
if (sensorDataSet.hasPower()) {
SensorData sensorData = sensorDataSet.getPower();
if (sensorData.hasValue() && sensorData.getState() == Sensor.SensorState.SENDING) {
power = Double.toString(sensorData.getValue());
}
}
if (sensorDataSet.hasCadence()) {
SensorData sensorData = sensorDataSet.getCadence();
if (sensorData.hasValue() && sensorData.getState() == Sensor.SensorState.SENDING) {
cadence = Double.toString(sensorData.getValue());
}
}
if (sensorDataSet.hasHeartRate()) {
SensorData sensorData = sensorDataSet.getHeartRate();
if (sensorData.hasValue() && sensorData.getState() == Sensor.SensorState.SENDING) {
heartRate = Double.toString(sensorData.getValue());
}
}
}
}
pointIndex++;
writeCommaSeparatedLine(Integer.toString(segmentIndex),
Integer.toString(pointIndex),
Double.toString(location.getLatitude()),
Double.toString(location.getLongitude()),
getAltitude(location),
getBearing(location),
getAccuracy(location),
getSpeed(location),
StringUtils.formatDateTimeIso8601(location.getTime()),
power,
cadence,
heartRate);
}
private String getAltitude(Location location) {
return location.hasAltitude() ? Double.toString(location.getAltitude()) : null;
}
private String getBearing(Location location) {
return location.hasBearing() ? Double.toString(location.getBearing()) : null;
}
private String getAccuracy(Location location) {
return location.hasAccuracy() ? SHORT_FORMAT.format(location.getAccuracy()) : null;
}
private String getSpeed(Location location) {
return location.hasSpeed() ? SHORT_FORMAT.format(location.getSpeed()) : null;
}
/**
* Writes a single line of a CSV file.
*
* @param values the values to be written as CSV
*/
private void writeCommaSeparatedLine(String... values) {
StringBuilder builder = new StringBuilder();
boolean isFirst = true;
for (String value : values) {
if (!isFirst) {
builder.append(',');
}
isFirst = false;
builder.append('"');
if (value != null) {
builder.append(value.replaceAll("\"", "\"\""));
}
builder.append('"');
}
printWriter.println(builder.toString());
}
}
@@ -0,0 +1,239 @@
/*
* 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.export;
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.io.file.TrackFileFormat;
import com.google.android.apps.mytracks.util.LocationUtils;
import com.google.common.annotations.VisibleForTesting;
import android.content.Context;
import android.database.Cursor;
import android.location.Location;
import java.io.OutputStream;
/**
* Track Writer for writing tracks to an {@link OutputStream}.
*
* @author Sandor Dornbush
* @author Rodrigo Damazio
*/
public class FileTrackExporter extends AbstractTrackExporter {
private final MyTracksProviderUtils myTracksProviderUtils;
private final Track[] tracks;
private final TrackWriter trackWriter;
private final TrackExporterListener trackExporterListener;
/**
* Constructor.
*
* @param context the context
* @param myTracksProviderUtils the my tracks provider utils
* @param tracks the tracks
* @param trackFileFormat the track file format
* @param trackExporterListener the track export listener
*/
public FileTrackExporter(MyTracksProviderUtils myTracksProviderUtils,
Track[] tracks, TrackFileFormat trackFileFormat, Context context, boolean inZip,
TrackExporterListener trackExporterListener) {
this(myTracksProviderUtils, tracks, trackFileFormat.newTrackWriter(context, inZip),
trackExporterListener);
}
@VisibleForTesting
public FileTrackExporter(MyTracksProviderUtils myTracksProviderUtils, Track[] tracks,
TrackWriter trackWriter, TrackExporterListener trackExporterListener) {
this.myTracksProviderUtils = myTracksProviderUtils;
this.tracks = tracks;
this.trackWriter = trackWriter;
this.trackExporterListener = trackExporterListener;
}
@Override
void performWrite(OutputStream outputStream) throws InterruptedException {
trackWriter.prepare(outputStream);
trackWriter.writeHeader(tracks[0]);
long startTime = tracks[0].getTripStatistics().getStartTime();
for (int i = 0; i < tracks.length; i++) {
writeWaypoints(tracks[i]);
long offset = tracks[i].getTripStatistics().getStartTime() - startTime;
writeLocations(tracks[i], offset);
}
trackWriter.writeFooter();
trackWriter.close();
}
/**
* Writes the waypoints.
*/
private void writeWaypoints(Track track) throws InterruptedException {
/*
* TODO: Stream through the 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.
*/
boolean hasWaypoints = false;
Cursor cursor = null;
try {
cursor = myTracksProviderUtils.getWaypointCursor(
track.getId(), -1L, Constants.MAX_LOADED_WAYPOINTS_POINTS);
if (cursor != null && cursor.moveToFirst()) {
/*
* Yes, this will skip the first waypoint and that is intentional as the
* first waypoint holds the stats for the track.
*/
while (cursor.moveToNext()) {
if (Thread.interrupted()) {
throw new InterruptedException();
}
if (!hasWaypoints) {
trackWriter.writeBeginWaypoints();
hasWaypoints = true;
}
Waypoint waypoint = myTracksProviderUtils.createWaypoint(cursor);
trackWriter.writeWaypoint(waypoint);
}
}
} finally {
if (cursor != null) {
cursor.close();
}
}
if (hasWaypoints) {
trackWriter.writeEndWaypoints();
}
}
/**
* Writes the locations.
*/
private void writeLocations(Track track, long offset) throws InterruptedException {
boolean wroteTrack = false;
boolean wroteSegment = false;
boolean isLastLocationValid = false;
TrackWriterLocationFactory locationFactory = new TrackWriterLocationFactory();
LocationIterator iterator = myTracksProviderUtils.getTrackPointLocationIterator(
track.getId(), -1L, false, locationFactory);
try {
int locationNumber = 0;
while (iterator.hasNext()) {
if (Thread.interrupted()) {
throw new InterruptedException();
}
Location location = iterator.next();
setLocationTime(location, offset);
locationNumber++;
boolean isLocationValid = LocationUtils.isValidLocation(location);
boolean isSegmentValid = isLocationValid && isLastLocationValid;
if (!wroteTrack && isSegmentValid) {
// Found the first two consecutive locations that are valid
trackWriter.writeBeginTrack(track, locationFactory.lastLocation);
wroteTrack = true;
}
if (isSegmentValid) {
if (!wroteSegment) {
// Start a segment
trackWriter.writeOpenSegment();
wroteSegment = true;
// Write the previous location, which we had previously skipped
trackWriter.writeLocation(locationFactory.lastLocation);
}
// Write the current location
trackWriter.writeLocation(location);
if (trackExporterListener != null) {
trackExporterListener.onProgressUpdate(locationNumber, track.getNumberOfPoints());
}
} else {
if (wroteSegment) {
trackWriter.writeCloseSegment();
wroteSegment = false;
}
}
locationFactory.swapLocations();
isLastLocationValid = isLocationValid;
}
if (wroteSegment) {
trackWriter.writeCloseSegment();
wroteSegment = false;
}
if (wroteTrack) {
Location lastValidTrackPoint = myTracksProviderUtils.getLastValidTrackPoint(track.getId());
setLocationTime(lastValidTrackPoint, offset);
trackWriter.writeEndTrack(track, lastValidTrackPoint);
} else {
// Write an empty track
trackWriter.writeBeginTrack(track, null);
trackWriter.writeEndTrack(track, null);
}
} finally {
iterator.close();
}
}
/**
* Sets a location time.
*
* @param location the location
* @param offset the time offset
*/
private void setLocationTime(Location location, long offset) {
if (location != null) {
location.setTime(location.getTime() - offset);
}
}
/**
* Track writer location factory. Keeping the last two locations.
*
* @author Jimmy Shih
*/
private 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 tempLocation = lastLocation;
lastLocation = currentLocation;
currentLocation = tempLocation;
if (currentLocation != null) {
currentLocation.reset();
}
}
}
}
@@ -0,0 +1,191 @@
/*
* 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.export;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.io.file.TrackFileFormat;
import com.google.android.apps.mytracks.util.StringUtils;
import com.google.android.maps.mytracks.R;
import android.content.Context;
import android.location.Location;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.text.NumberFormat;
import java.util.Locale;
/**
* Write track as GPX to a file.
*
* @author Sandor Dornbush
*/
public class GpxTrackWriter implements TrackWriter {
private static final NumberFormat ELEVATION_FORMAT = NumberFormat.getInstance(Locale.US);
private static final NumberFormat COORDINATE_FORMAT = NumberFormat.getInstance(Locale.US);
static {
/*
* GPX readers expect to see fractional numbers with US-style punctuation.
* That is, they want periods for decimal points, rather than commas.
*/
ELEVATION_FORMAT.setMaximumFractionDigits(1);
ELEVATION_FORMAT.setGroupingUsed(false);
COORDINATE_FORMAT.setMaximumFractionDigits(6);
COORDINATE_FORMAT.setMaximumIntegerDigits(3);
COORDINATE_FORMAT.setGroupingUsed(false);
}
private final Context context;
private PrintWriter printWriter;
public GpxTrackWriter(Context context) {
this.context = context;
}
@Override
public String getExtension() {
return TrackFileFormat.GPX.getExtension();
}
@Override
public void prepare(OutputStream outputStream) {
this.printWriter = new PrintWriter(outputStream);
}
@Override
public void close() {
if (printWriter != null) {
printWriter.flush();
printWriter = null;
}
}
@Override
public void writeHeader(Track track) {
if (printWriter != null) {
printWriter.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
printWriter.println("<gpx");
printWriter.println("version=\"1.1\"");
printWriter.println(
"creator=\"" + context.getString(R.string.send_google_by_my_tracks, "", "") + "\"");
printWriter.println("xmlns=\"http://www.topografix.com/GPX/1/1\"");
printWriter.println(
"xmlns:topografix=\"http://www.topografix.com/GPX/Private/TopoGrafix/0/1\"");
printWriter.println("xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"");
printWriter.println("xsi:schemaLocation=\"http://www.topografix.com/GPX/1/1"
+ " http://www.topografix.com/GPX/1/1/gpx.xsd"
+ " http://www.topografix.com/GPX/Private/TopoGrafix/0/1"
+ " http://www.topografix.com/GPX/Private/TopoGrafix/0/1/topografix.xsd\">");
printWriter.println("<metadata>");
printWriter.println("<name>" + StringUtils.formatCData(track.getName()) + "</name>");
printWriter.println("<desc>" + StringUtils.formatCData(track.getDescription()) + "</desc>");
printWriter.println("</metadata>");
}
}
@Override
public void writeFooter() {
if (printWriter != null) {
printWriter.println("</gpx>");
}
}
@Override
public void writeBeginTrack(Track track, Location firstLocation) {
if (printWriter != null) {
printWriter.println("<trk>");
printWriter.println("<name>" + StringUtils.formatCData(track.getName()) + "</name>");
printWriter.println("<desc>" + StringUtils.formatCData(track.getDescription()) + "</desc>");
printWriter.println("<type>" + StringUtils.formatCData(track.getCategory()) + "</type>");
printWriter.println("<extensions><topografix:color>c0c0c0</topografix:color></extensions>");
}
}
@Override
public void writeEndTrack(Track track, Location lastLocation) {
if (printWriter != null) {
printWriter.println("</trk>");
}
}
@Override
public void writeOpenSegment() {
printWriter.println("<trkseg>");
}
@Override
public void writeCloseSegment() {
printWriter.println("</trkseg>");
}
@Override
public void writeLocation(Location location) {
if (printWriter != null) {
printWriter.println("<trkpt " + formatLocation(location) + ">");
if (location.hasAltitude()) {
printWriter.println("<ele>" + ELEVATION_FORMAT.format(location.getAltitude()) + "</ele>");
}
printWriter.println(
"<time>" + StringUtils.formatDateTimeIso8601(location.getTime()) + "</time>");
printWriter.println("</trkpt>");
}
}
@Override
public void writeBeginWaypoints() {
// Do nothing
}
@Override
public void writeEndWaypoints() {
// Do nothing
}
@Override
public void writeWaypoint(Waypoint waypoint) {
if (printWriter != null) {
Location location = waypoint.getLocation();
if (location != null) {
printWriter.println("<wpt " + formatLocation(location) + ">");
if (location.hasAltitude()) {
printWriter.println("<ele>" + ELEVATION_FORMAT.format(location.getAltitude()) + "</ele>");
}
printWriter.println(
"<time>" + StringUtils.formatDateTimeIso8601(location.getTime()) + "</time>");
printWriter.println("<name>" + StringUtils.formatCData(waypoint.getName()) + "</name>");
printWriter.println("<cmt>" + StringUtils.formatCData(waypoint.getType().name()) + "</cmt>");
printWriter.println(
"<desc>" + StringUtils.formatCData(waypoint.getDescription()) + "</desc>");
printWriter.println("<type>" + StringUtils.formatCData(waypoint.getCategory()) + "</type>");
printWriter.println("</wpt>");
}
}
}
/**
* Formats a location with latitude and longitude coordinates.
*
* @param location the location
*/
private String formatLocation(Location location) {
return "lat=\"" + COORDINATE_FORMAT.format(location.getLatitude()) + "\" lon=\""
+ COORDINATE_FORMAT.format(location.getLongitude()) + "\"";
}
}
@@ -0,0 +1,471 @@
/*
* 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.export;
import com.google.android.apps.mytracks.content.DescriptionGenerator;
import com.google.android.apps.mytracks.content.DescriptionGeneratorImpl;
import com.google.android.apps.mytracks.content.MyTracksLocation;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.content.Sensor;
import com.google.android.apps.mytracks.content.Sensor.SensorData;
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.content.Waypoint.WaypointType;
import com.google.android.apps.mytracks.io.file.TrackFileFormat;
import com.google.android.apps.mytracks.util.GoogleEarthUtils;
import com.google.android.apps.mytracks.util.StringUtils;
import com.google.android.maps.mytracks.R;
import com.google.common.annotations.VisibleForTesting;
import android.content.Context;
import android.database.Cursor;
import android.location.Location;
import android.net.Uri;
import java.io.File;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.util.ArrayList;
/**
* Write track as KML to a file.
*
* @author Leif Hendrik Wilden
*/
public class KmlTrackWriter implements TrackWriter {
private static final String WAYPOINT_STYLE = "waypoint";
private static final String STATISTICS_STYLE = "statistics";
private static final String START_STYLE = "start";
private static final String END_STYLE = "end";
private static final String TRACK_STYLE = "track";
private static final String SCHEMA_ID = "schema";
private static final String CADENCE = "cadence";
private static final String HEART_RATE = "heart_rate";
private static final String POWER = "power";
private static final String
WAYPOINT_ICON = "http://maps.google.com/mapfiles/kml/pushpin/blue-pushpin.png";
private static final String
STATISTICS_ICON = "http://maps.google.com/mapfiles/kml/pushpin/ylw-pushpin.png";
private static final String
START_ICON = "http://maps.google.com/mapfiles/kml/paddle/grn-circle.png";
private static final String
END_ICON = "http://maps.google.com/mapfiles/kml/paddle/red-circle.png";
private static final String
TRACK_ICON = "http://earth.google.com/images/kml-icons/track-directional/track-0.png";
private final Context context;
private final boolean inKmz;
private final DescriptionGenerator descriptionGenerator;
private final MyTracksProviderUtils myTracksProviderUtils;
private PrintWriter printWriter;
private ArrayList<Integer> powerList = new ArrayList<Integer>();
private ArrayList<Integer> cadenceList = new ArrayList<Integer>();
private ArrayList<Integer> heartRateList = new ArrayList<Integer>();
private boolean hasPower;
private boolean hasCadence;
private boolean hasHeartRate;
public KmlTrackWriter(Context context, boolean inKmz) {
this(context, inKmz, new DescriptionGeneratorImpl(context));
}
@VisibleForTesting
KmlTrackWriter(Context context, boolean inKmz, DescriptionGenerator descriptionGenerator) {
this.context = context;
this.inKmz = inKmz;
this.descriptionGenerator = descriptionGenerator;
this.myTracksProviderUtils = MyTracksProviderUtils.Factory.get(context);
}
@Override
public String getExtension() {
return TrackFileFormat.KML.getExtension();
}
@Override
public void prepare(OutputStream outputStream) {
this.printWriter = new PrintWriter(outputStream);
}
@Override
public void close() {
if (printWriter != null) {
printWriter.flush();
printWriter = null;
}
}
@Override
public void writeHeader(Track track) {
if (printWriter != null) {
printWriter.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
printWriter.println("<kml xmlns=\"http://www.opengis.net/kml/2.2\"");
printWriter.println("xmlns:gx=\"http://www.google.com/kml/ext/2.2\"");
printWriter.println("xmlns:atom=\"http://www.w3.org/2005/Atom\">");
printWriter.println("<Document>");
printWriter.println("<open>1</open>");
printWriter.println("<visibility>1</visibility>");
printWriter.println("<name>" + StringUtils.formatCData(track.getName()) + "</name>");
printWriter.println("<atom:author><atom:name>"
+ StringUtils.formatCData(context.getString(R.string.send_google_by_my_tracks, "", ""))
+ "</atom:name></atom:author>");
writeTrackStyle();
writePlacemarkerStyle(START_STYLE, START_ICON, 32, 1);
writePlacemarkerStyle(END_STYLE, END_ICON, 32, 1);
writePlacemarkerStyle(STATISTICS_STYLE, STATISTICS_ICON, 20, 2);
writePlacemarkerStyle(WAYPOINT_STYLE, WAYPOINT_ICON, 20, 2);
printWriter.println("<Schema id=\"" + SCHEMA_ID + "\">");
writeSensorStyle(POWER, context.getString(R.string.description_sensor_power));
writeSensorStyle(CADENCE, context.getString(R.string.description_sensor_cadence));
writeSensorStyle(HEART_RATE, context.getString(R.string.description_sensor_heart_rate));
printWriter.println("</Schema>");
}
}
@Override
public void writeFooter() {
if (printWriter != null) {
printWriter.println("</Document>");
printWriter.println("</kml>");
}
}
@Override
public void writeBeginWaypoints() {
if (printWriter != null) {
printWriter.println("<Folder><name>"
+ StringUtils.formatCData(context.getString(R.string.menu_markers)) + "</name>");
printWriter.println("<open>1</open>");
}
}
@Override
public void writeEndWaypoints() {
if (printWriter != null) {
printWriter.println("</Folder>");
}
}
@Override
public void writeWaypoint(Waypoint waypoint) {
if (printWriter != null) {
String styleName = waypoint.getType() == WaypointType.STATISTICS ? STATISTICS_STYLE
: WAYPOINT_STYLE;
String photoUrl = waypoint.getPhotoUrl();
if (photoUrl != null && !photoUrl.equals("")) {
float heading = getHeading(waypoint.getTrackId(), waypoint.getLocation());
writePhotoOverlay(waypoint.getName(), waypoint.getCategory(), waypoint.getDescription(),
styleName, waypoint.getLocation(), photoUrl, heading);
} else {
writePlacemark(waypoint.getName(), waypoint.getCategory(), waypoint.getDescription(),
styleName, waypoint.getLocation());
}
}
}
@Override
public void writeBeginTrack(Track track, Location firstLocation) {
if (printWriter != null) {
String name = context.getString(R.string.marker_label_start, track.getName());
writePlacemark(name, "", "", START_STYLE, firstLocation);
printWriter.println("<Placemark id=\"" + GoogleEarthUtils.TOUR_FEATURE_ID_VALUE + "\">");
printWriter.println("<name>" + StringUtils.formatCData(track.getName()) + "</name>");
printWriter.println(
"<description>" + StringUtils.formatCData(track.getDescription()) + "</description>");
printWriter.println("<styleUrl>#" + TRACK_STYLE + "</styleUrl>");
writeCategory(track.getCategory());
printWriter.println("<gx:MultiTrack>");
printWriter.println("<altitudeMode>absolute</altitudeMode>");
printWriter.println("<gx:interpolate>1</gx:interpolate>");
}
}
@Override
public void writeEndTrack(Track track, Location lastLocation) {
if (printWriter != null) {
printWriter.println("</gx:MultiTrack>");
printWriter.println("</Placemark>");
String name = context.getString(R.string.marker_label_end, track.getName());
String description = descriptionGenerator.generateTrackDescription(track, null, null, false);
writePlacemark(name, "", description, END_STYLE, lastLocation);
}
}
@Override
public void writeOpenSegment() {
if (printWriter != null) {
printWriter.println("<gx:Track>");
hasPower = false;
hasCadence = false;
hasHeartRate = false;
powerList.clear();
cadenceList.clear();
heartRateList.clear();
}
}
@Override
public void writeCloseSegment() {
if (printWriter != null) {
printWriter.println("<ExtendedData>");
printWriter.println("<SchemaData schemaUrl=\"#" + SCHEMA_ID + "\">");
if (hasPower) {
writeSensorData(powerList, POWER);
}
if (hasCadence) {
writeSensorData(cadenceList, CADENCE);
}
if (hasHeartRate) {
writeSensorData(heartRateList, HEART_RATE);
}
printWriter.println("</SchemaData>");
printWriter.println("</ExtendedData>");
printWriter.println("</gx:Track>");
}
}
@Override
public void writeLocation(Location location) {
if (printWriter != null) {
printWriter.println(
"<when>" + StringUtils.formatDateTimeIso8601(location.getTime()) + "</when>");
printWriter.println("<gx:coord>" + getCoordinates(location, " ") + "</gx:coord>");
if (location instanceof MyTracksLocation) {
SensorDataSet sensorDataSet = ((MyTracksLocation) location).getSensorDataSet();
int power = -1;
int cadence = -1;
int heartRate = -1;
if (sensorDataSet != null) {
if (sensorDataSet.hasPower()) {
SensorData sensorData = sensorDataSet.getPower();
if (sensorData.hasValue() && sensorData.getState() == Sensor.SensorState.SENDING) {
hasPower = true;
power = sensorData.getValue();
}
}
if (sensorDataSet.hasCadence()) {
SensorData sensorData = sensorDataSet.getCadence();
if (sensorData.hasValue() && sensorData.getState() == Sensor.SensorState.SENDING) {
hasCadence = true;
cadence = sensorData.getValue();
}
}
if (sensorDataSet.hasHeartRate()) {
SensorData sensorData = sensorDataSet.getHeartRate();
if (sensorData.hasValue() && sensorData.getState() == Sensor.SensorState.SENDING) {
hasHeartRate = true;
heartRate = sensorData.getValue();
}
}
}
powerList.add(power);
cadenceList.add(cadence);
heartRateList.add(heartRate);
}
}
}
/**
* Writes the sensor data.
*
* @param list a list of sensor data
* @param name the name of the sensor data
*/
private void writeSensorData(ArrayList<Integer> list, String name) {
printWriter.println("<gx:SimpleArrayData name=\"" + name + "\">");
for (int i = 0; i < list.size(); i++) {
printWriter.println("<gx:value>" + list.get(i) + "</gx:value>");
}
printWriter.println("</gx:SimpleArrayData>");
}
/**
* Writes a placemark.
*
* @param name the name
* @param category the category
* @param description the description
* @param styleName the style name
* @param location the location
*/
private void writePlacemark(
String name, String category, String description, String styleName, Location location) {
if (location != null) {
printWriter.println("<Placemark>");
printWriter.println("<name>" + StringUtils.formatCData(name) + "</name>");
printWriter.println(
"<description>" + StringUtils.formatCData(description) + "</description>");
printWriter.println("<TimeStamp><when>"
+ StringUtils.formatDateTimeIso8601(location.getTime()) + "</when></TimeStamp>");
printWriter.println("<styleUrl>#" + styleName + "</styleUrl>");
writeCategory(category);
printWriter.println("<Point>");
printWriter.println("<coordinates>" + getCoordinates(location, ",") + "</coordinates>");
printWriter.println("</Point>");
printWriter.println("</Placemark>");
}
}
/**
* Writes a photo overlay.
*
* @param name the name
* @param category the category
* @param description the description
* @param styleName the style name
* @param location the location
* @param photoUrl the photo url
* @param heading the heading
*/
private void writePhotoOverlay(String name, String category, String description, String styleName,
Location location, String photoUrl, float heading) {
if (location != null) {
printWriter.println("<PhotoOverlay>");
printWriter.println("<name>" + StringUtils.formatCData(name) + "</name>");
printWriter.println(
"<description>" + StringUtils.formatCData(description) + "</description>");
printWriter.print("<Camera>");
printWriter.print("<longitude>" + location.getLongitude() + "</longitude>");
printWriter.print("<latitude>" + location.getLatitude() + "</latitude>");
printWriter.print("<altitude>20</altitude>");
printWriter.print("<heading>" + heading + "</heading>");
printWriter.print("<tilt>90</tilt>");
printWriter.println("</Camera>");
printWriter.println("<TimeStamp><when>"
+ StringUtils.formatDateTimeIso8601(location.getTime()) + "</when></TimeStamp>");
printWriter.println("<styleUrl>#" + styleName + "</styleUrl>");
writeCategory(category);
if (inKmz) {
Uri uri = Uri.parse(photoUrl);
printWriter.println("<Icon><href>" + KmzTrackExporter.KMZ_IMAGES_DIR + File.separatorChar
+ uri.getLastPathSegment() + "</href></Icon>");
} else {
printWriter.println("<Icon><href>" + Uri.decode(photoUrl) + "</href></Icon>");
}
printWriter.print("<ViewVolume>");
printWriter.print("<near>10</near>");
printWriter.print("<leftFov>-60</leftFov>");
printWriter.print("<rightFov>60</rightFov>");
printWriter.print("<bottomFov>-45</bottomFov>");
printWriter.print("<topFov>45</topFov>");
printWriter.println("</ViewVolume>");
printWriter.println("<Point>");
printWriter.println("<coordinates>" + getCoordinates(location, ",") + "</coordinates>");
printWriter.println("</Point>");
printWriter.println("</PhotoOverlay>");
}
}
/**
* Gets the heading to a location.
*
* @param trackId the track id containing the location
* @param location the location
*/
private float getHeading(long trackId, Location location) {
long trackPointId = myTracksProviderUtils.getTrackPointId(trackId, location);
if (trackPointId == -1L) {
return location.getBearing();
}
Cursor cursor = null;
Location viewLocation;
try {
cursor = myTracksProviderUtils.getTrackPointCursor(trackId, trackPointId, 10, true);
if (cursor == null || cursor.getCount() == 0) {
return location.getBearing();
}
cursor.moveToPosition(cursor.getCount() - 1);
viewLocation = myTracksProviderUtils.createTrackPoint(cursor);
} finally {
if (cursor != null) {
cursor.close();
}
}
return viewLocation.bearingTo(location);
}
private String getCoordinates(Location location, String separator) {
StringBuffer buffer = new StringBuffer();
buffer.append(location.getLongitude() + separator + location.getLatitude());
if (location.hasAltitude()) {
buffer.append(separator + location.getAltitude());
}
return buffer.toString();
}
/**
* Writes the category.
*
* @param category the category
*/
private void writeCategory(String category) {
if (category == null || category.equals("")) {
return;
}
printWriter.println("<ExtendedData>");
printWriter.println(
"<Data name=\"type\"><value>" + StringUtils.formatCData(category) + "</value></Data>");
printWriter.println("</ExtendedData>");
}
/**
* Writes the track style.
*/
private void writeTrackStyle() {
printWriter.println("<Style id=\"" + TRACK_STYLE + "\">");
printWriter.println("<LineStyle><color>7f0000ff</color><width>4</width></LineStyle>");
printWriter.println("<IconStyle>");
printWriter.println("<scale>1.3</scale>");
printWriter.println("<Icon><href>" + TRACK_ICON + "</href></Icon>");
printWriter.println("</IconStyle>");
printWriter.println("</Style>");
}
/**
* Writes a placemarker style.
*
* @param name the name of the style
* @param url the url of the style icon
* @param x the x position of the hotspot
* @param y the y position of the hotspot
*/
private void writePlacemarkerStyle(String name, String url, int x, int y) {
printWriter.println("<Style id=\"" + name + "\"><IconStyle>");
printWriter.println("<scale>1.3</scale>");
printWriter.println("<Icon><href>" + url + "</href></Icon>");
printWriter.println(
"<hotSpot x=\"" + x + "\" y=\"" + y + "\" xunits=\"pixels\" yunits=\"pixels\"/>");
printWriter.println("</IconStyle></Style>");
}
/**
* Writes a sensor style.
*
* @param name the name of the sesnor
* @param displayName the sensor display name
*/
private void writeSensorStyle(String name, String displayName) {
printWriter.println("<gx:SimpleArrayField name=\"" + name + "\" type=\"int\">");
printWriter.println("<displayName>" + StringUtils.formatCData(displayName) + "</displayName>");
printWriter.println("</gx:SimpleArrayField>");
}
}
@@ -0,0 +1,143 @@
/*
* Copyright 2013 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.export;
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 android.database.Cursor;
import android.net.Uri;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
/**
* KMZ track exporter.
*
* @author Jimmy Shih
*/
public class KmzTrackExporter extends AbstractTrackExporter {
public static final String KMZ_EXTENSION = "kmz";
public static final String KMZ_IMAGES_DIR = "images";
public static final String KMZ_KML_FILE = "doc.kml";
private static final int BUFFER_SIZE = 4096;
private final MyTracksProviderUtils myTracksProviderUtils;
private final FileTrackExporter fileTrackExporter;
private final Track[] tracks;
/**
* Constructor.
*
* @param myTracksProviderUtils the my tracks provider utils
* @param fileTrackExporter the file track exporter
* @param tracks the tracks to export
*/
public KmzTrackExporter(MyTracksProviderUtils myTracksProviderUtils,
FileTrackExporter fileTrackExporter, Track[] tracks) {
this.myTracksProviderUtils = myTracksProviderUtils;
this.fileTrackExporter = fileTrackExporter;
this.tracks = tracks;
}
@Override
public boolean isSuccess() {
return fileTrackExporter.isSuccess() && super.isSuccess();
}
@Override
void performWrite(OutputStream outputStream) throws InterruptedException, IOException {
ZipOutputStream zipOutputStream = null;
try {
zipOutputStream = new ZipOutputStream(outputStream);
// Add kml file
ZipEntry zipEntry = new ZipEntry(KMZ_KML_FILE);
zipOutputStream.putNextEntry(zipEntry);
fileTrackExporter.writeTrack(zipOutputStream);
zipOutputStream.closeEntry();
if (!fileTrackExporter.isSuccess()) {
throw new IOException();
}
// Add photos
addImages(zipOutputStream);
} finally {
if (zipOutputStream != null) {
zipOutputStream.close();
}
}
}
private void addImages(ZipOutputStream zipOutputStream) throws InterruptedException, IOException {
for (Track track : tracks) {
Cursor cursor = null;
try {
cursor = myTracksProviderUtils.getWaypointCursor(track.getId(), -1L, -1);
if (cursor != null && cursor.moveToFirst()) {
/*
* Yes, this will skip the first waypoint and that is intentional as
* the first waypoint holds the stats for the track.
*/
while (cursor.moveToNext()) {
if (Thread.interrupted()) {
throw new InterruptedException();
}
Waypoint waypoint = myTracksProviderUtils.createWaypoint(cursor);
String photoUrl = waypoint.getPhotoUrl();
if (photoUrl != null && !photoUrl.equals("")) {
addImage(zipOutputStream, photoUrl);
}
}
}
} finally {
if (cursor != null) {
cursor.close();
}
}
}
}
private void addImage(ZipOutputStream zipOutputStream, String photoUrl) throws IOException {
Uri uri = Uri.parse(photoUrl);
ZipEntry zipEntry = new ZipEntry(
KMZ_IMAGES_DIR + File.separatorChar + uri.getLastPathSegment());
zipOutputStream.putNextEntry(zipEntry);
FileInputStream fileInputStream = null;
try {
fileInputStream = new FileInputStream(new File(uri.getPath()));
byte[] buffer = new byte[BUFFER_SIZE];
int byteCount = 0;
while ((byteCount = fileInputStream.read(buffer)) != -1) {
zipOutputStream.write(buffer, 0, byteCount);
}
} finally {
if (fileInputStream != null) {
fileInputStream.close();
}
}
zipOutputStream.closeEntry();
}
}
@@ -0,0 +1,226 @@
/*
* 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.export;
import com.google.android.apps.mytracks.io.file.TrackFileFormat;
import com.google.android.apps.mytracks.util.DialogUtils;
import com.google.android.apps.mytracks.util.FileUtils;
import com.google.android.apps.mytracks.util.GoogleEarthUtils;
import com.google.android.apps.mytracks.util.IntentUtils;
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.Intent;
import android.os.Bundle;
import android.widget.Toast;
import java.io.File;
/**
* An activity for saving tracks to the external storage. If saving a specific
* track, option to save it to a temp directory and play the track afterward.
*
* @author Rodrigo Damazio
*/
public class SaveActivity extends Activity {
public static final String EXTRA_TRACK_FILE_FORMAT = "track_file_format";
public static final String EXTRA_TRACK_IDS = "track_ids";
public static final String EXTRA_PLAY_TRACK = "play_track";
private static final int DIALOG_PROGRESS_ID = 0;
private static final int DIALOG_RESULT_ID = 1;
private TrackFileFormat trackFileFormat;
private long[] trackIds;
private boolean playTrack;
private String directoryDisplayName;
private SaveAsyncTask saveAsyncTask;
private ProgressDialog progressDialog;
// the number of tracks successfully saved
private int successCount;
// the number of tracks to save
private int totalCount;
// the last successfully saved path
private String savedPath;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Intent intent = getIntent();
trackFileFormat = intent.getParcelableExtra(EXTRA_TRACK_FILE_FORMAT);
trackIds = intent.getLongArrayExtra(EXTRA_TRACK_IDS);
playTrack = intent.getBooleanExtra(EXTRA_PLAY_TRACK, false);
if (!FileUtils.isExternalStorageWriteable()) {
Toast.makeText(this, R.string.external_storage_not_writable, Toast.LENGTH_LONG).show();
finish();
return;
}
directoryDisplayName = playTrack ? FileUtils.getDirectoryDisplayName(
trackFileFormat.getExtension(), FileUtils.TEMP_DIR)
: FileUtils.getDirectoryDisplayName(trackFileFormat.getExtension());
String directoryPath = playTrack ? FileUtils.getDirectoryPath(
trackFileFormat.getExtension(), FileUtils.TEMP_DIR)
: FileUtils.getDirectoryPath(trackFileFormat.getExtension());
File directory = new File(directoryPath);
if (!FileUtils.ensureDirectoryExists(directory)) {
Toast.makeText(this, R.string.external_storage_not_writable, Toast.LENGTH_LONG).show();
finish();
return;
}
Object retained = getLastNonConfigurationInstance();
if (retained instanceof SaveAsyncTask) {
saveAsyncTask = (SaveAsyncTask) retained;
saveAsyncTask.setActivity(this);
} else {
saveAsyncTask = new SaveAsyncTask(this, trackFileFormat, trackIds, directory, playTrack);
saveAsyncTask.execute();
}
}
@Override
public Object onRetainNonConfigurationInstance() {
saveAsyncTask.setActivity(null);
return saveAsyncTask;
}
@Override
protected Dialog onCreateDialog(int id) {
switch (id) {
case DIALOG_PROGRESS_ID:
progressDialog = DialogUtils.createHorizontalProgressDialog(this,
R.string.export_external_storage_progress_message,
new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
saveAsyncTask.cancel(true);
dialog.dismiss();
}
}, directoryDisplayName);
return progressDialog;
case DIALOG_RESULT_ID:
int iconId;
int titleId;
String message;
String totalTracks = getResources()
.getQuantityString(R.plurals.tracks, totalCount, totalCount);
if (successCount == totalCount) {
if (totalCount == 0) {
iconId = android.R.drawable.ic_dialog_info;
titleId = R.string.export_external_storage_no_track_title;
message = getString(R.string.export_external_storage_no_track);
} else {
iconId = R.drawable.ic_dialog_success;
titleId = R.string.generic_success_title;
message = getString(
R.string.export_external_storage_success, totalTracks, directoryDisplayName);
}
} else {
iconId = android.R.drawable.ic_dialog_alert;
titleId = R.string.generic_error_title;
message = getString(R.string.export_external_storage_error, successCount, totalTracks,
directoryDisplayName);
}
AlertDialog.Builder builder = new AlertDialog.Builder(this).setCancelable(true)
.setIcon(iconId).setMessage(message)
.setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
dialog.dismiss();
finish();
}
}).setPositiveButton(R.string.generic_ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int arg1) {
dialog.dismiss();
finish();
}
}).setTitle(titleId);
if (!playTrack && trackIds.length == 1 && trackIds[0] != -1L && successCount == totalCount
&& savedPath != null) {
builder.setNegativeButton(
R.string.share_track_share_file, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
Intent intent = IntentUtils.newShareFileIntent(
SaveActivity.this, trackIds[0], savedPath, trackFileFormat);
startActivity(
Intent.createChooser(intent, getString(R.string.share_track_picker_title)));
finish();
}
});
}
return builder.create();
default:
return null;
}
}
/**
* Invokes when the associated AsyncTask completes.
*
* @param aSuccessCount the number of tracks successfully saved
* @param aTotalCount the number of tracks to save
* @param aSavedPath the last successfully saved path
*/
public void onAsyncTaskCompleted(int aSuccessCount, int aTotalCount, String aSavedPath) {
successCount = aSuccessCount;
totalCount = aTotalCount;
savedPath = aSavedPath;
removeDialog(DIALOG_PROGRESS_ID);
if (playTrack && successCount == 1 && totalCount == 1 && savedPath != null) {
startActivity(GoogleEarthUtils.getPlayInEarthIntent(savedPath));
finish();
} else {
showDialog(DIALOG_RESULT_ID);
}
}
/**
* Shows the progress dialog.
*/
public void showProgressDialog() {
showDialog(DIALOG_PROGRESS_ID);
}
/**
* Sets the progress dialog value.
*
* @param number the number of points saved
* @param max the maximum number of points
*/
public void setProgressDialogValue(int number, int max) {
if (progressDialog != null) {
progressDialog.setIndeterminate(false);
progressDialog.setMax(max);
progressDialog.setProgress(Math.min(number, max));
}
}
}
@@ -0,0 +1,272 @@
/*
* Copyright 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.io.file.export;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.content.TracksColumns;
import com.google.android.apps.mytracks.io.file.TrackFileFormat;
import com.google.android.apps.mytracks.util.FileUtils;
import com.google.android.apps.mytracks.util.PreferencesUtils;
import com.google.android.apps.mytracks.util.SystemUtils;
import com.google.android.maps.mytracks.R;
import android.content.Context;
import android.database.Cursor;
import android.os.AsyncTask;
import android.os.PowerManager.WakeLock;
import android.util.Log;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
/**
* Async Task to save tracks to the external storage.
*
* @author Jimmy Shih
*/
public class SaveAsyncTask extends AsyncTask<Void, Integer, Boolean> {
private static final String TAG = SaveAsyncTask.class.getSimpleName();
private SaveActivity saveActivity;
private final TrackFileFormat trackFileFormat;
private final long[] trackIds;
private final File directory;
private final boolean playTrack;
private final Context context;
private final MyTracksProviderUtils myTracksProviderUtils;
private WakeLock wakeLock;
private TrackExporter trackExporter;
// true if the AsyncTask has completed
private boolean completed;
// the number of tracks successfully saved
private int successCount;
// the number of tracks to save
private int totalCount;
// the last successfully saved path
private String savedPath;
/**
* Creates an AsyncTask.
*
* @param saveActivity the activity currently associated with this task
* @param trackFileFormat the track file format
* @param trackIds the track ids to save. To save all, set to size 1 with
* trackIds[0] == -1L
* @param directory the directory to save to
* @param playTrack true to play track
*/
public SaveAsyncTask(SaveActivity saveActivity, TrackFileFormat trackFileFormat, long[] trackIds,
File directory, boolean playTrack) {
this.saveActivity = saveActivity;
this.trackFileFormat = trackFileFormat;
this.trackIds = trackIds;
this.directory = directory;
this.playTrack = playTrack;
context = saveActivity.getApplicationContext();
myTracksProviderUtils = MyTracksProviderUtils.Factory.get(context);
completed = false;
successCount = 0;
totalCount = 0;
savedPath = null;
}
/**
* Sets the current activity associated with this AyncTask.
*
* @param saveActivity the current activity, can be null
*/
public void setActivity(SaveActivity saveActivity) {
this.saveActivity = saveActivity;
if (completed && saveActivity != null) {
saveActivity.onAsyncTaskCompleted(successCount, totalCount, savedPath);
}
}
@Override
protected void onPreExecute() {
if (saveActivity != null) {
saveActivity.showProgressDialog();
}
}
@Override
protected Boolean doInBackground(Void... params) {
try {
boolean isRecording = PreferencesUtils.getLong(saveActivity, R.string.recording_track_id_key)
!= PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
boolean isPaused = PreferencesUtils.getBoolean(saveActivity,
R.string.recording_track_paused_key, PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT);
// Get the wake lock if not recording or paused
if (!isRecording || isPaused) {
wakeLock = SystemUtils.acquireWakeLock(saveActivity, wakeLock);
}
if (trackIds.length == 1 && trackIds[0] == -1L) {
return saveAllTracks();
} else {
totalCount = 1;
Track[] tracks = new Track[trackIds.length];
for (int i = 0; i < trackIds.length; i++) {
tracks[i] = myTracksProviderUtils.getTrack(trackIds[i]);
if (tracks[i] == null) {
Log.d(TAG, "No track for " + trackIds[i]);
return false;
}
}
if (saveTracks(tracks)) {
successCount = 1;
return true;
} else {
return false;
}
}
} finally {
if (wakeLock != null && wakeLock.isHeld()) {
wakeLock.release();
}
}
}
@Override
protected void onProgressUpdate(Integer... values) {
if (saveActivity != null) {
saveActivity.setProgressDialogValue(values[0], values[1]);
}
}
@Override
protected void onPostExecute(Boolean result) {
completed = true;
if (saveActivity != null) {
saveActivity.onAsyncTaskCompleted(successCount, totalCount, savedPath);
}
}
@Override
protected void onCancelled() {
completed = true;
if (saveActivity != null) {
saveActivity.onAsyncTaskCompleted(successCount, totalCount, null);
}
}
/**
* Saves tracks to one file.
*
* @param tracks the tracks
*/
private Boolean saveTracks(Track[] tracks) {
if (tracks.length == 0) {
return false;
}
Track track = tracks[0];
boolean useKmz = !playTrack && trackFileFormat == TrackFileFormat.KML;
String extension = useKmz ? KmzTrackExporter.KMZ_EXTENSION : trackFileFormat.getExtension();
// Make sure the file doesn't exist yet (possibly by changing the filename)
String fileName = FileUtils.buildUniqueFileName(directory, track.getName(), extension);
if (fileName == null) {
Log.d(TAG, "Unable to get a unique filename for " + track.getName());
return false;
}
FileTrackExporter fileTrackExporter = new FileTrackExporter(myTracksProviderUtils, tracks,
trackFileFormat, context, useKmz, new TrackExporterListener() {
@Override
public void onProgressUpdate(int number, int max) {
/*
* If only saving one track, update the progress dialog once every
* 500 points
*/
if (trackIds.length == 1 && trackIds[0] != -1L && number % 500 == 0) {
publishProgress(number, max);
}
}
});
trackExporter = useKmz ? new KmzTrackExporter(myTracksProviderUtils, fileTrackExporter, tracks)
: fileTrackExporter;
File file = new File(directory, fileName);
FileOutputStream fileOutputStream = null;
try {
fileOutputStream = new FileOutputStream(file);
trackExporter.writeTrack(fileOutputStream);
if (trackExporter.isSuccess()) {
savedPath = file.getAbsolutePath();
return true;
} else {
if (!file.delete()) {
Log.w(TAG, "Failed to delete file " + file.getAbsolutePath());
}
return false;
}
} catch (FileNotFoundException e) {
Log.e(TAG, "Unable to open file " + file.getName(), e);
return false;
} finally {
if (fileOutputStream != null) {
try {
fileOutputStream.close();
} catch (IOException e) {
Log.e(TAG, "Unable to close file output stream", e);
}
}
}
}
/**
* Saves all the tracks.
*/
private Boolean saveAllTracks() {
Cursor cursor = null;
try {
cursor = myTracksProviderUtils.getTrackCursor(null, null, TracksColumns._ID);
if (cursor == null) {
return false;
}
totalCount = cursor.getCount();
for (int i = 0; i < totalCount; i++) {
if (isCancelled()) {
return false;
}
cursor.moveToPosition(i);
Track track = myTracksProviderUtils.createTrack(cursor);
if (track != null && saveTracks(new Track[] { track })) {
successCount++;
}
publishProgress(i + 1, totalCount);
}
return true;
} finally {
if (cursor != null) {
cursor.close();
}
}
}
}
@@ -0,0 +1,330 @@
/*
* 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.export;
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.TrackFileFormat;
import com.google.android.apps.mytracks.util.StringUtils;
import com.google.android.apps.mytracks.util.SystemUtils;
import com.google.android.maps.mytracks.R;
import android.content.Context;
import android.location.Location;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.util.Locale;
/**
* Write track as TCX to a file. See http://developer.garmin.com/schemas/tcx/v2/
* for info on TCX. <br>
* The TCX file output is verified by uploading the file to
* http://connect.garmin.com/.
*
* @author Sandor Dornbush
* @author Dominik Ršttsches
*/
public class TcxTrackWriter implements TrackWriter {
/**
* TCX sport type. See the TCX spec.
*
* @author Jimmy Shih
*/
private enum SportType {
RUNNING("Running"),
BIKING("Biking"),
OTHER("Other");
private final String name;
private SportType(String name) {
this.name = name;
}
/**
* Gets the name of the sport type
*/
public String getName() {
return name;
}
}
// My Tracks categories that are considered as TCX biking sport type.
private static final int TCX_SPORT_BIKING_IDS[] = {
R.string.activity_type_cycling,
R.string.activity_type_dirt_bike,
R.string.activity_type_mountain_biking,
R.string.activity_type_road_biking,
R.string.activity_type_track_cycling };
// My Tracks categories that are considered as TCX running sport type.
private static final int TCX_SPORT_RUNNING_IDS[] = {
R.string.activity_type_running,
R.string.activity_type_speed_walking,
R.string.activity_type_street_running,
R.string.activity_type_track_running,
R.string.activity_type_trail_running,
R.string.activity_type_walking };
private final Context context;
private PrintWriter printWriter;
private SportType sportType;
public TcxTrackWriter(Context context) {
this.context = context;
}
@Override
public void prepare(OutputStream outputStream) {
this.printWriter = new PrintWriter(outputStream);
}
@Override
public void close() {
if (printWriter != null) {
printWriter.flush();
printWriter = null;
}
}
@Override
public String getExtension() {
return TrackFileFormat.TCX.getExtension();
}
@Override
public void writeHeader(Track track) {
if (printWriter != null) {
printWriter.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
printWriter.println("<TrainingCenterDatabase"
+ " xmlns=\"http://www.garmin.com/xmlschemas/TrainingCenterDatabase/v2\"");
printWriter.println("xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"");
printWriter.println("xsi:schemaLocation="
+ "\"http://www.garmin.com/xmlschemas/TrainingCenterDatabase/v2"
+ " http://www.garmin.com/xmlschemas/TrainingCenterDatabasev2.xsd\">");
}
}
@Override
public void writeFooter() {
if (printWriter != null) {
printWriter.println("<Author xsi:type=\"Application_t\">");
printWriter.println("<Name>"
+ StringUtils.formatCData(context.getString(R.string.send_google_by_my_tracks, "", ""))
+ "</Name>");
// <Build>, <LangID>, and <PartNumber> are required by type=Application_t.
printWriter.println("<Build>");
writeVersion();
printWriter.println("</Build>");
printWriter.println("<LangID>" + Locale.getDefault().getLanguage() + "</LangID>");
printWriter.println("<PartNumber>000-00000-00</PartNumber>");
printWriter.println("</Author>");
printWriter.println("</TrainingCenterDatabase>");
}
}
@Override
public void writeBeginTrack(Track track, Location firstPoint) {
sportType = getSportType(track.getCategory());
if (printWriter != null) {
String startTime = StringUtils.formatDateTimeIso8601(
track.getTripStatistics().getStartTime());
long totalTimeInSeconds = track.getTripStatistics().getTotalTime() / 1000;
printWriter.println("<Activities>");
printWriter.println("<Activity Sport=\"" + sportType.getName() + "\">");
printWriter.println("<Id>" + startTime + "</Id>");
printWriter.println("<Lap StartTime=\"" + startTime + "\">");
printWriter.println("<TotalTimeSeconds>" + totalTimeInSeconds + "</TotalTimeSeconds>");
printWriter.println("<DistanceMeters>" + track.getTripStatistics().getTotalDistance()
+ "</DistanceMeters>");
// <Calories> is required, just put in 0.
printWriter.println("<Calories>0</Calories>");
printWriter.println("<Intensity>Active</Intensity>");
printWriter.println("<TriggerMethod>Manual</TriggerMethod>");
}
}
@Override
public void writeEndTrack(Track track, Location lastPoint) {
if (printWriter != null) {
printWriter.println("</Lap>");
printWriter.println("<Notes>" + StringUtils.formatCData(track.getDescription()) + "</Notes>");
printWriter.println("<Creator xsi:type=\"Device_t\">");
printWriter.println("<Name>"
+ StringUtils.formatCData(context.getString(R.string.send_google_by_my_tracks, "", ""))
+ "</Name>");
// <UnitId>, <ProductID>, and <Version> are required for type=Device_t.
printWriter.println("<UnitId>0</UnitId>");
printWriter.println("<ProductID>0</ProductID>");
writeVersion();
printWriter.println("</Creator>");
printWriter.println("</Activity>");
printWriter.println("</Activities>");
}
}
@Override
public void writeOpenSegment() {
if (printWriter != null) {
printWriter.println("<Track>");
}
}
@Override
public void writeCloseSegment() {
if (printWriter != null) {
printWriter.println("</Track>");
}
}
@Override
public void writeLocation(Location location) {
if (printWriter != null) {
printWriter.println("<Trackpoint>");
printWriter.println("<Time>" + StringUtils.formatDateTimeIso8601(location.getTime()) + "</Time>");
printWriter.println("<Position>");
printWriter.println("<LatitudeDegrees>" + location.getLatitude() + "</LatitudeDegrees>");
printWriter.println("<LongitudeDegrees>" + location.getLongitude() + "</LongitudeDegrees>");
printWriter.println("</Position>");
if (location.hasAltitude()) {
printWriter.println("<AltitudeMeters>" + location.getAltitude() + "</AltitudeMeters>");
}
if (location instanceof MyTracksLocation) {
SensorDataSet sensorDataSet = ((MyTracksLocation) location).getSensorDataSet();
if (sensorDataSet != null) {
boolean heartRateAvailable = sensorDataSet.hasHeartRate()
&& sensorDataSet.getHeartRate().hasValue()
&& sensorDataSet.getHeartRate().getState() == Sensor.SensorState.SENDING;
boolean cadenceAvailable = sensorDataSet.hasCadence()
&& sensorDataSet.getCadence().hasValue()
&& sensorDataSet.getCadence().getState() == Sensor.SensorState.SENDING;
boolean powerAvailable = sensorDataSet.hasPower()
&& sensorDataSet.getPower().hasValue()
&& sensorDataSet.getPower().getState() == Sensor.SensorState.SENDING;
if (heartRateAvailable) {
printWriter.println("<HeartRateBpm>");
printWriter.println("<Value>" + sensorDataSet.getHeartRate().getValue() + "</Value>");
printWriter.println("</HeartRateBpm>");
}
// <Cadence> needs to be put before <Extensions>.
// According to the TCX spec, <Cadence> is only for the biking sport
// type. For others, use <RunCadence> in <Extensions>.
if (cadenceAvailable && sportType == SportType.BIKING) {
// The spec requires the max value be 254.
printWriter.println(
"<Cadence>" + Math.min(254, sensorDataSet.getCadence().getValue()) + "</Cadence>");
}
if ((cadenceAvailable && sportType != SportType.BIKING) || powerAvailable) {
printWriter.println("<Extensions>");
printWriter.println(
"<TPX xmlns=\"http://www.garmin.com/xmlschemas/ActivityExtension/v2\">");
// <RunCadence> needs to be put before <Watts>.
if (cadenceAvailable && sportType != SportType.BIKING) {
// The spec requires the max value to be 254.
printWriter.println("<RunCadence>"
+ Math.min(254, sensorDataSet.getCadence().getValue()) + "</RunCadence>");
}
if (powerAvailable) {
printWriter.println("<Watts>" + sensorDataSet.getPower().getValue() + "</Watts>");
}
printWriter.println("</TPX>");
printWriter.println("</Extensions>");
}
}
}
printWriter.println("</Trackpoint>");
}
}
@Override
public void writeBeginWaypoints() {
// Do nothing.
}
@Override
public void writeEndWaypoints() {
// Do nothing.
}
@Override
public void writeWaypoint(Waypoint waypoint) {
// Do nothing.
}
/**
* Writes the TCX Version.
*/
private void writeVersion() {
// Split the My Tracks version code into VersionMajor, VersionMinor, and,
// BuildMajor to fit the integer type requirement for these fields in the
// TCX spec.
String[] versionComponents = SystemUtils.getMyTracksVersion(context).split("\\.");
int versionMajor = versionComponents.length > 0 ? Integer.valueOf(versionComponents[0]) : 0;
int versionMinor = versionComponents.length > 1 ? Integer.valueOf(versionComponents[1]) : 0;
int buildMajor = versionComponents.length > 2 ? Integer.valueOf(versionComponents[2]) : 0;
printWriter.println("<Version>");
printWriter.println("<VersionMajor>" + versionMajor + "</VersionMajor>");
printWriter.println("<VersionMinor>" + versionMinor + "</VersionMinor>");
// According to TCX spec, these are optional. But http://connect.garmin.com
// requires them.
printWriter.println("<BuildMajor>" + buildMajor + "</BuildMajor>");
printWriter.println("<BuildMinor>0</BuildMinor>");
printWriter.println("</Version>");
}
/**
* Gets the sport type from the category.
*
* @param category the category
*/
private SportType getSportType(String category) {
category = category.trim();
// For tracks with localized category.
for (int i : TCX_SPORT_RUNNING_IDS) {
if (category.equalsIgnoreCase(context.getString(i))) {
return SportType.RUNNING;
}
}
for (int i : TCX_SPORT_BIKING_IDS) {
if (category.equalsIgnoreCase(context.getString(i))) {
return SportType.BIKING;
}
}
// For tracks without localized category.
if (category.equalsIgnoreCase(SportType.RUNNING.getName())) {
return SportType.RUNNING;
} else if (category.equalsIgnoreCase(SportType.BIKING.getName())) {
return SportType.BIKING;
} else {
return SportType.OTHER;
}
}
}
@@ -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.file.export;
import java.io.OutputStream;
/**
* Track exporting for exporting track to an {@link OutputStream}.
*
* @author Jimmy Shih
*/
public interface TrackExporter {
/**
* Write track to an output stream
*
* @param outputStream the output stream
*/
void writeTrack(OutputStream outputStream);
/**
* Returns true if {@link #writeTrack(OutputStream)} completed successfully.
*/
boolean isSuccess();
}
@@ -0,0 +1,33 @@
/*
* 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.export;
/**
* Listener for {@link TrackExporter} progress.
*
* @author Jimmy Shih
*/
public interface TrackExporterListener {
/**
* Called to update progress.
*
* @param number the number of locations written
* @param max the maximum number of locations in a track, for calculation of
* completion percentage
*/
public void onProgressUpdate(int number, int max);
}
@@ -0,0 +1,133 @@
/*
* 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.export;
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 tracks to a file.
*
* The expected sequence of calls is:
* <ul>
* <li>{@link #prepare}</li>
* <li>{@link #writeHeader}</li>
* <li>For each track:</li>
* <ul>
* <li>{@link #writeBeginWaypoints}
* <li>For each waypoint: {@link #writeWaypoint}
* <li>{@link #writeEndWaypoints}
* <li>{@link #writeBeginTrack}
* <li>For each segment:
* <ul>
* <li>{@link #writeOpenSegment}</li>
* <li>For each location in the segment: {@link #writeLocation}</li>
* <li>{@link #writeCloseSegment}</li>
* </ul>
* </li>
* <li>{@link #writeEndTrack}</li>
* </ul>
* <li>{@link #writeFooter}</li>
* <li>{@link #close}</li>
* </ul>
*
* @author Rodrigo Damazio
*/
public interface TrackWriter {
/**
* Gets the file extension (i.e. gpx, kml, ...)
*/
public String getExtension();
/**
* Sets up the file handler.
*
* @param outputStream the output stream for the file handler
*/
public void prepare(OutputStream outputStream);
/**
* Closes the underlying file handler.
*/
public void close();
/**
* Writes the header for a file
*
* @param track the track
*/
public void writeHeader(Track track);
/**
* Writes the footer.
*/
public void writeFooter();
/**
* Writes the beginning of the waypoints.
*/
public void writeBeginWaypoints();
/**
* Writes the end of the waypoints.
*/
public void writeEndWaypoints();
/**
* Writes a waypoint.
*
* @param waypoint the waypoint
*/
public void writeWaypoint(Waypoint waypoint);
/**
* Writes the beginning of the track.
*
* @param track the track
* @param firstLocation the first location
*/
public void writeBeginTrack(Track track, Location firstLocation);
/**
* Writes the end of the track.
*
* @param track the track
* @param lastLocation the last location
*/
public void writeEndTrack(Track track, Location lastLocation);
/**
* Writes the statements necessary to open a new segment.
*/
public void writeOpenSegment();
/**
* Writes the statements necessary to close a segment.
*/
public void writeCloseSegment();
/**
* Writes a location.
*
* @param location the location
*/
public void writeLocation(Location location);
}
@@ -0,0 +1,69 @@
/*
* 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.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.apps.mytracks.io.spreadsheets.SendSpreadsheetsActivity;
import com.google.android.apps.mytracks.util.IntentUtils;
import com.google.android.maps.mytracks.R;
import com.google.common.annotations.VisibleForTesting;
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.export_google_fusion_tables);
}
@Override
protected void startNextActivity(boolean success, boolean isCancel) {
sendRequest.setFusionTablesSuccess(success);
Class<?> next = getNextClass(sendRequest, isCancel);
Intent intent = IntentUtils.newIntent(this, next)
.putExtra(SendRequest.SEND_REQUEST_KEY, sendRequest);
startActivity(intent);
finish();
}
@VisibleForTesting
Class<?> getNextClass(SendRequest request, boolean isCancel) {
if (isCancel) {
return UploadResultActivity.class;
} else {
if (request.isSendSpreadsheets()) {
return SendSpreadsheetsActivity.class;
} else {
return UploadResultActivity.class;
}
}
}
}
@@ -0,0 +1,405 @@
// 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.DescriptionGenerator;
import com.google.android.apps.mytracks.content.DescriptionGeneratorImpl;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.content.Waypoint.WaypointType;
import com.google.android.apps.mytracks.io.sendtogoogle.AbstractSendAsyncTask;
import com.google.android.apps.mytracks.io.sendtogoogle.SendToGoogleUtils;
import com.google.android.apps.mytracks.io.sync.SyncUtils;
import com.google.android.apps.mytracks.util.LocationUtils;
import com.google.android.apps.mytracks.util.PreferencesUtils;
import com.google.android.gms.auth.GoogleAuthException;
import com.google.android.gms.auth.UserRecoverableAuthException;
import com.google.android.maps.mytracks.R;
import com.google.api.client.extensions.android.http.AndroidHttp;
import com.google.api.client.googleapis.GoogleUrl;
import com.google.api.client.googleapis.extensions.android.gms.auth.GoogleAccountCredential;
import com.google.api.client.googleapis.extensions.android.gms.auth.UserRecoverableAuthIOException;
import com.google.api.client.http.ByteArrayContent;
import com.google.api.client.http.HttpContent;
import com.google.api.client.json.gson.GsonFactory;
import com.google.api.services.drive.Drive;
import com.google.api.services.drive.model.Permission;
import com.google.api.services.fusiontables.Fusiontables;
import com.google.api.services.fusiontables.Fusiontables.Query.Sql;
import com.google.api.services.fusiontables.model.Column;
import com.google.api.services.fusiontables.model.PointStyle;
import com.google.api.services.fusiontables.model.StyleFunction;
import com.google.api.services.fusiontables.model.StyleSetting;
import com.google.api.services.fusiontables.model.Table;
import com.google.api.services.fusiontables.model.Template;
import android.accounts.Account;
import android.content.Context;
import android.database.Cursor;
import android.location.Location;
import android.util.Log;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* AsyncTask to send a track to Google Fusion Tables.
*
* @author Jimmy Shih
*/
public class SendFusionTablesAsyncTask extends AbstractSendAsyncTask {
private static final String TAG = SendFusionTablesAsyncTask.class.getSimpleName();
private static final int MAX_POINTS_PER_UPLOAD = 2048;
private static final int PROGRESS_CREATE_TABLE = 0;
private static final int PROGRESS_SET_STYLE = 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_blue";
private static final String MARKER_TYPE_STATISTICS = "large_yellow";
private final long trackId;
private final Account account;
private final Context context;
private final MyTracksProviderUtils myTracksProviderUtils;
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);
}
@Override
protected void closeConnection() {}
@Override
protected boolean performTask() {
try {
// Reset the per upload states
currentSegment = 1;
GoogleAccountCredential credential = SendToGoogleUtils.getGoogleAccountCredential(
context, account.name, SendToGoogleUtils.FUSION_TABLES_SCOPE);
if (credential == null) {
return false;
}
Fusiontables fusiontables = new Fusiontables.Builder(
AndroidHttp.newCompatibleTransport(), new GsonFactory(), credential).build();
Track track = myTracksProviderUtils.getTrack(trackId);
if (track == null) {
Log.d(TAG, "No track for " + trackId);
return false;
}
// Create a new table
publishProgress(PROGRESS_CREATE_TABLE);
String tableId = createNewTable(fusiontables, track);
if (tableId == null) {
return retryTask();
}
publishProgress(PROGRESS_SET_STYLE);
setStyle(fusiontables, tableId);
setTemplate(fusiontables, tableId);
if (!setPermission(track, tableId)) {
Log.d(TAG, "Cannot set permission for table " + tableId);
return false;
}
// Upload all the track points plus the start and end markers
publishProgress(PROGRESS_UPLOAD_DATA_MIN);
if (!uploadAllTrackPoints(fusiontables, tableId, track)) {
return false;
}
// Upload all the waypoints
publishProgress(PROGRESS_UPLOAD_WAYPOINTS);
if (!uploadWaypoints(fusiontables, tableId)) {
return false;
}
publishProgress(PROGRESS_COMPLETE);
return true;
} catch (UserRecoverableAuthException e) {
SendToGoogleUtils.sendNotification(
context, account.name, e.getIntent(), SendToGoogleUtils.FUSION_TABLES_NOTIFICATION_ID);
return false;
} catch (GoogleAuthException e) {
return retryTask();
} catch (UserRecoverableAuthIOException e) {
SendToGoogleUtils.sendNotification(
context, account.name, e.getIntent(), SendToGoogleUtils.FUSION_TABLES_NOTIFICATION_ID);
return false;
} catch (IOException e) {
return retryTask();
}
}
@Override
protected void invalidateToken() {}
/**
* Creates a new table.
*
* @param fusiontables fusion tables
* @param track the track
* @return the table id if success.
*/
private String createNewTable(Fusiontables fusiontables, Track track) throws IOException {
Table table = new Table();
table.setName(track.getName());
table.setDescription(track.getDescription());
table.setIsExportable(true);
table.setColumns(Arrays.asList(new Column().setName("name").setType("STRING"),
new Column().setName("description").setType("STRING"),
new Column().setName("geometry").setType("LOCATION"),
new Column().setName("icon").setType("STRING")));
return fusiontables.table().insert(table).execute().getTableId();
}
private void setStyle(Fusiontables fusiontables, String tableId) throws IOException {
StyleFunction styleFunction = new StyleFunction();
styleFunction.setColumnName("icon");
PointStyle pointStyle = new PointStyle();
pointStyle.setIconStyler(styleFunction);
StyleSetting styleSetting = new StyleSetting();
styleSetting.setTableId(tableId);
styleSetting.setMarkerOptions(pointStyle);
fusiontables.style().insert(tableId, styleSetting).execute();
}
private void setTemplate(Fusiontables fusiontables, String tableId) throws IOException {
Template template = new Template();
template.setTableId(tableId);
template.setAutomaticColumnNames(Arrays.asList("name", "description"));
fusiontables.template().insert(tableId, template).execute();
}
private boolean setPermission(Track track, String tableId) throws IOException, GoogleAuthException {
boolean defaultTablePublic = PreferencesUtils.getBoolean(context,
R.string.export_google_fusion_tables_public_key,
PreferencesUtils.EXPORT_GOOGLE_FUSION_TABLES_PUBLIC_DEFAULT);
if (!defaultTablePublic) {
return true;
}
GoogleAccountCredential driveCredential = SendToGoogleUtils.getGoogleAccountCredential(
context, account.name, SendToGoogleUtils.DRIVE_SCOPE);
if (driveCredential == null) {
return false;
}
Drive drive = SyncUtils.getDriveService(driveCredential);
Permission permission = new Permission();
permission.setRole("reader");
permission.setType("anyone");
permission.setValue("");
drive.permissions().insert(tableId, permission).execute();
shareUrl = SendFusionTablesUtils.getMapUrl(track, tableId);
return true;
}
/**
* Uploads all the points in a track. *
*
* @param fusiontables fusion tables
* @param tableId the table id
* @param track the track
* @return true if success.
*/
private boolean uploadAllTrackPoints(Fusiontables fusiontables, String tableId, Track track)
throws IOException {
Cursor cursor = null;
try {
cursor = myTracksProviderUtils.getTrackPointCursor(trackId, -1L, -1, false);
if (cursor == null) {
Log.d(TAG, "Location cursor is null");
return false;
}
int count = cursor.getCount();
List<Location> locations = new ArrayList<Location>(MAX_POINTS_PER_UPLOAD);
Location lastValidLocation = null;
boolean sentStartMarker = false;
for (int i = 0; i < count; i++) {
cursor.moveToPosition(i);
Location location = myTracksProviderUtils.createTrackPoint(cursor);
locations.add(location);
if (LocationUtils.isValidLocation(location)) {
lastValidLocation = location;
}
if (!sentStartMarker && lastValidLocation != null) {
// Create a start marker
String name = context.getString(R.string.marker_label_start, track.getName());
createNewPoint(fusiontables, tableId, name, "", lastValidLocation, MARKER_TYPE_START);
sentStartMarker = true;
}
// Upload periodically
int readCount = i + 1;
if (readCount % MAX_POINTS_PER_UPLOAD == 0) {
if (!prepareAndUploadPoints(fusiontables, tableId, track, locations, false)) {
Log.d(TAG, "Unable to upload points");
return false;
}
updateProgress(readCount, count);
locations.clear();
}
}
// Do a final upload with the remaining locations
if (!prepareAndUploadPoints(fusiontables, tableId, track, locations, true)) {
Log.d(TAG, "Unable to upload points");
return false;
}
// Create an end marker
if (lastValidLocation != null) {
String name = context.getString(R.string.marker_label_end, track.getName());
DescriptionGenerator descriptionGenerator = new DescriptionGeneratorImpl(context);
String description = descriptionGenerator.generateTrackDescription(track, null, null, true);
createNewPoint(
fusiontables, tableId, name, description, lastValidLocation, MARKER_TYPE_END);
}
return true;
} finally {
if (cursor != null) {
cursor.close();
}
}
}
/**
* Prepares and uploads a list of locations from a track.
*
* @param fusiontables fusion tables
* @param tableId the table id
* @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(Fusiontables fusiontables, String tableId, Track track,
List<Location> locations, boolean lastBatch) throws IOException {
// 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));
}
createNewLineString(fusiontables, tableId, splitTrack);
currentSegment++;
}
return true;
}
/**
* Uploads all the waypoints.
*
* @param fusiontables fusion tables
* @param tableId the table id
* @return true if success.
* @throws IOException
*/
private boolean uploadWaypoints(Fusiontables fusiontables, String tableId) throws IOException {
Cursor cursor = null;
try {
cursor = myTracksProviderUtils.getWaypointCursor(
trackId, -1L, 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);
String type = wpt.getType() == WaypointType.STATISTICS ? MARKER_TYPE_STATISTICS
: MARKER_TYPE_WAYPOINT;
String description = wpt.getDescription().replaceAll("\n", "<br>");
createNewPoint(
fusiontables, tableId, wpt.getName(), description, wpt.getLocation(), type);
}
}
return true;
} finally {
if (cursor != null) {
cursor.close();
}
}
}
/**
* Creates a new row in Google Fusion Tables representing a marker as a point.
*
* @param fusiontables fusion tables
* @param tableId the table id
* @param name the marker name
* @param description the marker description
* @param location the marker location
* @param type the marker type
*/
private void createNewPoint(Fusiontables fusiontables, String tableId, String name,
String description, Location location, String type) throws IOException {
String values = SendFusionTablesUtils.formatSqlValues(
name, description, SendFusionTablesUtils.getKmlPoint(location), type);
Sql sql = fusiontables.query()
.sql("INSERT INTO " + tableId + " (name,description,geometry,icon) VALUES " + values);
sql.execute();
}
/**
* Creates a new row in Google Fusion Tables representing the track as a line
* segment.
*
* @param fusiontables fusion tables
* @param tableId the table id
* @param track the track
*/
private void createNewLineString(Fusiontables fusiontables, String tableId, Track track)
throws IOException {
String values = SendFusionTablesUtils.formatSqlValues(track.getName(), track.getDescription(),
SendFusionTablesUtils.getKmlLineString(track.getLocations()));
String sql = "INSERT INTO " + tableId + " (name,description,geometry) VALUES " + values;
HttpContent content = ByteArrayContent.fromString(null, "sql=" + sql);
GoogleUrl url = new GoogleUrl("https://www.googleapis.com/fusiontables/v1/query");
fusiontables.getRequestFactory().buildPostRequest(url, content).execute();
}
/**
* 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 = (double) uploaded / total;
double scaledPercentage = totalPercentage
* (PROGRESS_UPLOAD_DATA_MAX - PROGRESS_UPLOAD_DATA_MIN) + PROGRESS_UPLOAD_DATA_MIN;
publishProgress((int) scaledPercentage);
}
}
@@ -0,0 +1,155 @@
/*
* 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.common.annotations.VisibleForTesting;
import android.location.Location;
import android.util.Log;
import java.util.ArrayList;
import java.util.Locale;
/**
* Utilities for sending a track to Google Fusion Tables.
*
* @author Jimmy Shih
*/
public class SendFusionTablesUtils {
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
* @param tableId the table id
* @return the url.
*/
public static String getMapUrl(Track track, String tableId) {
if (track == null || track.getTripStatistics() == null) {
Log.e(TAG, "Invalid track");
return null;
}
double latE6;
double lonE6;
int z;
if (track.getNumberOfPoints() < 2) {
// Use Google's latitude and longitude
latE6 = 37.423 * 1.E6;
lonE6 = -122.084 * 1.E6;
z = 2;
} else {
TripStatistics stats = track.getTripStatistics();
latE6 = stats.getBottom() + (stats.getTop() - stats.getBottom()) / 2;
lonE6 = stats.getLeft() + (stats.getRight() - stats.getLeft()) / 2;
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, tableId, 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.
*/
@VisibleForTesting
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());
}
}
}
@@ -0,0 +1,457 @@
/*
* 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 = null;
try {
encodedUri = URLEncoder.encode(uri, "UTF-8");
} catch (UnsupportedEncodingException uee) {
// Should not happen
throw new IllegalStateException("Cannot encode " + uri, uee);
}
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,103 @@
/*
* 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 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
throw new IllegalStateException("Cannot encode " + value, uee);
}
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,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,102 @@
// 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;
import java.util.Locale;
/**
* 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(Locale.US, 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,219 @@
// Copyright 2009 Google Inc. All Rights Reserved.
package com.google.android.apps.mytracks.io.gdata.maps;
import android.location.Location;
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 {
/** 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;
// Points of this feature in order.
private final Vector<Location> points = new Vector<Location>();
/** 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 location the new point to add
*/
public void addPoint(Location location) {
points.add(location);
}
/**
* 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);
}
/**
* Gets the point at a given index.
*
* @param index the index
*/
public Location getPoint(int index) {
if (index >= points.size()) {
return null;
}
return points.get(index);
}
/**
* Gets the number of points.
*/
public int getPointCount() {
return points.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,178 @@
// 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).getLongitude());
pointBuilder.append(',');
pointBuilder.append(feature.getPoint(i).getLatitude());
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).getLongitude())
+ ","
+ Double.toString(feature.getPoint(0).getLatitude())
+ ",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", "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", "rawtypes" })
@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,71 @@
/*
* 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.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.apps.mytracks.io.spreadsheets.SendSpreadsheetsActivity;
import com.google.android.apps.mytracks.util.IntentUtils;
import com.google.android.maps.mytracks.R;
import com.google.common.annotations.VisibleForTesting;
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());
}
@Override
protected String getServiceName() {
return getString(R.string.export_google_maps);
}
@Override
protected void startNextActivity(boolean success, boolean isCancel) {
sendRequest.setMapsSuccess(success);
Class<?> next = getNextClass(sendRequest, isCancel);
Intent intent = IntentUtils.newIntent(this, next)
.putExtra(SendRequest.SEND_REQUEST_KEY, sendRequest);
startActivity(intent);
finish();
}
@VisibleForTesting
Class<?> getNextClass(SendRequest request, boolean isCancel) {
if (isCancel) {
return UploadResultActivity.class;
} else {
if (request.isSendFusionTables()) {
return SendFusionTablesActivity.class;
} else if (request.isSendSpreadsheets()) {
return SendSpreadsheetsActivity.class;
} else {
return UploadResultActivity.class;
}
}
}
}
@@ -0,0 +1,507 @@
/*
* 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.DescriptionGenerator;
import com.google.android.apps.mytracks.content.DescriptionGeneratorImpl;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.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.TripStatisticsUpdater;
import com.google.android.apps.mytracks.util.LocationUtils;
import com.google.android.apps.mytracks.util.PreferencesUtils;
import com.google.android.common.gdata.AndroidXmlParserFactory;
import com.google.android.maps.mytracks.R;
import com.google.common.annotations.VisibleForTesting;
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.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;
@VisibleForTesting
static final int PROGRESS_UPLOAD_DATA_MIN = 10;
@VisibleForTesting
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 MyTracksProviderUtils myTracksProviderUtils;
private final Context context;
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) {
this(activity, trackId, account, MyTracksProviderUtils.Factory.get(
activity.getApplicationContext()));
}
/**
* This constructor is created for test.
*/
@VisibleForTesting
public SendMapsAsyncTask(SendMapsActivity activity, long trackId, Account account,
MyTracksProviderUtils myTracksProviderUtils) {
super(activity);
this.trackId = trackId;
this.account = account;
this.myTracksProviderUtils = myTracksProviderUtils;
context = activity.getApplicationContext();
gDataClient = GDataClientFactory.getGDataClient(context);
mapsClient = new MapsClient(
gDataClient, new XmlMapsGDataParserFactory(new AndroidXmlParserFactory()));
}
@Override
protected void closeConnection() {
if (gDataClient != null) {
gDataClient.close();
}
}
@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, "No track for " + trackId);
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.
*/
@VisibleForTesting
boolean fetchSendMapId(Track track) {
if (isCancelled()) {
return false;
}
boolean defaultMapPublic = PreferencesUtils.getBoolean(context,
R.string.export_google_maps_public_key, PreferencesUtils.EXPORT_GOOGLE_MAPS_PUBLIC_DEFAULT);
try {
String description = track.getCategory() + "\n" + track.getDescription() + "\n"
+ context.getString(R.string.send_google_by_my_tracks, "", "");
mapId = SendMapsUtils.createNewMap(
track.getName(), description, defaultMapPublic, mapsClient, authToken);
shareUrl = MapsClient.buildMapUrl(mapId);
} 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.
*/
@VisibleForTesting
boolean uploadAllTrackPoints(Track track) {
Cursor cursor = null;
try {
cursor = myTracksProviderUtils.getTrackPointCursor(trackId, -1L, -1, false);
if (cursor == null) {
Log.d(TAG, "Location cursor is null");
return false;
}
int count = cursor.getCount();
List<Location> locations = new ArrayList<Location>(MAX_POINTS_PER_UPLOAD);
Location lastValidLocation = null;
boolean sentStartMarker = false;
// For chart server, limit the number of elevation readings to 250.
int elevationSamplingFrequency = Math.max(1, (int) (count / 250.0));
Vector<Double> distances = new Vector<Double>();
Vector<Double> elevations = new Vector<Double>();
TripStatisticsUpdater tripStatisticsUpdater = new TripStatisticsUpdater(
track.getTripStatistics().getStartTime());
int recordingDistanceInterval = PreferencesUtils.getInt(context,
R.string.recording_distance_interval_key,
PreferencesUtils.RECORDING_DISTANCE_INTERVAL_DEFAULT);
for (int i = 0; i < count; i++) {
cursor.moveToPosition(i);
Location location = myTracksProviderUtils.createTrackPoint(cursor);
locations.add(location);
if (LocationUtils.isValidLocation(location)) {
lastValidLocation = location;
}
if (!sentStartMarker && lastValidLocation != null) {
// Create a start marker
if (!uploadMarker(context.getString(R.string.marker_label_start, track.getName()), "",
START_ICON_URL, lastValidLocation)) {
Log.d(TAG, "Unable to create a start marker");
return false;
}
sentStartMarker = true;
}
tripStatisticsUpdater.addLocation(location, recordingDistanceInterval, PreferencesUtils
.getInt(context, R.string.stats_weight_key, PreferencesUtils.STATS_WEIGHT_DEFAULT));
if (i % elevationSamplingFrequency == 0) {
distances.add(tripStatisticsUpdater.getTripStatistics().getTotalDistance());
elevations.add(tripStatisticsUpdater.getSmoothedElevation());
}
// 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, count);
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 (lastValidLocation != null) {
distances.add(tripStatisticsUpdater.getTripStatistics().getTotalDistance());
elevations.add(tripStatisticsUpdater.getSmoothedElevation());
DescriptionGenerator descriptionGenerator = new DescriptionGeneratorImpl(context);
track.setDescription(
descriptionGenerator.generateTrackDescription(track, distances, elevations, true));
if (!uploadMarker(context.getString(R.string.marker_label_end, track.getName()),
track.getDescription(), END_ICON_URL, lastValidLocation)) {
Log.d(TAG, "Unable to create an end marker");
return false;
}
}
return true;
} finally {
if (cursor != null) {
cursor.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.
*/
@VisibleForTesting
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.
*/
@VisibleForTesting
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.
*/
@VisibleForTesting
boolean uploadWaypoints() {
Cursor cursor = null;
try {
cursor = myTracksProviderUtils.getWaypointCursor(
trackId, -1L, 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
*/
@VisibleForTesting
void updateProgress(int uploaded, int total) {
publishProgress(getPercentage(uploaded, total));
}
/**
* Count the percentage of the number of locations uploaded.
*
* @param uploaded the number of uploaded locations
* @param total the number of total locations
*/
@VisibleForTesting
static int getPercentage(int uploaded, int total) {
double totalPercentage = (double) uploaded / total;
double scaledPercentage = totalPercentage
* (PROGRESS_UPLOAD_DATA_MAX - PROGRESS_UPLOAD_DATA_MIN) + PROGRESS_UPLOAD_DATA_MIN;
return (int) scaledPercentage;
}
/**
* Gets the mapID.
*
* @return mapId
*/
@VisibleForTesting
String getMapId() {
return mapId;
}
/**
* Sets the value of mapsGDataConverter.
*
* @param mapsGDataConverter new value of mapsGDataConverter
*/
@VisibleForTesting
void setMapsGDataConverter(MapsGDataConverter mapsGDataConverter) {
this.mapsGDataConverter = mapsGDataConverter;
}
}
@@ -0,0 +1,199 @@
/*
* 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.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.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() {}
/**
* 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, 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(), 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 location the marker
*/
@VisibleForTesting
static MapsFeature buildMapsMarkerFeature(
String title, String description, String iconUrl, Location location) {
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(location);
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(location);
}
return mapsFeature;
}
}
@@ -0,0 +1,135 @@
/*
* 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.util.DialogUtils;
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 DIALOG_PROGRESS_ID = 0;
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) {
if (id != DIALOG_PROGRESS_ID) {
return null;
}
progressDialog = DialogUtils.createHorizontalProgressDialog(
this, R.string.send_google_progress_message, new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
asyncTask.cancel(true);
startNextActivity(false, true);
}
}, getServiceName());
return progressDialog;
}
/**
* Invokes when the associated AsyncTask completes.
*
* @param success true if the AsyncTask is successful
*/
public void onAsyncTaskCompleted(boolean success, String shareUrl) {
sendRequest.setShareUrl(shareUrl);
startNextActivity(success, false);
}
/**
* Shows the progress dialog.
*/
public void showProgressDialog() {
showDialog(DIALOG_PROGRESS_ID);
}
/**
* Sets the progress dialog value.
*
* @param value the dialog value
*/
public void setProgressDialogValue(int value) {
if (progressDialog != null) {
progressDialog.setIndeterminate(false);
progressDialog.setProgress(value);
progressDialog.setMax(100);
}
}
/**
* 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,143 @@
/*
* 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;
/**
* The share url from the AsyncTask.
*/
protected String shareUrl;
/**
* 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;
shareUrl = null;
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, shareUrl);
}
}
@Override
protected void onPreExecute() {
activity.showProgressDialog();
}
@Override
protected Boolean doInBackground(Void... params) {
try {
return performTask();
} finally {
closeConnection();
}
}
@Override
protected void onProgressUpdate(Integer... values) {
if (activity != null) {
activity.setProgressDialogValue(values[0]);
}
}
@Override
protected void onPostExecute(Boolean result) {
success = result;
completed = true;
if (activity != null) {
activity.onAsyncTaskCompleted(success, shareUrl);
}
}
/**
* 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();
/**
* Performs the AsyncTask.
*
* @return true if success
*/
protected abstract boolean performTask();
/**
* Invalidates the auth token.
*/
protected abstract void invalidateToken();
}
@@ -0,0 +1,240 @@
/*
* 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 Drive, Google Maps, Google
* Fusion Tables, and Google Spreadsheets.
*
* @author Jimmy Shih
*/
public class SendRequest implements Parcelable {
public static final String SEND_REQUEST_KEY = "sendRequest";
private long trackId = -1L;
private boolean sendDrive = false;
private boolean sendMaps = false;
private boolean sendFusionTables = false;
private boolean sendSpreadsheets = false;
private boolean driveSync = false; // to enable Drive sync
private boolean driveSyncConfirm = false;
private boolean driveShare = false; // to enable Drive share
private String driveShareEmails = null; // for driveShare, emails to share
private boolean driveSharePublic = false; // for driveShare, share as public
private Account account = null;
private boolean driveSuccess = false;
private boolean mapsSuccess = false;
private boolean spreadsheetsSuccess = false;
private boolean fusionTablesSuccess = false;
private String shareUrl = null;
/**
* Creates a new send request.
*
* @param trackId the track id
*/
public SendRequest(long trackId) {
this.trackId = trackId;
}
public long getTrackId() {
return trackId;
}
public boolean isSendDrive() {
return sendDrive;
}
public void setSendDrive(boolean sendDrive) {
this.sendDrive = sendDrive;
}
public boolean isSendMaps() {
return sendMaps;
}
public void setSendMaps(boolean sendMaps) {
this.sendMaps = sendMaps;
}
public boolean isSendFusionTables() {
return sendFusionTables;
}
public void setSendFusionTables(boolean sendFusionTables) {
this.sendFusionTables = sendFusionTables;
}
public boolean isSendSpreadsheets() {
return sendSpreadsheets;
}
public void setSendSpreadsheets(boolean sendSpreadsheets) {
this.sendSpreadsheets = sendSpreadsheets;
}
public boolean isDriveSync() {
return driveSync;
}
public void setDriveSync(boolean driveSync) {
this.driveSync = driveSync;
}
public boolean isDriveSyncConfirm() {
return driveSyncConfirm;
}
public void setDriveSyncConfirm(boolean driveSyncConfirm) {
this.driveSyncConfirm = driveSyncConfirm;
}
public boolean isDriveShare() {
return driveShare;
}
public void setDriveShare(boolean driveShare) {
this.driveShare = driveShare;
}
public String getDriveShareEmails() {
return driveShareEmails;
}
public void setDriveShareEmails(String driveShareEmails) {
this.driveShareEmails = driveShareEmails;
}
public boolean isDriveSharePublic() {
return driveSharePublic;
}
public void setDriveSharePublic(boolean driveSharePublic) {
this.driveSharePublic = driveSharePublic;
}
public Account getAccount() {
return account;
}
public void setAccount(Account account) {
this.account = account;
}
public boolean isDriveSuccess() {
return driveSuccess;
}
public void setDriveSuccess(boolean driveSuccess) {
this.driveSuccess = driveSuccess;
}
public boolean isMapsSuccess() {
return mapsSuccess;
}
public void setMapsSuccess(boolean mapsSuccess) {
this.mapsSuccess = mapsSuccess;
}
public boolean isFusionTablesSuccess() {
return fusionTablesSuccess;
}
public void setFusionTablesSuccess(boolean fusionTablesSuccess) {
this.fusionTablesSuccess = fusionTablesSuccess;
}
public boolean isSpreadsheetsSuccess() {
return spreadsheetsSuccess;
}
public void setSpreadsheetsSuccess(boolean spreadsheetsSuccess) {
this.spreadsheetsSuccess = spreadsheetsSuccess;
}
public String getShareUrl() {
return shareUrl;
}
public void setShareUrl(String shareUrl) {
this.shareUrl = shareUrl;
}
private SendRequest(Parcel in) {
trackId = in.readLong();
sendDrive = in.readByte() == 1;
sendMaps = in.readByte() == 1;
sendFusionTables = in.readByte() == 1;
sendSpreadsheets = in.readByte() == 1;
driveSync = in.readByte() == 1;
driveSyncConfirm = in.readByte() == 1;
driveShare = in.readByte() == 1;
driveShareEmails = in.readString();
driveSharePublic = in.readByte() == 1;
account = in.readParcelable(null);
driveSuccess = in.readByte() == 1;
mapsSuccess = in.readByte() == 1;
fusionTablesSuccess = in.readByte() == 1;
spreadsheetsSuccess = in.readByte() == 1;
shareUrl = in.readString();
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel out, int flags) {
out.writeLong(trackId);
out.writeByte((byte) (sendDrive ? 1 : 0));
out.writeByte((byte) (sendMaps ? 1 : 0));
out.writeByte((byte) (sendFusionTables ? 1 : 0));
out.writeByte((byte) (sendSpreadsheets ? 1 : 0));
out.writeByte((byte) (driveSync ? 1 : 0));
out.writeByte((byte) (driveSyncConfirm ? 1 : 0));
out.writeByte((byte) (driveShare ? 1 : 0));
out.writeString(driveShareEmails);
out.writeByte((byte) (driveSharePublic ? 1 : 0));
out.writeParcelable(account, 0);
out.writeByte((byte) (driveSuccess ? 1 : 0));
out.writeByte((byte) (mapsSuccess ? 1 : 0));
out.writeByte((byte) (fusionTablesSuccess ? 1 : 0));
out.writeByte((byte) (spreadsheetsSuccess ? 1 : 0));
out.writeString(shareUrl);
}
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,204 @@
/*
* 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 com.google.android.gms.auth.GoogleAuthException;
import com.google.android.maps.mytracks.R;
import com.google.api.client.googleapis.extensions.android.gms.auth.GoogleAccountCredential;
import com.google.api.services.drive.DriveScopes;
import com.google.api.services.fusiontables.FusiontablesScopes;
import com.google.common.annotations.VisibleForTesting;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.location.Location;
import android.support.v4.app.NotificationCompat;
import android.util.Log;
import java.io.IOException;
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();
public static final String DRIVE_SCOPE = DriveScopes.DRIVE;
public static final int DRIVE_NOTIFICATION_ID = 1;
public static final String FUSION_TABLES_SCOPE = FusiontablesScopes.FUSIONTABLES;
public static final int FUSION_TABLES_NOTIFICATION_ID = 2;
public static final String SPREADSHEETS_SCOPE = "https://spreadsheets.google.com/feeds";
public static final int SPREADSHEETS_NOTIFICATION_ID = 3;
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 = createNewSegment(
track, locations.size() > 0 ? locations.get(0).getTime() : -1L);
for (Location location : locations) {
/*
* Latitude is greater than 90 if the location is a pause/resume
* separator.
*/
if (location.getLatitude() > 90) {
endSegment(segment, location.getTime(), splitTracks);
segment = createNewSegment(track, location.getTime());
} else {
segment.addLocation(location);
}
}
endSegment(segment, locations.size() > 0 ? locations.get(locations.size() - 1).getTime() : -1L,
splitTracks);
return splitTracks;
}
/**
* Creates a new segment for a track.
*
* @param track the track
* @param startTime the segment start time
*/
private static Track createNewSegment(Track track, long startTime) {
Track segment = new Track();
segment.setId(track.getId());
segment.setName(track.getName());
segment.setDescription("");
segment.setCategory(track.getCategory());
TripStatistics segmentTripStatistics = segment.getTripStatistics();
segmentTripStatistics.setStartTime(startTime);
return segment;
}
/**
* Ends a segment. Adds to the array of track segments if the segment is
* valid.
*
* @param segment the segment
* @param stopTime the stop time
* @param splitTracks the array of track segments
*/
@VisibleForTesting
static boolean endSegment(Track segment, long stopTime, 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 false;
}
// Set its stop time
segment.getTripStatistics().setStopTime(stopTime);
/*
* 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);
return true;
}
/**
* Gets the google account credential.
*
* @param context the context
* @param accountName the account name
* @param scope the scope
*/
public static GoogleAccountCredential getGoogleAccountCredential(
Context context, String accountName, String scope) throws IOException, GoogleAuthException {
GoogleAccountCredential credential = GoogleAccountCredential.usingOAuth2(context, scope);
credential.setSelectedAccountName(accountName);
credential.getToken();
return credential;
}
/**
* Gets the OAuth2 token.
*
* @param context the context
* @param accountName the account name
* @param scope the scope
*/
public static String getToken(Context context, String accountName, String scope)
throws IOException, GoogleAuthException {
GoogleAccountCredential credential = GoogleAccountCredential.usingOAuth2(context, scope);
credential.setSelectedAccountName(accountName);
return credential.getToken();
}
/**
* Sends a notification to request permission.
*
* @param context the context
* @param accountName the account name
* @param intent the intent
* @param notificaitonId the notification id
*/
public static void sendNotification(
Context context, String accountName, Intent intent, int notificaitonId) {
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK).addFlags(Intent.FLAG_FROM_BACKGROUND);
PendingIntent pendingIntent = PendingIntent.getActivity(
context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
NotificationCompat.Builder builder = new NotificationCompat.Builder(context).setAutoCancel(true)
.setContentIntent(pendingIntent)
.setContentText(context.getString(R.string.permission_request_message, accountName))
.setContentTitle(context.getString(R.string.permission_request_title))
.setSmallIcon(android.R.drawable.ic_dialog_alert)
.setTicker(context.getString(R.string.permission_request_title));
NotificationManager notificationManager = (NotificationManager) context.getSystemService(
Context.NOTIFICATION_SERVICE);
notificationManager.notify(notificaitonId, builder.build());
}
/**
* Cancels any notification to request a permission.
*
* @param context the context
* @param notificationId the notification id
*/
public static void cancelNotification(Context context, int notificationId) {
NotificationManager notificationManager = (NotificationManager) context.getSystemService(
Context.NOTIFICATION_SERVICE);
notificationManager.cancel(notificationId);
}
}
@@ -0,0 +1,144 @@
/*
* 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.MyTracksProviderUtils;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.fragments.ChooseActivityDialogFragment;
import com.google.android.apps.mytracks.fragments.ChooseActivityDialogFragment.ChooseActivityCaller;
import com.google.android.apps.mytracks.util.IntentUtils;
import com.google.android.maps.mytracks.R;
import com.google.common.annotations.VisibleForTesting;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
import android.support.v4.app.FragmentActivity;
import android.util.Log;
import android.view.View;
/**
* A dialog to show the result of uploading to Google services.
*
* @author Jimmy Shih
*/
public class UploadResultActivity extends FragmentActivity implements ChooseActivityCaller {
private static final String TAG = UploadResultActivity.class.getSimpleName();
@VisibleForTesting
static final int DIALOG_RESULT_ID = 0;
@VisibleForTesting
protected View view;
private SendRequest sendRequest;
private String shareUrl;
private Dialog resultDialog;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
sendRequest = getIntent().getParcelableExtra(SendRequest.SEND_REQUEST_KEY);
shareUrl = sendRequest.getShareUrl();
Track track = MyTracksProviderUtils.Factory.get(this).getTrack(sendRequest.getTrackId());
if (track == null) {
Log.d(TAG, "No track for " + sendRequest.getTrackId());
finish();
return;
}
if (sendRequest.isDriveSuccess() && shareUrl != null) {
new ChooseActivityDialogFragment().show(
getSupportFragmentManager(), ChooseActivityDialogFragment.CHOOSE_ACTIVITY_DIALOG_TAG);
return;
}
showDialog(DIALOG_RESULT_ID);
}
@Override
protected Dialog onCreateDialog(int id) {
if (id != DIALOG_RESULT_ID) {
return null;
}
int serviceName;
int serviceUrl;
boolean success;
if (sendRequest.isSendDrive()) {
serviceName = R.string.export_google_drive;
serviceUrl = R.string.export_google_drive_url;
success = sendRequest.isDriveSuccess();
} else if (sendRequest.isSendMaps()) {
serviceName = R.string.export_google_maps;
serviceUrl = R.string.export_google_maps_url;
success = sendRequest.isMapsSuccess();
} else if (sendRequest.isSendFusionTables()) {
serviceName = R.string.export_google_fusion_tables;
serviceUrl = R.string.export_google_fusion_tables_url;
success = sendRequest.isFusionTablesSuccess();
} else {
serviceName = R.string.export_google_spreadsheets;
serviceUrl = R.string.export_google_spreadsheets_url;
success = sendRequest.isSpreadsheetsSuccess();
}
String message = getString(
success ? R.string.export_google_success : R.string.export_google_error,
getString(serviceName), getString(serviceUrl));
AlertDialog.Builder builder = new AlertDialog.Builder(this).setCancelable(true)
.setIcon(success ? R.drawable.ic_dialog_success : android.R.drawable.ic_dialog_alert)
.setMessage(message)
.setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
finish();
}
})
.setPositiveButton(R.string.generic_ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
finish();
}
})
.setTitle(success ? R.string.generic_success_title : R.string.generic_error_title);
// Add a Share URL button if shareUrl exists
if (success && shareUrl != null) {
builder.setNegativeButton(
R.string.share_track_share_url, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
new ChooseActivityDialogFragment().show(getSupportFragmentManager(),
ChooseActivityDialogFragment.CHOOSE_ACTIVITY_DIALOG_TAG);
}
});
}
resultDialog = builder.create();
return resultDialog;
}
@Override
public void onChooseActivityDone(String packageName, String className) {
if (packageName != null && className != null) {
Intent intent = IntentUtils.newShareUrlIntent(
this, sendRequest.getTrackId(), shareUrl, packageName, className);
startActivity(intent);
}
finish();
}
}
@@ -0,0 +1,53 @@
/*
* 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.spreadsheets;
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.apps.mytracks.util.IntentUtils;
import com.google.android.maps.mytracks.R;
import android.content.Intent;
/**
* An activity to send a track to Google Spreadsheet.
*
* @author Jimmy Shih
*/
public class SendSpreadsheetsActivity extends AbstractSendActivity {
@Override
protected AbstractSendAsyncTask createAsyncTask() {
return new SendSpreadsheetsAsyncTask(this, sendRequest.getTrackId(), sendRequest.getAccount());
}
@Override
protected String getServiceName() {
return getString(R.string.export_google_spreadsheets);
}
@Override
protected void startNextActivity(boolean success, boolean isCancel) {
sendRequest.setSpreadsheetsSuccess(success);
Intent intent = IntentUtils.newIntent(this, UploadResultActivity.class)
.putExtra(SendRequest.SEND_REQUEST_KEY, sendRequest);
startActivity(intent);
finish();
}
}
@@ -0,0 +1,282 @@
/*
* 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.spreadsheets;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.io.sendtogoogle.AbstractSendAsyncTask;
import com.google.android.apps.mytracks.io.sendtogoogle.SendToGoogleUtils;
import com.google.android.apps.mytracks.io.sync.SyncUtils;
import com.google.android.apps.mytracks.stats.TripStatistics;
import com.google.android.apps.mytracks.util.PreferencesUtils;
import com.google.android.apps.mytracks.util.StringUtils;
import com.google.android.apps.mytracks.util.SystemUtils;
import com.google.android.gms.auth.GoogleAuthException;
import com.google.android.gms.auth.UserRecoverableAuthException;
import com.google.android.maps.mytracks.R;
import com.google.api.client.auth.oauth2.BearerToken;
import com.google.api.client.auth.oauth2.Credential;
import com.google.api.client.googleapis.extensions.android.gms.auth.GoogleAccountCredential;
import com.google.api.client.googleapis.extensions.android.gms.auth.UserRecoverableAuthIOException;
import com.google.api.client.http.ByteArrayContent;
import com.google.api.services.drive.Drive;
import com.google.api.services.drive.model.File;
import com.google.api.services.drive.model.FileList;
import com.google.common.annotations.VisibleForTesting;
import com.google.gdata.client.spreadsheet.SpreadsheetService;
import com.google.gdata.data.spreadsheet.ListEntry;
import com.google.gdata.data.spreadsheet.WorksheetEntry;
import com.google.gdata.data.spreadsheet.WorksheetFeed;
import com.google.gdata.util.ServiceException;
import android.accounts.Account;
import android.content.Context;
import android.util.Log;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.List;
import java.util.Locale;
/**
* AsyncTask to send a track to Google Spreadsheet.
*
* @author Jimmy Shih
*/
public class SendSpreadsheetsAsyncTask extends AbstractSendAsyncTask {
private static final String TAG = SendSpreadsheetsAsyncTask.class.getSimpleName();
private static final String
GOOGLE_SPREADSHEET_MIME_TYPE = "application/vnd.google-apps.spreadsheet";
private static final String
OPENDOCUMENT_SPREADSHEET_MIME_TYPE = "application/x-vnd.oasis.opendocument.spreadsheet";
@VisibleForTesting
public static final String GET_SPREADSHEET_QUERY =
"'root' in parents and title = '%s' and mimeType = '" + GOOGLE_SPREADSHEET_MIME_TYPE
+ "' and trashed = false";
@VisibleForTesting
public static final String
GET_WORKSHEETS_URI = "https://spreadsheets.google.com/feeds/worksheets/%s/private/full";
private static final int PROGRESS_GET_SPREADSHEET_ID = 0;
private static final int PROGRESS_GET_WORKSHEET_URL = 35;
private static final int PROGRESS_ADD_TRACK_INFO = 70;
private static final int PROGRESS_COMPLETE = 100;
private final long trackId;
private final Account account;
private final Context context;
private final MyTracksProviderUtils myTracksProviderUtils;
public SendSpreadsheetsAsyncTask(SendSpreadsheetsActivity activity, long trackId, Account account) {
super(activity);
this.trackId = trackId;
this.account = account;
context = activity.getApplicationContext();
myTracksProviderUtils = MyTracksProviderUtils.Factory.get(context);
}
@Override
protected void closeConnection() {}
@Override
protected boolean performTask() {
try {
SpreadsheetService spreadsheetService = new SpreadsheetService(
"MyTracks-" + SystemUtils.getMyTracksVersion(context));
Credential credential = new Credential(BearerToken.authorizationHeaderAccessMethod());
credential.setAccessToken(
SendToGoogleUtils.getToken(context, account.name, SendToGoogleUtils.SPREADSHEETS_SCOPE));
spreadsheetService.setOAuth2Credentials(credential);
Track track = myTracksProviderUtils.getTrack(trackId);
if (track == null) {
Log.d(TAG, "No track for " + trackId);
return false;
}
String title = context.getString(R.string.my_tracks_app_name);
if (track.getCategory() != null && !track.getCategory().equals("")) {
title += "-" + track.getCategory();
}
publishProgress(PROGRESS_GET_SPREADSHEET_ID);
String spreadsheetId = getSpreadSheetId(title);
if (spreadsheetId == null) {
Log.d(TAG, "Unable to get the spreadsheet ID for " + title);
return false;
}
publishProgress(PROGRESS_GET_WORKSHEET_URL);
URL worksheetUrl = getWorksheetUrl(spreadsheetService, spreadsheetId);
if (worksheetUrl == null) {
Log.d(TAG, "Unable to get the worksheet url for " + spreadsheetId);
return false;
}
publishProgress(PROGRESS_ADD_TRACK_INFO);
if (!addTrackInfo(spreadsheetService, worksheetUrl, track)) {
Log.d(TAG, "Unable to add track info");
return false;
}
publishProgress(PROGRESS_COMPLETE);
return true;
} catch (UserRecoverableAuthException e) {
SendToGoogleUtils.sendNotification(
context, account.name, e.getIntent(), SendToGoogleUtils.SPREADSHEETS_NOTIFICATION_ID);
return false;
} catch (GoogleAuthException e) {
Log.e(TAG, "GoogleaAuthException", e);
return retryTask();
} catch (UserRecoverableAuthIOException e) {
SendToGoogleUtils.sendNotification(
context, account.name, e.getIntent(), SendToGoogleUtils.SPREADSHEETS_NOTIFICATION_ID);
return false;
} catch (IOException e) {
Log.e(TAG, "IOException", e);
return retryTask();
} catch (ServiceException e) {
Log.e(TAG, "ServiceException", e);
return retryTask();
}
}
@Override
protected void invalidateToken() {}
/**
* Gets the spreadsheet id.
*
* @param fileName the file name
*/
private String getSpreadSheetId(String fileName) throws IOException, GoogleAuthException {
if (isCancelled()) {
return null;
}
GoogleAccountCredential driveCredential = SendToGoogleUtils.getGoogleAccountCredential(
context, account.name, SendToGoogleUtils.DRIVE_SCOPE);
if (driveCredential == null) {
return null;
}
Drive drive = SyncUtils.getDriveService(driveCredential);
com.google.api.services.drive.Drive.Files.List list = drive.files()
.list().setQ(String.format(Locale.US, GET_SPREADSHEET_QUERY, fileName));
FileList result = list.execute();
for (File file : result.getItems()) {
if (file.getSharedWithMeDate() == null) {
return file.getId();
}
}
if (isCancelled()) {
return null;
}
InputStream inputStream = null;
try {
inputStream = context.getResources().openRawResource(R.raw.mytracks_empty_spreadsheet);
byte[] b = new byte[inputStream.available()];
inputStream.read(b);
ByteArrayContent fileContent = new ByteArrayContent(OPENDOCUMENT_SPREADSHEET_MIME_TYPE, b);
File file = new File();
file.setTitle(fileName);
file.setMimeType(OPENDOCUMENT_SPREADSHEET_MIME_TYPE);
return drive.files().insert(file, fileContent).setConvert(true).execute().getId();
} finally {
if (inputStream != null) {
inputStream.close();
}
}
}
/**
* Gets the worksheet url.
*
* @param spreadsheetService the spreadsheet service
* @param spreadsheetId the spreadsheet id
*/
private URL getWorksheetUrl(SpreadsheetService spreadsheetService, String spreadsheetId)
throws IOException, ServiceException {
if (isCancelled()) {
return null;
}
URL url = new URL(String.format(Locale.US, GET_WORKSHEETS_URI, spreadsheetId));
WorksheetFeed feed = spreadsheetService.getFeed(url, WorksheetFeed.class);
List<WorksheetEntry> worksheets = feed.getEntries();
if (worksheets.size() > 0) {
return worksheets.get(0).getListFeedUrl();
}
return null;
}
/**
* Adds track info to a worksheet.
*
* @param spreadsheetService the spreadsheet service
* @param worksheetUrl the worksheet url
* @param track the track
* @return true if completes.
*/
private boolean addTrackInfo(SpreadsheetService spreadsheetService, URL worksheetUrl, Track track)
throws IOException, ServiceException {
if (isCancelled()) {
return false;
}
TripStatistics tripStatistics = track.getTripStatistics();
boolean metricUnits = PreferencesUtils.isMetricUnits(context);
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);
ListEntry row = new ListEntry();
row.getCustomElements().setValueLocal("name", track.getName());
row.getCustomElements().setValueLocal("description", track.getDescription());
row.getCustomElements()
.setValueLocal("date", StringUtils.formatDateTime(context, tripStatistics.getStartTime()));
row.getCustomElements().setValueLocal(
"totaltime", StringUtils.formatElapsedTimeWithHour(tripStatistics.getTotalTime()));
row.getCustomElements().setValueLocal(
"movingtime", StringUtils.formatElapsedTimeWithHour(tripStatistics.getMovingTime()));
row.getCustomElements().setValueLocal(
"distance", SendSpreadsheetsUtils.getDistance(tripStatistics.getTotalDistance(), metricUnits));
row.getCustomElements().setValueLocal("distanceunit", distanceUnit);
row.getCustomElements().setValueLocal(
"averagespeed", SendSpreadsheetsUtils.getSpeed(tripStatistics.getAverageSpeed(), metricUnits));
row.getCustomElements().setValueLocal("averagemovingspeed",
SendSpreadsheetsUtils.getSpeed(tripStatistics.getAverageMovingSpeed(), metricUnits));
row.getCustomElements().setValueLocal(
"maxspeed", SendSpreadsheetsUtils.getSpeed(tripStatistics.getMaxSpeed(), metricUnits));
row.getCustomElements().setValueLocal("speedunit", speedUnit);
row.getCustomElements().setValueLocal("elevationgain",
SendSpreadsheetsUtils.getElevation(tripStatistics.getTotalElevationGain(), metricUnits));
row.getCustomElements().setValueLocal(
"minelevation", SendSpreadsheetsUtils.getElevation(tripStatistics.getMinElevation(), metricUnits));
row.getCustomElements().setValueLocal(
"maxelevation", SendSpreadsheetsUtils.getElevation(tripStatistics.getMaxElevation(), metricUnits));
row.getCustomElements().setValueLocal("elevationunit", elevationUnit);
ListEntry result = spreadsheetService.insert(worksheetUrl, row);
return result != null;
}
}
@@ -0,0 +1,79 @@
/*
* 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.spreadsheets;
import com.google.android.apps.mytracks.util.UnitConversions;
import java.text.NumberFormat;
import java.util.Locale;
/**
* Utilities for sending a track to Google Spreadsheet.
*
* @author Jimmy Shih
*/
public class SendSpreadsheetsUtils {
// Google Spreadsheet 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 SendSpreadsheetsUtils() {}
/**
* Gets the distance. Performs unit conversion and formatting.
*
* @param distanceInMeter the distance in meters
* @param metricUnits true to use metric
*/
public static final String getDistance(double distanceInMeter, boolean metricUnits) {
double distanceInKilometer = distanceInMeter * UnitConversions.M_TO_KM;
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
*/
public static final String getSpeed(double speedInMeterPerSecond, boolean metricUnits) {
double speedInKilometerPerHour = speedInMeterPerSecond * UnitConversions.MS_TO_KMH;
double speed = metricUnits ? speedInKilometerPerHour
: speedInKilometerPerHour * UnitConversions.KM_TO_MI;
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
*/
public static final String getElevation(double elevationInMeter, boolean metricUnits) {
double elevation = metricUnits ? elevationInMeter : elevationInMeter * UnitConversions.M_TO_FT;
return INTEGER_FORMAT.format(elevation);
}
}
@@ -0,0 +1,577 @@
/*
* Copyright 2013 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.sync;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.io.file.KmlFileTrackImporter;
import com.google.android.apps.mytracks.io.sendtogoogle.SendToGoogleUtils;
import com.google.android.apps.mytracks.util.PreferencesUtils;
import com.google.android.gms.auth.GoogleAuthException;
import com.google.android.gms.auth.UserRecoverableAuthException;
import com.google.android.maps.mytracks.R;
import com.google.api.client.googleapis.extensions.android.gms.auth.GoogleAccountCredential;
import com.google.api.client.googleapis.extensions.android.gms.auth.UserRecoverableAuthIOException;
import com.google.api.client.http.GenericUrl;
import com.google.api.client.http.HttpResponse;
import com.google.api.services.drive.Drive;
import com.google.api.services.drive.Drive.Changes;
import com.google.api.services.drive.Drive.Files;
import com.google.api.services.drive.model.About;
import com.google.api.services.drive.model.Change;
import com.google.api.services.drive.model.ChangeList;
import com.google.api.services.drive.model.File;
import com.google.api.services.drive.model.FileList;
import android.accounts.Account;
import android.content.AbstractThreadedSyncAdapter;
import android.content.ContentProviderClient;
import android.content.Context;
import android.content.SyncResult;
import android.database.Cursor;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.Log;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
/**
* SyncAdapter to sync tracks with Google Drive.
*
* @author Jimmy Shih
*/
public class SyncAdapter extends AbstractThreadedSyncAdapter {
private static final String TAG = SyncAdapter.class.getSimpleName();
// drive.about.get fields to get the largestChangeId
private static final String ABOUT_GET_FIELDS = "largestChangeId";
private final Context context;
private final MyTracksProviderUtils myTracksProviderUtils;
private Drive drive;
private String driveAccountName; // the account name associated with the drive
public SyncAdapter(Context context) {
super(context, true);
this.context = context;
this.myTracksProviderUtils = MyTracksProviderUtils.Factory.get(context);
}
@Override
public void onPerformSync(Account account, Bundle extras, String authority,
ContentProviderClient provider, SyncResult syncResult) {
if (!PreferencesUtils.getBoolean(
context, R.string.drive_sync_key, PreferencesUtils.DRIVE_SYNC_DEFAULT)) {
return;
}
if (account == null) {
return;
}
String googleAccount = PreferencesUtils.getString(
context, R.string.google_account_key, PreferencesUtils.GOOGLE_ACCOUNT_DEFAULT);
if (googleAccount == null || googleAccount.equals(PreferencesUtils.GOOGLE_ACCOUNT_DEFAULT)) {
return;
}
if (!googleAccount.equals(account.name)) {
return;
}
try {
GoogleAccountCredential credential = SendToGoogleUtils.getGoogleAccountCredential(
context, account.name, SendToGoogleUtils.DRIVE_SCOPE);
if (credential == null) {
return;
}
if (drive == null || !driveAccountName.equals(account.name)) {
drive = SyncUtils.getDriveService(credential);
driveAccountName = account.name;
}
File folder = SyncUtils.getMyTracksFolder(context, drive);
if (folder == null) {
return;
}
String folderId = folder.getId();
if (folderId == null) {
return;
}
long largestChangeId = PreferencesUtils.getLong(
context, R.string.drive_largest_change_id_key);
if (largestChangeId == PreferencesUtils.DRIVE_LARGEST_CHANGE_ID_DEFAULT) {
performInitialSync(folderId);
} else {
performIncrementalSync(folderId, largestChangeId);
}
insertNewTracks(folderId);
} catch (UserRecoverableAuthException e) {
SendToGoogleUtils.sendNotification(
context, account.name, e.getIntent(), SendToGoogleUtils.DRIVE_NOTIFICATION_ID);
} catch (GoogleAuthException e) {
Log.e(TAG, "GoogleAuthException", e);
} catch (UserRecoverableAuthIOException e) {
SendToGoogleUtils.sendNotification(
context, account.name, e.getIntent(), SendToGoogleUtils.DRIVE_NOTIFICATION_ID);
} catch (IOException e) {
Log.e(TAG, "IOException", e);
}
}
/**
* Performs initial sync.
*
* @param folderId the folder id
*/
private void performInitialSync(String folderId) throws IOException {
// Get the largest change id first to avoid race conditions
About about = drive.about().get().setFields(ABOUT_GET_FIELDS).execute();
long largestChangeId = about.getLargestChangeId();
// Get all the KML files in the "My Drive:/My Tracks" folder
Files.List myTracksFolderRequest = drive.files()
.list().setQ(String.format(Locale.US, SyncUtils.MY_TRACKS_FOLDER_FILES_QUERY, folderId));
Map<String, File> myTracksFolderMap = getFiles(myTracksFolderRequest, true);
// Handle tracks that are already uploaded to Google Drive
Set<String> syncedDriveIds = updateSyncedTracks(folderId);
for (String driveId : syncedDriveIds) {
myTracksFolderMap.remove(driveId);
}
// Get all the KML files in the "Shared with me:/" folder
Files.List sharedWithMeRequest = drive.files()
.list().setQ(SyncUtils.SHARED_WITH_ME_FILES_QUERY);
Map<String, File> sharedWithMeMap = getFiles(sharedWithMeRequest, false);
try {
insertNewDriveFiles(myTracksFolderMap.values());
insertNewDriveFiles(sharedWithMeMap.values());
PreferencesUtils.setLong(context, R.string.drive_largest_change_id_key, largestChangeId);
} catch (IOException e) {
// Remove all imported tracks
Cursor cursor = null;
try {
cursor = myTracksProviderUtils.getTrackCursor(SyncUtils.DRIVE_ID_TRACKS_QUERY, null, null);
if (cursor != null && cursor.moveToFirst()) {
do {
Track track = myTracksProviderUtils.createTrack(cursor);
if (!syncedDriveIds.contains(track.getDriveId())) {
myTracksProviderUtils.deleteTrack(track.getId());
}
} while (cursor.moveToNext());
}
} finally {
if (cursor != null) {
cursor.close();
}
}
throw e;
}
}
/**
* Updates synced tracks.
*
* @param folderId the folder id
* @return drive ids of the synced tracks
*/
private Set<String> updateSyncedTracks(String folderId) throws IOException {
Set<String> result = new HashSet<String>();
Cursor cursor = null;
try {
cursor = myTracksProviderUtils.getTrackCursor(SyncUtils.DRIVE_ID_TRACKS_QUERY, null, null);
if (cursor != null && cursor.moveToFirst()) {
do {
Track track = myTracksProviderUtils.createTrack(cursor);
String driveId = track.getDriveId();
if (driveId != null && !driveId.equals("")) {
if (!track.isSharedWithMe()) {
File driveFile = drive.files().get(driveId).execute();
if (SyncUtils.isValid(driveFile, folderId)) {
merge(track, driveFile);
result.add(driveId);
} else {
/*
* Track has a drive id, but the drive id is no longer valid.
* E.g., the file is moved to another folder. Clear the drive
* id.
*/
SyncUtils.updateTrackWithDriveFileInfo(myTracksProviderUtils, track, null);
}
}
}
} while (cursor.moveToNext());
}
} finally {
if (cursor != null) {
cursor.close();
}
}
return result;
}
/**
* Performs incremental sync.
*
* @param folderId the folder id
* @param largestChangeId the largest change id
*/
private void performIncrementalSync(String folderId, long largestChangeId) throws IOException {
String driveDeletedList = PreferencesUtils.getString(
context, R.string.drive_deleted_list_key, PreferencesUtils.DRIVE_DELETED_LIST_DEFAULT);
String deletedIds[] = TextUtils.split(driveDeletedList, ";");
for (String driveId : deletedIds) {
deleteDriveFile(driveId, folderId, true);
}
PreferencesUtils.setString(
context, R.string.drive_deleted_list_key, PreferencesUtils.DRIVE_DELETED_LIST_DEFAULT);
Map<String, File> changes = new HashMap<String, File>();
largestChangeId = getDriveChanges(folderId, largestChangeId, changes);
Cursor cursor = null;
try {
// Get all the local tracks with drive file id
cursor = myTracksProviderUtils.getTrackCursor(SyncUtils.DRIVE_ID_TRACKS_QUERY, null, null);
if (cursor != null && cursor.moveToFirst()) {
do {
Track track = myTracksProviderUtils.createTrack(cursor);
String driveId = track.getDriveId();
if (changes.containsKey(driveId)) {
// Track has changed
File driveFile = changes.get(driveId);
if (driveFile == null) {
Log.d(TAG, "Delete local track " + track.getName());
myTracksProviderUtils.deleteTrack(track.getId());
} else {
merge(track, driveFile);
}
changes.remove(driveId);
} else {
if (!track.isSharedWithMe()) {
// Handle the case the track has changed
File driveFile = drive.files().get(driveId).execute();
if (SyncUtils.isValid(driveFile, folderId)) {
merge(track, driveFile);
} else {
/*
* Track has a drive id, but the drive id is no longer valid.
* E.g., the file is moved to another folder. Clear the drive
* id.
*/
SyncUtils.updateTrackWithDriveFileInfo(myTracksProviderUtils, track, null);
}
}
}
} while (cursor.moveToNext());
}
// Handle new drive files
insertNewDriveFiles(changes.values());
PreferencesUtils.setLong(context, R.string.drive_largest_change_id_key, largestChangeId);
} finally {
if (cursor != null) {
cursor.close();
}
}
}
/**
* Inserts new tracks.
*
* @param folderId the folder id
*/
private void insertNewTracks(String folderId) throws IOException {
Cursor cursor = null;
try {
cursor = myTracksProviderUtils.getTrackCursor(SyncUtils.NO_DRIVE_ID_TRACKS_QUERY, null, null);
long recordingTrackId = PreferencesUtils.getLong(context, R.string.recording_track_id_key);
if (cursor != null && cursor.moveToFirst()) {
do {
Track track = myTracksProviderUtils.createTrack(cursor);
if (track.getId() == recordingTrackId) {
continue;
}
// If not successful, the next sync will retry again
SyncUtils.insertDriveFile(drive, folderId, context, myTracksProviderUtils, track, true);
} while (cursor.moveToNext());
}
} finally {
if (cursor != null) {
cursor.close();
}
}
}
/**
* Inserts new Google Drive files.
*
* @param driveFiles a collection of drive files to insert
* @throws IOException
*/
private void insertNewDriveFiles(Collection<File> driveFiles) throws IOException {
for (File driveFile : driveFiles) {
InputStream inputStream = null;
try {
if (driveFile == null) {
continue;
}
inputStream = downloadDriveFile(driveFile, true);
if (inputStream == null) {
continue;
}
KmlFileTrackImporter kmlFileTrackImporter = new KmlFileTrackImporter(context, -1L, null);
long trackId = kmlFileTrackImporter.importFile(inputStream);
if (trackId != -1L) {
Track track = myTracksProviderUtils.getTrack(trackId);
if (track == null) {
Log.e(TAG, "Unable to insert new drive file for " + driveFile.getId());
continue;
}
SyncUtils.updateTrackWithDriveFileInfo(myTracksProviderUtils, track, driveFile);
Log.d(TAG, "Add from Google Drive " + track.getName());
}
} catch (IOException e) {
Log.e(TAG, "Unable to insert new drive file for " + driveFile.getId(), e);
} finally {
if (inputStream != null) {
inputStream.close();
}
}
}
}
/**
* Gets all the files from a request.
*
* @param request the request
* @param excludeSharedWithMe true to exclude shared with me files
* @return a map of file id to file
*/
private Map<String, File> getFiles(Files.List request, boolean excludeSharedWithMe)
throws IOException {
Map<String, File> idToFileMap = new HashMap<String, File>();
do {
FileList files = request.execute();
for (File file : files.getItems()) {
if (excludeSharedWithMe && file.getSharedWithMeDate() != null) {
continue;
}
idToFileMap.put(file.getId(), file);
}
request.setPageToken(files.getNextPageToken());
} while (request.getPageToken() != null && request.getPageToken().length() > 0);
return idToFileMap;
}
/**
* Gets the Drive changes in the My Tracks folder. Includes deleted files.
*
* @param folderId the folder id
* @param changeId the largest change id
* @param changes a map of drive id to file for the changes
* @return an updated largest change id
*/
private long getDriveChanges(String folderId, long changeId, Map<String, File> changes)
throws IOException {
Changes.List request = drive.changes().list().setStartChangeId(changeId + 1);
do {
ChangeList changeList = request.execute();
long newId = changeList.getLargestChangeId().longValue();
for (Change change : changeList.getItems()) {
if (change.getDeleted()) {
changes.put(change.getFileId(), null);
} else {
File file = change.getFile();
if (SyncUtils.isInFolder(file, folderId) || SyncUtils.isSharedWithMe(file)) {
if (file.getLabels().getTrashed()) {
changes.put(change.getFileId(), null);
} else {
changes.put(change.getFileId(), file);
}
}
}
}
if (newId > changeId) {
changeId = newId;
}
request.setPageToken(changeList.getNextPageToken());
} while (request.getPageToken() != null && request.getPageToken().length() > 0);
Log.d(TAG, "Got drive changes: " + changes.size() + " " + changeId);
return changeId;
}
/**
* Merges a track with a drive file.
*
* @param track the track
* @param driveFile the drive file
*/
private void merge(Track track, File driveFile) throws IOException {
long modifiedTime = track.getModifiedTime();
long driveModifiedTime = driveFile.getModifiedDate().getValue();
if (modifiedTime > driveModifiedTime) {
Log.d(TAG, "Updating track change for track " + track.getName() + " and drive file "
+ driveFile.getTitle());
if (!SyncUtils.updateDriveFile(
drive, driveFile, context, myTracksProviderUtils, track, true)) {
Log.e(TAG, "Unable to update drive file");
track.setModifiedTime(driveModifiedTime);
myTracksProviderUtils.updateTrack(track);
}
} else if (modifiedTime < driveModifiedTime) {
Log.d(TAG, "Updating drive change for track " + track.getName() + " and drive file "
+ driveFile.getTitle());
if (!updateTrack(track, driveFile)) {
Log.e(TAG, "Unable to update drive change");
track.setModifiedTime(driveModifiedTime);
myTracksProviderUtils.updateTrack(track);
}
}
}
/**
* Updates track based on drive file changes. Returns true if successful.
*
* @param track the track
* @param driveFile the drive file
*/
private boolean updateTrack(Track track, File driveFile) throws IOException {
java.io.File file = null;
try {
file = SyncUtils.getFile(context, myTracksProviderUtils, track);
String digest = SyncUtils.md5(file);
if (digest != null && digest.equals(driveFile.getMd5Checksum())) {
track.setModifiedTime(driveFile.getModifiedDate().getValue());
myTracksProviderUtils.updateTrack(track);
return true;
}
} finally {
if (file != null) {
file.delete();
}
}
InputStream inputStream = null;
try {
inputStream = downloadDriveFile(driveFile, true);
if (inputStream == null) {
Log.e(TAG, "Unable to update track. Input stream is null for track " + track.getName());
return false;
}
KmlFileTrackImporter kmlFileTrackImporter = new KmlFileTrackImporter(
context, track.getId(), null);
long trackId = kmlFileTrackImporter.importFile(inputStream);
if (trackId != -1L) {
Track newTrack = myTracksProviderUtils.getTrack(trackId);
if (newTrack == null) {
Log.e(TAG, "Unable to merge, imported track is null");
} else {
SyncUtils.updateTrackWithDriveFileInfo(myTracksProviderUtils, newTrack, driveFile);
return true;
}
} else {
Log.e(TAG, "Unable to merge, track id is -1L");
}
} catch (IOException e) {
Log.e(TAG, "Unable to merge", e);
} finally {
if (inputStream != null) {
inputStream.close();
}
}
return false;
}
/**
* Deletes a drive file.
*
* @param driveId the drive id
* @param folderId the folder id
* @param canRetry true if can retry the request
*/
private void deleteDriveFile(String driveId, String folderId, boolean canRetry)
throws UserRecoverableAuthIOException {
try {
File driveFile = drive.files().get(driveId).execute();
if (SyncUtils.isInFolder(driveFile, folderId)) {
if (!driveFile.getLabels().getTrashed()) {
drive.files().trash(driveId).execute();
}
// if trashed, ignore
} else if (SyncUtils.isSharedWithMe(driveFile)) {
if (!driveFile.getLabels().getTrashed()) {
drive.files().delete(driveId).execute();
}
// if trashed, ignore
}
} catch (UserRecoverableAuthIOException e) {
throw e;
} catch (IOException e) {
if (canRetry) {
deleteDriveFile(driveId, folderId, false);
return;
}
Log.e(TAG, "Unable to delete Drive file for " + driveId, e);
}
}
/**
* Downloads a drive file.
*
* @param driveFile the drive file
*/
private InputStream downloadDriveFile(File driveFile, boolean canRetry) throws IOException {
if (driveFile.getDownloadUrl() == null || driveFile.getDownloadUrl().length() == 0) {
Log.d(TAG, "Drive file download url doesn't exist: " + driveFile.getTitle());
return null;
}
try {
HttpResponse httpResponse = drive.getRequestFactory()
.buildGetRequest(new GenericUrl(driveFile.getDownloadUrl())).execute();
if (httpResponse == null) {
Log.e(TAG, "http response is null");
return null;
}
return httpResponse.getContent();
} catch (UserRecoverableAuthIOException e) {
throw e;
} catch (IOException e) {
if (canRetry) {
return downloadDriveFile(driveFile, false);
}
throw e;
}
}
}
@@ -0,0 +1,47 @@
/*
* Copyright 2013 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.sync;
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
/**
* Service to sync with Google Drive.
*
* @author Jimmy Shih
*/
public class SyncService extends Service {
private static final Object lock = new Object();
private static SyncAdapter syncAdapter = null;
@Override
public void onCreate() {
synchronized (lock) {
if (syncAdapter == null) {
syncAdapter = new SyncAdapter(getApplicationContext());
}
}
}
@Override
public IBinder onBind(Intent intent) {
return syncAdapter.getSyncAdapterBinder();
}
}
@@ -0,0 +1,536 @@
/*
* Copyright 2013 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.sync;
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.TracksColumns;
import com.google.android.apps.mytracks.io.file.TrackFileFormat;
import com.google.android.apps.mytracks.io.file.export.FileTrackExporter;
import com.google.android.apps.mytracks.util.FileUtils;
import com.google.android.apps.mytracks.util.PreferencesUtils;
import com.google.android.maps.mytracks.R;
import com.google.api.client.extensions.android.http.AndroidHttp;
import com.google.api.client.googleapis.extensions.android.gms.auth.GoogleAccountCredential;
import com.google.api.client.googleapis.extensions.android.gms.auth.UserRecoverableAuthIOException;
import com.google.api.client.http.FileContent;
import com.google.api.client.json.gson.GsonFactory;
import com.google.api.services.drive.Drive;
import com.google.api.services.drive.Drive.Files.List;
import com.google.api.services.drive.model.File;
import com.google.api.services.drive.model.FileList;
import com.google.api.services.drive.model.ParentReference;
import com.google.common.annotations.VisibleForTesting;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.content.ContentResolver;
import android.content.Context;
import android.database.Cursor;
import android.os.Bundle;
import android.util.Log;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Locale;
/**
* Utilites for Google Drive sync.
*
* @author Jimmy Shih
*/
public class SyncUtils {
// Get tracks with drive id
public static final String DRIVE_ID_TRACKS_QUERY = TracksColumns.DRIVEID + " IS NOT NULL AND "
+ TracksColumns.DRIVEID + "!=''";
// Get tracks without drive id
public static final String NO_DRIVE_ID_TRACKS_QUERY = TracksColumns.DRIVEID + " IS NULL OR "
+ TracksColumns.DRIVEID + "=''";
// KML mime type
public static final String KML_MIME_TYPE = "application/vnd.google-earth.kml+xml";
// Get My Tracks folder KML files
public static final String MY_TRACKS_FOLDER_FILES_QUERY = "'%s' in parents and mimeType = '"
+ KML_MIME_TYPE + "' and trashed = false";
// Get shared with me KML files
public static final String SHARED_WITH_ME_FILES_QUERY = "sharedWithMe and mimeType = '"
+ KML_MIME_TYPE + "' and trashed = false";
// Folder mime type
private static final String FOLDER_MIME_TYPE = "application/vnd.google-apps.folder";
// Get My Tracks folder
@VisibleForTesting
public static final String MY_TRACKS_FOLDER_QUERY =
"'root' in parents and title = '%s' and mimeType = '" + FOLDER_MIME_TYPE
+ "' and trashed = false";
private static final String TAG = SyncUtils.class.getSimpleName();
private static final String SYNC_AUTHORITY = "com.google.android.maps.mytracks";
private SyncUtils() {}
/**
* Syncs now for the current account.
*
* @param context the context
*/
public static void syncNow(Context context) {
Account[] accounts = AccountManager.get(context).getAccountsByType(Constants.ACCOUNT_TYPE);
String googleAccount = PreferencesUtils.getString(
context, R.string.google_account_key, PreferencesUtils.GOOGLE_ACCOUNT_DEFAULT);
for (Account account : accounts) {
if (account.name.equals(googleAccount)) {
ContentResolver.cancelSync(account, SYNC_AUTHORITY);
ContentResolver.requestSync(account, SYNC_AUTHORITY, new Bundle());
break;
}
}
}
/**
* Disables sync.
*
* @param context the context
*/
public static void disableSync(Context context) {
Account[] accounts = AccountManager.get(context).getAccountsByType(Constants.ACCOUNT_TYPE);
for (Account account : accounts) {
ContentResolver.cancelSync(account, SYNC_AUTHORITY);
ContentResolver.setIsSyncable(account, SYNC_AUTHORITY, 0);
ContentResolver.setSyncAutomatically(account, SYNC_AUTHORITY, false);
}
}
/**
* Returns true if sync is active.
*
* @param context the context
*/
public static boolean isSyncActive(Context context) {
Account[] accounts = AccountManager.get(context).getAccountsByType(Constants.ACCOUNT_TYPE);
for (Account account : accounts) {
if (ContentResolver.isSyncActive(account, SYNC_AUTHORITY)) {
return true;
}
}
return false;
}
/**
* Enables sync.
*
* @param account the account
*/
public static void enableSync(Account account) {
ContentResolver.setIsSyncable(account, SYNC_AUTHORITY, 1);
ContentResolver.setSyncAutomatically(account, SYNC_AUTHORITY, true);
ContentResolver.requestSync(account, SYNC_AUTHORITY, new Bundle());
}
/**
* Clears the sync state. Assumes sync is turned off. Do not want clearing the
* state to cause sync activities.
*/
public static void clearSyncState(Context context) {
MyTracksProviderUtils myTracksProviderUtils = MyTracksProviderUtils.Factory.get(context);
Cursor cursor = null;
try {
cursor = myTracksProviderUtils.getTrackCursor(SyncUtils.DRIVE_ID_TRACKS_QUERY, null, null);
if (cursor != null && cursor.moveToFirst()) {
do {
Track track = myTracksProviderUtils.createTrack(cursor);
if (track.isSharedWithMe()) {
myTracksProviderUtils.deleteTrack(track.getId());
} else {
SyncUtils.updateTrackWithDriveFileInfo(myTracksProviderUtils, track, null);
}
} while (cursor.moveToNext());
}
} finally {
if (cursor != null) {
cursor.close();
}
}
PreferencesUtils.setLong(context, R.string.drive_largest_change_id_key,
PreferencesUtils.DRIVE_LARGEST_CHANGE_ID_DEFAULT);
// Clear the drive_deleted_list_key last
PreferencesUtils.setString(
context, R.string.drive_deleted_list_key, PreferencesUtils.DRIVE_DELETED_LIST_DEFAULT);
}
/**
* Gets the drive service.
*
* @param credential the credential
*/
public static Drive getDriveService(GoogleAccountCredential credential) {
return new Drive.Builder(AndroidHttp.newCompatibleTransport(), new GsonFactory(), credential)
.build();
}
/**
* Gets the My Tracks folder. Creates one if necessary.
*
* @param context the context
* @param drive the drive
*/
public static File getMyTracksFolder(Context context, Drive drive) throws IOException {
String folderName = context.getString(R.string.my_tracks_app_name);
List list = drive.files()
.list().setQ(String.format(Locale.US, MY_TRACKS_FOLDER_QUERY, folderName));
FileList result = list.execute();
for (File file : result.getItems()) {
if (file.getSharedWithMeDate() == null) {
return file;
}
}
File file = new File();
file.setTitle(folderName);
file.setMimeType(FOLDER_MIME_TYPE);
return drive.files().insert(file).execute();
}
/**
* Returns true if the drive file is a Shared with me KML file.
*
* @param driveFile the drive file
*/
public static boolean isSharedWithMe(File driveFile) {
if (driveFile == null) {
return false;
}
if (!SyncUtils.KML_MIME_TYPE.equals(driveFile.getMimeType())) {
return false;
}
return driveFile.getSharedWithMeDate() != null;
}
/**
* Returns true if the drive file is a valid KML file in the My Tracks folder
* and not trashed.
*
* @param driveFile the drive file
* @param folderId the My Tracks folder id
*/
public static boolean isValid(File driveFile, String folderId) {
return isInFolder(driveFile, folderId) && !driveFile.getLabels().getTrashed();
}
/**
* Returns true if the drive file is a KML file in the My Tracks folder.
*
* @param driveFile the drive file
* @param folderId the My Tracks folder id
*/
public static boolean isInFolder(File driveFile, String folderId) {
if (driveFile == null) {
return false;
}
if (!SyncUtils.KML_MIME_TYPE.equals(driveFile.getMimeType())) {
return false;
}
if (driveFile.getSharedWithMeDate() != null) {
return false;
}
for (ParentReference parentReference : driveFile.getParents()) {
String id = parentReference.getId();
if (id != null && id.equals(folderId)) {
return true;
}
}
return false;
}
/**
* Inserts a Drive file.
*
* @param drive the drive
* @param folderId the folder id
* @param context the context
* @param myTracksProviderUtils the myTracksProviderUtils
* @param track the track
* @param canRetry true if can retry
* @return the added drive file or null.
*/
public static File insertDriveFile(Drive drive, String folderId, Context context,
MyTracksProviderUtils myTracksProviderUtils, Track track, boolean canRetry)
throws IOException {
java.io.File file = getFile(context, myTracksProviderUtils, track);
if (file == null) {
Log.e(TAG, "Unable to add Drive file. File is null for track " + track.getName());
return null;
}
try {
Log.d(TAG, "Add Drive file for track " + track.getName());
File uploadedFile = insertDriveFile(drive, folderId, track.getName(), file, canRetry);
if (uploadedFile == null) {
Log.e(TAG, "Unable to add Drive file. Uploaded file is null for track " + track.getName());
return null;
}
SyncUtils.updateTrackWithDriveFileInfo(myTracksProviderUtils, track, uploadedFile);
return uploadedFile;
} finally {
file.delete();
}
}
/**
* Inserts a Drive file.
*
* @param drive the drive
* @param folderId the folder id
* @param name the track name
* @param file the track file
* @param canRetry true if can retry
*/
private static File insertDriveFile(
Drive drive, String folderId, String name, java.io.File file, boolean canRetry)
throws IOException {
try {
FileContent fileContent = new FileContent(KML_MIME_TYPE, file);
// file's parent
ParentReference parentReference = new ParentReference();
parentReference.setId(folderId);
ArrayList<ParentReference> parents = new ArrayList<ParentReference>();
parents.add(parentReference);
// file's metadata
File newMetaData = new File();
newMetaData.setTitle(name + "." + TrackFileFormat.KML.getExtension());
newMetaData.setMimeType(KML_MIME_TYPE);
newMetaData.setParents(parents);
return drive.files().insert(newMetaData, fileContent).execute();
} catch (UserRecoverableAuthIOException e) {
throw e;
} catch (IOException e) {
if (canRetry) {
return insertDriveFile(drive, folderId, name, file, false);
}
throw e;
}
}
/**
* Updates a Drive file. Returns true if successful.
*
* @param drive the drive
* @param driveFile the drive file
* @param context the context
* @param myTracksProviderUtils the myTracksProviderUtils
* @param track the track
* @param canRetry true if can retry
*/
public static boolean updateDriveFile(Drive drive, File driveFile, Context context,
MyTracksProviderUtils myTracksProviderUtils, Track track, boolean canRetry)
throws IOException {
Log.d(TAG, "Update drive file for track " + track.getName());
java.io.File file = SyncUtils.getFile(context, myTracksProviderUtils, track);
if (file == null) {
Log.e(TAG, "Unable to update drive file. File is null for track " + track.getName());
return false;
} else {
try {
String title = track.getName() + "." + TrackFileFormat.KML.getExtension();
File updatedFile;
String digest = md5(file);
if (digest != null && digest.equals(driveFile.getMd5Checksum())) {
if (title.equals(driveFile.getTitle())) {
updatedFile = driveFile;
} else {
updatedFile = updateDriveFile(drive, driveFile, title, null, canRetry);
}
} else {
updatedFile = updateDriveFile(drive, driveFile, title, file, canRetry);
}
if (updatedFile == null) {
Log.e(TAG,
"Unable to update drive file. Updated file is null for track " + track.getName());
return false;
}
long modifiedTime = updatedFile.getModifiedDate().getValue();
if (track.getModifiedTime() != modifiedTime) {
track.setModifiedTime(modifiedTime);
myTracksProviderUtils.updateTrack(track);
}
return true;
} finally {
file.delete();
}
}
}
/**
* Updates a Drive file.
*
* @param drive the drive
* @param driveFile the drive file
* @param driveTitle the drive title
* @param file the track file
* @param canRetry true if can retry
*/
private static File updateDriveFile(
Drive drive, File driveFile, String driveTitle, java.io.File file, boolean canRetry)
throws IOException {
try {
driveFile.setTitle(driveTitle);
if (file != null) {
return drive.files()
.update(driveFile.getId(), driveFile, new FileContent(KML_MIME_TYPE, file)).execute();
} else {
return drive.files().update(driveFile.getId(), driveFile).execute();
}
} catch (UserRecoverableAuthIOException e) {
throw e;
} catch (IOException e) {
if (canRetry) {
return updateDriveFile(drive, driveFile, driveTitle, file, false);
}
throw e;
}
}
/**
* Gets a file from a track.
*
* @param context the context
* @param myTracksProviderUtils the myTracksProviderUtils
* @param track the track
*/
public static java.io.File getFile(
Context context, MyTracksProviderUtils myTracksProviderUtils, Track track)
throws FileNotFoundException {
TrackFileFormat trackFileFormat = TrackFileFormat.KML;
java.io.File directory = new java.io.File(
context.getCacheDir(), trackFileFormat.getExtension());
if (!FileUtils.ensureDirectoryExists(directory)) {
Log.d(TAG, "Unable to create " + directory.getAbsolutePath());
return null;
}
java.io.File file = new java.io.File(directory,
FileUtils.buildUniqueFileName(directory, track.getName(), trackFileFormat.getExtension()));
FileTrackExporter fileTrackExporter = new FileTrackExporter(
myTracksProviderUtils, new Track[] { track }, trackFileFormat, context, false, null);
FileOutputStream fileOutputStream = null;
try {
fileOutputStream = new FileOutputStream(file);
fileTrackExporter.writeTrack(fileOutputStream);
if (fileTrackExporter.isSuccess()) {
return file;
} else {
if (!file.delete()) {
Log.d(TAG, "Unable to delete file for track " + track.getName());
}
Log.d(TAG, "Unable to get file for track " + track.getName());
return null;
}
} finally {
if (fileOutputStream != null) {
try {
fileOutputStream.close();
} catch (IOException e) {
Log.e(TAG, "Unable to close file output stream", e);
}
}
}
}
/**
* Updates track with drive file info.
*
* @param myTracksProviderUtils the myTracksProviderUtils
* @param track the track
* @param driveFile the drive file
*/
public static void updateTrackWithDriveFileInfo(
MyTracksProviderUtils myTracksProviderUtils, Track track, File driveFile) {
track.setDriveId(driveFile != null ? driveFile.getId() : "");
track.setModifiedTime(driveFile != null ? driveFile.getModifiedDate().getValue() : -1L);
track.setSharedWithMe(driveFile != null ? driveFile.getSharedWithMeDate() != null : false);
track.setSharedOwner(driveFile != null && driveFile.getSharedWithMeDate() != null
&& driveFile.getOwnerNames().size() > 0 ? driveFile.getOwnerNames().get(0)
: "");
myTracksProviderUtils.updateTrack(track);
}
/**
* Gets the md5 digest for the file.
*
* @param file the file
*/
public static String md5(java.io.File file) {
if (file == null) {
return null;
}
InputStream in = null;
byte[] digest;
try {
in = new FileInputStream(file);
MessageDigest digester = MessageDigest.getInstance("MD5");
byte[] bytes = new byte[8192];
int byteCount;
while ((byteCount = in.read(bytes)) > 0) {
digester.update(bytes, 0, byteCount);
}
digest = digester.digest();
StringBuilder builder = new StringBuilder(digest.length * 2);
for (byte b : digest) {
if ((b & 0xFF) < 0x10) {
builder.append("0");
}
builder.append(Integer.toHexString(b & 0xFF));
}
return builder.toString();
} catch (IOException e) {
Log.e(TAG, "IOException", e);
return null;
} catch (NoSuchAlgorithmException e) {
Log.e(TAG, "NoSuchAlgorithmException", e);
return null;
} finally {
if (in != null) {
try {
in.close();
} catch (IOException e) {
Log.e(TAG, "Unable to close inputstream", e);
return null;
}
}
}
}
}