Patch 3 CLs from

http://code.google.com/r/rdamazio-mytracks-staging2/
- 7781a7c23cf1
- 883427ab11f8
- d801f8f7c16b

- Use Halo them for api level 11 or higher.
- Use action bar for api level 11 or higher.
- Move all menu items for the 3 tabs to MenuManager.
This commit is contained in:
Jimmy Shih
2012-03-12 15:59:07 -07:00
282 changed files with 13236 additions and 9248 deletions
@@ -1,142 +0,0 @@
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks;
import com.google.android.maps.mytracks.R;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.util.Log;
/**
* Choose which account to upload track information to.
*
* @author Sandor Dornbush
*
* @deprecated To be removed.
*/
public class AccountChooser {
/**
* The last selected account.
*/
private int selectedAccountIndex = -1;
private Account selectedAccount = null;
/**
* An interface for receiving updates once the user has selected the account.
*/
public interface AccountHandler {
/**
* Handle the account being selected.
* @param account The selected account or null if none could be found
*/
public void onAccountSelected(Account account);
}
/**
* Chooses the best account to upload to.
* If no account is found the user will be alerted.
* If only one account is found that will be used.
* If multiple accounts are found the user will be allowed to choose.
*
* @param activity The parent activity
* @param handler The handler to be notified when an account has been selected
*/
public void chooseAccount(final Activity activity,
final AccountHandler handler) {
final Account[] accounts = AccountManager.get(activity)
.getAccountsByType(Constants.ACCOUNT_TYPE);
if (accounts.length < 1) {
alertNoAccounts(activity, handler);
return;
}
if (accounts.length == 1) {
handler.onAccountSelected(accounts[0]);
return;
}
// TODO This should be read out of a preference.
if (selectedAccount != null) {
handler.onAccountSelected(selectedAccount);
return;
}
// Let the user choose.
Log.e(Constants.TAG, "Multiple matching accounts found.");
final AlertDialog.Builder builder = new AlertDialog.Builder(activity);
builder.setTitle(R.string.send_google_choose_account_title);
builder.setCancelable(false);
builder.setPositiveButton(R.string.generic_ok,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
if (selectedAccountIndex >= 0) {
selectedAccount = accounts[selectedAccountIndex];
}
handler.onAccountSelected(selectedAccount);
}
});
builder.setNegativeButton(R.string.generic_cancel,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
handler.onAccountSelected(null);
}
});
String[] choices = new String[accounts.length];
for (int i = 0; i < accounts.length; i++) {
choices[i] = accounts[i].name;
}
builder.setSingleChoiceItems(choices, selectedAccountIndex,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
selectedAccountIndex = which;
}
});
builder.show();
}
public void setChosenAccount(String accountName, String accountType) {
selectedAccount = new Account(accountName, accountType);
}
/**
* Puts up a dialog alerting the user that no suitable account was found.
*/
private void alertNoAccounts(final Activity activity,
final AccountHandler handler) {
Log.e(Constants.TAG, "No matching accounts found.");
final AlertDialog.Builder builder = new AlertDialog.Builder(activity);
builder.setTitle(R.string.send_google_no_account_title);
builder.setMessage(R.string.send_google_no_account_message);
builder.setCancelable(true);
builder.setPositiveButton(R.string.generic_ok,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
handler.onAccountSelected(null);
}
});
builder.show();
}
}
@@ -3,7 +3,7 @@ package com.google.android.apps.mytracks;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.stats.TripStatistics;
import com.google.android.apps.mytracks.util.ApiFeatures;
import com.google.android.apps.mytracks.util.ApiAdapterFactory;
import com.google.android.maps.mytracks.R;
import android.app.Activity;
@@ -61,7 +61,7 @@ public class AggregatedStatsActivity extends Activity implements
this.tracksProvider = MyTracksProviderUtils.Factory.get(this);
// Show the action bar (or nothing at all).
ApiFeatures.getInstance().getApiAdapter().showActionBar(this);
ApiAdapterFactory.getApiAdapter().showActionBar(this);
setContentView(R.layout.stats);
@@ -29,6 +29,8 @@ import android.util.AttributeSet;
* @author Sandor Dornbush
*/
public class AntPreference extends Preference {
private final static int DEFAULT_PERSISTEDINT = 0;
public AntPreference(Context context) {
super(context);
@@ -41,7 +43,7 @@ public class AntPreference extends Preference {
}
private void init() {
int sensorId = getPersistedInt(0);
int sensorId = getPersistedInt(AntPreference.DEFAULT_PERSISTEDINT);
if (sensorId == 0) {
setSummary(R.string.settings_sensor_ant_not_paired);
} else {
@@ -60,4 +62,4 @@ public class AntPreference extends Preference {
}
});
}
}
}
@@ -19,6 +19,7 @@ import static android.content.Intent.ACTION_BOOT_COMPLETED;
import static com.google.android.apps.mytracks.Constants.RESUME_TRACK_EXTRA_NAME;
import static com.google.android.apps.mytracks.Constants.TAG;
import com.google.android.apps.mytracks.services.RemoveTempFilesService;
import com.google.android.apps.mytracks.services.TrackRecordingService;
import android.content.BroadcastReceiver;
@@ -50,6 +51,9 @@ public class BootReceiver extends BroadcastReceiver {
Intent startIntent = new Intent(context, TrackRecordingService.class);
startIntent.putExtra(RESUME_TRACK_EXTRA_NAME, true);
context.startService(startIntent);
Intent removeTempFilesIntent = new Intent(context, RemoveTempFilesService.class);
context.startService(removeTempFilesIntent);
} else {
Log.w(TAG, "BootReceiver: unsupported action");
}
@@ -26,13 +26,13 @@ import com.google.android.apps.mytracks.content.TrackDataHub;
import com.google.android.apps.mytracks.content.TrackDataHub.ListenerDataType;
import com.google.android.apps.mytracks.content.TrackDataListener;
import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.services.tasks.StatusAnnouncerFactory;
import com.google.android.apps.mytracks.stats.DoubleBuffer;
import com.google.android.apps.mytracks.stats.TripStatisticsBuilder;
import com.google.android.apps.mytracks.util.ApiFeatures;
import com.google.android.apps.mytracks.util.ApiAdapterFactory;
import com.google.android.apps.mytracks.util.LocationUtils;
import com.google.android.apps.mytracks.util.UnitConversions;
import com.google.android.maps.mytracks.R;
import com.google.common.annotations.VisibleForTesting;
import android.app.Activity;
import android.app.Dialog;
@@ -40,6 +40,7 @@ import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.location.Location;
import android.os.Bundle;
import android.speech.tts.TextToSpeech;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
@@ -113,13 +114,10 @@ public class ChartActivity extends Activity implements TrackDataListener {
super.onCreate(savedInstanceState);
// The volume we want to control is the Text-To-Speech volume
ApiFeatures apiFeatures = ApiFeatures.getInstance();
int volumeStream =
new StatusAnnouncerFactory(apiFeatures).getVolumeStream();
setVolumeControlStream(volumeStream);
setVolumeControlStream(TextToSpeech.Engine.DEFAULT_STREAM);
// Show the action bar (or nothing at all).
apiFeatures.getApiAdapter().showActionBar(this);
ApiAdapterFactory.getApiAdapter().showActionBar(this);
setContentView(R.layout.mytracks_charts);
ViewGroup layout = (ViewGroup) findViewById(R.id.elevation_chart);
@@ -242,7 +240,8 @@ public class ChartActivity extends Activity implements TrackDataListener {
* @param location the location to get data for (this method takes ownership of that location)
* @param result the resulting point to fill out
*/
private void fillDataPoint(Location location, double result[]) {
@VisibleForTesting
void fillDataPoint(Location location, double result[]) {
double timeOrDistance = Double.NaN,
elevation = Double.NaN,
speed = Double.NaN,
@@ -274,7 +273,6 @@ public class ChartActivity extends Activity implements TrackDataListener {
Mode mode = chartView.getMode();
switch (mode) {
case BY_DISTANCE:
timeOrDistance = profileLength / 1000.0;
if (lastLocation != null) {
double d = lastLocation.distanceTo(location);
if (metricUnits) {
@@ -283,6 +281,7 @@ public class ChartActivity extends Activity implements TrackDataListener {
profileLength += d * UnitConversions.KM_TO_MI;
}
}
timeOrDistance = profileLength * UnitConversions.M_TO_KM;
break;
case BY_TIME:
if (startTime == -1) {
@@ -310,7 +309,7 @@ public class ChartActivity extends Activity implements TrackDataListener {
&& (location.getSpeed() <= trackMaxSpeed)) {
speedBuffer.setNext(location.getSpeed());
}
speed = speedBuffer.getAverage() * 3.6;
speed = speedBuffer.getAverage() * UnitConversions.MS_TO_KMH;
if (!metricUnits) {
speed *= UnitConversions.KM_TO_MI;
}
@@ -462,4 +461,14 @@ public class ChartActivity extends Activity implements TrackDataListener {
});
return true;
}
@VisibleForTesting
ChartView getChartView() {
return chartView;
}
@VisibleForTesting
void setTrackMaxSpeed(double maxSpeed) {
trackMaxSpeed = maxSpeed;
}
}
@@ -17,6 +17,7 @@ package com.google.android.apps.mytracks;
import com.google.android.apps.mytracks.ChartView.Mode;
import com.google.android.maps.mytracks.R;
import com.google.common.annotations.VisibleForTesting;
import android.app.Dialog;
import android.content.Context;
@@ -118,4 +119,9 @@ public class ChartSettingsDialog extends Dialog {
public void setOnClickListener(OnClickListener clickListener) {
this.clickListener = clickListener;
}
}
@VisibleForTesting
CheckBox[] getSeries() {
return series;
}
}
@@ -21,6 +21,7 @@ import com.google.android.apps.mytracks.stats.ExtremityMonitor;
import com.google.android.apps.mytracks.util.StringUtils;
import com.google.android.apps.mytracks.util.UnitConversions;
import com.google.android.maps.mytracks.R;
import com.google.common.annotations.VisibleForTesting;
import android.content.Context;
import android.content.Intent;
@@ -407,11 +408,12 @@ public class ChartView extends View {
}
private int getWaypointX(Waypoint waypoint) {
return (mode == Mode.BY_DISTANCE)
? getX(metricUnits
? waypoint.getLength() / 1000.0
: waypoint.getLength() * UnitConversions.KM_TO_MI / 1000.0)
: getX(waypoint.getDuration());
if (mode == Mode.BY_DISTANCE) {
double lenghtInKm = waypoint.getLength() * UnitConversions.M_TO_KM;
return getX(metricUnits ? lenghtInKm : lenghtInKm * UnitConversions.KM_TO_MI);
} else {
return getX(waypoint.getDuration());
}
}
/**
@@ -884,4 +886,14 @@ public class ChartView extends View {
public void setChartValueSeriesEnabled(int index, boolean enabled) {
series[index].setEnabled(enabled);
}
@VisibleForTesting
int getZoomLevel() {
return zoomLevel;
}
@VisibleForTesting
int getMaxZoomLevel() {
return maxZoomLevel;
}
}
@@ -16,7 +16,7 @@
package com.google.android.apps.mytracks;
import com.google.android.apps.mytracks.util.ApiFeatures;
import com.google.android.apps.mytracks.util.ApiAdapterFactory;
import com.google.android.maps.mytracks.R;
import android.app.AlertDialog;
@@ -63,8 +63,7 @@ class CheckUnits {
Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = useMetricPreferences.edit();
String key = context.getString(R.string.metric_units_key);
ApiFeatures.getInstance().getApiAdapter().applyPreferenceChanges(
editor.putBoolean(key, metric));
ApiAdapterFactory.getApiAdapter().applyPreferenceChanges(editor.putBoolean(key, metric));
}
});
builder.setOnCancelListener(new DialogInterface.OnCancelListener() {
@@ -76,7 +75,7 @@ class CheckUnits {
}
private static void recordCheckPerformed(SharedPreferences preferences) {
ApiFeatures.getInstance().getApiAdapter().applyPreferenceChanges(
ApiAdapterFactory.getApiAdapter().applyPreferenceChanges(
preferences.edit().putBoolean(CHECK_UNITS_PREFERENCE_KEY, true));
}
@@ -35,52 +35,40 @@ public abstract class Constants {
/*
* onActivityResult request codes:
*
* TODO: These should be moved to the
* activities that call startActivityForResult. Thus do not need to keep a
* global order across all activities.
*/
public static final int GET_LOGIN = 0;
public static final int GET_MAP = 1;
public static final int SHOW_TRACK = 2;
public static final int AUTHENTICATE_TO_MAPS = 3;
public static final int AUTHENTICATE_TO_FUSION_TABLES = 4;
public static final int AUTHENTICATE_TO_DOCLIST = 5;
public static final int AUTHENTICATE_TO_TRIX = 6;
public static final int SHARE_GPX_FILE = 7;
public static final int SHARE_KML_FILE = 8;
public static final int SHARE_CSV_FILE = 9;
public static final int SHARE_TCX_FILE = 10;
public static final int SAVE_GPX_FILE = 11;
public static final int SAVE_KML_FILE = 12;
public static final int SAVE_CSV_FILE = 13;
public static final int SAVE_TCX_FILE = 14;
public static final int SHOW_WAYPOINT = 15;
public static final int WELCOME = 16;
public static final int CHOOSE_ACCOUNT = 17;
public static final int SHOW_TRACK = 0;
public static final int SHARE_GPX_FILE = 1;
public static final int SHARE_KML_FILE = 2;
public static final int SHARE_CSV_FILE = 3;
public static final int SHARE_TCX_FILE = 4;
public static final int SAVE_GPX_FILE = 5;
public static final int SAVE_KML_FILE = 6;
public static final int SAVE_CSV_FILE = 7;
public static final int SAVE_TCX_FILE = 8;
public static final int SHOW_WAYPOINT = 9;
public static final int WELCOME = 10;
/*
* Context menu ids:
* Context menu ids. Sorted alphabetically.
*/
public static final int MENU_EDIT = 100;
public static final int MENU_CLEAR_MAP = 100;
public static final int MENU_DELETE = 101;
public static final int MENU_SEND_TO_GOOGLE = 102;
public static final int MENU_SHARE = 103;
public static final int MENU_SHOW = 104;
public static final int MENU_SHARE_MAP = 200;
public static final int MENU_SHARE_FUSION_TABLE = 201;
public static final int MENU_SHARE_GPX_FILE = 202;
public static final int MENU_SHARE_KML_FILE = 203;
public static final int MENU_SHARE_CSV_FILE = 204;
public static final int MENU_SHARE_TCX_FILE = 205;
public static final int MENU_WRITE_TO_SD_CARD = 206;
public static final int MENU_SAVE_GPX_FILE = 207;
public static final int MENU_SAVE_KML_FILE = 208;
public static final int MENU_SAVE_CSV_FILE = 209;
public static final int MENU_SAVE_TCX_FILE = 210;
public static final int MENU_CLEAR_MAP = 211;
public static final int MENU_EDIT = 102;
public static final int MENU_PLAY = 103;
public static final int MENU_SAVE_CSV_FILE = 104;
public static final int MENU_SAVE_GPX_FILE = 105;
public static final int MENU_SAVE_KML_FILE = 106;
public static final int MENU_SAVE_TCX_FILE = 107;
public static final int MENU_SEND_TO_GOOGLE = 108;
public static final int MENU_SHARE = 109;
public static final int MENU_SHARE_CSV_FILE = 110;
public static final int MENU_SHARE_FUSION_TABLE = 111;
public static final int MENU_SHARE_GPX_FILE = 112;
public static final int MENU_SHARE_KML_FILE = 113;
public static final int MENU_SHARE_MAP = 114;
public static final int MENU_SHARE_TCX_FILE = 115;
public static final int MENU_SHOW = 116;
public static final int MENU_WRITE_TO_SD_CARD = 117;
/*
* Main screen tab tags:
*/
@@ -16,7 +16,7 @@
package com.google.android.apps.mytracks;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.util.ApiFeatures;
import com.google.android.apps.mytracks.util.ApiAdapterFactory;
import com.google.android.maps.mytracks.R;
import android.app.AlertDialog;
@@ -66,7 +66,7 @@ public class DeleteAllTracks extends Handler {
Editor editor = prefs.edit();
// TODO: Go through data manager
editor.putLong(context.getString(R.string.selected_track_key), -1);
ApiFeatures.getInstance().getApiAdapter().applyPreferenceChanges(editor);
ApiAdapterFactory.getApiAdapter().applyPreferenceChanges(editor);
if (done != null) {
Handler h = new Handler();
h.post(done);
@@ -19,7 +19,7 @@ import static com.google.android.apps.mytracks.Constants.TAG;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.content.TracksColumns;
import com.google.android.apps.mytracks.util.ApiFeatures;
import com.google.android.apps.mytracks.util.ApiAdapterFactory;
import com.google.android.apps.mytracks.util.UriUtils;
import com.google.android.maps.mytracks.R;
@@ -110,7 +110,7 @@ public class DeleteTrack extends Activity
Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
if (preferences.getLong(selectedKey, -1) == deleteTrackId) {
Editor editor = preferences.edit().putLong(selectedKey, -1);
ApiFeatures.getInstance().getApiAdapter().applyPreferenceChanges(editor);
ApiAdapterFactory.getApiAdapter().applyPreferenceChanges(editor);
}
}
}
@@ -1,77 +0,0 @@
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks;
import static com.google.android.apps.mytracks.Constants.TAG;
import android.app.Activity;
import android.app.Dialog;
import android.util.Log;
import android.view.WindowManager.BadTokenException;
/**
* A class to handle all dialog related events for My Tracks.
*
* @author Sandor Dornbush
*/
public class DialogManager {
/**
* The equivalent of {@link Dialog#show()}, but for a specific dialog
* instance.
*/
public static void showDialogSafely(Activity activity, final Dialog dialog) {
if (activity.isFinishing()) {
Log.w(TAG, "Activity finishing - not showing dialog");
return;
}
activity.runOnUiThread(new Runnable() {
public void run() {
try {
dialog.show();
} catch (BadTokenException e) {
Log.w(TAG, "Could not display dialog", e);
} catch (IllegalStateException e) {
Log.w(TAG, "Could not display dialog", e);
}
}
});
}
/**
* The equivalent of {@link Dialog#dismiss()}, but for a specific dialog
* instance.
*/
public static void dismissDialogSafely(Activity activity, final Dialog dialog) {
if (activity.isFinishing()) {
Log.w(TAG, "Activity finishing - not dismissing dialog");
return;
}
activity.runOnUiThread(new Runnable() {
public void run() {
try {
dialog.dismiss();
} catch (IllegalArgumentException e) {
// This will be thrown if this dialog was not shown before.
}
}
});
}
private DialogManager() {}
}
@@ -81,12 +81,12 @@ public class ExportAllTracks {
String exportFileFormat = activity.getString(R.string.track_list_export_file);
String fileTypes[] = activity.getResources().getStringArray(R.array.file_types);
String[] choices = new String[fileTypes.length];
for (int i = 0; i < fileTypes.length; i++) {
choices[i] = String.format(exportFileFormat, fileTypes[i]);
}
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
builder.setTitle(R.string.track_list_export_all);
builder.setSingleChoiceItems(choices, 0, itemClick);
@@ -160,7 +160,7 @@ public class ExportAllTracks {
try {
MyTracksProviderUtils providerUtils =
MyTracksProviderUtils.Factory.get(activity);
cursor = providerUtils.getTracksCursor("");
cursor = providerUtils.getTracksCursor(null, null, TracksColumns._ID);
if (cursor == null) {
return;
}
@@ -25,25 +25,26 @@ import com.google.android.apps.mytracks.content.TrackDataListener;
import com.google.android.apps.mytracks.content.TracksColumns;
import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.io.file.SaveActivity;
import com.google.android.apps.mytracks.io.sendtogoogle.SendType;
import com.google.android.apps.mytracks.io.sendtogoogle.SendRequest;
import com.google.android.apps.mytracks.io.sendtogoogle.UploadServiceChooserActivity;
import com.google.android.apps.mytracks.services.tasks.StatusAnnouncerFactory;
import com.google.android.apps.mytracks.stats.TripStatistics;
import com.google.android.apps.mytracks.util.ApiFeatures;
import com.google.android.apps.mytracks.util.ApiAdapterFactory;
import com.google.android.apps.mytracks.util.GeoRect;
import com.google.android.apps.mytracks.util.LocationUtils;
import com.google.android.apps.mytracks.util.PlayTrackUtils;
import com.google.android.maps.GeoPoint;
import com.google.android.maps.MapController;
import com.google.android.maps.MapView;
import com.google.android.maps.mytracks.R;
import android.app.Dialog;
import android.content.ContentUris;
import android.content.Intent;
import android.location.Location;
import android.net.Uri;
import android.os.Bundle;
import android.os.Parcelable;
import android.provider.Settings;
import android.speech.tts.TextToSpeech;
import android.util.Log;
import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo;
@@ -71,6 +72,8 @@ public class MapActivity extends com.google.android.maps.MapActivity
implements View.OnTouchListener, View.OnClickListener,
TrackDataListener {
private static final int DIALOG_INSTALL_EARTH = 0;
// Saved instance state keys:
// ---------------------------
@@ -91,6 +94,25 @@ public class MapActivity extends com.google.android.maps.MapActivity
*/
private boolean myLocationWasZoomedIn;
/**
* The ID of a track on which we want to show a waypoint.
* The waypoint will be shown as soon as the track is loaded.
*/
private long showWaypointTrackId;
/**
* The ID of a waypoint which we want to show.
* The waypoint will be shown as soon as its track is loaded.
*/
private long showWaypointId;
/**
* The track that's currently selected.
* This differs from {@link TrackDataHub#getSelectedTrackId} in that this one is only set after
* actual track data has been received.
*/
private long selectedTrackId;
/**
* The current pointer location.
* This is kept to quickly center on it when the user requests.
@@ -136,13 +158,10 @@ public class MapActivity extends com.google.android.maps.MapActivity
super.onCreate(bundle);
// The volume we want to control is the Text-To-Speech volume
ApiFeatures apiFeatures = ApiFeatures.getInstance();
int volumeStream =
new StatusAnnouncerFactory(apiFeatures).getVolumeStream();
setVolumeControlStream(volumeStream);
setVolumeControlStream(TextToSpeech.Engine.DEFAULT_STREAM);
// Show the action bar (or nothing at all).
apiFeatures.getApiAdapter().showActionBar(this);
ApiAdapterFactory.getApiAdapter().showActionBar(this);
// Inflate the layout:
setContentView(R.layout.mytracks_layout);
@@ -219,6 +238,16 @@ public class MapActivity extends com.google.android.maps.MapActivity
super.onPause();
}
@Override
protected Dialog onCreateDialog(int id) {
switch (id) {
case DIALOG_INSTALL_EARTH:
return PlayTrackUtils.createInstallEarthDialog(this);
default:
return null;
}
}
// Utility functions:
// -------------------
@@ -352,6 +381,47 @@ public class MapActivity extends com.google.android.maps.MapActivity
}
}
/**
* Zooms and pans the map so that the given waypoint is visible, when the given track is loaded.
* If the track is already loaded, it does that immediately.
*
* @param trackId the ID of the track on which to show the waypoint
* @param waypointId the ID of the waypoint to show
*/
public void showWaypoint(long trackId, long waypointId) {
synchronized (this) {
if (trackId == selectedTrackId) {
showWaypoint(waypointId);
return;
}
showWaypointTrackId = trackId;
showWaypointId = waypointId;
}
}
/**
* Does the proper zooming/panning for a just-loaded track.
* This may be either zooming to a waypoint that has been previously selected, or
* zooming to the whole track.
*
* @param track the loaded track
*/
private void zoomLoadedTrack(Track track) {
synchronized (this) {
if (track.getId() == showWaypointTrackId) {
// There's a waypoint to show in this track.
showWaypoint(showWaypointId);
showWaypointId = 0L;
showWaypointTrackId = 0L;
} else {
// Zoom out to show the whole track.
zoomMapToBoundaries(track);
}
}
}
@Override
public void onSelectedTrackChanged(final Track track, final boolean isRecording) {
runOnUiThread(new Runnable() {
@@ -365,7 +435,12 @@ public class MapActivity extends com.google.android.maps.MapActivity
if (trackSelected) {
busyPane.setVisibility(View.VISIBLE);
zoomMapToBoundaries(track);
synchronized (this) {
// Need to get the track ID only at this point, to prevent a race condition
// among showWaypoint, zoomLoadedTrack and dataHub.loadTrack.
selectedTrackId = track.getId();
zoomLoadedTrack(track);
}
mapOverlay.setShowEndMarker(!isRecording);
busyPane.setVisibility(View.GONE);
@@ -386,6 +461,7 @@ public class MapActivity extends com.google.android.maps.MapActivity
String shareFileFormat = getString(R.string.track_list_share_file);
String fileTypes[] = getResources().getStringArray(R.array.file_types);
menu.add(Menu.NONE, Constants.MENU_PLAY, Menu.NONE, R.string.track_list_play);
menu.add(Menu.NONE, Constants.MENU_SEND_TO_GOOGLE, Menu.NONE,
R.string.track_list_send_google);
SubMenu share = menu.addSubMenu(
@@ -423,50 +499,52 @@ public class MapActivity extends com.google.android.maps.MapActivity
Intent intent;
long trackId = dataHub.getSelectedTrackId();
switch (item.getItemId()) {
case Constants.MENU_EDIT:
intent = new Intent(this, TrackDetail.class).putExtra(TrackDetail.TRACK_ID, trackId);
startActivity(intent);
return true;
case Constants.MENU_PLAY:
if (PlayTrackUtils.isEarthInstalled(this)) {
PlayTrackUtils.playTrack(this, trackId);
return true;
} else {
showDialog(DIALOG_INSTALL_EARTH);
return true;
}
case Constants.MENU_SEND_TO_GOOGLE:
intent = new Intent(this, UploadServiceChooserActivity.class);
intent.putExtra(UploadServiceChooserActivity.TRACK_ID, trackId);
intent = new Intent(this, UploadServiceChooserActivity.class)
.putExtra(SendRequest.SEND_REQUEST_KEY, new SendRequest(trackId, true, true, true));
startActivity(intent);
return true;
case Constants.MENU_SHARE_MAP:
intent = new Intent(this, UploadServiceChooserActivity.class);
intent.putExtra(UploadServiceChooserActivity.TRACK_ID, trackId);
intent.putExtra(UploadServiceChooserActivity.SEND_TYPE, (Parcelable) SendType.MAPS);
intent = new Intent(this, UploadServiceChooserActivity.class)
.putExtra(SendRequest.SEND_REQUEST_KEY, new SendRequest(trackId, true, false, false));
startActivity(intent);
return true;
case Constants.MENU_SHARE_FUSION_TABLE:
intent = new Intent(this, UploadServiceChooserActivity.class);
intent.putExtra(UploadServiceChooserActivity.TRACK_ID, trackId);
intent.putExtra(
UploadServiceChooserActivity.SEND_TYPE, (Parcelable) SendType.FUSION_TABLES);
intent = new Intent(this, UploadServiceChooserActivity.class)
.putExtra(SendRequest.SEND_REQUEST_KEY, new SendRequest(trackId, false, true, false));
startActivity(intent);
return true;
case Constants.MENU_SAVE_GPX_FILE:
case Constants.MENU_SAVE_KML_FILE:
case Constants.MENU_SAVE_CSV_FILE:
case Constants.MENU_SAVE_TCX_FILE:
case Constants.MENU_SHARE_GPX_FILE:
case Constants.MENU_SHARE_KML_FILE:
case Constants.MENU_SHARE_CSV_FILE:
case Constants.MENU_SHARE_TCX_FILE:
case Constants.MENU_SAVE_GPX_FILE:
case Constants.MENU_SAVE_KML_FILE:
case Constants.MENU_SAVE_CSV_FILE:
case Constants.MENU_SAVE_TCX_FILE:
SaveActivity.handleExportTrackAction(
this, trackId, Constants.getActionFromMenuId(item.getItemId()));
return true;
case Constants.MENU_EDIT: {
intent = new Intent(this, TrackDetail.class);
intent.putExtra(TrackDetail.TRACK_ID, trackId);
startActivity(intent);
case Constants.MENU_CLEAR_MAP:
dataHub.unloadCurrentTrack();
return true;
}
case Constants.MENU_DELETE: {
case Constants.MENU_DELETE:
Uri uri = ContentUris.withAppendedId(TracksColumns.CONTENT_URI, trackId);
intent = new Intent(Intent.ACTION_DELETE, uri);
startActivity(intent);
return true;
}
case Constants.MENU_CLEAR_MAP:
dataHub.unloadCurrentTrack();
return true;
default:
return super.onMenuItemSelected(featureId, item);
}
@@ -22,6 +22,7 @@ import com.google.android.apps.mytracks.maps.TrackPathPainter;
import com.google.android.apps.mytracks.maps.TrackPathPainterFactory;
import com.google.android.apps.mytracks.maps.TrackPathUtilities;
import com.google.android.apps.mytracks.util.LocationUtils;
import com.google.android.apps.mytracks.util.UnitConversions;
import com.google.android.maps.GeoPoint;
import com.google.android.maps.MapView;
import com.google.android.maps.Overlay;
@@ -103,7 +104,7 @@ public class MapOverlay extends Overlay implements OnSharedPreferenceChangeListe
public CachedLocation(Location location) {
this.valid = LocationUtils.isValidLocation(location);
this.geoPoint = valid ? LocationUtils.getGeoPoint(location) : null;
this.speed = (int) Math.floor(location.getSpeed() * 3.6);
this.speed = (int) Math.floor(location.getSpeed() * UnitConversions.MS_TO_KMH);
}
};
@@ -1,175 +0,0 @@
/*
* 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;
import static com.google.android.apps.mytracks.Constants.TAG;
import com.google.android.apps.mytracks.io.AuthManager;
import com.google.android.apps.mytracks.io.AuthManager.AuthCallback;
import com.google.android.apps.mytracks.io.AuthManagerFactory;
import com.google.android.apps.mytracks.io.maps.MapsConstants;
import com.google.android.apps.mytracks.io.maps.MapsFacade;
import com.google.android.maps.mytracks.R;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ListView;
import android.widget.TextView;
/**
* Activity which displays the list of current Google Maps for the user.
* Returns RESULT_OK if the user picked a map, and returns "mapid" as an extra.
*
* @author Rodrigo Damazio
*/
public class MapsList extends Activity implements MapsFacade.MapsListCallback {
private static final int GET_LOGIN = 1;
public static final String EXTRA_ACCOUNT_NAME = "accountName";
public static final String EXTRA_ACCOUNT_TYPE = "accountType";
private MapsFacade mapsClient;
private AuthManager auth;
private MapsListAdapter listAdapter;
private final OnItemClickListener clickListener =
new OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View v, int position,
long id) {
Intent result = new Intent();
result.putExtra("mapid", (String) listAdapter.getItem(position));
setResult(RESULT_OK, result);
finish();
}
};
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
auth = AuthManagerFactory.getAuthManager(this, GET_LOGIN, null, true,
MapsConstants.SERVICE_NAME);
setContentView(R.layout.list);
listAdapter = new MapsListAdapter(this);
ListView list = (ListView) findViewById(R.id.maplist);
list.setOnItemClickListener(clickListener);
list.setAdapter(listAdapter);
startLogin();
}
private void startLogin() {
// Starts in the UI thread.
// TODO fix this for non-froyo devices.
if (AuthManagerFactory.useModernAuthManager()) {
Intent intent = getIntent();
String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
if (accountName == null || accountType == null) {
Log.e(TAG, "Didn't receive account name or type");
setResult(RESULT_CANCELED);
finish();
return;
}
doLogin(auth.getAccountObject(accountName, accountType));
} else {
doLogin(null);
}
}
private void doLogin(final Object account) {
// Starts in the UI thread.
auth.doLogin(new AuthCallback() {
@Override
public void onAuthResult(boolean success) {
if (!success) {
setResult(RESULT_CANCELED);
finish();
return;
}
// Runs in UI thread.
mapsClient = new MapsFacade(MapsList.this, auth);
startLookup();
}
}, account);
}
private void startLookup() {
// Starts in the UI thread.
new Thread() {
@Override
public void run() {
// Communication with Maps happens in its own thread.
// This will call onReceivedMapListing below.
final boolean success = mapsClient.getMapsList(MapsList.this);
runOnUiThread(new Runnable() {
@Override
public void run() {
// Updating the UI when done happens in the UI thread.
onLookupDone(success);
}
});
}
}.start();
}
@Override
public void onReceivedMapListing(final String mapId, final String title,
final String description, final boolean isPublic) {
// Starts in the communication thread.
runOnUiThread(new Runnable() {
@Override
public void run() {
// Updating the list with new contents happens in the UI thread.
listAdapter.addMapListing(mapId, title, description, isPublic);
}
});
}
private void onLookupDone(boolean success) {
// Starts in the UI thread.
findViewById(R.id.loading).setVisibility(View.GONE);
if (!success) {
findViewById(R.id.failed).setVisibility(View.VISIBLE);
}
TextView emptyView = (TextView) findViewById(R.id.mapslist_empty);
ListView list = (ListView) findViewById(R.id.maplist);
list.setEmptyView(emptyView);
}
@Override
protected void onActivityResult(int requestCode, int resultCode,
Intent data) {
if (requestCode == GET_LOGIN) {
auth.authResult(resultCode, data);
}
super.onActivityResult(requestCode, resultCode, data);
}
}
@@ -1,135 +0,0 @@
package com.google.android.apps.mytracks;
import com.google.android.maps.mytracks.R;
import android.app.Activity;
import android.database.DataSetObserver;
import android.graphics.Color;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ListAdapter;
import android.widget.TextView;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import java.util.Vector;
public class MapsListAdapter implements ListAdapter {
private Vector<String[]> mapsList;
private Vector<Boolean> publicList;
private Set<DataSetObserver> observerSet;
private final Activity activity;
public MapsListAdapter(Activity activity) {
this.activity = activity;
mapsList = new Vector<String[]>();
publicList = new Vector<Boolean>();
observerSet = new HashSet<DataSetObserver>();
}
public void addMapListing(String mapId, String title,
String description, boolean isPublic) {
synchronized (mapsList) {
// Search through the maps list to see if it has the mapid and
// remove it if so, so that we can replace it with updated info
for (int i = 0; i < mapsList.size(); ++i) {
if (mapId.equals(mapsList.get(i)[0])) {
mapsList.remove(i);
publicList.remove(i);
--i;
}
}
mapsList.add(new String[] { mapId, title, description });
publicList.add(isPublic);
}
Iterator<DataSetObserver> iter = observerSet.iterator();
while (iter.hasNext()) {
iter.next().onChanged();
}
}
public String[] getMapListingArray(int position) {
return mapsList.get(position);
}
@Override
public boolean areAllItemsEnabled() {
return true;
}
@Override
public boolean isEnabled(int position) {
return true;
}
@Override
public int getCount() {
return mapsList.size();
}
@Override
public Object getItem(int position) {
return mapsList.get(position)[0];
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public int getItemViewType(int position) {
return 0;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null) {
convertView =
activity.getLayoutInflater().inflate(R.layout.listitem, parent, false);
}
String[] map = mapsList.get(position);
((TextView) convertView.findViewById(R.id.maplistitem)).setText(map[1]);
((TextView) convertView.findViewById(R.id.maplistdesc)).setText(map[2]);
TextView publicUnlisted =
(TextView) convertView.findViewById(R.id.maplistpublic);
if (publicList.get(position)) {
publicUnlisted.setTextColor(Color.RED);
publicUnlisted.setText(R.string.maps_list_public_label);
} else {
publicUnlisted.setTextColor(Color.GREEN);
publicUnlisted.setText(R.string.maps_list_unlisted_label);
}
return convertView;
}
@Override
public int getViewTypeCount() {
return 1;
}
@Override
public boolean hasStableIds() {
return false;
}
@Override
public boolean isEmpty() {
return mapsList.isEmpty();
}
@Override
public void registerDataSetObserver(DataSetObserver observer) {
observerSet.add(observer);
}
@Override
public void unregisterDataSetObserver(DataSetObserver observer) {
observerSet.remove(observer);
}
}
@@ -33,7 +33,9 @@ import android.view.MenuItem;
class MenuManager {
private final MyTracks activity;
private MenuItem recordTrack;
private MenuItem stopRecording;
public MenuManager(MyTracks activity) {
this.activity = activity;
}
@@ -46,6 +48,8 @@ class MenuManager {
*/
public boolean onCreateOptionsMenu(Menu menu) {
activity.getMenuInflater().inflate(R.menu.main, menu);
// TODO: Replace search button with search widget if API level >= 11
return true;
}
@@ -64,13 +68,11 @@ class MenuManager {
boolean isSatelliteMode, String currentTabTag) {
menu.findItem(R.id.menu_markers)
.setEnabled(hasRecorded && hasSelectedTrack);
menu.findItem(R.id.menu_record_track)
.setEnabled(!isRecording)
.setVisible(!isRecording);
menu.findItem(R.id.menu_stop_recording)
.setEnabled(isRecording)
.setVisible(isRecording);
recordTrack = menu.findItem(R.id.menu_record_track);
stopRecording = menu.findItem(R.id.menu_stop_recording);
updateActionItems(isRecording);
menu.findItem(R.id.menu_chart_settings)
.setVisible(CHART_TAB_TAG.equals(currentTabTag));
@@ -84,6 +86,16 @@ class MenuManager {
: R.string.menu_map_view_satellite_mode);
}
/**
* Updates the action items.
*
* @param isRecording true if recording a track
*/
private void updateActionItems(boolean isRecording) {
recordTrack.setEnabled(!isRecording).setVisible(!isRecording);
stopRecording.setEnabled(isRecording).setVisible(isRecording);
}
/**
* Called when an option from the menu is selected.
*
@@ -94,10 +106,12 @@ class MenuManager {
switch (item.getItemId()) {
case R.id.menu_record_track: {
activity.startRecording();
updateActionItems(true);
return true;
}
case R.id.menu_stop_recording: {
activity.stopRecording();
updateActionItems(false);
return true;
}
case R.id.menu_tracks: {
@@ -123,6 +137,10 @@ class MenuManager {
case R.id.menu_help: {
return startActivity(WelcomeActivity.class);
}
case R.id.menu_search: {
// TODO: Pass the current track ID and current location to do some fancier ranking.
activity.onSearchRequested();
}
case R.id.menu_chart_settings: {
activity.showChartSettings();
return true;
@@ -20,19 +20,18 @@ import static com.google.android.apps.mytracks.Constants.MAP_TAB_TAG;
import static com.google.android.apps.mytracks.Constants.STATS_TAB_TAG;
import static com.google.android.apps.mytracks.Constants.TAG;
import com.google.android.apps.analytics.GoogleAnalyticsTracker;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.content.TrackDataHub;
import com.google.android.apps.mytracks.content.TracksColumns;
import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.content.WaypointCreationRequest;
import com.google.android.apps.mytracks.io.file.TempFileCleaner;
import com.google.android.apps.mytracks.content.WaypointsColumns;
import com.google.android.apps.mytracks.services.ITrackRecordingService;
import com.google.android.apps.mytracks.services.ServiceUtils;
import com.google.android.apps.mytracks.services.TrackRecordingServiceConnection;
import com.google.android.apps.mytracks.services.tasks.StatusAnnouncerFactory;
import com.google.android.apps.mytracks.util.ApiFeatures;
import com.google.android.apps.mytracks.util.ApiLevelAdapter;
import com.google.android.apps.mytracks.util.EulaUtil;
import com.google.android.apps.mytracks.util.AnalyticsUtils;
import com.google.android.apps.mytracks.util.ApiAdapterFactory;
import com.google.android.apps.mytracks.util.EulaUtils;
import com.google.android.apps.mytracks.util.SystemUtils;
import com.google.android.apps.mytracks.util.UriUtils;
import com.google.android.maps.mytracks.R;
@@ -50,6 +49,7 @@ import android.content.res.Resources;
import android.net.Uri;
import android.os.Bundle;
import android.os.RemoteException;
import android.speech.tts.TextToSpeech;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
@@ -94,11 +94,6 @@ public class MyTracks extends TabActivity implements OnTouchListener {
*/
private MyTracksProviderUtils providerUtils;
/**
* Google Analytics tracker
*/
private GoogleAnalyticsTracker tracker;
/*
* Tabs/View navigation:
*/
@@ -142,18 +137,11 @@ public class MyTracks extends TabActivity implements OnTouchListener {
protected void onCreate(Bundle savedInstanceState) {
Log.d(TAG, "MyTracks.onCreate");
super.onCreate(savedInstanceState);
ApiFeatures apiFeatures = ApiFeatures.getInstance();
ApiLevelAdapter apiAdapter = apiFeatures.getApiAdapter();
if (!SystemUtils.isRelease(this)) {
apiAdapter.enableStrictMode();
ApiAdapterFactory.getApiAdapter().enableStrictMode();
}
tracker = GoogleAnalyticsTracker.getInstance();
// Start the tracker in manual dispatch mode...
tracker.start(getString(R.string.my_tracks_analytics_id), getApplicationContext());
tracker.setProductVersion("android-mytracks", SystemUtils.getMyTracksVersion(this));
tracker.trackPageView("/appstart");
tracker.dispatch();
AnalyticsUtils.sendPageViews(this, "/appstart");
providerUtils = MyTracksProviderUtils.Factory.get(this);
preferences = getSharedPreferences(Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
@@ -161,13 +149,13 @@ public class MyTracks extends TabActivity implements OnTouchListener {
menuManager = new MenuManager(this);
serviceConnection = new TrackRecordingServiceConnection(this, serviceBindCallback);
// The volume we want to control is the Text-To-Speech volume
int volumeStream =
new StatusAnnouncerFactory(apiFeatures).getVolumeStream();
setVolumeControlStream(volumeStream);
setVolumeControlStream(TextToSpeech.Engine.DEFAULT_STREAM);
// Show the action bar (or nothing at all).
apiAdapter.showActionBar(this);
ApiAdapterFactory.getApiAdapter().showActionBar(this);
// If the user just starts typing (on a device with a keyboard), we start a search.
setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL);
final Resources res = getResources();
final TabHost tabHost = getTabHost();
@@ -199,7 +187,7 @@ public class MyTracks extends TabActivity implements OnTouchListener {
tabHost.addView(layout);
layout.setOnTouchListener(this);
if (!EulaUtil.getEulaValue(this)) {
if (!EulaUtils.getEulaValue(this)) {
showDialog(DIALOG_EULA_ID);
}
}
@@ -223,6 +211,19 @@ public class MyTracks extends TabActivity implements OnTouchListener {
&& UriUtils.matchesContentUri(data, TracksColumns.CONTENT_URI)) {
long trackId = ContentUris.parseId(data);
dataHub.loadTrack(trackId);
} else if (Intent.ACTION_VIEW.equals(action)
&& WaypointsColumns.CONTENT_ITEMTYPE.equals(intent.getType())
&& UriUtils.matchesContentUri(data, WaypointsColumns.CONTENT_URI)) {
// TODO(rdamazio): Waypoint URIs should be base/trackid/waypointid
long waypointId = ContentUris.parseId(data);
Waypoint waypoint = providerUtils.getWaypoint(waypointId);
long trackId = waypoint.getTrackId();
// Request that the waypoint is shown (now or when the right track is loaded).
showWaypoint(trackId, waypointId);
// Load the right track, if not loaded already.
dataHub.loadTrack(trackId);
}
}
@@ -246,14 +247,7 @@ public class MyTracks extends TabActivity implements OnTouchListener {
@Override
protected void onStop() {
Log.d(TAG, "MyTracks.onStop");
dataHub.stop();
tracker.dispatch();
tracker.stop();
// Clean up any temporary track files.
TempFileCleaner.clean();
super.onStop();
}
@@ -270,11 +264,11 @@ public class MyTracks extends TabActivity implements OnTouchListener {
case DIALOG_EULA_ID:
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(R.string.eula_title);
builder.setMessage(EulaUtil.getEulaMessage(this));
builder.setMessage(EulaUtils.getEulaMessage(this));
builder.setPositiveButton(R.string.eula_accept, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
EulaUtil.setEulaValue(MyTracks.this);
EulaUtils.setEulaValue(MyTracks.this);
Intent startIntent = new Intent(MyTracks.this, WelcomeActivity.class);
startActivityForResult(startIntent, Constants.WELCOME);
}
@@ -396,6 +390,17 @@ public class MyTracks extends TabActivity implements OnTouchListener {
}
}
private void showWaypoint(long trackId, long waypointId) {
MapActivity map =
(MapActivity) getLocalActivityManager().getActivity("tab1");
if (map != null) {
getTabHost().setCurrentTab(0);
map.showWaypoint(trackId, waypointId);
} else {
Log.e(TAG, "Couldnt' get map tab");
}
}
@Override
public boolean onTouch(View v, MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
@@ -16,18 +16,25 @@
package com.google.android.apps.mytracks;
import com.google.android.apps.mytracks.content.TrackDataHub;
import com.google.android.apps.mytracks.services.RemoveTempFilesService;
import android.app.Application;
import android.content.Intent;
/**
* MyTracksApplication for keeping global state.
*
* @author jshih@google.com (Jimmy Shih)
* @author Jimmy Shih
*/
public class MyTracksApplication extends Application {
private TrackDataHub trackDataHub;
@Override
public void onCreate() {
startService(new Intent(this, RemoveTempFilesService.class));
}
/**
* Gets the application's TrackDataHub.
*
@@ -0,0 +1,266 @@
/*
* 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;
import static com.google.android.apps.mytracks.Constants.TAG;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.content.SearchEngine;
import com.google.android.apps.mytracks.content.SearchEngine.ScoredResult;
import com.google.android.apps.mytracks.content.SearchEngine.SearchQuery;
import com.google.android.apps.mytracks.content.SearchEngineProvider;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.content.TracksColumns;
import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.content.WaypointsColumns;
import com.google.android.apps.mytracks.stats.TripStatistics;
import com.google.android.apps.mytracks.util.StringUtils;
import com.google.android.maps.mytracks.R;
import android.app.ListActivity;
import android.app.SearchManager;
import android.content.ContentUris;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.location.Location;
import android.location.LocationManager;
import android.net.Uri;
import android.os.Bundle;
import android.provider.SearchRecentSuggestions;
import android.util.Log;
import android.view.View;
import android.widget.ListView;
import android.widget.SimpleAdapter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.SortedSet;
/**
* Activity to search for tracks or waypoints.
*
* @author Rodrigo Damazio
*/
public class SearchActivity extends ListActivity {
private static final String ICON_FIELD = "icon";
private static final String NAME_FIELD = "name";
private static final String DESCRIPTION_FIELD = "description";
private static final String CATEGORY_FIELD = "category";
private static final String TIME_FIELD = "time";
private static final String STATS_FIELD = "stats";
private static final String TRACK_ID_FIELD = "trackId";
private static final String WAYPOINT_ID_FIELD = "waypointId";
private static final String EXTRA_CURRENT_TRACK_ID = "trackId";
private static final boolean LOG_SCORES = true;
private MyTracksProviderUtils providerUtils;
private SearchEngine engine;
private LocationManager locationManager;
private SharedPreferences preferences;
private SearchRecentSuggestions suggestions;
private boolean metricUnits;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
providerUtils = MyTracksProviderUtils.Factory.get(this);
engine = new SearchEngine(providerUtils);
suggestions = SearchEngineProvider.newHelper(this);
locationManager = (LocationManager) getSystemService(LOCATION_SERVICE);
preferences = getSharedPreferences(Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
setContentView(R.layout.search_list);
handleIntent(getIntent());
}
@Override
protected void onResume() {
super.onResume();
metricUnits =
preferences.getBoolean(getString(R.string.metric_units_key), true);
}
@Override
protected void onNewIntent(Intent intent) {
setIntent(intent);
handleIntent(intent);
}
private void handleIntent(Intent intent) {
if (!Intent.ACTION_SEARCH.equals(intent.getAction())) {
Log.e(TAG, "Got bad search intent: " + intent);
finish();
return;
}
String textQuery = intent.getStringExtra(SearchManager.QUERY);
Location currentLocation = locationManager.getLastKnownLocation("gps");
long currentTrackId = intent.getLongExtra(EXTRA_CURRENT_TRACK_ID, -1);
long currentTimestamp = System.currentTimeMillis();
final SearchQuery query =
new SearchQuery(textQuery, currentLocation, currentTrackId, currentTimestamp);
// Do the actual search in a separate thread.
new Thread() {
@Override
public void run() {
doSearch(query);
}
}.start();
}
private void doSearch(SearchQuery query) {
SortedSet<ScoredResult> scoredResults = engine.search(query);
final List<? extends Map<String, ?>> displayResults = prepareResultsforDisplay(scoredResults);
if (LOG_SCORES) {
Log.i(TAG, "Search scores: " + displayResults);
}
// Then go back to the UI thread to display them.
runOnUiThread(new Runnable() {
@Override
public void run() {
showSearchResults(displayResults);
}
});
// Save the query as a suggestion for the future.
suggestions.saveRecentQuery(query.textQuery, null);
}
private List<Map<String, Object>> prepareResultsforDisplay(
Collection<ScoredResult> scoredResults) {
ArrayList<Map<String, Object>> output = new ArrayList<Map<String, Object>>(scoredResults.size());
for (ScoredResult result : scoredResults) {
Map<String, Object> resultMap = new HashMap<String, Object>();
if (result.track != null) {
prepareTrackForDisplay(result.track, resultMap);
} else {
prepareWaypointForDisplay(result.waypoint, resultMap);
}
resultMap.put("score", result.score);
output.add(resultMap);
}
return output;
}
private void prepareWaypointForDisplay(Waypoint waypoint, Map<String, Object> resultMap) {
// Look up the owner track.
// TODO: It may be more appropriate to do this as a join in the retrieval phase of the search.
String trackName = null;
long trackId = waypoint.getTrackId();
if (trackId > 0) {
Track track = providerUtils.getTrack(trackId);
if (track != null) {
trackName = track.getName();
}
}
resultMap.put(ICON_FIELD, waypoint.getType() == Waypoint.TYPE_STATISTICS
? R.drawable.ylw_pushpin : R.drawable.blue_pushpin);
resultMap.put(NAME_FIELD, waypoint.getName());
resultMap.put(DESCRIPTION_FIELD, waypoint.getDescription());
resultMap.put(CATEGORY_FIELD, waypoint.getCategory());
// In the same place as we show time for tracks, show the track name for waypoints.
resultMap.put(TIME_FIELD, getString(R.string.track_list_track_name, trackName));
resultMap.put(STATS_FIELD, StringUtils.formatDateTime(this, waypoint.getLocation().getTime()));
resultMap.put(TRACK_ID_FIELD, waypoint.getTrackId());
resultMap.put(WAYPOINT_ID_FIELD, waypoint.getId());
}
private void prepareTrackForDisplay(Track track, Map<String, Object> resultMap) {
TripStatistics stats = track.getStatistics();
resultMap.put(ICON_FIELD, R.drawable.track);
resultMap.put(NAME_FIELD, track.getName());
resultMap.put(DESCRIPTION_FIELD, track.getDescription());
resultMap.put(CATEGORY_FIELD, track.getCategory());
resultMap.put(TIME_FIELD, StringUtils.formatDateTime(this, stats.getStartTime()));
resultMap.put(STATS_FIELD, StringUtils.formatTimeDistance(
this, stats.getTotalTime(), stats.getTotalDistance(), metricUnits));
resultMap.put(TRACK_ID_FIELD, track.getId());
}
/**
* Shows the given search results.
* Must be run from the UI thread.
*
* @param data the results to show, properly ordered
*/
private void showSearchResults(List<? extends Map<String, ?>> data) {
SimpleAdapter adapter = new SimpleAdapter(this, data,
// TODO: Custom view for search results.
R.layout.mytracks_list_item,
new String[] {
ICON_FIELD,
NAME_FIELD,
DESCRIPTION_FIELD,
CATEGORY_FIELD,
TIME_FIELD,
STATS_FIELD,
},
new int[] {
R.id.track_list_item_icon,
R.id.track_list_item_name,
R.id.track_list_item_description,
R.id.track_list_item_category,
R.id.track_list_item_time,
R.id.track_list_item_stats,
});
setListAdapter(adapter);
}
@Override
protected void onListItemClick(ListView l, View v, int position, long id) {
@SuppressWarnings("unchecked")
Map<String, Object> clickedData = (Map<String, Object>) getListAdapter().getItem(position);
startActivity(createViewDataIntent(clickedData));
}
private Intent createViewDataIntent(Map<String, Object> clickedData) {
Intent intent = new Intent(Intent.ACTION_VIEW);
if (clickedData.containsKey(WAYPOINT_ID_FIELD)) {
long waypointId = (Long) clickedData.get(WAYPOINT_ID_FIELD);
Uri uri = ContentUris.withAppendedId(WaypointsColumns.CONTENT_URI, waypointId);
intent.setDataAndType(uri, WaypointsColumns.CONTENT_ITEMTYPE);
} else {
long trackId = (Long) clickedData.get(TRACK_ID_FIELD);
Uri uri = ContentUris.withAppendedId(TracksColumns.CONTENT_URI, trackId);
intent.setDataAndType(uri, TracksColumns.CONTENT_ITEMTYPE);
}
return intent;
}
}
@@ -73,18 +73,18 @@ public class SensorStateActivity extends Activity {
runOnUiThread(stateUpdater);
}
};
/**
* A temporary sensor manager, when none is available.
*/
private SensorManager tempSensorManager = null;
/**
* A state flag set to true when the activity is active/visible,
* i.e. after resume, and before pause
*
* Used to avoid updating after the pause event, because sometimes an update
* i.e. after resume, and before pause
*
* Used to avoid updating after the pause event, because sometimes an update
* event occurs even after the timer is cancelled. In this case,
* it could cause the {@link #tempSensorManager} to be recreated, after it
* is destroyed at the pause event.
@@ -111,7 +111,7 @@ public class SensorStateActivity extends Activity {
@Override
protected void onResume() {
super.onResume();
isVisible = true;
serviceConnection.bindIfRunning();
@@ -123,7 +123,7 @@ public class SensorStateActivity extends Activity {
@Override
protected void onPause() {
isVisible = false;
timer.cancel();
timer.purge();
timer = null;
@@ -139,9 +139,9 @@ public class SensorStateActivity extends Activity {
private void updateState() {
Log.d(TAG, "Updating SensorStateActivity");
ITrackRecordingService service = serviceConnection.getServiceIfBound();
// Check if service is available, and recording.
boolean isRecording = false;
if (service != null) {
@@ -151,7 +151,7 @@ public class SensorStateActivity extends Activity {
Log.e(TAG, "Unable to determine if service is recording.", e);
}
}
// If either service isn't available, or not recording.
if (!isRecording) {
updateFromTempSensorManager();
@@ -159,19 +159,17 @@ public class SensorStateActivity extends Activity {
updateFromSysSensorManager();
}
}
private void updateFromTempSensorManager() {
// Use variables to hold the sensor state and data set.
Sensor.SensorState currentState = null;
Sensor.SensorDataSet currentDataSet = null;
// If no temp sensor manager is present, create one, and start it.
if (tempSensorManager == null) {
tempSensorManager = SensorManagerFactory.getSensorManager(this);
if (tempSensorManager != null)
tempSensorManager.onStartTrack();
tempSensorManager = SensorManagerFactory.getInstance().getSensorManager(this);
}
// If a temp sensor manager is available, use states from temp sensor
// manager.
if (tempSensorManager != null) {
@@ -182,15 +180,15 @@ public class SensorStateActivity extends Activity {
// Update the sensor state, and sensor data, using the variables.
updateSensorStateAndData(currentState, currentDataSet);
}
private void updateFromSysSensorManager() {
// Use variables to hold the sensor state and data set.
Sensor.SensorState currentState = null;
Sensor.SensorDataSet currentDataSet = null;
ITrackRecordingService service = serviceConnection.getServiceIfBound();
// If a temp sensor manager is present, shut it down,
// If a temp sensor manager is present, shut it down,
// probably recording just started.
stopTempSensorManager();
@@ -208,7 +206,7 @@ public class SensorStateActivity extends Activity {
} catch (InvalidProtocolBufferException e) {
Log.e(TAG, "Could not read sensor data.", e);
}
try {
currentState = Sensor.SensorState.valueOf(service.getSensorState());
} catch (RemoteException e) {
@@ -220,17 +218,17 @@ public class SensorStateActivity extends Activity {
// Update the sensor state, and sensor data, using the variables.
updateSensorStateAndData(currentState, currentDataSet);
}
/**
* Stops the temporary sensor manager, if one exists.
* Stops the temporary sensor manager, if one exists.
*/
private void stopTempSensorManager() {
if (tempSensorManager != null) {
tempSensorManager.shutdown();
SensorManagerFactory.getInstance().releaseSensorManager(tempSensorManager);
tempSensorManager = null;
}
}
private void updateSensorStateAndData(Sensor.SensorState state, Sensor.SensorDataSet dataSet) {
updateSensorState(state == null ? Sensor.SensorState.NONE : state);
updateSensorData(dataSet);
@@ -20,13 +20,14 @@ import static com.google.android.apps.mytracks.Constants.TAG;
import com.google.android.apps.mytracks.io.backup.BackupActivityHelper;
import com.google.android.apps.mytracks.io.backup.BackupPreferencesListener;
import com.google.android.apps.mytracks.services.sensors.ant.AntUtils;
import com.google.android.apps.mytracks.services.tasks.StatusAnnouncerFactory;
import com.google.android.apps.mytracks.util.ApiFeatures;
import com.google.android.apps.mytracks.util.ApiAdapterFactory;
import com.google.android.apps.mytracks.util.BluetoothDeviceUtils;
import com.google.android.apps.mytracks.util.DialogUtils;
import com.google.android.apps.mytracks.util.UnitConversions;
import com.google.android.maps.mytracks.R;
import android.app.AlertDialog;
import android.app.Dialog;
import android.bluetooth.BluetoothAdapter;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.content.Intent;
@@ -44,6 +45,7 @@ import android.preference.PreferenceCategory;
import android.preference.PreferenceManager;
import android.preference.PreferenceScreen;
import android.provider.Settings;
import android.speech.tts.TextToSpeech;
import android.util.Log;
import android.widget.Toast;
@@ -60,6 +62,9 @@ import java.util.Set;
*/
public class SettingsActivity extends PreferenceActivity {
private static final int DIALOG_CONFIRM_RESET = 0;
private static final int DIALOG_CONFIRM_ACCESS = 1;
// Value when the task frequency is off.
private static final String TASK_FREQUENCY_OFF = "0";
@@ -102,10 +107,7 @@ public class SettingsActivity extends PreferenceActivity {
super.onCreate(icicle);
// The volume we want to control is the Text-To-Speech volume
ApiFeatures apiFeatures = ApiFeatures.getInstance();
int volumeStream =
new StatusAnnouncerFactory(apiFeatures).getVolumeStream();
setVolumeControlStream(volumeStream);
setVolumeControlStream(TextToSpeech.Engine.DEFAULT_STREAM);
// Tell it where to read/write preferences
PreferenceManager preferenceManager = getPreferenceManager();
@@ -113,23 +115,13 @@ public class SettingsActivity extends PreferenceActivity {
preferenceManager.setSharedPreferencesMode(0);
// Set up automatic preferences backup
backupListener = apiFeatures.getApiAdapter().getBackupPreferencesListener(this);
backupListener = ApiAdapterFactory.getApiAdapter().getBackupPreferencesListener(this);
preferences = preferenceManager.getSharedPreferences();
preferences.registerOnSharedPreferenceChangeListener(backupListener);
// Load the preferences to be displayed
addPreferencesFromResource(R.xml.preferences);
// Disable voice announcement if not available
if (!apiFeatures.hasTextToSpeech()) {
IntegerListPreference announcementFrequency =
(IntegerListPreference) findPreference(
getString(R.string.announcement_frequency_key));
announcementFrequency.setEnabled(false);
announcementFrequency.setValue(TASK_FREQUENCY_OFF);
announcementFrequency.setSummary(R.string.settings_recording_voice_not_available);
}
setRecordingIntervalOptions();
setAutoResumeTimeoutOptions();
@@ -158,31 +150,19 @@ public class SettingsActivity extends PreferenceActivity {
resetPreference.setOnPreferenceClickListener(new OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference arg0) {
onResetPreferencesClick();
showDialog(DIALOG_CONFIRM_RESET);
return true;
}
});
// Add a confirmation dialog for the 'Allow access' preference.
final CheckBoxPreference allowAccessPreference = (CheckBoxPreference) findPreference(
CheckBoxPreference allowAccessPreference = (CheckBoxPreference) findPreference(
getString(R.string.allow_access_key));
allowAccessPreference.setOnPreferenceChangeListener(new OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
if ((Boolean) newValue) {
AlertDialog dialog = new AlertDialog.Builder(SettingsActivity.this)
.setCancelable(true)
.setTitle(getString(R.string.settings_sharing_allow_access))
.setMessage(getString(R.string.settings_sharing_allow_access_confirm_message))
.setPositiveButton(android.R.string.ok, new OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int button) {
allowAccessPreference.setChecked(true);
}
})
.setNegativeButton(android.R.string.cancel, null)
.create();
dialog.show();
showDialog(DIALOG_CONFIRM_ACCESS);
return false;
} else {
return true;
@@ -190,7 +170,33 @@ public class SettingsActivity extends PreferenceActivity {
}
});
}
@Override
protected Dialog onCreateDialog(int id) {
switch (id) {
case DIALOG_CONFIRM_RESET:
return DialogUtils.createConfirmationDialog(
this, R.string.settings_reset_confirm_message, new OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int button) {
onResetPreferencesConfirmed();
}
});
case DIALOG_CONFIRM_ACCESS:
return DialogUtils.createConfirmationDialog(
this, R.string.settings_sharing_allow_access_confirm_message, new OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int button) {
CheckBoxPreference pref = (CheckBoxPreference) findPreference(
getString(R.string.allow_access_key));
pref.setChecked(true);
}
});
default:
return null;
}
}
/**
* Sets the display options for the 'Time between points' option.
*/
@@ -496,7 +502,7 @@ public class SettingsActivity extends PreferenceActivity {
format = getString(stringId);
options[i] = String.format(format, value);
} else {
double mile = value / UnitConversions.MI_TO_FEET;
double mile = value * UnitConversions.FT_TO_MI;
format = getString(R.string.value_float_mile);
options[i] = String.format(format, mile);
}
@@ -539,7 +545,7 @@ public class SettingsActivity extends PreferenceActivity {
}
options[i] = String.format(format, value);
} else {
double mile = value / UnitConversions.MI_TO_FEET;
double mile = value * UnitConversions.FT_TO_MI;
if (values[i].equals(GPS_ACCURACY_POOR)) {
format = getString(R.string.value_float_mile_poor_gps);
} else {
@@ -557,20 +563,17 @@ public class SettingsActivity extends PreferenceActivity {
* Configures preference actions related to bluetooth.
*/
private void configureBluetoothPreferences() {
if (BluetoothDeviceUtils.isBluetoothMethodSupported()) {
// Populate the list of bluetooth devices
populateBluetoothDeviceList();
// Make the pair devices preference go to the system preferences
findPreference(getString(R.string.bluetooth_pairing_key))
.setOnPreferenceClickListener(new OnPreferenceClickListener() {
public boolean onPreferenceClick(Preference preference) {
Intent settingsIntent = new Intent(Settings.ACTION_BLUETOOTH_SETTINGS);
startActivity(settingsIntent);
return false;
}
});
}
// Populate the list of bluetooth devices
populateBluetoothDeviceList();
// Make the pair devices preference go to the system preferences
findPreference(getString(R.string.bluetooth_pairing_key)).setOnPreferenceClickListener(
new OnPreferenceClickListener() {
public boolean onPreferenceClick(Preference preference) {
Intent settingsIntent = new Intent(Settings.ACTION_BLUETOOTH_SETTINGS);
startActivity(settingsIntent);
return false;
}
});
}
/**
@@ -582,7 +585,10 @@ public class SettingsActivity extends PreferenceActivity {
List<String> entryValues = new ArrayList<String>();
// The actual devices
BluetoothDeviceUtils.getInstance().populateDeviceLists(entries, entryValues);
BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
if (bluetoothAdapter != null) {
BluetoothDeviceUtils.populateDeviceLists(bluetoothAdapter, entries, entryValues);
}
CharSequence[] entriesArray = entries.toArray(new CharSequence[entries.size()]);
CharSequence[] entryValuesArray = entryValues.toArray(new CharSequence[entryValues.size()]);
@@ -592,24 +598,6 @@ public class SettingsActivity extends PreferenceActivity {
devicesPreference.setEntries(entriesArray);
}
/** Callback for when user asks to reset all settings. */
private void onResetPreferencesClick() {
AlertDialog dialog = new AlertDialog.Builder(this)
.setCancelable(true)
.setTitle(R.string.settings_reset)
.setMessage(R.string.settings_reset_dialog_message)
.setPositiveButton(android.R.string.ok,
new OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int button) {
onResetPreferencesConfirmed();
}
})
.setNegativeButton(android.R.string.cancel, null)
.create();
dialog.show();
}
/** Callback for when user confirms resetting all settings. */
private void onResetPreferencesConfirmed() {
// Change preferences in a separate thread.
@@ -656,7 +644,7 @@ public class SettingsActivity extends PreferenceActivity {
String metricspeed = prefs.getString(getString(id), null);
int englishspeed;
try {
englishspeed = (int) (Double.parseDouble(metricspeed) * UnitConversions.KMH_TO_MPH);
englishspeed = (int) (Double.parseDouble(metricspeed) * UnitConversions.KM_TO_MI);
} catch (NumberFormatException e) {
englishspeed = 0;
}
@@ -675,7 +663,7 @@ public class SettingsActivity extends PreferenceActivity {
// Convert miles/h to km/h
try {
metricspeed = String.valueOf(
(int) (Double.parseDouble(newValue) * UnitConversions.MPH_TO_KMH) + 1);
(int) (Double.parseDouble(newValue) * UnitConversions.MI_TO_KM));
} catch (NumberFormatException e) {
metricspeed = "0";
}
@@ -685,7 +673,7 @@ public class SettingsActivity extends PreferenceActivity {
SharedPreferences prefs = getPreferenceManager().getSharedPreferences();
Editor editor = prefs.edit();
editor.putString(getString(id), metricspeed);
ApiFeatures.getInstance().getApiAdapter().applyPreferenceChanges(editor);
ApiAdapterFactory.getApiAdapter().applyPreferenceChanges(editor);
}
/**
@@ -23,8 +23,8 @@ import com.google.android.apps.mytracks.content.TrackDataHub.ListenerDataType;
import com.google.android.apps.mytracks.content.TrackDataListener;
import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.services.ServiceUtils;
import com.google.android.apps.mytracks.services.tasks.StatusAnnouncerFactory;
import com.google.android.apps.mytracks.util.ApiFeatures;
import com.google.android.apps.mytracks.util.ApiAdapterFactory;
import com.google.android.apps.mytracks.util.UnitConversions;
import com.google.android.maps.mytracks.R;
import android.app.Activity;
@@ -32,6 +32,7 @@ import android.content.Context;
import android.content.SharedPreferences;
import android.location.Location;
import android.os.Bundle;
import android.speech.tts.TextToSpeech;
import android.util.DisplayMetrics;
import android.util.Log;
import android.widget.ScrollView;
@@ -104,13 +105,10 @@ public class StatsActivity extends Activity implements TrackDataListener {
utils = new StatsUtilities(this);
// The volume we want to control is the Text-To-Speech volume
ApiFeatures apiFeatures = ApiFeatures.getInstance();
int volumeStream =
new StatusAnnouncerFactory(apiFeatures).getVolumeStream();
setVolumeControlStream(volumeStream);
setVolumeControlStream(TextToSpeech.Engine.DEFAULT_STREAM);
// Show the action bar (or nothing at all).
apiFeatures.getApiAdapter().showActionBar(this);
// Show the action bar (or nothing at all).
ApiAdapterFactory.getApiAdapter().showActionBar(this);
setContentView(R.layout.stats);
@@ -193,7 +191,7 @@ public class StatsActivity extends Activity implements TrackDataListener {
utils.setAltitude(R.id.elevation_register, l.getAltitude());
utils.setLatLong(R.id.latitude_register, l.getLatitude());
utils.setLatLong(R.id.longitude_register, l.getLongitude());
utils.setSpeed(R.id.speed_register, l.getSpeed() * 3.6);
utils.setSpeed(R.id.speed_register, l.getSpeed() * UnitConversions.MS_TO_KMH);
}
private void showUnknownLocation() {
@@ -213,10 +213,10 @@ public class StatsUtilities {
double minElevation, double maxElevation, double elevationGain,
double minGrade, double maxGrade) {
setTime(R.id.moving_time_register, movingTime);
setDistance(R.id.total_distance_register, totalDistance / 1000);
setSpeed(R.id.average_speed_register, averageSpeed * 3.6);
setSpeed(R.id.average_moving_speed_register, averageMovingSpeed * 3.6);
setSpeed(R.id.max_speed_register, maxSpeed * 3.6);
setDistance(R.id.total_distance_register, totalDistance * UnitConversions.M_TO_KM);
setSpeed(R.id.average_speed_register, averageSpeed * UnitConversions.MS_TO_KMH);
setSpeed(R.id.average_moving_speed_register, averageMovingSpeed * UnitConversions.MS_TO_KMH);
setSpeed(R.id.max_speed_register, maxSpeed * UnitConversions.MS_TO_KMH);
setAltitude(R.id.min_elevation_register, minElevation);
setAltitude(R.id.max_elevation_register, maxElevation);
setAltitude(R.id.elevation_gain_register, elevationGain);
@@ -226,11 +226,11 @@ public class StatsUtilities {
public void setAllStats(TripStatistics stats) {
setTime(R.id.moving_time_register, stats.getMovingTime());
setDistance(R.id.total_distance_register, stats.getTotalDistance() / 1000);
setSpeed(R.id.average_speed_register, stats.getAverageSpeed() * 3.6);
setDistance(R.id.total_distance_register, stats.getTotalDistance() * UnitConversions.M_TO_KM);
setSpeed(R.id.average_speed_register, stats.getAverageSpeed() * UnitConversions.MS_TO_KMH);
setSpeed(R.id.average_moving_speed_register,
stats.getAverageMovingSpeed() * 3.6);
setSpeed(R.id.max_speed_register, stats.getMaxSpeed() * 3.6);
stats.getAverageMovingSpeed() * UnitConversions.MS_TO_KMH);
setSpeed(R.id.max_speed_register, stats.getMaxSpeed() * UnitConversions.MS_TO_KMH);
setAltitude(R.id.min_elevation_register, stats.getMinElevation());
setAltitude(R.id.max_elevation_register, stats.getMaxElevation());
setAltitude(R.id.elevation_gain_register, stats.getTotalElevationGain());
@@ -19,14 +19,15 @@ import static com.google.android.apps.mytracks.Constants.TAG;
import com.google.android.apps.mytracks.content.TracksColumns;
import com.google.android.apps.mytracks.io.file.SaveActivity;
import com.google.android.apps.mytracks.io.sendtogoogle.SendType;
import com.google.android.apps.mytracks.io.sendtogoogle.SendRequest;
import com.google.android.apps.mytracks.io.sendtogoogle.UploadServiceChooserActivity;
import com.google.android.apps.mytracks.services.ServiceUtils;
import com.google.android.apps.mytracks.services.TrackRecordingServiceConnection;
import com.google.android.apps.mytracks.util.PlayTrackUtils;
import com.google.android.apps.mytracks.util.StringUtils;
import com.google.android.apps.mytracks.util.UnitConversions;
import com.google.android.maps.mytracks.R;
import android.app.Dialog;
import android.app.ListActivity;
import android.content.ContentUris;
import android.content.Context;
@@ -36,7 +37,6 @@ import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.Parcelable;
import android.util.Log;
import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo;
@@ -62,6 +62,8 @@ public class TrackList extends ListActivity
implements SharedPreferences.OnSharedPreferenceChangeListener,
View.OnClickListener {
private static final int DIALOG_INSTALL_EARTH = 0;
private int contextPosition = -1;
private long trackId = -1;
private ListView listView = null;
@@ -89,6 +91,7 @@ public class TrackList extends ListActivity
String shareFileFormat = getString(R.string.track_list_share_file);
String fileTypes[] = getResources().getStringArray(R.array.file_types);
menu.add(Menu.NONE, Constants.MENU_PLAY, Menu.NONE, R.string.track_list_play);
menu.add(Menu.NONE, Constants.MENU_SEND_TO_GOOGLE, Menu.NONE,
R.string.track_list_send_google);
SubMenu share = menu.addSubMenu(
@@ -166,50 +169,50 @@ public class TrackList extends ListActivity
return true;
}
case Constants.MENU_EDIT: {
intent = new Intent(this, TrackDetail.class);
intent.putExtra(TrackDetail.TRACK_ID, trackId);
intent = new Intent(this, TrackDetail.class).putExtra(TrackDetail.TRACK_ID, trackId);
startActivity(intent);
return true;
}
case Constants.MENU_SHARE:
case Constants.MENU_WRITE_TO_SD_CARD:
return false;
case Constants.MENU_PLAY:
if (PlayTrackUtils.isEarthInstalled(this)) {
PlayTrackUtils.playTrack(this, trackId);
return true;
} else {
showDialog(DIALOG_INSTALL_EARTH);
return true;
}
case Constants.MENU_SEND_TO_GOOGLE:
intent = new Intent(this, UploadServiceChooserActivity.class);
intent.putExtra(UploadServiceChooserActivity.TRACK_ID, trackId);
intent = new Intent(this, UploadServiceChooserActivity.class)
.putExtra(SendRequest.SEND_REQUEST_KEY, new SendRequest(trackId, true, true, true));
startActivity(intent);
return true;
case Constants.MENU_SHARE_MAP:
intent = new Intent(this, UploadServiceChooserActivity.class);
intent.putExtra(UploadServiceChooserActivity.TRACK_ID, trackId);
intent.putExtra(UploadServiceChooserActivity.SEND_TYPE, (Parcelable) SendType.MAPS);
intent = new Intent(this, UploadServiceChooserActivity.class)
.putExtra(SendRequest.SEND_REQUEST_KEY, new SendRequest(trackId, true, false, false));
startActivity(intent);
return true;
case Constants.MENU_SHARE_FUSION_TABLE:
intent = new Intent(this, UploadServiceChooserActivity.class);
intent.putExtra(UploadServiceChooserActivity.TRACK_ID, trackId);
intent.putExtra(
UploadServiceChooserActivity.SEND_TYPE, (Parcelable) SendType.FUSION_TABLES);
intent = new Intent(this, UploadServiceChooserActivity.class)
.putExtra(SendRequest.SEND_REQUEST_KEY, new SendRequest(trackId, false, true, false));
startActivity(intent);
return true;
case Constants.MENU_SAVE_GPX_FILE:
case Constants.MENU_SAVE_KML_FILE:
case Constants.MENU_SAVE_CSV_FILE:
case Constants.MENU_SAVE_TCX_FILE:
case Constants.MENU_SHARE_GPX_FILE:
case Constants.MENU_SHARE_KML_FILE:
case Constants.MENU_SHARE_CSV_FILE:
case Constants.MENU_SHARE_TCX_FILE:
SaveActivity.handleExportTrackAction(this, trackId,
Constants.getActionFromMenuId(item.getItemId()));
case Constants.MENU_SAVE_GPX_FILE:
case Constants.MENU_SAVE_KML_FILE:
case Constants.MENU_SAVE_CSV_FILE:
case Constants.MENU_SAVE_TCX_FILE:
SaveActivity.handleExportTrackAction(
this, trackId, Constants.getActionFromMenuId(item.getItemId()));
return true;
case Constants.MENU_DELETE: {
intent = new Intent(Intent.ACTION_DELETE);
case Constants.MENU_DELETE:
Uri uri = ContentUris.withAppendedId(TracksColumns.CONTENT_URI, trackId);
intent.setDataAndType(uri, TracksColumns.CONTENT_ITEMTYPE);
intent = new Intent(Intent.ACTION_DELETE)
.setDataAndType(uri, TracksColumns.CONTENT_ITEMTYPE);
startActivity(intent);
return true;
}
default:
Log.w(TAG, "Unknown menu item: " + item.getItemId() + "(" + item.getTitle() + ")");
return super.onMenuItemSelected(featureId, item);
@@ -286,6 +289,27 @@ public class TrackList extends ListActivity
super.onDestroy();
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.search_only, menu);
return true;
}
@Override
protected Dialog onCreateDialog(int id) {
switch (id) {
case DIALOG_INSTALL_EARTH:
return PlayTrackUtils.createInstallEarthDialog(this);
default:
return null;
}
}
/* Callback from menu/search_only.xml */
public void onSearch(@SuppressWarnings("unused") MenuItem i) {
onSearchRequested();
}
private void updateButtonsEnabled() {
View deleteAll = findViewById(R.id.tracklist_btn_delete_all);
View exportAll = findViewById(R.id.tracklist_btn_export_all);
@@ -304,9 +328,9 @@ public class TrackList extends ListActivity
new String[] { TracksColumns.NAME, TracksColumns.STARTTIME,
TracksColumns.TOTALDISTANCE, TracksColumns.DESCRIPTION,
TracksColumns.CATEGORY },
new int[] { R.id.trackdetails_item_name, R.id.trackdetails_item_time,
R.id.trackdetails_item_stats, R.id.trackdetails_item_description,
R.id.trackdetails_item_category });
new int[] { R.id.track_list_item_name, R.id.track_list_item_time,
R.id.track_list_item_stats, R.id.track_list_item_description,
R.id.track_list_item_category });
final int startTimeIdx =
tracksCursor.getColumnIndexOrThrow(TracksColumns.STARTTIME);
@@ -323,28 +347,11 @@ public class TrackList extends ListActivity
long time = cursor.getLong(startTimeIdx);
textView.setText(StringUtils.formatDateTime(TrackList.this, time));
} else if (columnIndex == totalDistanceIdx) {
double length = cursor.getDouble(totalDistanceIdx);
String lengthUnit = null;
if (metricUnits) {
if (length > 1000) {
length /= 1000;
lengthUnit = getString(R.string.unit_kilometer);
} else {
lengthUnit = getString(R.string.unit_meter);
}
} else {
if (length > UnitConversions.MI_TO_M) {
length /= UnitConversions.MI_TO_M;
lengthUnit = getString(R.string.unit_mile);
} else {
length *= UnitConversions.M_TO_FT;
lengthUnit = getString(R.string.unit_feet);
}
}
textView.setText(String.format("%s %.2f %s",
StringUtils.formatElapsedTime(cursor.getLong(totalTimeIdx)),
length,
lengthUnit));
double totalDistance = cursor.getDouble(totalDistanceIdx);
long totalTime = cursor.getLong(totalTimeIdx);
String totalDistanceStr = StringUtils.formatTimeDistance(
TrackList.this, totalTime, totalDistance, metricUnits);
textView.setText(totalDistanceStr);
} else {
textView.setText(cursor.getString(columnIndex));
if (textView.getText().length() < 1) {
@@ -355,6 +362,7 @@ public class TrackList extends ListActivity
}
return true;
}
});
setListAdapter(adapter);
}
@@ -17,6 +17,7 @@ package com.google.android.apps.mytracks;
import static com.google.android.apps.mytracks.Constants.TAG;
import com.google.android.apps.mytracks.content.DescriptionGeneratorImpl;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.content.WaypointCreationRequest;
@@ -198,6 +199,17 @@ public class WaypointsList extends ListActivity
super.onDestroy();
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.search_only, menu);
return true;
}
/* Callback from menu/search_only.xml */
public void onSearch(@SuppressWarnings("unused") MenuItem i) {
onSearchRequested();
}
@Override
public void onClick(View v) {
WaypointCreationRequest request;
@@ -266,10 +278,10 @@ public class WaypointsList extends ListActivity
if (columnIndex == timeIdx) {
long time = cursor.getLong(timeIdx);
TextView textView = (TextView) view;
if (time == 0) {
textView.setVisibility(View.GONE);
} else {
} else {
textView.setText(StringUtils.formatDateTime(WaypointsList.this, time));
textView.setVisibility(View.VISIBLE);
}
@@ -308,7 +320,7 @@ public class WaypointsList extends ListActivity
public void onClick(DialogInterface dialogInterface, int i) {
dialogInterface.dismiss();
providerUtils.deleteWaypoint(waypointId,
new StringUtils(WaypointsList.this));
new DescriptionGeneratorImpl(WaypointsList.this));
}
});
builder.setNegativeButton(getString(R.string.generic_no),
@@ -15,7 +15,7 @@
*/
package com.google.android.apps.mytracks;
import com.google.android.apps.mytracks.util.EulaUtil;
import com.google.android.apps.mytracks.util.EulaUtils;
import com.google.android.apps.mytracks.util.SystemUtils;
import com.google.android.maps.mytracks.R;
@@ -78,7 +78,7 @@ public class WelcomeActivity extends Activity {
case DIALOG_EULA_ID:
builder = new AlertDialog.Builder(this);
builder.setTitle(R.string.eula_title);
builder.setMessage(EulaUtil.getEulaMessage(this));
builder.setMessage(EulaUtils.getEulaMessage(this));
builder.setPositiveButton(R.string.generic_ok, null);
builder.setCancelable(true);
return builder.create();
@@ -0,0 +1,257 @@
/*
* 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.content;
import com.google.android.apps.mytracks.stats.TripStatistics;
import com.google.android.apps.mytracks.util.ChartURLGenerator;
import com.google.android.apps.mytracks.util.StringUtils;
import com.google.android.apps.mytracks.util.UnitConversions;
import com.google.android.maps.mytracks.R;
import com.google.common.annotations.VisibleForTesting;
import android.content.Context;
import android.util.Pair;
import java.util.Vector;
/**
* An implementation of {@link DescriptionGenerator} for My Tracks.
*
* @author Jimmy Shih
*/
public class DescriptionGeneratorImpl implements DescriptionGenerator {
private static final String HTML_LINE_BREAK = "<br>";
private static final String TEXT_LINE_BREAK = "\n";
private Context context;
public DescriptionGeneratorImpl(Context context) {
this.context = context;
}
@Override
public String generateTrackDescription(
Track track, Vector<Double> distances, Vector<Double> elevations) {
StringBuilder builder = new StringBuilder();
// Created by
String url = context.getString(R.string.my_tracks_web_url);
builder.append(context.getString(
R.string.send_google_by_my_tracks, "<a href='http://" + url + "'>", "</a>"));
builder.append("<p>");
builder.append(generateTripStatisticsDescription(track.getStatistics(), true));
// Activity type
String trackCategory = track.getCategory();
String category = trackCategory != null && trackCategory.length() > 0 ? trackCategory
: context.getString(R.string.value_unknown);
builder.append(context.getString(R.string.description_activity_type, category));
builder.append(HTML_LINE_BREAK);
// Elevation chart
if (distances != null && elevations != null) {
builder.append("<img border=\"0\" src=\""
+ ChartURLGenerator.getChartUrl(distances, elevations, track, context) + "\"/>");
builder.append(HTML_LINE_BREAK);
}
return builder.toString();
}
@Override
public String generateWaypointDescription(Waypoint waypoint) {
return generateTripStatisticsDescription(waypoint.getStatistics(), false);
}
/**
* Generates a description for a {@link TripStatistics}.
*
* @param stats the trip statistics
* @param html true to use "<br>" for line break instead of "\n"
*/
private String generateTripStatisticsDescription(TripStatistics stats, boolean html) {
String lineBreak = html ? HTML_LINE_BREAK : TEXT_LINE_BREAK;
StringBuilder builder = new StringBuilder();
// Total distance
writeDistance(
stats.getTotalDistance(), builder, R.string.description_total_distance, lineBreak);
// Total time
writeTime(stats.getTotalTime(), builder, R.string.description_total_time, lineBreak);
// Moving time
writeTime(stats.getMovingTime(), builder, R.string.description_moving_time, lineBreak);
// Average speed
Pair<Double, Double> averageSpeed = writeSpeed(
stats.getAverageSpeed(), builder, R.string.description_average_speed, lineBreak);
// Average moving speed
Pair<Double, Double> averageMovingSpeed = writeSpeed(stats.getAverageMovingSpeed(), builder,
R.string.description_average_moving_speed, lineBreak);
// Max speed
Pair<Double, Double> maxSpeed = writeSpeed(
stats.getMaxSpeed(), builder, R.string.description_max_speed, lineBreak);
// Average pace
writePace(averageSpeed, builder, R.string.description_average_pace, lineBreak);
// Average moving pace
writePace(averageMovingSpeed, builder, R.string.description_average_moving_pace, lineBreak);
// Min pace
writePace(maxSpeed, builder, R.string.description_min_pace, lineBreak);
// Max elevation
writeElevation(stats.getMaxElevation(), builder, R.string.description_max_elevation, lineBreak);
// Min elevation
writeElevation(stats.getMinElevation(), builder, R.string.description_min_elevation, lineBreak);
// Elevation gain
writeElevation(
stats.getTotalElevationGain(), builder, R.string.description_elevation_gain, lineBreak);
// Max grade
writeGrade(stats.getMaxGrade(), builder, R.string.description_max_grade, lineBreak);
// Min grade
writeGrade(stats.getMinGrade(), builder, R.string.description_min_grade, lineBreak);
// Recorded time
builder.append(
context.getString(R.string.description_recorded_time,
StringUtils.formatDateTime(context, stats.getStartTime())));
builder.append(lineBreak);
return builder.toString();
}
/**
* Writes distance.
*
* @param distance distance in meters
* @param builder StringBuilder to append distance
* @param resId resource id of distance string
* @param lineBreak line break string
*/
@VisibleForTesting
void writeDistance(double distance, StringBuilder builder, int resId, String lineBreak) {
double distanceInKm = distance * UnitConversions.M_TO_KM;
double distanceInMi = distanceInKm * UnitConversions.KM_TO_MI;
builder.append(context.getString(resId, distanceInKm, distanceInMi));
builder.append(lineBreak);
}
/**
* Writes time.
*
* @param time time in milliseconds.
* @param builder StringBuilder to append time
* @param resId resource id of time string
* @param lineBreak line break string
*/
@VisibleForTesting
void writeTime(long time, StringBuilder builder, int resId, String lineBreak) {
builder.append(context.getString(resId, StringUtils.formatElapsedTime(time)));
builder.append(lineBreak);
}
/**
* Writes speed.
*
* @param speed speed in meters per second
* @param builder StringBuilder to append speed
* @param resId resource id of speed string
* @param lineBreak line break string
* @return a pair of speed, first in kilometers per hour, second in miles per
* hour.
*/
@VisibleForTesting
Pair<Double, Double> writeSpeed(
double speed, StringBuilder builder, int resId, String lineBreak) {
double speedInKmHr = speed * UnitConversions.MS_TO_KMH;
double speedInMiHr = speedInKmHr * UnitConversions.KM_TO_MI;
builder.append(context.getString(resId, speedInKmHr, speedInMiHr));
builder.append(lineBreak);
return Pair.create(speedInKmHr, speedInMiHr);
}
/**
* Writes pace.
*
* @param speed a pair of speed, first in kilometers per hour, second in miles
* per hour
* @param builder StringBuilder to append pace
* @param resId resource id of pace string
* @param lineBreak line break string
*/
@VisibleForTesting
void writePace(
Pair<Double, Double> speed, StringBuilder builder, int resId, String lineBreak) {
double paceInMinKm = getPace(speed.first);
double paceInMinMi = getPace(speed.second);
builder.append(context.getString(resId, paceInMinKm, paceInMinMi));
builder.append(lineBreak);
}
/**
* Writes elevation.
*
* @param elevation elevation in meters
* @param builder StringBuilder to append elevation
* @param resId resource id of elevation string
* @param lineBreak line break string
*/
@VisibleForTesting
void writeElevation(
double elevation, StringBuilder builder, int resId, String lineBreak) {
long elevationInM = Math.round(elevation);
long elevationInFt = Math.round(elevation * UnitConversions.M_TO_FT);
builder.append(context.getString(resId, elevationInM, elevationInFt));
builder.append(lineBreak);
}
/**
* Writes grade.
*
* @param grade grade in fraction
* @param builder StringBuilder to append grade
* @param resId resource id grade string
* @param lineBreak line break string
*/
@VisibleForTesting
void writeGrade(double grade, StringBuilder builder, int resId, String lineBreak) {
long gradeInPercent = Double.isNaN(grade) || Double.isInfinite(grade) ? 0L
: Math.round(grade * 100);
builder.append(context.getString(resId, gradeInPercent));
builder.append(lineBreak);
}
/**
* Gets pace (in minutes) from speed.
*
* @param speed speed in hours
*/
@VisibleForTesting
double getPace(double speed) {
return speed == 0 ? 0.0 : 60.0 / speed; // convert from hours to minutes
}
}
@@ -16,7 +16,6 @@
package com.google.android.apps.mytracks.content;
import com.google.android.apps.mytracks.Constants;
import com.google.android.apps.mytracks.util.ApiFeatures;
import com.google.android.maps.mytracks.R;
import android.content.ContentProvider;
@@ -417,12 +416,9 @@ public class MyTracksProvider extends ContentProvider {
} else {
throw new IllegalArgumentException("Unknown URL " + url);
}
if (ApiFeatures.getInstance().canReuseSQLiteQueryBuilder()) {
Log.i(Constants.TAG,
"Build query: " + qb.buildQuery(projection, selection, selectionArgs,
null, null, sortOrder, null));
}
Log.i(Constants.TAG, "Build query: "
+ qb.buildQuery(projection, selection, selectionArgs, null, null, sortOrder, null));
Cursor c = qb.query(db, projection, selection, selectionArgs, null, null,
sortOrder);
c.setNotificationUri(getContext().getContentResolver(), url);
@@ -0,0 +1,405 @@
/*
* 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.content;
import com.google.android.apps.mytracks.stats.TripStatistics;
import com.google.android.apps.mytracks.util.LocationUtils;
import com.google.android.apps.mytracks.util.UnitConversions;
import android.database.Cursor;
import android.location.Location;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.SortedSet;
import java.util.TreeSet;
/**
* Engine for searching for tracks and waypoints by text.
*
* @author Rodrigo Damazio
*/
public class SearchEngine {
/** WHERE query to get tracks by name. */
private static final String TRACK_SELECTION_QUERY =
TracksColumns.NAME + " LIKE ? OR " +
TracksColumns.DESCRIPTION + " LIKE ? OR " +
TracksColumns.CATEGORY + " LIKE ?";
/** WHERE query to get waypoints by name. */
private static final String WAYPOINT_SELECTION_QUERY =
WaypointsColumns.NAME + " LIKE ? OR " +
WaypointsColumns.DESCRIPTION + " LIKE ? OR " +
WaypointsColumns.CATEGORY + " LIKE ?";
/** Order of track results. */
private static final String TRACK_SELECTION_ORDER = TracksColumns._ID + " DESC LIMIT 1000";
/** Order of waypoint results. */
private static final String WAYPOINT_SELECTION_ORDER = WaypointsColumns._ID + " DESC";
/** How much we promote a match in the track category. */
private static final double TRACK_CATEGORY_PROMOTION = 2.0;
/** How much we promote a match in the track description. */
private static final double TRACK_DESCRIPTION_PROMOTION = 8.0;
/** How much we promote a match in the track name. */
private static final double TRACK_NAME_PROMOTION = 16.0;
/** How much we promote a waypoint result if it's in the currently-selected track. */
private static final double CURRENT_TRACK_WAYPOINT_PROMOTION = 2.0;
/** How much we promote a track result if it's the currently-selected track. */
private static final double CURRENT_TRACK_DEMOTION = 0.5;
/** Maximum number of waypoints which will be retrieved and scored. */
private static final int MAX_SCORED_WAYPOINTS = 100;
/** Oldest timestamp for which we rank based on time (2000-01-01 00:00:00.000) */
private static final long OLDEST_ALLOWED_TIMESTAMP = 946692000000L;
/**
* Description of a search query, along with all contextual data needed to execute it.
*/
public static class SearchQuery {
public SearchQuery(String textQuery, Location currentLocation, long currentTrackId,
long currentTimestamp) {
this.textQuery = textQuery.toLowerCase();
this.currentLocation = currentLocation;
this.currentTrackId = currentTrackId;
this.currentTimestamp = currentTimestamp;
}
public final String textQuery;
public final Location currentLocation;
public final long currentTrackId;
public final long currentTimestamp;
}
/**
* Description of a search result which has been retrieved and scored.
*/
public static class ScoredResult {
ScoredResult(Track track, double score) {
this.track = track;
this.waypoint = null;
this.score = score;
}
ScoredResult(Waypoint waypoint, double score) {
this.track = null;
this.waypoint = waypoint;
this.score = score;
}
public final Track track;
public final Waypoint waypoint;
public final double score;
@Override
public String toString() {
return "ScoredResult ["
+ (track != null ? ("trackId=" + track.getId() + ", ") : "")
+ (waypoint != null ? ("wptId=" + waypoint.getId() + ", ") : "")
+ "score=" + score + "]";
}
}
/** Comparador for scored results. */
private static final Comparator<ScoredResult> SCORED_RESULT_COMPARATOR =
new Comparator<ScoredResult>() {
@Override
public int compare(ScoredResult r1, ScoredResult r2) {
// Score ordering.
int scoreDiff = Double.compare(r2.score, r1.score);
if (scoreDiff != 0) {
return scoreDiff;
}
// Make tracks come before waypoints.
if (r1.waypoint != null && r2.track != null) {
return 1;
} else if (r1.track != null && r2.waypoint != null) {
return -1;
}
// Finally, use arbitrary ordering, by ID.
long id1 = r1.track != null ? r1.track.getId() : r1.waypoint.getId();
long id2 = r2.track != null ? r2.track.getId() : r2.waypoint.getId();
long idDiff = id2 - id1;
return Long.signum(idDiff);
}
};
private final MyTracksProviderUtils providerUtils;
public SearchEngine(MyTracksProviderUtils providerUtils) {
this.providerUtils = providerUtils;
}
/**
* Executes a search query and returns a set of sorted results.
*
* @param query the query to execute
* @return a set of results, sorted according to their score
*/
public SortedSet<ScoredResult> search(SearchQuery query) {
ArrayList<Track> tracks = new ArrayList<Track>();
ArrayList<Waypoint> waypoints = new ArrayList<Waypoint>();
TreeSet<ScoredResult> scoredResults = new TreeSet<ScoredResult>(SCORED_RESULT_COMPARATOR);
retrieveTracks(query, tracks);
retrieveWaypoints(query, waypoints);
scoreTrackResults(tracks, query, scoredResults);
scoreWaypointResults(waypoints, query, scoredResults);
return scoredResults;
}
/**
* Retrieves tracks matching the given query from the database.
*
* @param query the query to retrieve for
* @param tracks list to fill with the resulting tracks
*/
private void retrieveTracks(SearchQuery query, ArrayList<Track> tracks) {
String queryLikeSelection = "%" + query.textQuery + "%";
String[] trackSelectionArgs = new String[] {
queryLikeSelection,
queryLikeSelection,
queryLikeSelection };
Cursor tracksCursor = providerUtils.getTracksCursor(
TRACK_SELECTION_QUERY, trackSelectionArgs, TRACK_SELECTION_ORDER);
if (tracksCursor != null) {
try {
tracks.ensureCapacity(tracksCursor.getCount());
while (tracksCursor.moveToNext()) {
tracks.add(providerUtils.createTrack(tracksCursor));
}
} finally {
tracksCursor.close();
}
}
}
/**
* Retrieves waypoints matching the given query from the database.
*
* @param query the query to retrieve for
* @param waypoints list to fill with the resulting waypoints
*/
private void retrieveWaypoints(SearchQuery query, ArrayList<Waypoint> waypoints) {
String queryLikeSelection2 = "%" + query.textQuery + "%";
String[] waypointSelectionArgs = new String[] {
queryLikeSelection2,
queryLikeSelection2,
queryLikeSelection2 };
Cursor waypointsCursor = providerUtils.getWaypointsCursor(
WAYPOINT_SELECTION_QUERY, waypointSelectionArgs, WAYPOINT_SELECTION_ORDER,
MAX_SCORED_WAYPOINTS);
if (waypointsCursor != null) {
try {
waypoints.ensureCapacity(waypointsCursor.getCount());
while (waypointsCursor.moveToNext()) {
Waypoint waypoint = providerUtils.createWaypoint(waypointsCursor);
if (LocationUtils.isValidLocation(waypoint.getLocation())) {
waypoints.add(waypoint);
}
}
} finally {
waypointsCursor.close();
}
}
}
/**
* Scores a collection of track results.
*
* @param tracks the results to score
* @param query the query to score for
* @param output the collection to fill with scored results
*/
private void scoreTrackResults(Collection<Track> tracks, SearchQuery query, Collection<ScoredResult> output) {
for (Track track : tracks) {
// Calculate the score.
double score = scoreTrackResult(query, track);
// Add to the output.
output.add(new ScoredResult(track, score));
}
}
/**
* Scores a single track result.
*
* @param query the query to score for
* @param track the results to score
* @return the score for the track
*/
private double scoreTrackResult(SearchQuery query, Track track) {
double score = 1.0;
score *= getTitleBoost(query, track.getName(), track.getDescription(), track.getCategory());
TripStatistics statistics = track.getStatistics();
// TODO: Also boost for proximity to the currently-centered position on the map.
score *= getDistanceBoost(query, statistics.getMeanLatitude(), statistics.getMeanLongitude());
long meanTimestamp = (statistics.getStartTime() + statistics.getStopTime()) / 2L;
score *= getTimeBoost(query, meanTimestamp);
// Score the currently-selected track lower (user is already there, wouldn't be searching for it).
if (track.getId() == query.currentTrackId) {
score *= CURRENT_TRACK_DEMOTION;
}
return score;
}
/**
* Scores a collection of waypoint results.
*
* @param waypoints the results to score
* @param query the query to score for
* @param output the collection to fill with scored results
*/
private void scoreWaypointResults(Collection<Waypoint> waypoints, SearchQuery query, Collection<ScoredResult> output) {
for (Waypoint waypoint : waypoints) {
// Calculate the score.
double score = scoreWaypointResult(query, waypoint);
// Add to the output.
output.add(new ScoredResult(waypoint, score));
}
}
/**
* Scores a single waypoint result.
*
* @param query the query to score for
* @param waypoint the results to score
* @return the score for the waypoint
*/
private double scoreWaypointResult(SearchQuery query, Waypoint waypoint) {
double score = 1.0;
Location location = waypoint.getLocation();
score *= getTitleBoost(query, waypoint.getName(), waypoint.getDescription(), waypoint.getCategory());
// TODO: Also boost for proximity to the currently-centered position on the map.
score *= getDistanceBoost(query, location.getLatitude(), location.getLongitude());
score *= getTimeBoost(query, location.getTime());
// Score waypoints in the currently-selected track higher (searching inside the current track).
if (query.currentTrackId != -1 && waypoint.getTrackId() == query.currentTrackId) {
score *= CURRENT_TRACK_WAYPOINT_PROMOTION;
}
return score;
}
/**
* Calculates the boosting of the score due to the field(s) in which the match occured.
*
* @param query the query to boost for
* @param name the name of the track or waypoint
* @param description the description of the track or waypoint
* @param category the category of the track or waypoint
* @return the total boost to be applied to the result
*/
private double getTitleBoost(SearchQuery query,
String name, String description, String category) {
// Title boost: track name > description > category.
double boost = 1.0;
if (name.toLowerCase().contains(query.textQuery)) {
boost *= TRACK_NAME_PROMOTION;
}
if (description.toLowerCase().contains(query.textQuery)) {
boost *= TRACK_DESCRIPTION_PROMOTION;
}
if (category.toLowerCase().contains(query.textQuery)) {
boost *= TRACK_CATEGORY_PROMOTION;
}
return boost;
}
/**
* Calculates the boosting of the score due to the recency of the matched entity.
*
* @param query the query to boost for
* @param timestamp the timestamp to calculate the boost for
* @return the total boost to be applied to the result
*/
private double getTimeBoost(SearchQuery query, long timestamp) {
if (timestamp < OLDEST_ALLOWED_TIMESTAMP) {
// Safety: if timestamp is too old or invalid, don't rank based on time.
return 1.0;
}
// Score recent tracks higher.
long timeAgoHours = (query.currentTimestamp - timestamp) / (60L * 60L * 1000L);
if (timeAgoHours > 0L) {
return squash(timeAgoHours);
} else {
// Should rarely happen (track recorded in the last hour).
return Double.POSITIVE_INFINITY;
}
}
/**
* Calculates the boosting of the score due to proximity to a location.
*
* @param query the query to boost for
* @param latitude the latitude to calculate the boost for
* @param longitude the longitude to calculate the boost for
* @return the total boost to be applied to the result
*/
private double getDistanceBoost(SearchQuery query, double latitude, double longitude) {
if (query.currentLocation == null) {
return 1.0;
}
float[] distanceResults = new float[1];
Location.distanceBetween(
latitude, longitude,
query.currentLocation.getLatitude(), query.currentLocation.getLongitude(),
distanceResults);
// Score tracks close to the current location higher.
double distanceKm = distanceResults[0] * UnitConversions.M_TO_KM;
if (distanceKm > 0.0) {
// Use the inverse of the amortized distance.
return squash(distanceKm);
} else {
// Should rarely happen (distance is exactly 0).
return Double.POSITIVE_INFINITY;
}
}
/**
* Squashes a number by calculating 1 / log (1 + x).
*/
private static double squash(double x) {
return 1.0 / Math.log1p(x);
}
}
@@ -0,0 +1,44 @@
/*
* 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.content;
import android.content.Context;
import android.content.SearchRecentSuggestionsProvider;
import android.provider.SearchRecentSuggestions;
/**
* Content provider for search suggestions.
*
* @author Rodrigo Damazio
*/
public class SearchEngineProvider extends SearchRecentSuggestionsProvider {
private static final String AUTHORITY = "com.google.android.maps.mytracks.search";
private static final int MODE = DATABASE_MODE_QUERIES;
public SearchEngineProvider() {
setupSuggestions(AUTHORITY, MODE);
}
// TODO: Also add suggestions from the database.
/**
* Creates and returns a helper for adding recent queries or clearing the recent query history.
*/
public static SearchRecentSuggestions newHelper(Context context) {
return new SearchRecentSuggestions(context, AUTHORITY, MODE);
}
}
@@ -28,7 +28,7 @@ import com.google.android.apps.mytracks.content.MyTracksProviderUtils.DoubleBuff
import com.google.android.apps.mytracks.content.MyTracksProviderUtils.LocationIterator;
import com.google.android.apps.mytracks.content.TrackDataListener.ProviderState;
import com.google.android.apps.mytracks.content.TrackDataListeners.ListenerRegistration;
import com.google.android.apps.mytracks.util.ApiFeatures;
import com.google.android.apps.mytracks.util.ApiAdapterFactory;
import com.google.android.apps.mytracks.util.LocationUtils;
import com.google.android.maps.mytracks.R;
@@ -196,7 +196,7 @@ public class TrackDataHub {
private DoubleBufferedLocationFactory locationFactory;
private boolean started = false;
/**
* Builds a new {@link TrackDataHub} instance.
*/
@@ -274,7 +274,7 @@ public class TrackDataHub {
Log.w(TAG, "Not started, ignoring");
return;
}
// Unregister internal listeners even if there are external listeners registered.
dataSourceManager.unregisterAllListeners();
listenerHandlerThread.getLooper().quit();
@@ -399,7 +399,7 @@ public class TrackDataHub {
// Save the selection to memory and flush.
selectedTrackId = trackId;
ApiFeatures.getInstance().getApiAdapter().applyPreferenceChanges(
ApiAdapterFactory.getApiAdapter().applyPreferenceChanges(
preferences.edit().putLong(SELECTED_TRACK_KEY, trackId));
// Force it to reload data from the beginning.
@@ -1,93 +0,0 @@
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.io;
import android.content.Intent;
/**
* This interface describes a class that will fetch and maintain a Google
* authentication token.
*
* @author Sandor Dornbush
*/
public interface AuthManager {
/**
* Callback for authentication token retrieval operations.
*/
public interface AuthCallback {
/**
* Indicates that we're done fetching an auth token.
*
* @param success if true, indicates we have the requested auth token available
* to be retrieved using {@link AuthManager#getAuthToken}
*/
void onAuthResult(boolean success);
}
/**
* Initializes the login process. The user should be asked to login if they
* haven't already. The {@link AuthCallback} provided will be executed when the
* auth token fetching is done (successfully or not).
*
* @param whenFinished A {@link AuthCallback} to execute when the auth token
* fetching is done
*/
void doLogin(AuthCallback whenFinished, Object o);
/**
* The {@link android.app.Activity} owner of this class should call this
* function when it gets {@link android.app.Activity#onActivityResult} with
* the request code passed into the constructor. The resultCode and results
* should come directly from the {@link android.app.Activity#onActivityResult}
* function. This function will return true if an auth token was successfully
* fetched or the process is not finished.
*
* @param resultCode The result code passed in to the
* {@link android.app.Activity}'s
* {@link android.app.Activity#onActivityResult} function
* @param results The data passed in to the {@link android.app.Activity}'s
* {@link android.app.Activity#onActivityResult} function
*/
void authResult(int resultCode, Intent results);
/**
* Returns the current auth token. Response may be null if no valid auth
* token has been fetched.
*
* @return The current auth token or null if no auth token has been
* fetched
*/
String getAuthToken();
/**
* Invalidates the existing auth token and request a new one. The
* {@link Runnable} provided will be executed when the new auth token is
* successfully fetched.
*
* @param whenFinished A {@link Runnable} to execute when a new auth token
* is successfully fetched
*/
void invalidateAndRefresh(AuthCallback whenFinished);
/**
* Returns an object that represents the given account, if possible.
*
* @param accountName the name of the account
* @param accountType the type of the account
* @return the account object
*/
Object getAccountObject(String accountName, String accountType);
}
@@ -1,57 +0,0 @@
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.io;
import static com.google.android.apps.mytracks.Constants.TAG;
import android.app.Activity;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
/**
* A factory for getting the platform specific AuthManager.
*
* @author Sandor Dornbush
*/
public class AuthManagerFactory {
private AuthManagerFactory() {
}
/**
* Returns whether the modern AuthManager should be used
*/
public static boolean useModernAuthManager() {
return Integer.parseInt(Build.VERSION.SDK) >= 7;
}
/**
* Get a right {@link AuthManager} for the platform.
* @return A new AuthManager
*/
public static AuthManager getAuthManager(Activity activity, int code,
Bundle extras, boolean requireGoogle, String service) {
if (useModernAuthManager()) {
Log.i(TAG, "Creating modern auth manager: " + service);
return new ModernAuthManager(activity, service);
} else {
Log.i(TAG, "Creating legacy auth manager: " + service);
return new AuthManagerOld(activity, code, extras, requireGoogle, service);
}
}
}
@@ -1,141 +0,0 @@
/*
* Copyright 2009 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.io;
import com.google.android.googlelogindist.GoogleLoginServiceConstants;
import com.google.android.googlelogindist.GoogleLoginServiceHelper;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
/**
* AuthManager keeps track of the current auth token for a user. The advantage
* over just passing around a String is that this class can renew the auth
* token if necessary, and it will change for all classes using this
* AuthManager.
*/
public class AuthManagerOld implements AuthManager {
/** The activity that will handle auth result callbacks. */
private final Activity activity;
/** The code used to tell the activity that it is an auth result. */
private final int code;
/** Extras to pass into the getCredentials function. */
private final Bundle extras;
/** True if the account must be a Google account (not a domain account). */
private final boolean requireGoogle;
/** The name of the service to authorize for. */
private final String service;
/** The handler to call when a new auth token is fetched. */
private AuthCallback authCallback;
/** The most recently fetched auth token or null if none is available. */
private String authToken;
/**
* AuthManager requires many of the same parameters as
* {@link GoogleLoginServiceHelper#getCredentials(Activity, int, Bundle,
* boolean, String, boolean)}. The activity must have
* a handler in {@link Activity#onActivityResult} that calls
* {@link #authResult(int, Intent)} if the request code is the code given
* here.
*
* @param activity An activity with a handler in
* {@link Activity#onActivityResult} that calls
* {@link #authResult(int, Intent)} when {@literal code} is the request
* code
* @param code The request code to pass to
* {@link Activity#onActivityResult} when
* {@link #authResult(int, Intent)} should be called
* @param extras A {@link Bundle} of extras for
* {@link GoogleLoginServiceHelper}
* @param requireGoogle True if the account must be a Google account
* @param service The name of the service to authenticate as
*/
public AuthManagerOld(Activity activity, int code, Bundle extras,
boolean requireGoogle, String service) {
this.activity = activity;
this.code = code;
this.extras = extras;
this.requireGoogle = requireGoogle;
this.service = service;
}
@Override
public void doLogin(AuthCallback callback, Object o) {
authCallback = callback;
activity.runOnUiThread(new LoginRunnable());
}
/**
* Runnable which actually gets login credentials.
*/
private class LoginRunnable implements Runnable {
@Override
public void run() {
GoogleLoginServiceHelper.getCredentials(
activity, code, extras, requireGoogle, service, true);
}
}
@Override
public void authResult(int resultCode, Intent results) {
if (resultCode != Activity.RESULT_OK) {
runAuthCallback(false);
return;
}
authToken = results.getStringExtra(GoogleLoginServiceConstants.AUTHTOKEN_KEY);
if (authToken == null) {
// Retry, without prompting the user.
GoogleLoginServiceHelper.getCredentials(
activity, code, extras, requireGoogle, service, false);
} else {
// Notify all active listeners that we have a new auth token.
runAuthCallback(true);
}
}
private void runAuthCallback(boolean success) {
authCallback.onAuthResult(success);
authCallback = null;
}
@Override
public String getAuthToken() {
return authToken;
}
@Override
public void invalidateAndRefresh(AuthCallback callback) {
authCallback = callback;
activity.runOnUiThread(new Runnable() {
public void run() {
GoogleLoginServiceHelper.invalidateAuthToken(activity, code, authToken);
}
});
}
@Override
public Object getAccountObject(String accountName, String accountType) {
throw new UnsupportedOperationException("Legacy auth manager knows nothing about accounts");
}
}
@@ -1,217 +0,0 @@
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.io;
import static com.google.android.apps.mytracks.Constants.ACCOUNT_TYPE;
import static com.google.android.apps.mytracks.Constants.TAG;
import com.google.android.apps.mytracks.AccountChooser;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.accounts.AccountManagerCallback;
import android.accounts.AccountManagerFuture;
import android.accounts.AuthenticatorException;
import android.accounts.OperationCanceledException;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import java.io.IOException;
/**
* AuthManager keeps track of the current auth token for a user. The advantage
* over just passing around a String is that this class can renew the auth
* token if necessary, and it will change for all classes using this
* AuthManager.
*/
public class ModernAuthManager implements AuthManager {
/** The activity that will handle auth result callbacks. */
private final Activity activity;
/** The name of the service to authorize for. */
private final String service;
/** The most recently fetched auth token or null if none is available. */
private String authToken;
private final AccountManager accountManager;
private AuthCallback authCallback;
private Account lastAccount;
/**
* AuthManager requires many of the same parameters as
* {@link com.google.android.googlelogindist.GoogleLoginServiceHelper
* #getCredentials(Activity, int, Bundle, boolean, String, boolean)}.
* The activity must have a handler in {@link Activity#onActivityResult} that
* calls {@link #authResult(int, Intent)} if the request code is the code
* given here.
*
* @param activity An activity with a handler in
* {@link Activity#onActivityResult} that calls
* {@link #authResult(int, Intent)} when {@literal code} is the request
* code
* @param service The name of the service to authenticate as
*/
public ModernAuthManager(Activity activity, String service) {
this.activity = activity;
this.service = service;
this.accountManager = AccountManager.get(activity);
}
/**
* Call this to do the initial login. The user will be asked to login if
* they haven't already. The {@link Runnable} provided will be executed
* when the auth token is successfully fetched.
*
* @param runnable A {@link Runnable} to execute when the auth token
* has been successfully fetched and is available via
* {@link #getAuthToken()}
*/
public void doLogin(AuthCallback runnable, Object o) {
this.authCallback = runnable;
if (!(o instanceof Account)) {
throw new IllegalArgumentException("ModernAuthManager requires an account.");
}
Account account = (Account) o;
doLogin(account);
}
private void doLogin(Account account) {
// Keep the account in case we need to retry.
this.lastAccount = account;
// NOTE: Many Samsung phones have a crashing bug in
// AccountManager#getAuthToken(Account, String, boolean, AccountManagerCallback<Bundle>)
// so we use the other version of the method.
// More details here:
// http://forum.xda-developers.com/showthread.php?p=15155487
// http://android.git.kernel.org/?p=platform/frameworks/base.git;a=blob;f=core/java/android/accounts/AccountManagerService.java
accountManager.getAuthToken(account, service, null, activity,
new AccountManagerCallback<Bundle>() {
public void run(AccountManagerFuture<Bundle> future) {
try {
authToken = future.getResult().getString(
AccountManager.KEY_AUTHTOKEN);
Log.i(TAG, "Got auth token");
} catch (OperationCanceledException e) {
Log.e(TAG, "Auth token operation Canceled", e);
} catch (IOException e) {
Log.e(TAG, "Auth token IO exception", e);
} catch (AuthenticatorException e) {
Log.e(TAG, "Authentication Failed", e);
}
runAuthCallback();
}
}, null /* handler */);
}
/**
* The {@link Activity} passed into the constructor should call this
* function when it gets {@link Activity#onActivityResult} with the request
* code passed into the constructor. The resultCode and results should
* come directly from the {@link Activity#onActivityResult} function. This
* function will return true if an auth token was successfully fetched or
* the process is not finished.
*
* @param resultCode The result code passed in to the {@link Activity}'s
* {@link Activity#onActivityResult} function
* @param results The data passed in to the {@link Activity}'s
* {@link Activity#onActivityResult} function
*/
public void authResult(int resultCode, Intent results) {
boolean retry = false;
if (results == null) {
Log.e(TAG, "No auth token!!");
} else {
authToken = results.getStringExtra(AccountManager.KEY_AUTHTOKEN);
retry = results.getBooleanExtra("retry", false);
}
if (authToken == null && retry) {
Log.i(TAG, "Retrying to get auth result");
doLogin(lastAccount);
return;
}
runAuthCallback();
}
/**
* Returns the current auth token. Response may be null if no valid auth
* token has been fetched.
*
* @return The current auth token or null if no auth token has been
* fetched
*/
public String getAuthToken() {
return authToken;
}
/**
* Invalidates the existing auth token and request a new one. The callback
* provided will be executed when the new auth token is successfully fetched.
*
* @param callback A callback to execute when a new auth token is successfully
* fetched
*/
public void invalidateAndRefresh(final AuthCallback callback) {
this.authCallback = callback;
activity.runOnUiThread(new Runnable() {
public void run() {
accountManager.invalidateAuthToken(ACCOUNT_TYPE, authToken);
authToken = null;
AccountChooser accountChooser = new AccountChooser();
accountChooser.chooseAccount(activity,
new AccountChooser.AccountHandler() {
@Override
public void onAccountSelected(Account account) {
if (account != null) {
doLogin(account);
} else {
runAuthCallback();
}
}
});
}
});
}
private void runAuthCallback() {
lastAccount = null;
if (authCallback != null) {
(new Thread() {
@Override
public void run() {
authCallback.onAuthResult(authToken != null);
authCallback = null;
}
}).start();
}
}
@Override
public Object getAccountObject(String accountName, String accountType) {
return new Account(accountName, accountType);
}
}
@@ -1,280 +0,0 @@
/*
* 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;
import com.google.android.apps.mytracks.Constants;
import com.google.android.apps.mytracks.ProgressIndicator;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.io.docs.DocsHelper;
import com.google.android.apps.mytracks.io.gdata.GDataClientFactory;
import com.google.android.apps.mytracks.io.gdata.GDataWrapper;
import com.google.android.common.gdata.AndroidXmlParserFactory;
import com.google.android.maps.mytracks.R;
import com.google.wireless.gdata.client.GDataClient;
import com.google.wireless.gdata.client.GDataServiceClient;
import com.google.wireless.gdata.docs.DocumentsClient;
import com.google.wireless.gdata.docs.SpreadsheetsClient;
import com.google.wireless.gdata.docs.XmlDocsGDataParserFactory;
import android.app.Activity;
import android.content.Context;
import android.content.SharedPreferences;
import android.util.Log;
import java.io.IOException;
/**
* A helper class used to transmit tracks statistics to Google Docs/Trix.
*
* @author Sandor Dornbush
*/
public class SendToDocs {
/** The GData service name for Google Spreadsheets (aka Trix) */
public static final String GDATA_SERVICE_NAME_TRIX = "wise";
/** The GData service name for the Google Docs Document List */
public static final String GDATA_SERVICE_NAME_DOCLIST = "writely";
private final Activity activity;
private final AuthManager trixAuth;
private final AuthManager docListAuth;
private final ProgressIndicator progressIndicator;
private final boolean metricUnits;
private boolean success = true;
private Runnable onCompletion = null;
public SendToDocs(Activity activity, AuthManager trixAuth,
AuthManager docListAuth, ProgressIndicator progressIndicator) {
this.activity = activity;
this.trixAuth = trixAuth;
this.docListAuth = docListAuth;
this.progressIndicator = progressIndicator;
SharedPreferences preferences = activity.getSharedPreferences(
Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
if (preferences != null) {
metricUnits =
preferences.getBoolean(activity.getString(R.string.metric_units_key),
true);
} else {
metricUnits = true;
}
}
public void sendToDocs(final long trackId) {
Log.d(Constants.TAG,
"Sending to Google Docs: trackId = " + trackId);
new Thread("SendToGoogleDocs") {
@Override
public void run() {
doUpload(trackId);
}
}.start();
}
private void doUpload(long trackId) {
success = false;
try {
if (trackId == -1) {
Log.w(Constants.TAG, "Cannot get track id.");
return;
}
// Get the track from the provider:
Track track =
MyTracksProviderUtils.Factory.get(activity).getTrack(trackId);
if (track == null) {
Log.w(Constants.TAG, "Cannot get track.");
return;
}
// Transmit track stats via GData feed:
// -------------------------------
Log.d(Constants.TAG, "SendToDocs: Uploading to spreadsheet");
success = uploadToDocs(track);
Log.d(Constants.TAG, "SendToDocs: Done.");
} finally {
if (onCompletion != null) {
activity.runOnUiThread(onCompletion);
}
}
}
public boolean wasSuccess() {
return success;
}
public void setOnCompletion(Runnable onCompletion) {
this.onCompletion = onCompletion;
}
/**
* Uploads the statistics about a track to Google Docs using the docs GData
* feed.
*
* @param track the track
*/
private boolean uploadToDocs(Track track) {
GDataWrapper<GDataServiceClient> docListWrapper = new GDataWrapper<GDataServiceClient>();
docListWrapper.setAuthManager(docListAuth);
docListWrapper.setRetryOnAuthFailure(true);
GDataWrapper<GDataServiceClient> trixWrapper = new GDataWrapper<GDataServiceClient>();
trixWrapper.setAuthManager(trixAuth);
trixWrapper.setRetryOnAuthFailure(true);
DocsHelper docsHelper = new DocsHelper();
GDataClient androidClient = null;
try {
androidClient = GDataClientFactory.getGDataClient(activity);
SpreadsheetsClient gdataClient = new SpreadsheetsClient(androidClient,
new XmlDocsGDataParserFactory(new AndroidXmlParserFactory()));
trixWrapper.setClient(gdataClient);
Log.d(Constants.TAG,
"GData connection prepared: " + this.docListAuth);
String sheetTitle = "My Tracks";
if (track.getCategory() != null && !track.getCategory().equals("")) {
sheetTitle += "-" + track.getCategory();
}
DocumentsClient docsGdataClient = new DocumentsClient(androidClient,
new XmlDocsGDataParserFactory(new AndroidXmlParserFactory()));
docListWrapper.setClient(docsGdataClient);
// First try to find the spreadsheet:
String spreadsheetId = null;
try {
spreadsheetId = docsHelper.requestSpreadsheetId(docListWrapper,
sheetTitle);
} catch (IOException e) {
Log.i(Constants.TAG, "Spreadsheet lookup failed.", e);
return false;
}
if (spreadsheetId == null) {
progressIndicator.setProgressValue(65);
// Waiting a few seconds and trying again. Maybe the server just had a
// hickup (unfortunately that happens quite a lot...).
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
Log.e(Constants.TAG, "Sleep interrupted", e);
}
try {
spreadsheetId = docsHelper.requestSpreadsheetId(docListWrapper,
sheetTitle);
} catch (IOException e) {
Log.i(Constants.TAG, "2nd spreadsheet lookup failed.", e);
return false;
}
}
// We were unable to find an existing spreadsheet, so create a new one.
progressIndicator.setProgressValue(70);
if (spreadsheetId == null) {
Log.i(Constants.TAG, "Creating new spreadsheet: " + sheetTitle);
try {
spreadsheetId = docsHelper.createSpreadsheet(activity, docListWrapper,
sheetTitle);
} catch (IOException e) {
Log.i(Constants.TAG, "Failed to create new spreadsheet "
+ sheetTitle, e);
return false;
}
progressIndicator.setProgressValue(80);
if (spreadsheetId == null) {
progressIndicator.setProgressValue(85);
// The previous creation might have succeeded even though GData
// reported an error. Seems to be a know bug,
// see http://code.google.com/p/gdata-issues/issues/detail?id=929
// Try to find the created spreadsheet:
Log.w(Constants.TAG,
"Create might have failed. Trying to find created document.");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
Log.e(Constants.TAG, "Sleep interrupted", e);
}
try {
spreadsheetId = docsHelper.requestSpreadsheetId(docListWrapper,
sheetTitle);
} catch (IOException e) {
Log.i(Constants.TAG, "Failed create-failed lookup", e);
return false;
}
if (spreadsheetId == null) {
progressIndicator.setProgressValue(87);
// Re-try
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
Log.e(Constants.TAG, "Sleep interrupted", e);
}
try {
spreadsheetId = docsHelper.requestSpreadsheetId(docListWrapper,
sheetTitle);
} catch (IOException e) {
Log.i(Constants.TAG, "Failed create-failed relookup", e);
return false;
}
}
if (spreadsheetId == null) {
Log.i(Constants.TAG,
"Creating new spreadsheet really failed.");
return false;
}
}
}
String worksheetId = null;
try {
worksheetId = docsHelper.getWorksheetId(trixWrapper, spreadsheetId);
if (worksheetId == null) {
throw new IOException("Worksheet ID lookup returned empty");
}
} catch (IOException e) {
Log.i(Constants.TAG, "Looking up worksheet id failed.", e);
return false;
}
progressIndicator.setProgressValue(90);
docsHelper.addTrackRow(activity, trixAuth, spreadsheetId, worksheetId,
track, metricUnits);
Log.i(Constants.TAG, "Done uploading to docs.");
} catch (IOException e) {
Log.e(Constants.TAG, "Unable to upload docs.", e);
return false;
} finally {
if (androidClient != null) {
androidClient.close();
}
}
return true;
}
}
@@ -1,716 +0,0 @@
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.io;
import static com.google.android.apps.mytracks.Constants.TAG;
import com.google.android.apps.mytracks.Constants;
import com.google.android.apps.mytracks.ProgressIndicator;
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.GDataWrapper;
import com.google.android.apps.mytracks.io.gdata.GDataWrapper.QueryFunction;
import com.google.android.apps.mytracks.io.sendtogoogle.SendType;
import com.google.android.apps.mytracks.stats.DoubleBuffer;
import com.google.android.apps.mytracks.stats.TripStatistics;
import com.google.android.apps.mytracks.util.ApiFeatures;
import com.google.android.apps.mytracks.util.LocationUtils;
import com.google.android.apps.mytracks.util.StringUtils;
import com.google.android.apps.mytracks.util.SystemUtils;
import com.google.android.apps.mytracks.util.UnitConversions;
import com.google.android.maps.mytracks.R;
import com.google.api.client.googleapis.GoogleHeaders;
import com.google.api.client.googleapis.MethodOverride;
import com.google.api.client.http.GenericUrl;
import com.google.api.client.http.HttpRequest;
import com.google.api.client.http.HttpRequestFactory;
import com.google.api.client.http.HttpResponse;
import com.google.api.client.http.HttpResponseException;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.http.InputStreamContent;
import com.google.api.client.util.Strings;
import android.app.Activity;
import android.content.Context;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.location.Location;
import android.util.Log;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Vector;
/**
* A helper class used to transmit tracks to Google Fusion Tables.
* A new instance should be used for each upload.
*
* @author Leif Hendrik Wilden
*/
public class SendToFusionTables implements Runnable {
private static final String CONTENT_TYPE = "application/x-www-form-urlencoded";
/**
* Listener invoked when sending to fusion tables completes.
*/
public interface OnSendCompletedListener {
void onSendCompleted(String tableId, boolean success);
}
/** The GData service id for Fusion Tables. */
public static final String SERVICE_ID = "fusiontables";
/** The path for viewing a map visualization of a table. */
private static final String FUSIONTABLES_MAP =
"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";
/** Standard base feed url for Fusion Tables. */
private static final String FUSIONTABLES_BASE_FEED_URL =
"https://www.google.com/fusiontables/api/query";
private static final int MAX_POINTS_PER_UPLOAD = 2048;
private static final String GDATA_VERSION = "2";
// This class reports upload status to the user as a completion percentage
// using a progress bar. Progress is defined as follows:
//
// 0% Getting track metadata
// 5% Creating Fusion Table (GData to FT server)
// 10%-90% Uploading the track data to Fusion Tables
// 95% Uploading waypoints
// 100% Done
private static final int PROGRESS_INITIALIZATION = 0;
private static final int PROGRESS_FUSION_TABLE_CREATE = 5;
private static final int PROGRESS_UPLOAD_DATA_MIN = 10;
private static final int PROGRESS_UPLOAD_DATA_MAX = 90;
private static final int PROGRESS_UPLOAD_WAYPOINTS = 95;
private static final int PROGRESS_COMPLETE = 100;
private final Activity context;
private final AuthManager auth;
private final long trackId;
private final ProgressIndicator progressIndicator;
private final OnSendCompletedListener onCompletion;
private final StringUtils stringUtils;
private final MyTracksProviderUtils providerUtils;
// Progress status
private int totalLocationsRead;
private int totalLocationsPrepared;
private int totalLocationsUploaded;
private int totalLocations;
private int totalSegmentsUploaded;
private HttpRequestFactory httpRequestFactory;
private String tableId;
private static String MARKER_TYPE_START = "large_green";
private static String MARKER_TYPE_END = "large_red";
private static String MARKER_TYPE_WAYPOINT = "large_yellow";
public SendToFusionTables(Activity context, AuthManager auth, long trackId,
ProgressIndicator progressIndicator, OnSendCompletedListener onCompletion) {
this.context = context;
this.auth = auth;
this.trackId = trackId;
this.progressIndicator = progressIndicator;
this.onCompletion = onCompletion;
this.stringUtils = new StringUtils(context);
this.providerUtils = MyTracksProviderUtils.Factory.get(context);
HttpTransport transport = ApiFeatures.getInstance().getApiAdapter().getHttpTransport();
httpRequestFactory = transport.createRequestFactory(new MethodOverride());
}
@Override
public void run() {
Log.d(Constants.TAG, "Sending to Fusion tables: trackId = " + trackId);
doUpload();
}
public static String getMapVisualizationUrl(Track track) {
if (track == null || track.getStatistics() == null || track.getTableId() == null) {
Log.w(TAG, "Unable to get track URL");
return null;
}
// TODO(leifhendrik): Determine correct bounding box and zoom level that will show the entire track.
TripStatistics stats = track.getStatistics();
double latE6 = stats.getBottom() + (stats.getTop() - stats.getBottom()) / 2;
double lonE6 = stats.getLeft() + (stats.getRight() - stats.getLeft()) / 2;
int z = 15;
// We explicitly format with Locale.US because we need the latitude and
// longitude to be formatted in a locale-independent manner. Specifically,
// we need the decimal separator to be a period rather than a comma.
return String.format(Locale.US, FUSIONTABLES_MAP, track.getTableId(),
latE6 / 1.E6, lonE6 / 1.E6, z);
}
private void doUpload() {
boolean success = true;
try {
progressIndicator.setProgressValue(PROGRESS_INITIALIZATION);
progressIndicator.setProgressMessage(
context.getString(R.string.send_google_progress_reading_track));
// Get the track meta-data
Track track = providerUtils.getTrack(trackId);
if (track == null) {
Log.w(Constants.TAG, "Cannot get track.");
return;
}
String originalDescription = track.getDescription();
// Create a new table:
progressIndicator.setProgressValue(PROGRESS_FUSION_TABLE_CREATE);
String creatingFormat = context.getString(R.string.send_google_progress_creating);
String serviceName = context.getString(SendType.FUSION_TABLES.getServiceName());
progressIndicator.setProgressMessage(String.format(creatingFormat, serviceName));
if (!createNewTable(track) || !makeTableUnlisted()) {
return;
}
progressIndicator.setProgressValue(PROGRESS_UPLOAD_DATA_MIN);
String sendingFormat = context.getString(R.string.send_google_progress_sending);
progressIndicator.setProgressMessage(String.format(sendingFormat, serviceName));
// Upload all of the segments of the track plus start/end markers
if (!uploadAllTrackPoints(track, originalDescription)) {
return;
}
progressIndicator.setProgressValue(PROGRESS_UPLOAD_WAYPOINTS);
// Upload all the waypoints.
if (!uploadWaypoints(track)) {
return;
}
Log.d(Constants.TAG, "SendToFusionTables: Done: " + success);
progressIndicator.setProgressValue(PROGRESS_COMPLETE);
} finally {
final boolean finalSuccess = success;
context.runOnUiThread(new Runnable() {
public void run() {
if (onCompletion != null) {
onCompletion.onSendCompleted(tableId, finalSuccess);
}
}
});
}
}
/**
* Creates a new table.
* If successful sets {@link #tableId}.
*
* @return true in case of success.
*/
private boolean createNewTable(Track track) {
Log.d(Constants.TAG, "Creating a new fusion table.");
String query = "CREATE TABLE '" + sqlEscape(track.getName()) +
"' (name:STRING,description:STRING,geometry:LOCATION,marker:STRING)";
return runUpdate(query);
}
private boolean makeTableUnlisted() {
Log.d(Constants.TAG, "Setting visibility to unlisted.");
String query = "UPDATE TABLE " + tableId + " SET VISIBILITY = UNLISTED";
return runUpdate(query);
}
/**
* Formats given values SQL style. Escapes single quotes with a backslash.
*
* @param values the values to format
* @return the values formatted as: ('value1','value2',...,'value_n').
*/
private static String values(String... values) {
StringBuilder builder = new StringBuilder("(");
for (int i = 0; i < values.length; i++) {
if (i > 0) {
builder.append(',');
}
builder.append('\'');
builder.append(sqlEscape(values[i]));
builder.append('\'');
}
builder.append(')');
return builder.toString();
}
private static String sqlEscape(String value) {
return value.replaceAll("'", "''");
}
/**
* Creates a new row representing a marker.
*
* @param name the marker name
* @param description the marker description
* @param location the marker location
* @return true in case of success.
*/
private boolean createNewPoint(String name, String description, Location location,
String marker) {
Log.d(Constants.TAG, "Creating a new row with a point.");
String query = "INSERT INTO " + tableId + " (name,description,geometry,marker) VALUES "
+ values(name, description, getKmlPoint(location), marker);
return runUpdate(query);
}
/**
* Creates a new row representing a line segment.
*
* @param track the track/segment to draw
* @return true in case of success.
*/
private boolean createNewLineString(Track track) {
Log.d(Constants.TAG, "Creating a new row with a point.");
String query = "INSERT INTO " + tableId
+ " (name,description,geometry) VALUES "
+ values(track.getName(), track.getDescription(), getKmlLineString(track));
return runUpdate(query);
}
private boolean uploadAllTrackPoints(final Track track, String originalDescription) {
SharedPreferences preferences = context.getSharedPreferences(
Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
boolean metricUnits = true;
if (preferences != null) {
metricUnits = preferences.getBoolean(context.getString(R.string.metric_units_key), true);
}
Cursor locationsCursor = providerUtils.getLocationsCursor(track.getId(), 0, -1, false);
try {
if (locationsCursor == null || !locationsCursor.moveToFirst()) {
Log.w(Constants.TAG, "Unable to get any points to upload");
return false;
}
totalLocationsRead = 0;
totalLocationsPrepared = 0;
totalLocationsUploaded = 0;
totalLocations = locationsCursor.getCount();
totalSegmentsUploaded = 0;
// Limit the number of elevation readings. Ideally we would want around 250.
int elevationSamplingFrequency =
Math.max(1, (int) (totalLocations / 250.0));
Log.d(Constants.TAG,
"Using elevation sampling factor: " + elevationSamplingFrequency
+ " on " + totalLocations);
double totalDistance = 0;
Vector<Double> distances = new Vector<Double>();
Vector<Double> elevations = new Vector<Double>();
DoubleBuffer elevationBuffer = new DoubleBuffer(Constants.ELEVATION_SMOOTHING_FACTOR);
List<Location> locations = new ArrayList<Location>(MAX_POINTS_PER_UPLOAD);
Location lastLocation = null;
do {
if (totalLocationsRead % 100 == 0) {
updateTrackDataUploadProgress();
}
Location loc = providerUtils.createLocation(locationsCursor);
locations.add(loc);
if (totalLocationsRead == 0) {
// Put a marker at the first point of the first valid segment:
String name = track.getName() + " " + context.getString(R.string.marker_label_start);
createNewPoint(name, "", loc, MARKER_TYPE_START);
}
// Add to the elevation profile.
if (loc != null && LocationUtils.isValidLocation(loc)) {
// All points go into the smoothing buffer...
elevationBuffer.setNext(metricUnits ? loc.getAltitude()
: loc.getAltitude() * UnitConversions.M_TO_FT);
if (lastLocation != null) {
double dist = lastLocation.distanceTo(loc);
totalDistance += dist;
}
// ...but only a few points are really used to keep the url short.
if (totalLocationsRead % elevationSamplingFrequency == 0) {
distances.add(totalDistance);
elevations.add(elevationBuffer.getAverage());
}
}
// If the location was not valid, it's a segment split, so make sure the
// distance between the previous segment and the new one is not accounted
// for in the next iteration.
lastLocation = loc;
// Every now and then, upload the accumulated points
if (totalLocationsRead % MAX_POINTS_PER_UPLOAD == MAX_POINTS_PER_UPLOAD - 1) {
if (!prepareAndUploadPoints(track, locations)) {
return false;
}
}
totalLocationsRead++;
} while (locationsCursor.moveToNext());
// Do a final upload with what's left
if (!prepareAndUploadPoints(track, locations)) {
return false;
}
// Put an end marker at the last point of the last valid segment:
if (lastLocation != null) {
track.setDescription("<p>" + originalDescription + "</p><p>"
+ stringUtils.generateTrackDescription(track, distances, elevations)
+ "</p>");
String name = track.getName() + " " + context.getString(R.string.marker_label_end);
return createNewPoint(name, track.getDescription(), lastLocation, MARKER_TYPE_END);
}
return true;
} finally {
if (locationsCursor != null) {
locationsCursor.close();
}
}
}
/**
* Appends the given location to the string in the format:
* longitude,latitude[,altitude]
*
* @param location the location to be added
* @param builder the string builder to use
*/
private void appendCoordinate(Location location, StringBuilder builder) {
builder
.append(location.getLongitude())
.append(",")
.append(location.getLatitude());
if (location.hasAltitude()) {
builder.append(",");
builder.append(location.getAltitude());
}
}
/**
* Gets a KML Point tag for the given location.
*
* @param location The location.
* @return the kml.
*/
private String getKmlPoint(Location location) {
StringBuilder builder = new StringBuilder("<Point><coordinates>");
appendCoordinate(location, builder);
builder.append("</coordinates></Point>");
return builder.toString();
}
/**
* Returns a KML LineString.
*
* @param track the track.
* @return the KML LineString.
*/
private String getKmlLineString(Track track) {
StringBuilder builder = new StringBuilder("<LineString><coordinates>");
for (Location location : track.getLocations()) {
appendCoordinate(location, builder);
builder.append(' ');
}
builder.append("</coordinates></LineString>");
return builder.toString();
}
private boolean prepareAndUploadPoints(Track track, List<Location> locations) {
updateTrackDataUploadProgress();
int numLocations = locations.size();
if (numLocations < 2) {
Log.d(Constants.TAG, "Not preparing/uploading too few points");
totalLocationsUploaded += numLocations;
return true;
}
// Prepare/pre-process the points
ArrayList<Track> splitTracks = prepareLocations(track, locations);
// Start uploading them
for (Track splitTrack : splitTracks) {
if (totalSegmentsUploaded > 1) {
splitTrack.setName(splitTrack.getName() + " "
+ String.format(
context.getString(R.string.send_google_track_part_label), totalSegmentsUploaded));
}
totalSegmentsUploaded++;
Log.d(Constants.TAG,
"SendToFusionTables: Prepared feature for upload w/ "
+ splitTrack.getLocations().size() + " points.");
// Transmit tracks via GData feed:
// -------------------------------
Log.d(Constants.TAG,
"SendToFusionTables: Uploading to table " + tableId + " w/ auth " + auth);
if (!uploadTrackPoints(splitTrack)) {
Log.e(Constants.TAG, "Uploading failed");
return false;
}
}
locations.clear();
totalLocationsUploaded += numLocations;
updateTrackDataUploadProgress();
return true;
}
/**
* Prepares a buffer of locations for transmission to google fusion tables.
*
* @param track the original track with meta data
* @param locations locations on the track
* @return an array of tracks each with a sub section of the points in the
* original buffer
*/
private ArrayList<Track> prepareLocations(
Track track, Iterable<Location> locations) {
ArrayList<Track> splitTracks = new ArrayList<Track>();
// Create segments from each full track:
Track segment = new Track();
TripStatistics segmentStats = segment.getStatistics();
TripStatistics trackStats = track.getStatistics();
segment.setId(track.getId());
segment.setName(track.getName());
segment.setDescription(/* track.getDescription() */ "");
segment.setCategory(track.getCategory());
segmentStats.setStartTime(trackStats.getStartTime());
segmentStats.setStopTime(trackStats.getStopTime());
boolean startNewTrackSegment = false;
for (Location loc : locations) {
if (totalLocationsPrepared % 100 == 0) {
updateTrackDataUploadProgress();
}
if (loc.getLatitude() > 90) {
startNewTrackSegment = true;
}
if (startNewTrackSegment) {
// Close up the last segment.
prepareTrackSegment(segment, splitTracks);
Log.d(Constants.TAG,
"MyTracksSendToFusionTables: Starting new track segment...");
startNewTrackSegment = false;
segment = new Track();
segment.setId(track.getId());
segment.setName(track.getName());
segment.setDescription(/* track.getDescription() */ "");
segment.setCategory(track.getCategory());
}
if (loc.getLatitude() <= 90) {
segment.addLocation(loc);
if (segmentStats.getStartTime() < 0) {
segmentStats.setStartTime(loc.getTime());
}
}
totalLocationsPrepared++;
}
prepareTrackSegment(segment, splitTracks);
return splitTracks;
}
/**
* Prepares a track segment for sending to google fusion tables.
* The main steps are:
* - correcting end time
* - decimating locations
* - splitting into smaller tracks.
*
* The final track pieces will be put in the array list splitTracks.
*
* @param segment the original segment of the track
* @param splitTracks an array of smaller track segments
*/
private void prepareTrackSegment(
Track segment, ArrayList<Track> splitTracks) {
TripStatistics segmentStats = segment.getStatistics();
if (segmentStats.getStopTime() < 0
&& segment.getLocations().size() > 0) {
segmentStats.setStopTime(segment.getLocations().size() - 1);
}
/*
* Decimate to 2 meter precision. Fusion tables doesn't like too many
* points:
*/
LocationUtils.decimate(segment, 2.0);
/* If the track still has > 2500 points, split it in pieces: */
final int maxPoints = 2500;
if (segment.getLocations().size() > maxPoints) {
splitTracks.addAll(LocationUtils.split(segment, maxPoints));
} else if (segment.getLocations().size() >= 2) {
splitTracks.add(segment);
}
}
private boolean uploadTrackPoints(Track splitTrack) {
int numLocations = splitTrack.getLocations().size();
if (numLocations < 2) {
// Need at least two points for a polyline:
Log.w(Constants.TAG, "Not uploading too few points");
return true;
}
return createNewLineString(splitTrack);
}
/**
* Uploads all of the waypoints associated with this track to a table.
*
* @param track The track to upload waypoints for.
*
* @return True on success.
*/
private boolean uploadWaypoints(final Track track) {
// 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 success = true;
Cursor c = null;
try {
c = providerUtils.getWaypointsCursor(
track.getId(), 0,
Constants.MAX_LOADED_WAYPOINTS_POINTS);
if (c != null) {
if (c.moveToFirst()) {
// This will skip the 1st waypoint (it carries the stats for the
// last segment).
while (c.moveToNext()) {
Waypoint wpt = providerUtils.createWaypoint(c);
Log.d(Constants.TAG, "SendToFusionTables: Creating waypoint.");
success = createNewPoint(wpt.getName(), wpt.getDescription(), wpt.getLocation(),
MARKER_TYPE_WAYPOINT);
if (!success) {
break;
}
}
}
}
if (!success) {
Log.w(Constants.TAG, "SendToFusionTables: upload waypoints failed.");
}
return success;
} finally {
if (c != null) {
c.close();
}
}
}
private void updateTrackDataUploadProgress() {
// The percent of the total that represents the completed part of this
// segment. We calculate it as an absolute percentage, and then scale it
// to fit the completion percentage range alloted to track data upload.
double totalPercentage =
(totalLocationsRead + totalLocationsPrepared + totalLocationsUploaded)
/ (totalLocations * 3.0);
double scaledPercentage = totalPercentage
* (PROGRESS_UPLOAD_DATA_MAX - PROGRESS_UPLOAD_DATA_MIN) + PROGRESS_UPLOAD_DATA_MIN;
progressIndicator.setProgressValue((int) scaledPercentage);
}
/**
* Runs an update query. Handles authentication.
*
* @param query The given SQL like query
* @return true in case of success
*/
private boolean runUpdate(final String query) {
GDataWrapper<HttpRequestFactory> wrapper = new GDataWrapper<HttpRequestFactory>();
wrapper.setAuthManager(auth);
wrapper.setRetryOnAuthFailure(true);
wrapper.setClient(httpRequestFactory);
Log.d(Constants.TAG, "GData connection prepared: " + this.auth);
wrapper.runQuery(new QueryFunction<HttpRequestFactory>() {
@Override
public void query(HttpRequestFactory factory)
throws IOException, GDataWrapper.ParseException, GDataWrapper.HttpException,
GDataWrapper.AuthenticationException {
GenericUrl url = new GenericUrl(FUSIONTABLES_BASE_FEED_URL);
String sql = "sql=" + URLEncoder.encode(query, "UTF-8");
ByteArrayInputStream inputStream = new ByteArrayInputStream(Strings.toBytesUtf8(sql));
InputStreamContent isc = new InputStreamContent(null, inputStream );
HttpRequest request = factory.buildPostRequest(url, isc);
GoogleHeaders headers = new GoogleHeaders();
headers.setApplicationName("Google-MyTracks-" + SystemUtils.getMyTracksVersion(context));
headers.gdataVersion = GDATA_VERSION;
headers.setGoogleLogin(auth.getAuthToken());
headers.setContentType(CONTENT_TYPE);
request.setHeaders(headers);
Log.d(Constants.TAG, "Running update query " + url.toString() + ": " + sql);
HttpResponse response;
try {
response = request.execute();
} catch (HttpResponseException e) {
throw new GDataWrapper.HttpException(e.getResponse().getStatusCode(),
e.getResponse().getStatusMessage());
}
boolean success = response.isSuccessStatusCode();
if (success) {
byte[] result = new byte[1024];
int read = response.getContent().read(result);
String s = new String(result, 0, read, "UTF8");
String[] lines = s.split(Strings.LINE_SEPARATOR);
if (lines[0].equals("tableid")) {
tableId = lines[1];
Log.d(Constants.TAG, "tableId = " + tableId);
} else {
Log.w(Constants.TAG, "Unrecognized response: " + lines[0]);
}
} else {
Log.d(Constants.TAG,
"Query failed: " + response.getStatusMessage() + " ("
+ response.getStatusCode() + ")");
throw new GDataWrapper.HttpException(
response.getStatusCode(), response.getStatusMessage());
}
}
});
return wrapper.getErrorType() == GDataWrapper.ERROR_NO_ERROR;
}
}
@@ -1,458 +0,0 @@
/*
* 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;
import static com.google.android.apps.mytracks.Constants.TAG;
import com.google.android.apps.mytracks.Constants;
import com.google.android.apps.mytracks.ProgressIndicator;
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.maps.MapsFacade;
import com.google.android.apps.mytracks.io.sendtogoogle.SendType;
import com.google.android.apps.mytracks.stats.DoubleBuffer;
import com.google.android.apps.mytracks.stats.TripStatistics;
import com.google.android.apps.mytracks.util.LocationUtils;
import com.google.android.apps.mytracks.util.StringUtils;
import com.google.android.apps.mytracks.util.UnitConversions;
import com.google.android.maps.mytracks.R;
import android.app.Activity;
import android.content.Context;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.location.Location;
import android.util.Log;
import java.util.ArrayList;
import java.util.List;
import java.util.Vector;
/**
* A helper class used to transmit tracks to Google Maps.
* A new instance should be used for each upload.
*
* 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 Fusion Tables API.
*
* @author Leif Hendrik Wilden
*/
public class SendToMaps implements Runnable {
public static final String NEW_MAP_ID = "new";
private static final int MAX_POINTS_PER_UPLOAD = 2048;
private final Activity context;
private final AuthManager auth;
private final long trackId;
private final ProgressIndicator progressIndicator;
private final OnSendCompletedListener onCompletion;
private final StringUtils stringUtils;
private final MyTracksProviderUtils providerUtils;
private String mapId;
private MapsFacade mapsClient;
// Progress status
private int totalLocationsRead;
private int totalLocationsPrepared;
private int totalLocationsUploaded;
private int totalLocations;
private int totalSegmentsUploaded;
public interface OnSendCompletedListener {
void onSendCompleted(String mapId, boolean success);
}
public SendToMaps(Activity context, String mapId, AuthManager auth,
long trackId, ProgressIndicator progressIndicator,
OnSendCompletedListener onCompletion) {
this.context = context;
this.mapId = mapId;
this.auth = auth;
this.trackId = trackId;
this.progressIndicator = progressIndicator;
this.onCompletion = onCompletion;
this.stringUtils = new StringUtils(context);
this.providerUtils = MyTracksProviderUtils.Factory.get(context);
}
@Override
public void run() {
Log.d(TAG, "Sending to Maps: trackId = " + trackId);
doUpload();
}
private void doUpload() {
boolean success = true;
try {
progressIndicator.setProgressMessage(
context.getString(R.string.send_google_progress_reading_track));
// Get the track meta-data
Track track = providerUtils.getTrack(trackId);
if (track == null) {
Log.w(Constants.TAG, "Cannot get track.");
return;
}
String originalDescription = track.getDescription();
track.setDescription("<p>" + track.getDescription() + "</p><p>"
+ stringUtils.generateTrackDescription(track, null, null) + "</p>");
mapsClient = new MapsFacade(context, auth);
// Create a new map if necessary:
boolean isNewMap = mapId.equals(NEW_MAP_ID);
if (isNewMap) {
SharedPreferences preferences = context.getSharedPreferences(
Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
boolean mapPublic = true;
if (preferences != null) {
mapPublic = preferences.getBoolean(
context.getString(R.string.default_map_public_key), true);
}
String creatingFormat = context.getString(R.string.send_google_progress_creating);
String serviceName = context.getString(SendType.MAPS.getServiceName());
progressIndicator.setProgressMessage(String.format(creatingFormat, serviceName));
StringBuilder mapIdBuilder = new StringBuilder();
success = mapsClient.createNewMap(
track.getName(), track.getCategory(), originalDescription, mapPublic, mapIdBuilder);
mapId = mapIdBuilder.toString();
}
// Upload all of the segments of the track plus start/end markers
if (success) {
success = uploadAllTrackPoints(track, originalDescription);
}
// Put waypoints.
if (success) {
Cursor c = providerUtils.getWaypointsCursor(
track.getId(), 0,
Constants.MAX_LOADED_WAYPOINTS_POINTS);
if (c != null) {
try {
if (c.getCount() > 1 && c.moveToFirst()) {
// This will skip the 1st waypoint (it carries the stats for the
// last segment).
ArrayList<Waypoint> waypoints = new ArrayList<Waypoint>(c.getCount() - 1);
while (c.moveToNext()) {
Waypoint wpt = providerUtils.createWaypoint(c);
waypoints.add(wpt);
}
success = mapsClient.uploadWaypoints(mapId, waypoints);
}
} finally {
c.close();
}
} else {
success = false;
}
if (!success) {
Log.w(TAG, "SendToMaps: upload waypoints failed.");
}
}
Log.d(TAG, "SendToMaps: Done: " + success);
progressIndicator.setProgressValue(100);
} finally {
if (mapsClient != null) {
mapsClient.cleanUp();
}
final boolean finalSuccess = success;
context.runOnUiThread(new Runnable() {
public void run() {
if (onCompletion != null) {
onCompletion.onSendCompleted(mapId, finalSuccess);
}
}
});
}
}
private boolean uploadAllTrackPoints(
final Track track, String originalDescription) {
SharedPreferences preferences = context.getSharedPreferences(
Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
boolean metricUnits = true;
if (preferences != null) {
metricUnits =
preferences.getBoolean(context.getString(R.string.metric_units_key),
true);
}
Cursor locationsCursor =
providerUtils.getLocationsCursor(track.getId(), 0, -1, false);
try {
if (locationsCursor == null || !locationsCursor.moveToFirst()) {
Log.w(TAG, "Unable to get any points to upload");
return false;
}
totalLocationsRead = 0;
totalLocationsPrepared = 0;
totalLocationsUploaded = 0;
totalLocations = locationsCursor.getCount();
totalSegmentsUploaded = 0;
// Limit the number of elevation readings. Ideally we would want around 250.
int elevationSamplingFrequency =
Math.max(1, (int) (totalLocations / 250.0));
Log.d(TAG,
"Using elevation sampling factor: " + elevationSamplingFrequency
+ " on " + totalLocations);
double totalDistance = 0;
Vector<Double> distances = new Vector<Double>();
Vector<Double> elevations = new Vector<Double>();
DoubleBuffer elevationBuffer =
new DoubleBuffer(Constants.ELEVATION_SMOOTHING_FACTOR);
List<Location> locations = new ArrayList<Location>(MAX_POINTS_PER_UPLOAD);
progressIndicator.setProgressMessage(
context.getString(R.string.send_google_progress_reading_track));
Location lastLocation = null;
do {
if (totalLocationsRead % 100 == 0) {
updateProgress();
}
Location loc = providerUtils.createLocation(locationsCursor);
locations.add(loc);
if (totalLocationsRead == 0) {
// Put a marker at the first point of the first valid segment:
mapsClient.uploadMarker(mapId, track.getName(), track.getDescription(), loc, true);
}
// Add to the elevation profile.
if (loc != null && LocationUtils.isValidLocation(loc)) {
// All points go into the smoothing buffer...
elevationBuffer.setNext(metricUnits ? loc.getAltitude()
: loc.getAltitude() * UnitConversions.M_TO_FT);
if (lastLocation != null) {
double dist = lastLocation.distanceTo(loc);
totalDistance += dist;
}
// ...but only a few points are really used to keep the url short.
if (totalLocationsRead % elevationSamplingFrequency == 0) {
distances.add(totalDistance);
elevations.add(elevationBuffer.getAverage());
}
}
// If the location was not valid, it's a segment split, so make sure the
// distance between the previous segment and the new one is not accounted
// for in the next iteration.
lastLocation = loc;
// Every now and then, upload the accumulated points
if (totalLocationsRead % MAX_POINTS_PER_UPLOAD
== MAX_POINTS_PER_UPLOAD - 1) {
if (!prepareAndUploadPoints(track, locations)) {
return false;
}
}
totalLocationsRead++;
} while (locationsCursor.moveToNext());
// Do a final upload with what's left
if (!prepareAndUploadPoints(track, locations)) {
return false;
}
// Put an end marker at the last point of the last valid segment:
if (lastLocation != null) {
track.setDescription("<p>" + originalDescription + "</p><p>"
+ stringUtils.generateTrackDescription(
track, distances, elevations)
+ "</p>");
return mapsClient.uploadMarker(mapId, track.getName(), track.getDescription(),
lastLocation, false);
}
return true;
} finally {
if (locationsCursor != null) {
locationsCursor.close();
}
}
}
private boolean prepareAndUploadPoints(Track track, List<Location> locations) {
progressIndicator.setProgressMessage(
context.getString(R.string.send_google_progress_preparing_track));
updateProgress();
int numLocations = locations.size();
if (numLocations < 2) {
Log.d(TAG, "Not preparing/uploading too few points");
totalLocationsUploaded += numLocations;
return true;
}
// Prepare/pre-process the points
ArrayList<Track> splitTracks = prepareLocations(track, locations);
// Start uploading them
String sendingFormat = context.getString(R.string.send_google_progress_sending);
String serviceName = context.getString(SendType.MAPS.getServiceName());
progressIndicator.setProgressMessage(String.format(sendingFormat, serviceName));
for (Track splitTrack : splitTracks) {
if (totalSegmentsUploaded > 1) {
splitTrack.setName(splitTrack.getName() + " "
+ String.format(
context.getString(R.string.send_google_track_part_label), totalSegmentsUploaded));
}
totalSegmentsUploaded++;
Log.d(TAG,
"SendToMaps: Prepared feature for upload w/ "
+ splitTrack.getLocations().size() + " points.");
// Transmit tracks via GData feed:
// -------------------------------
Log.d(TAG,
"SendToMaps: Uploading to map " + mapId + " w/ auth " + auth);
if (!mapsClient.uploadTrackPoints(mapId, splitTrack.getName(), splitTrack.getLocations())) {
Log.e(TAG, "Uploading failed");
return false;
}
}
locations.clear();
totalLocationsUploaded += numLocations;
updateProgress();
return true;
}
/**
* Prepares a buffer of locations for transmission to google maps.
*
* @param track the original track with meta data
* @param locations a buffer of locations on the track
* @return an array of tracks each with a sub section of the points in the
* original buffer
*/
private ArrayList<Track> prepareLocations(
Track track, Iterable<Location> locations) {
ArrayList<Track> splitTracks = new ArrayList<Track>();
// Create segments from each full track:
Track segment = new Track();
TripStatistics segmentStats = segment.getStatistics();
TripStatistics trackStats = track.getStatistics();
segment.setId(track.getId());
segment.setName(track.getName());
segment.setDescription(/* track.getDescription() */ "");
segment.setCategory(track.getCategory());
segmentStats.setStartTime(trackStats.getStartTime());
segmentStats.setStopTime(trackStats.getStopTime());
boolean startNewTrackSegment = false;
for (Location loc : locations) {
if (totalLocationsPrepared % 100 == 0) {
updateProgress();
}
if (loc.getLatitude() > 90) {
startNewTrackSegment = true;
}
if (startNewTrackSegment) {
// Close up the last segment.
prepareTrackSegment(segment, splitTracks);
Log.d(TAG,
"MyTracksSendToMaps: Starting new track segment...");
startNewTrackSegment = false;
segment = new Track();
segment.setId(track.getId());
segment.setName(track.getName());
segment.setDescription(/* track.getDescription() */ "");
segment.setCategory(track.getCategory());
}
if (loc.getLatitude() <= 90) {
segment.addLocation(loc);
if (segmentStats.getStartTime() < 0) {
segmentStats.setStartTime(loc.getTime());
}
}
totalLocationsPrepared++;
}
prepareTrackSegment(segment, splitTracks);
return splitTracks;
}
/**
* Prepares a track segment for sending to google maps.
* The main steps are:
* - correcting end time
* - decimating locations
* - splitting into smaller tracks.
*
* The final track pieces will be put in the array list splitTracks.
*
* @param segment the original segment of the track
* @param splitTracks an array of smaller track segments
*/
private void prepareTrackSegment(
Track segment, ArrayList<Track> splitTracks) {
TripStatistics segmentStats = segment.getStatistics();
if (segmentStats.getStopTime() < 0
&& segment.getLocations().size() > 0) {
segmentStats.setStopTime(segment.getLocations().size() - 1);
}
/*
* Decimate to 2 meter precision. Mapshop doesn't like too many
* points:
*/
LocationUtils.decimate(segment, 2.0);
/* It the track still has > 500 points, split it in pieces: */
if (segment.getLocations().size() > 500) {
splitTracks.addAll(LocationUtils.split(segment, 500));
} else if (segment.getLocations().size() >= 2) {
splitTracks.add(segment);
}
}
/**
* Sets the current upload progress.
*/
private void updateProgress() {
// The percent of the total that represents the completed part of this
// segment.
int totalPercentage =
(totalLocationsRead + totalLocationsPrepared + totalLocationsUploaded) * 100
/ (totalLocations * 3);
totalPercentage = Math.min(99, totalPercentage);
progressIndicator.setProgressValue(totalPercentage);
}
}
@@ -16,7 +16,7 @@
package com.google.android.apps.mytracks.io.backup;
import com.google.android.apps.mytracks.content.ContentTypeIds;
import com.google.android.apps.mytracks.util.ApiFeatures;
import com.google.android.apps.mytracks.util.ApiAdapterFactory;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
@@ -106,7 +106,7 @@ class PreferenceBackupHelper {
byte typeId = reader.readByte();
readAndSetPreference(name, typeId, reader, editor);
}
ApiFeatures.getInstance().getApiAdapter().applyPreferenceChanges(editor);
ApiAdapterFactory.getApiAdapter().applyPreferenceChanges(editor);
}
/**
@@ -1,369 +0,0 @@
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.io.docs;
import com.google.android.apps.mytracks.Constants;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.io.AuthManager;
import com.google.android.apps.mytracks.io.gdata.GDataWrapper;
import com.google.android.apps.mytracks.io.gdata.GDataWrapper.QueryFunction;
import com.google.android.apps.mytracks.stats.TripStatistics;
import com.google.android.apps.mytracks.util.ResourceUtils;
import com.google.android.apps.mytracks.util.StringUtils;
import com.google.android.maps.mytracks.R;
import com.google.wireless.gdata.client.GDataServiceClient;
import com.google.wireless.gdata.client.HttpException;
import com.google.wireless.gdata.data.Entry;
import com.google.wireless.gdata.docs.SpreadsheetsClient;
import com.google.wireless.gdata.docs.SpreadsheetsClient.WorksheetEntry;
import com.google.wireless.gdata.parser.GDataParser;
import com.google.wireless.gdata.parser.ParseException;
import com.google.wireless.gdata2.client.AuthenticationException;
import android.content.Context;
import android.util.Log;
import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.URL;
import java.net.URLConnection;
import java.util.concurrent.atomic.AtomicReference;
/**
* This class contains helper methods for interacting with Google Docs and
* Google Spreadsheets.
*
* @author Sandor Dornbush
* @author Matthew Simmons
*/
public class DocsHelper {
private static final String DOCS_FEED_URL =
"https://docs.google.com/feeds/documents/private/full";
private static final String DOCS_SPREADSHEET_URL =
"https://docs.google.com/feeds/documents/private/full/spreadsheet%3A";
private static final String DOCS_WORKSHEETS_URL_FORMAT =
"https://spreadsheets.google.com/feeds/worksheets/%s/private/full";
private static final String DOCS_MY_SPREADSHEETS_FEED_URL =
"https://docs.google.com/feeds/documents/private/full?category=mine,spreadsheet";
private static final String DOCS_SPREADSHEET_URL_FORMAT =
"https://spreadsheets.google.com/feeds/list/%s/%s/private/full";
private static final String CONTENT_TYPE_PARAM = "Content-Type";
private static final String OPENDOCUMENT_SPREADSHEET_MIME_TYPE =
"application/x-vnd.oasis.opendocument.spreadsheet";
private static final String ATOM_FEED_MIME_TYPE = "application/atom+xml";
/**
* Creates a new MyTracks spreadsheet with the given name.
*
* @param context The context associated with this request.
* @param docListWrapper The GData handle for the Document List service.
* @param name The name for the newly-created spreadsheet.
* @return The spreadsheet ID, if one is created. {@code null} will be
* returned if a GData error didn't occur, but no spreadsheet ID was
* returned.
*/
public String createSpreadsheet(final Context context,
final GDataWrapper<GDataServiceClient> docListWrapper, final String name) throws IOException {
final AtomicReference<String> idSaver = new AtomicReference<String>();
boolean success = docListWrapper.runQuery(new QueryFunction<GDataServiceClient>() {
@Override
public void query(GDataServiceClient client) throws IOException,
GDataWrapper.AuthenticationException {
// Construct and send request
URL url = new URL(DOCS_FEED_URL);
URLConnection conn = url.openConnection();
conn.addRequestProperty(CONTENT_TYPE_PARAM,
OPENDOCUMENT_SPREADSHEET_MIME_TYPE);
conn.addRequestProperty("Slug", name);
conn.addRequestProperty("Authorization",
"GoogleLogin auth=" +
docListWrapper.getAuthManager().getAuthToken());
conn.setDoOutput(true);
OutputStream os = conn.getOutputStream();
ResourceUtils.readBinaryFileToOutputStream(
context, R.raw.mytracks_empty_spreadsheet, os);
// Get the response
// TODO: The following is a horrible ugly hack.
// Hopefully we can retire it when there is a proper gdata api.
BufferedReader rd = null;
String line;
StringBuilder resultBuilder = new StringBuilder();
try {
rd = new BufferedReader(new InputStreamReader(conn.getInputStream()));
while ((line = rd.readLine()) != null) {
resultBuilder.append(line);
}
os.close();
rd.close();
} catch (FileNotFoundException e) {
// The GData API sometimes throws an error, even though creation of
// the document succeeded. In that case let's just return. The caller
// then needs to check if the doc actually exists.
return;
} finally {
os.close();
if (rd != null) {
rd.close();
}
}
String result = resultBuilder.toString();
// Try to find the id.
int idTagIndex = result.indexOf("<id>");
if (idTagIndex == -1) {
return;
}
int idTagCloseIndex = result.indexOf("</id>", idTagIndex);
if (idTagCloseIndex == -1) {
return;
}
int idStringStart = result.indexOf(DOCS_SPREADSHEET_URL, idTagIndex);
if (idStringStart == -1) {
return;
}
String id = result.substring(
idStringStart + DOCS_SPREADSHEET_URL.length(), idTagCloseIndex);
Log.i(Constants.TAG, "Created new spreadsheet: " + id);
idSaver.set(id);
}});
if (!success) {
throw newIOException(docListWrapper,
"Failed to create new spreadsheet.");
}
return idSaver.get();
}
/**
* Retrieve the ID of a spreadsheet with the given name.
*
* @param docListWrapper The GData handle for the Document List service.
* @param title The name of the spreadsheet whose ID is to be retrieved.
* @return The spreadsheet ID, if it can be retrieved. {@code null} will
* be returned if no spreadsheet exists by the given name.
* @throws IOException If an error occurs during the GData request.
*/
public String requestSpreadsheetId(final GDataWrapper<GDataServiceClient> docListWrapper,
final String title) throws IOException {
final AtomicReference<String> idSaver = new AtomicReference<String>();
boolean result = docListWrapper.runQuery(new QueryFunction<GDataServiceClient>() {
@Override
public void query(GDataServiceClient client)
throws IOException, GDataWrapper.ParseException, GDataWrapper.HttpException,
GDataWrapper.AuthenticationException {
GDataParser listParser;
try {
listParser = client.getParserForFeed(Entry.class,
DOCS_MY_SPREADSHEETS_FEED_URL,
docListWrapper.getAuthManager().getAuthToken());
listParser.init();
while (listParser.hasMoreData()) {
Entry entry = listParser.readNextEntry(null);
String entryTitle = entry.getTitle();
Log.i(Constants.TAG, "Found docs entry: " + entryTitle);
if (entryTitle.equals(title)) {
String entryId = entry.getId();
int lastSlash = entryId.lastIndexOf('/');
idSaver.set(entryId.substring(lastSlash + 15));
break;
}
}
} catch (ParseException e) {
throw new GDataWrapper.ParseException(e);
} catch (HttpException e) {
throw new GDataWrapper.HttpException(e.getStatusCode(), e.getMessage());
}
}
});
if (!result) {
throw newIOException(docListWrapper,
"Failed to retrieve spreadsheet list.");
}
return idSaver.get();
}
/**
* Retrieve the ID of the first worksheet in the named spreadsheet.
*
* @param trixWrapper The GData handle for the spreadsheet service.
* @param spreadsheetId The GData ID for the given spreadsheet.
* @return The worksheet ID, if it can be retrieved. {@code null} will be
* returned if the GData request returns without error, but without an
* ID.
* @throws IOException If an error occurs during the GData request.
*/
public String getWorksheetId(final GDataWrapper<GDataServiceClient> trixWrapper,
final String spreadsheetId) throws IOException {
final AtomicReference<String> idSaver = new AtomicReference<String>();
boolean result = trixWrapper.runQuery(new QueryFunction<GDataServiceClient>() {
@Override
public void query(GDataServiceClient client)
throws GDataWrapper.AuthenticationException, IOException, GDataWrapper.ParseException, GDataWrapper.HttpException {
String uri = String.format(DOCS_WORKSHEETS_URL_FORMAT, spreadsheetId);
GDataParser sheetParser;
try {
sheetParser = ((SpreadsheetsClient) client).getParserForWorksheetsFeed(uri,
trixWrapper.getAuthManager().getAuthToken());
sheetParser.init();
if (!sheetParser.hasMoreData()) {
Log.i(Constants.TAG, "Found no worksheets");
return;
}
// Grab the first worksheet.
WorksheetEntry worksheetEntry =
(WorksheetEntry) sheetParser.readNextEntry(new WorksheetEntry());
int lastSlash = worksheetEntry.getId().lastIndexOf('/');
idSaver.set(worksheetEntry.getId().substring(lastSlash + 1));
} catch (ParseException e) {
throw new GDataWrapper.ParseException(e);
} catch (AuthenticationException e) {
throw new GDataWrapper.AuthenticationException(e);
}
}
});
if (!result) {
throw newIOException(trixWrapper, "Failed to retrieve worksheet ID.");
}
return idSaver.get();
}
/**
* Add a row to a worksheet containing the stats for a given track.
*
* @param context The context associated with this request.
* @param trixAuth The GData authorization for the spreadsheet service.
* @param spreadsheetId The spreadsheet to be modified.
* @param worksheetId The worksheet to be modified.
* @param track The track whose stats are to be written.
* @param metricUnits True if metric units are to be used. If false,
* imperial units will be used.
* @throws IOException If an error occurs while updating the worksheet.
*/
public void addTrackRow(Context context, AuthManager trixAuth,
String spreadsheetId, String worksheetId, Track track,
boolean metricUnits) throws IOException {
String worksheetUri = String.format(DOCS_SPREADSHEET_URL_FORMAT,
spreadsheetId, worksheetId);
TripStatistics stats = track.getStatistics();
String distanceUnit = context.getString(metricUnits ?
R.string.unit_kilometer : R.string.unit_mile);
String speedUnit = context.getString(metricUnits ?
R.string.unit_kilometer_per_hour : R.string.unit_mile_per_hour);
String elevationUnit = context.getString(metricUnits ?
R.string.unit_meter : R.string.unit_feet);
// Prepare the Post-Text we are going to send.
DocsTagBuilder tagBuilder = new DocsTagBuilder(metricUnits)
.append("name", track.getName())
.append("description", track.getDescription())
.append("date", StringUtils.formatDateTime(context, stats.getStartTime()))
.append("totaltime", StringUtils.formatElapsedTime(stats.getTotalTime()))
.append("movingtime", StringUtils.formatElapsedTime(stats.getMovingTime()))
.appendLargeUnits("distance", stats.getTotalDistance() / 1000)
.append("distanceunit", distanceUnit)
.appendLargeUnits("averagespeed", stats.getAverageSpeed() * 3.6)
.appendLargeUnits("averagemovingspeed",
stats.getAverageMovingSpeed() * 3.6)
.appendLargeUnits("maxspeed", stats.getMaxSpeed() * 3.6)
.append("speedunit", speedUnit)
.appendSmallUnits("elevationgain", stats.getTotalElevationGain())
.appendSmallUnits("minelevation", stats.getMinElevation())
.appendSmallUnits("maxelevation", stats.getMaxElevation())
.append("elevationunit", elevationUnit);
if (track.getMapId().length() > 0) {
tagBuilder.append("map", String.format("%s?msa=0&msid=%s",
Constants.MAPSHOP_BASE_URL, track.getMapId()));
}
String postText = new StringBuilder()
.append("<entry xmlns='http://www.w3.org/2005/Atom' "
+ "xmlns:gsx='http://schemas.google.com/spreadsheets/"
+ "2006/extended'>")
.append(tagBuilder.build())
.append("</entry>")
.toString();
Log.i(Constants.TAG,
"Inserting at: " + spreadsheetId + " => " + worksheetUri);
Log.i(Constants.TAG, postText);
writeRowData(trixAuth, worksheetUri, postText);
Log.i(Constants.TAG, "Post finished.");
}
/**
* Writes spreadsheet row data to the indicated worksheet.
*
* @param trixAuth The GData authorization for the spreadsheet service.
* @param worksheetUri The URI of the worksheet to be altered.
* @param postText The XML tags describing the change to be made.
* @throws IOException Thrown if an error occurs during the write.
*/
protected void writeRowData(AuthManager trixAuth, String worksheetUri,
String postText) throws IOException {
// No need for a wrapper because we know that the authorization was good
// enough to get this far.
URL url = new URL(worksheetUri);
URLConnection conn = url.openConnection();
conn.addRequestProperty(CONTENT_TYPE_PARAM, ATOM_FEED_MIME_TYPE);
conn.addRequestProperty("Authorization",
"GoogleLogin auth=" + trixAuth.getAuthToken());
conn.setDoOutput(true);
OutputStreamWriter wr = new OutputStreamWriter(conn.getOutputStream());
wr.write(postText);
wr.flush();
// Get the response.
// TODO: Should we parse the response, rather than simply throwing it away?
BufferedReader rd =
new BufferedReader(new InputStreamReader(conn.getInputStream()));
String line;
while ((line = rd.readLine()) != null) {
// Process line.
Log.i(Constants.TAG, "r: " + line);
}
wr.close();
rd.close();
}
private static IOException newIOException(GDataWrapper<GDataServiceClient> wrapper,
String message) {
return new IOException(String.format("%s: %d: %s", message,
wrapper.getErrorType(), wrapper.getErrorMessage()));
}
}
@@ -1,118 +0,0 @@
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.io.docs;
import com.google.android.apps.mytracks.util.StringUtils;
import com.google.android.apps.mytracks.util.UnitConversions;
import java.text.NumberFormat;
import java.util.Locale;
/**
* <p>This class builds a string of XML tags used to talk to Docs using GData.
*
* <p>Sample Usage:
*
* <code>
* String tags = new DocsTagBuilder(false)
* .append("tagName", "tagValue")
* .appendLargeUnits("bigTagName", 1.0)
* .build();
* </code>
*
* <p>results in:
*
* <code>
* <gsx:tagName><![CDATA[tagValue]]></gsx:tagName>
* <gsx:bigTagName><![CDATA[1.00]]></gsx:bigTagName>
* </code>
*
* @author Matthew Simmons
*/
class DocsTagBuilder {
// Google Docs can only parse numbers in the English locale.
private static final NumberFormat LARGE_UNIT_FORMAT = NumberFormat.getNumberInstance(
Locale.ENGLISH);
private static final NumberFormat SMALL_UNIT_FORMAT = NumberFormat.getIntegerInstance(
Locale.ENGLISH);
static {
LARGE_UNIT_FORMAT.setMaximumFractionDigits(2);
LARGE_UNIT_FORMAT.setMinimumFractionDigits(2);
}
protected final boolean metricUnits;
protected final StringBuilder stringBuilder;
/**
* @param metricUnits True if metric units are to be used. If false,
* imperial units will be used.
*/
DocsTagBuilder(boolean metricUnits) {
this.metricUnits = metricUnits;
this.stringBuilder = new StringBuilder();
}
/** Appends a tag containing a string value */
DocsTagBuilder append(String name, String value) {
appendTag(name, value);
return this;
}
/**
* Appends a tag containing a numeric value. The value will be formatted
* according to the large distance of the specified measurement system (i.e.
* kilometers or miles).
*
* @param name The tag name.
* @param d The value to be formatted, in kilometers.
*/
DocsTagBuilder appendLargeUnits(String name, double d) {
double value = metricUnits ? d : (d * UnitConversions.KM_TO_MI);
appendTag(name, LARGE_UNIT_FORMAT.format(value));
return this;
}
/**
* Appends a tag containing a numeric value. The value will be formatted
* according to the small distance of the specified measurement system (i.e.
* meters or feet).
*
* @param name The tag name.
* @param d The value to be formatted, in meters.
*/
DocsTagBuilder appendSmallUnits(String name, double d) {
double value = metricUnits ? d : (d * UnitConversions.M_TO_FT);
appendTag(name, SMALL_UNIT_FORMAT.format(value));
return this;
}
/** Returns a string containing all tags which have been added. */
String build() {
return stringBuilder.toString();
}
private void appendTag(String name, String value) {
stringBuilder.append("<gsx:")
.append(name)
.append(">")
.append(StringUtils.stringAsCData(value))
.append("</gsx:")
.append(name)
.append(">");
}
}
@@ -0,0 +1,51 @@
/*
* Copyright 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.io.docs;
import com.google.android.apps.mytracks.io.sendtogoogle.AbstractSendActivity;
import com.google.android.apps.mytracks.io.sendtogoogle.AbstractSendAsyncTask;
import com.google.android.apps.mytracks.io.sendtogoogle.SendRequest;
import com.google.android.apps.mytracks.io.sendtogoogle.UploadResultActivity;
import com.google.android.maps.mytracks.R;
import android.content.Intent;
/**
* An activity to send a track to Google Docs.
*
* @author Jimmy Shih
*/
public class SendDocsActivity extends AbstractSendActivity {
@Override
protected AbstractSendAsyncTask createAsyncTask() {
return new SendDocsAsyncTask(this, sendRequest.getTrackId(), sendRequest.getAccount());
}
@Override
protected String getServiceName() {
return getString(R.string.send_google_docs);
}
@Override
protected void startNextActivity(boolean success, boolean isCancel) {
sendRequest.setDocsSuccess(success);
Intent intent = new Intent(this, UploadResultActivity.class)
.putExtra(SendRequest.SEND_REQUEST_KEY, sendRequest);
startActivity(intent);
finish();
}
}
@@ -0,0 +1,314 @@
/*
* Copyright 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.io.docs;
import com.google.android.apps.mytracks.Constants;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.io.gdata.GDataClientFactory;
import com.google.android.apps.mytracks.io.gdata.docs.DocumentsClient;
import com.google.android.apps.mytracks.io.gdata.docs.SpreadsheetsClient;
import com.google.android.apps.mytracks.io.gdata.docs.XmlDocsGDataParserFactory;
import com.google.android.apps.mytracks.io.sendtogoogle.AbstractSendAsyncTask;
import com.google.android.common.gdata.AndroidXmlParserFactory;
import com.google.android.maps.mytracks.R;
import com.google.wireless.gdata.client.GDataClient;
import com.google.wireless.gdata.client.HttpException;
import com.google.wireless.gdata.parser.ParseException;
import com.google.wireless.gdata2.client.AuthenticationException;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.accounts.AuthenticatorException;
import android.accounts.OperationCanceledException;
import android.content.Context;
import android.util.Log;
import java.io.IOException;
/**
* AsyncTask to send a track to Google Docs.
*
* @author Jimmy Shih
*/
public class SendDocsAsyncTask extends AbstractSendAsyncTask {
private static final int PROGRESS_GET_SPREADSHEET_ID = 0;
private static final int PROGRESS_CREATE_SPREADSHEET = 25;
private static final int PROGRESS_GET_WORKSHEET_ID = 50;
private static final int PROGRESS_ADD_TRACK_INFO = 75;
private static final int PROGRESS_COMPLETE = 100;
private static final String TAG = SendDocsAsyncTask.class.getSimpleName();
private final long trackId;
private final Account account;
private final Context context;
private final MyTracksProviderUtils myTracksProviderUtils;
private final GDataClient gDataClient;
private final DocumentsClient documentsClient;
private final SpreadsheetsClient spreadsheetsClient;
// The following variables are for per upload states
private String documentsAuthToken;
private String spreadsheetsAuthToken;
private String spreadsheetId;
private String worksheetId;
public SendDocsAsyncTask(SendDocsActivity activity, long trackId, Account account) {
super(activity);
this.trackId = trackId;
this.account = account;
context = activity.getApplicationContext();
myTracksProviderUtils = MyTracksProviderUtils.Factory.get(context);
gDataClient = GDataClientFactory.getGDataClient(context);
documentsClient = new DocumentsClient(
gDataClient, new XmlDocsGDataParserFactory(new AndroidXmlParserFactory()));
spreadsheetsClient = new SpreadsheetsClient(
gDataClient, new XmlDocsGDataParserFactory(new AndroidXmlParserFactory()));
}
@Override
protected void closeConnection() {
if (gDataClient != null) {
gDataClient.close();
}
}
@Override
protected void saveResult() {
// No action for Google Docs
}
@Override
protected boolean performTask() {
// Reset the per upload states
documentsAuthToken = null;
spreadsheetsAuthToken = null;
spreadsheetId = null;
worksheetId = null;
try {
documentsAuthToken = AccountManager.get(context).blockingGetAuthToken(
account, documentsClient.getServiceName(), false);
spreadsheetsAuthToken = AccountManager.get(context).blockingGetAuthToken(
account, spreadsheetsClient.getServiceName(), false);
} catch (OperationCanceledException e) {
Log.d(TAG, "Unable to get auth token", e);
return retryTask();
} catch (AuthenticatorException e) {
Log.d(TAG, "Unable to get auth token", e);
return retryTask();
} catch (IOException e) {
Log.d(TAG, "Unable to get auth token", e);
return retryTask();
}
Track track = myTracksProviderUtils.getTrack(trackId);
if (track == null) {
Log.d(TAG, "Track is null");
return false;
}
String title = context.getString(R.string.my_tracks_app_name);
if (track.getCategory() != null && !track.getCategory().equals("")) {
title += "-" + track.getCategory();
}
// Get the spreadsheet ID
publishProgress(PROGRESS_GET_SPREADSHEET_ID);
if (!fetchSpreadSheetId(title, false)) {
return retryTask();
}
// Create a new spreadsheet if necessary
publishProgress(PROGRESS_CREATE_SPREADSHEET);
if (spreadsheetId == null) {
if (!createSpreadSheet(title)) {
Log.d(TAG, "Unable to create a new spreadsheet");
return false;
}
// The previous creation might have succeeded even though GData
// reported an error. Seems to be a know bug.
// See http://code.google.com/p/gdata-issues/issues/detail?id=929
// Try to find the created spreadsheet.
if (spreadsheetId == null) {
if (!fetchSpreadSheetId(title, true)) {
Log.d(TAG, "Unable to check if the new spreadsheet is created");
return false;
}
if (spreadsheetId == null) {
Log.d(TAG, "Unable to create a new spreadsheet");
return false;
}
}
}
// Get the worksheet ID
publishProgress(PROGRESS_GET_WORKSHEET_ID);
if (!fetchWorksheetId()) {
return retryTask();
}
if (worksheetId == null) {
Log.d(TAG, "Unable to get a worksheet ID");
return false;
}
// Add the track info
publishProgress(PROGRESS_ADD_TRACK_INFO);
if (!addTrackInfo(track)) {
Log.d(TAG, "Unable to add track info");
return false;
}
publishProgress(PROGRESS_COMPLETE);
return true;
}
@Override
protected void invalidateToken() {
AccountManager.get(context).invalidateAuthToken(Constants.ACCOUNT_TYPE, documentsAuthToken);
AccountManager.get(context).invalidateAuthToken(Constants.ACCOUNT_TYPE, spreadsheetsAuthToken);
}
/**
* Fetches the spreadsheet id. Sets the instance variable
* {@link SendDocsAsyncTask#spreadsheetId}.
*
* @param title the spreadsheet title
* @param waitFirst wait before checking
* @return true if completes.
*/
private boolean fetchSpreadSheetId(String title, boolean waitFirst) {
if (isCancelled()) {
return false;
}
if (waitFirst) {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
Log.d(TAG, "Unable to wait", e);
return false;
}
}
try {
spreadsheetId = SendDocsUtils.getSpreadsheetId(title, documentsClient, documentsAuthToken);
} catch (ParseException e) {
Log.d(TAG, "Unable to fetch spreadsheet ID", e);
return false;
} catch (HttpException e) {
Log.d(TAG, "Unable to fetch spreadsheet ID", e);
return false;
} catch (IOException e) {
Log.d(TAG, "Unable to fetch spreadsheet ID", e);
return false;
}
if (spreadsheetId == null) {
// Waiting a few seconds and trying again. Maybe the server just had a
// hickup (unfortunately that happens quite a lot...).
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
Log.d(TAG, "Unable to wait", e);
return false;
}
try {
spreadsheetId = SendDocsUtils.getSpreadsheetId(title, documentsClient, documentsAuthToken);
} catch (ParseException e) {
Log.d(TAG, "Unable to fetch spreadsheet ID", e);
return false;
} catch (HttpException e) {
Log.d(TAG, "Unable to fetch spreadsheet ID", e);
return false;
} catch (IOException e) {
Log.d(TAG, "Unable to fetch spreadsheet ID", e);
return false;
}
}
return true;
}
/**
* Creates a spreadsheet. If successful, sets the instance variable
* {@link SendDocsAsyncTask#spreadsheetId}.
*
* @param spreadsheetTitle the spreadsheet title
* @return true if completes.
*/
private boolean createSpreadSheet(String spreadsheetTitle) {
if (isCancelled()) {
return false;
}
try {
spreadsheetId = SendDocsUtils.createSpreadsheet(spreadsheetTitle, documentsAuthToken, context);
} catch (IOException e) {
Log.d(TAG, "Unable to create spreadsheet", e);
return false;
}
return true;
}
/**
* Fetches the worksheet ID. Sets the instance variable
* {@link SendDocsAsyncTask#worksheetId}.
*
* @return true if completes.
*/
private boolean fetchWorksheetId() {
if (isCancelled()) {
return false;
}
try {
worksheetId = SendDocsUtils.getWorksheetId(
spreadsheetId, spreadsheetsClient, spreadsheetsAuthToken);
} catch (IOException e) {
Log.d(TAG, "Unable to fetch worksheet ID", e);
return false;
} catch (AuthenticationException e) {
Log.d(TAG, "Unable to fetch worksheet ID", e);
return false;
} catch (ParseException e) {
Log.d(TAG, "Unable to fetch worksheet ID", e);
return false;
}
return true;
}
/**
* Adds track info to a worksheet.
*
* @param track the track
* @return true if completes.
*/
private boolean addTrackInfo(Track track) {
if (isCancelled()) {
return false;
}
try {
SendDocsUtils.addTrackInfo(track, spreadsheetId, worksheetId, spreadsheetsAuthToken, context);
} catch (IOException e) {
Log.d(TAG, "Unable to add track info", e);
return false;
}
return true;
}
}
@@ -0,0 +1,410 @@
/*
* Copyright 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.io.docs;
import com.google.android.apps.mytracks.Constants;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.io.gdata.docs.DocumentsClient;
import com.google.android.apps.mytracks.io.gdata.docs.SpreadsheetsClient;
import com.google.android.apps.mytracks.io.gdata.docs.SpreadsheetsClient.WorksheetEntry;
import com.google.android.apps.mytracks.stats.TripStatistics;
import com.google.android.apps.mytracks.util.ResourceUtils;
import com.google.android.apps.mytracks.util.StringUtils;
import com.google.android.apps.mytracks.util.UnitConversions;
import com.google.android.maps.mytracks.R;
import com.google.common.annotations.VisibleForTesting;
import com.google.wireless.gdata.client.HttpException;
import com.google.wireless.gdata.data.Entry;
import com.google.wireless.gdata.parser.GDataParser;
import com.google.wireless.gdata.parser.ParseException;
import com.google.wireless.gdata2.client.AuthenticationException;
import android.content.Context;
import android.content.SharedPreferences;
import android.util.Log;
import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLEncoder;
import java.text.NumberFormat;
import java.util.Locale;
/**
* Utilities for sending a track to Google Docs.
*
* @author Sandor Dornbush
* @author Matthew Simmons
*/
public class SendDocsUtils {
private static final String GET_SPREADSHEET_BY_TITLE_URI =
"https://docs.google.com/feeds/documents/private/full?"
+ "category=mine,spreadsheet&title=%s&title-exact=true";
private static final String CREATE_SPREADSHEET_URI =
"https://docs.google.com/feeds/documents/private/full";
private static final String GET_WORKSHEETS_URI =
"https://spreadsheets.google.com/feeds/worksheets/%s/private/full";
private static final String GET_WORKSHEET_URI =
"https://spreadsheets.google.com/feeds/list/%s/%s/private/full";
private static final String SPREADSHEET_ID_PREFIX =
"https://docs.google.com/feeds/documents/private/full/spreadsheet%3A";
private static final String CONTENT_TYPE = "Content-Type";
private static final String ATOM_FEED_MIME_TYPE = "application/atom+xml";
private static final String OPENDOCUMENT_SPREADSHEET_MIME_TYPE =
"application/x-vnd.oasis.opendocument.spreadsheet";
private static final String AUTHORIZATION = "Authorization";
private static final String AUTHORIZATION_PREFIX = "GoogleLogin auth=";
private static final String SLUG = "Slug";
// Google Docs can only parse numbers in the English locale.
private static final NumberFormat NUMBER_FORMAT = NumberFormat.getNumberInstance(Locale.ENGLISH);
private static final NumberFormat INTEGER_FORMAT = NumberFormat.getIntegerInstance(
Locale.ENGLISH);
static {
NUMBER_FORMAT.setMaximumFractionDigits(2);
NUMBER_FORMAT.setMinimumFractionDigits(2);
}
private static final String TAG = SendDocsUtils.class.getSimpleName();
private SendDocsUtils() {}
/**
* Gets the spreadsheet id of a spreadsheet. Returns null if the spreadsheet
* doesn't exist.
*
* @param title the title of the spreadsheet
* @param documentsClient the documents client
* @param authToken the auth token
* @return spreadsheet id or null if it doesn't exist.
*/
public static String getSpreadsheetId(
String title, DocumentsClient documentsClient, String authToken)
throws IOException, ParseException, HttpException {
GDataParser gDataParser = null;
try {
String uri = String.format(GET_SPREADSHEET_BY_TITLE_URI, URLEncoder.encode(title));
gDataParser = documentsClient.getParserForFeed(Entry.class, uri, authToken);
gDataParser.init();
while (gDataParser.hasMoreData()) {
Entry entry = gDataParser.readNextEntry(null);
String entryTitle = entry.getTitle();
if (entryTitle.equals(title)) {
return getEntryId(entry);
}
}
return null;
} finally {
if (gDataParser != null) {
gDataParser.close();
}
}
}
/**
* Gets the id from an entry. Returns null if not available.
*
* @param entry the entry
*/
@VisibleForTesting
static String getEntryId(Entry entry) {
String entryId = entry.getId();
if (entryId.startsWith(SPREADSHEET_ID_PREFIX)) {
return entryId.substring(SPREADSHEET_ID_PREFIX.length());
}
return null;
}
/**
* Creates a new spreadsheet with the given title. Returns the spreadsheet ID
* if successful. Returns null otherwise. Note that it is possible that a new
* spreadsheet is created, but the returned ID is null.
*
* @param title the title
* @param authToken the auth token
* @param context the context
*/
public static String createSpreadsheet(String title, String authToken, Context context)
throws IOException {
URL url = new URL(CREATE_SPREADSHEET_URI);
URLConnection conn = url.openConnection();
conn.addRequestProperty(CONTENT_TYPE, OPENDOCUMENT_SPREADSHEET_MIME_TYPE);
conn.addRequestProperty(SLUG, title);
conn.addRequestProperty(AUTHORIZATION, AUTHORIZATION_PREFIX + authToken);
conn.setDoOutput(true);
OutputStream outputStream = conn.getOutputStream();
ResourceUtils.readBinaryFileToOutputStream(
context, R.raw.mytracks_empty_spreadsheet, outputStream);
// Get the response
BufferedReader bufferedReader = null;
StringBuilder resultBuilder = new StringBuilder();
try {
bufferedReader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
String line;
while ((line = bufferedReader.readLine()) != null) {
resultBuilder.append(line);
}
} catch (FileNotFoundException e) {
// The GData API sometimes throws an error, even though creation of
// the document succeeded. In that case let's just return. The caller
// then needs to check if the doc actually exists.
Log.d(TAG, "Unable to read result after creating a spreadsheet", e);
return null;
} finally {
outputStream.close();
if (bufferedReader != null) {
bufferedReader.close();
}
}
return getNewSpreadsheetId(resultBuilder.toString());
}
/**
* Gets the spreadsheet id from a create spreadsheet result.
*
* @param result the create spreadsheet result
*/
@VisibleForTesting
static String getNewSpreadsheetId(String result) {
int idTagIndex = result.indexOf("<id>");
if (idTagIndex == -1) {
return null;
}
int idTagCloseIndex = result.indexOf("</id>", idTagIndex);
if (idTagCloseIndex == -1) {
return null;
}
int idStringStart = result.indexOf(SPREADSHEET_ID_PREFIX, idTagIndex);
if (idStringStart == -1) {
return null;
}
return result.substring(idStringStart + SPREADSHEET_ID_PREFIX.length(), idTagCloseIndex);
}
/**
* Gets the first worksheet ID of a spreadsheet. Returns null if not
* available.
*
* @param spreadsheetId the spreadsheet ID
* @param spreadsheetClient the spreadsheet client
* @param authToken the auth token
*/
public static String getWorksheetId(
String spreadsheetId, SpreadsheetsClient spreadsheetClient, String authToken)
throws IOException, AuthenticationException, ParseException {
GDataParser gDataParser = null;
try {
String uri = String.format(GET_WORKSHEETS_URI, spreadsheetId);
gDataParser = spreadsheetClient.getParserForWorksheetsFeed(uri, authToken);
gDataParser.init();
if (!gDataParser.hasMoreData()) {
Log.d(TAG, "No worksheet");
return null;
}
// Get the first worksheet
WorksheetEntry worksheetEntry =
(WorksheetEntry) gDataParser.readNextEntry(new WorksheetEntry());
return getWorksheetEntryId(worksheetEntry);
} finally {
if (gDataParser != null) {
gDataParser.close();
}
}
}
/**
* Gets the worksheet id from a worksheet entry. Returns null if not available.
*
* @param entry the worksheet entry
*/
@VisibleForTesting
static String getWorksheetEntryId(WorksheetEntry entry) {
String id = entry.getId();
int lastSlash = id.lastIndexOf('/');
if (lastSlash == -1) {
Log.d(TAG, "No id");
return null;
}
return id.substring(lastSlash + 1);
}
/**
* Adds a track's info as a row in a worksheet.
*
* @param track the track
* @param spreadsheetId the spreadsheet ID
* @param worksheetId the worksheet ID
* @param authToken the auth token
* @param context the context
*/
public static void addTrackInfo(
Track track, String spreadsheetId, String worksheetId, String authToken, Context context)
throws IOException {
String worksheetUri = String.format(GET_WORKSHEET_URI, spreadsheetId, worksheetId);
SharedPreferences prefs = context.getSharedPreferences(
Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
boolean metricUnits = prefs.getBoolean(context.getString(R.string.metric_units_key), true);
addRow(worksheetUri, getRowContent(track, metricUnits, context), authToken);
}
/**
* Gets the row content containing the track's info.
*
* @param track the track
* @param metricUnits true to use metric
* @param context the context
*/
@VisibleForTesting
static String getRowContent(Track track, boolean metricUnits, Context context) {
TripStatistics stats = track.getStatistics();
String distanceUnit = context.getString(
metricUnits ? R.string.unit_kilometer : R.string.unit_mile);
String speedUnit = context.getString(
metricUnits ? R.string.unit_kilometer_per_hour : R.string.unit_mile_per_hour);
String elevationUnit = context.getString(
metricUnits ? R.string.unit_meter : R.string.unit_feet);
StringBuilder builder = new StringBuilder().append("<entry xmlns='http://www.w3.org/2005/Atom' "
+ "xmlns:gsx='http://schemas.google.com/spreadsheets/2006/extended'>");
appendTag(builder, "name", track.getName());
appendTag(builder, "description", track.getDescription());
appendTag(builder, "date", StringUtils.formatDateTime(context, stats.getStartTime()));
appendTag(builder, "totaltime", StringUtils.formatElapsedTimeWithHour(stats.getTotalTime()));
appendTag(builder, "movingtime", StringUtils.formatElapsedTimeWithHour(stats.getMovingTime()));
appendTag(builder, "distance", getDistance(stats.getTotalDistance(), metricUnits));
appendTag(builder, "distanceunit", distanceUnit);
appendTag(builder, "averagespeed", getSpeed(stats.getAverageSpeed(), metricUnits));
appendTag(builder, "averagemovingspeed", getSpeed(stats.getAverageMovingSpeed(), metricUnits));
appendTag(builder, "maxspeed", getSpeed(stats.getMaxSpeed(), metricUnits));
appendTag(builder, "speedunit", speedUnit);
appendTag(builder, "elevationgain", getElevation(stats.getTotalElevationGain(), metricUnits));
appendTag(builder, "minelevation", getElevation(stats.getMinElevation(), metricUnits));
appendTag(builder, "maxelevation", getElevation(stats.getMaxElevation(), metricUnits));
appendTag(builder, "elevationunit", elevationUnit);
if (track.getMapId().length() > 0) {
appendTag(builder, "map",
String.format("%s?msa=0&msid=%s", Constants.MAPSHOP_BASE_URL, track.getMapId()));
}
builder.append("</entry>");
return builder.toString();
}
/**
* Appends a name-value pair as a gsx tag to a string builder.
*
* @param stringBuilder the string builder
* @param name the name
* @param value the value
*/
@VisibleForTesting
static void appendTag(StringBuilder stringBuilder, String name, String value) {
stringBuilder
.append("<gsx:")
.append(name)
.append(">")
.append(StringUtils.formatCData(value))
.append("</gsx:")
.append(name)
.append(">");
}
/**
* Gets the distance. Performs unit conversion and formatting.
*
* @param distanceInMeter the distance in meters
* @param metricUnits true to use metric
*/
@VisibleForTesting
static final String getDistance(double distanceInMeter, boolean metricUnits) {
double distanceInKilometer = distanceInMeter * 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
*/
@VisibleForTesting
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
*/
@VisibleForTesting
static final String getElevation(double elevationInMeter, boolean metricUnits) {
double elevation = metricUnits ? elevationInMeter : elevationInMeter * UnitConversions.M_TO_FT;
return INTEGER_FORMAT.format(elevation);
}
/**
* Adds a row to a Google Spreadsheet worksheet.
*
* @param worksheetUri the worksheet URI
* @param rowContent the row content
* @param authToken the auth token
*/
private static final void addRow(String worksheetUri, String rowContent, String authToken)
throws IOException {
URL url = new URL(worksheetUri);
URLConnection conn = url.openConnection();
conn.addRequestProperty(CONTENT_TYPE, ATOM_FEED_MIME_TYPE);
conn.addRequestProperty(AUTHORIZATION, AUTHORIZATION_PREFIX + authToken);
conn.setDoOutput(true);
OutputStreamWriter writer = new OutputStreamWriter(conn.getOutputStream());
writer.write(rowContent);
writer.flush();
// Get the response
BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
while ((reader.readLine()) != null) {
// Just read till the end
}
writer.close();
reader.close();
}
}
@@ -15,31 +15,37 @@
*/
package com.google.android.apps.mytracks.io.file;
import com.google.android.apps.mytracks.content.MyTracksLocation;
import com.google.android.apps.mytracks.content.Sensor;
import com.google.android.apps.mytracks.content.Sensor.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.TrackWriterFactory.TrackFileFormat;
import com.google.android.apps.mytracks.util.FileUtils;
import com.google.android.apps.mytracks.util.StringUtils;
import com.google.android.maps.mytracks.R;
import android.content.Context;
import android.location.Location;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.text.NumberFormat;
import java.util.Date;
/**
* Exports a track as a CSV file, according to RFC 4180.
*
* The first field is a type:
* TRACK - track description
* P - point
* WAYPOINT - waypoint
*
* For each type, the fields are:
*
* TRACK,,,,,,,,name,description,
* P,time,lat,lon,alt,bearing,accurancy,speed,,,segmentIdx
* WAYPOINT,time,lat,lon,alt,bearing,accuracy,speed,name,description,
* 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
*/
@@ -51,111 +57,41 @@ public class CsvTrackWriter implements TrackFormatWriter {
SHORT_FORMAT.setMaximumFractionDigits(4);
}
private int segmentIdx = 0;
private int numFields = -1;
private PrintWriter pw;
private final Context context;
private PrintWriter printWriter;
private Track track;
private int segmentIndex;
private int pointIndex;
public CsvTrackWriter(Context context) {
this.context = context;
}
@Override
public String getExtension() {
return TrackFileFormat.CSV.getExtension();
}
@SuppressWarnings("hiding")
@Override
public void prepare(Track track, OutputStream out) {
this.track = track;
this.pw = new PrintWriter(out);
public void prepare(Track aTrack, OutputStream out) {
track = aTrack;
printWriter = new PrintWriter(out);
segmentIndex = 0;
pointIndex = 0;
}
@Override
public void close() {
printWriter.close();
}
@Override
public void writeHeader() {
writeCommaSeparatedLine("TYPE", "TIME", "LAT", "LON", "ALT", "BEARING",
"ACCURACY", "SPEED", "NAME", "DESCRIPTION", "SEGMENT");
}
@Override
public void writeBeginTrack(Location firstPoint) {
writeCommaSeparatedLine("TRACK",
null, null, null, null, null, null, null,
track.getName(), track.getDescription(),
null);
}
@Override
public void writeOpenSegment() {
// Do nothing
}
@Override
public void writeLocation(Location location) {
String timeStr = FileUtils.FILE_TIMESTAMP_FORMAT.format(new Date(location.getTime()));
writeCommaSeparatedLine("P",
timeStr,
Double.toString(location.getLatitude()),
Double.toString(location.getLongitude()),
Double.toString(location.getAltitude()),
Double.toString(location.getBearing()),
SHORT_FORMAT.format(location.getAccuracy()),
SHORT_FORMAT.format(location.getSpeed()),
null, null,
Integer.toString(segmentIdx));
}
@Override
public void writeWaypoint(Waypoint waypoint) {
Location location = waypoint.getLocation();
String timeStr = FileUtils.FILE_TIMESTAMP_FORMAT.format(new Date(location.getTime()));
writeCommaSeparatedLine("WAYPOINT",
timeStr,
Double.toString(location.getLatitude()),
Double.toString(location.getLongitude()),
Double.toString(location.getAltitude()),
Double.toString(location.getBearing()),
SHORT_FORMAT.format(location.getAccuracy()),
SHORT_FORMAT.format(location.getSpeed()),
waypoint.getName(),
waypoint.getDescription(),
null);
}
/**
* Writes a single line of a comma-separated-value file.
*
* @param strs the values to be written as comma-separated
*/
private void writeCommaSeparatedLine(String... strs) {
if (numFields == -1) {
numFields = strs.length;
} else if (strs.length != numFields) {
throw new IllegalArgumentException(
"CSV lines with different number of fields");
}
boolean isFirst = true;
for (String str : strs) {
if (!isFirst) {
pw.print(',');
}
isFirst = false;
if (str != null) {
pw.print('"');
pw.print(str.replaceAll("\"", "\"\""));
pw.print('"');
}
}
pw.println();
}
@Override
public void writeCloseSegment() {
segmentIdx++;
}
@Override
public void writeEndTrack(Location lastPoint) {
// Do nothing
writeCommaSeparatedLine(context.getString(R.string.track_detail_track_name),
context.getString(R.string.track_detail_activity_type_hint),
context.getString(R.string.track_detail_track_description));
writeCommaSeparatedLine(track.getName(), track.getCategory(), track.getDescription());
writeCommaSeparatedLine();
}
@Override
@@ -164,7 +100,144 @@ public class CsvTrackWriter implements TrackFormatWriter {
}
@Override
public void close() {
pw.close();
public void writeBeginWaypoints() {
writeCommaSeparatedLine(context.getString(R.string.marker_detail_marker_name),
context.getString(R.string.marker_detail_marker_type_hint),
context.getString(R.string.marker_detail_marker_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_location_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()),
Double.toString(location.getAltitude()),
Double.toString(location.getBearing()),
SHORT_FORMAT.format(location.getAccuracy()),
SHORT_FORMAT.format(location.getSpeed()),
StringUtils.formatDateTimeIso8601(location.getTime()));
}
@Override
public void writeBeginTrack(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_location_time),
context.getString(R.string.description_sensor_power),
context.getString(R.string.description_sensor_cadence),
context.getString(R.string.description_sensor_heart_rate),
context.getString(R.string.description_sensor_battery_level));
}
@Override
public void writeEndTrack(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;
String batteryLevel = 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());
}
}
if (sensorDataSet.hasBatteryLevel()) {
SensorData sensorData = sensorDataSet.getBatteryLevel();
if (sensorData.hasValue() && sensorData.getState() == Sensor.SensorState.SENDING) {
batteryLevel = Double.toString(sensorData.getValue());
}
}
}
}
pointIndex++;
writeCommaSeparatedLine(Integer.toString(segmentIndex),
Integer.toString(pointIndex),
Double.toString(location.getLatitude()),
Double.toString(location.getLongitude()),
Double.toString(location.getAltitude()),
Double.toString(location.getBearing()),
SHORT_FORMAT.format(location.getAccuracy()),
SHORT_FORMAT.format(location.getSpeed()),
StringUtils.formatDateTimeIso8601(location.getTime()),
power,
cadence,
heartRate,
batteryLevel);
}
/**
* 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;
if (value != null) {
builder.append('"');
builder.append(value.replaceAll("\"", "\"\""));
builder.append('"');
}
}
printWriter.println(builder.toString());
}
}
@@ -450,7 +450,7 @@ public class GpxImporter extends DefaultHandler {
// Parse the time
long time;
try {
time = StringUtils.parseXmlDateTime(content.trim());
time = StringUtils.getTime(content.trim());
} catch (IllegalArgumentException e) {
String msg = createErrorMessage("Unable to parse time: " + content);
throw new SAXException(msg, e);
@@ -18,53 +18,43 @@ package com.google.android.apps.mytracks.io.file;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.io.file.TrackWriterFactory.TrackFileFormat;
import com.google.android.apps.mytracks.util.FileUtils;
import com.google.android.apps.mytracks.util.StringUtils;
import com.google.android.maps.mytracks.R;
import android.content.Context;
import android.location.Location;
import android.os.Build;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.nio.charset.Charset;
import java.text.NumberFormat;
import java.util.Date;
import java.util.Locale;
/**
* Log of one track.
* Write track as GPX to a file.
*
* @author Sandor Dornbush
*/
public class GpxTrackWriter implements TrackFormatWriter {
private final NumberFormat elevationFormatter;
private final NumberFormat coordinateFormatter;
private PrintWriter pw = null;
private Track track;
public GpxTrackWriter() {
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.
elevationFormatter = NumberFormat.getInstance(Locale.US);
elevationFormatter.setMaximumFractionDigits(1);
elevationFormatter.setGroupingUsed(false);
ELEVATION_FORMAT.setMaximumFractionDigits(1);
ELEVATION_FORMAT.setGroupingUsed(false);
coordinateFormatter = NumberFormat.getInstance(Locale.US);
coordinateFormatter.setMaximumFractionDigits(5);
coordinateFormatter.setMaximumIntegerDigits(3);
coordinateFormatter.setGroupingUsed(false);
COORDINATE_FORMAT.setMaximumFractionDigits(5);
COORDINATE_FORMAT.setMaximumIntegerDigits(3);
COORDINATE_FORMAT.setGroupingUsed(false);
}
private String formatLocation(Location l) {
return "lat=\"" + coordinateFormatter.format(l.getLatitude())
+ "\" lon=\"" + coordinateFormatter.format(l.getLongitude()) + "\"";
}
private final Context context;
private Track track;
private PrintWriter printWriter;
@SuppressWarnings("hiding")
@Override
public void prepare(Track track, OutputStream out) {
this.track = track;
this.pw = new PrintWriter(out);
public GpxTrackWriter(Context context) {
this.context = context;
}
@Override
@@ -72,98 +62,120 @@ public class GpxTrackWriter implements TrackFormatWriter {
return TrackFileFormat.GPX.getExtension();
}
@Override
public void prepare(Track aTrack, OutputStream outputStream) {
this.track = aTrack;
this.printWriter = new PrintWriter(outputStream);
}
@Override
public void close() {
if (printWriter != null) {
printWriter.close();
printWriter = null;
}
}
@Override
public void writeHeader() {
if (pw != null) {
pw.format("<?xml version=\"1.0\" encoding=\"%s\" standalone=\"yes\"?>\n",
Charset.defaultCharset().name());
pw.println("<?xml-stylesheet type=\"text/xsl\" href=\"details.xsl\"?>");
pw.println("<gpx");
pw.println(" version=\"1.1\"");
pw.format(" creator=\"My Tracks running on %s\"\n", Build.MODEL);
pw.println(" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"");
pw.println(" xmlns=\"http://www.topografix.com/GPX/1/1\"");
pw.print(" xmlns:topografix=\"http://www.topografix.com/GPX/Private/"
+ "TopoGrafix/0/1\"");
pw.print(" xsi:schemaLocation=\"http://www.topografix.com/GPX/1/1 ");
pw.print("http://www.topografix.com/GPX/1/1/gpx.xsd ");
pw.print("http://www.topografix.com/GPX/Private/TopoGrafix/0/1 ");
pw.println("http://www.topografix.com/GPX/Private/TopoGrafix/0/1/"
+ "topografix.xsd\">");
// TODO: Author etc.
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 (pw != null) {
pw.println("</gpx>");
if (printWriter != null) {
printWriter.println("</gpx>");
}
}
@Override
public void writeBeginTrack(Location firstPoint) {
if (pw != null) {
pw.println("<trk>");
pw.println("<name>" + StringUtils.stringAsCData(track.getName())
+ "</name>");
pw.println("<desc>" + StringUtils.stringAsCData(track.getDescription())
+ "</desc>");
pw.println("<number>" + track.getId() + "</number>");
pw.println("<extensions><topografix:color>c0c0c0</topografix:color></extensions>");
public void writeBeginTrack(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("<extensions><topografix:color>c0c0c0</topografix:color></extensions>");
}
}
@Override
public void writeEndTrack(Location lastPoint) {
if (pw != null) {
pw.println("</trk>");
public void writeEndTrack(Location lastLocation) {
if (printWriter != null) {
printWriter.println("</trk>");
}
}
@Override
public void writeOpenSegment() {
pw.println("<trkseg>");
printWriter.println("<trkseg>");
}
@Override
public void writeCloseSegment() {
pw.println("</trkseg>");
printWriter.println("</trkseg>");
}
@Override
public void writeLocation(Location l) {
if (pw != null) {
pw.println("<trkpt " + formatLocation(l) + ">");
Date d = new Date(l.getTime());
pw.println("<ele>" + elevationFormatter.format(l.getAltitude()) + "</ele>");
pw.println("<time>" + FileUtils.FILE_TIMESTAMP_FORMAT.format(d) + "</time>");
pw.println("</trkpt>");
public void writeLocation(Location location) {
if (printWriter != null) {
printWriter.println("<trkpt " + formatLocation(location) + ">");
printWriter.println("<ele>" + ELEVATION_FORMAT.format(location.getAltitude()) + "</ele>");
printWriter.println("<time>" + StringUtils.formatDateTimeIso8601(location.getTime()) + "</time>");
printWriter.println("</trkpt>");
}
}
@Override
public void close() {
if (pw != null) {
pw.close();
pw = null;
}
public void writeBeginWaypoints() {
// Do nothing
}
@Override
public void writeEndWaypoints() {
// Do nothing
}
@Override
public void writeWaypoint(Waypoint waypoint) {
if (pw != null) {
Location l = waypoint.getLocation();
if (l != null) {
pw.println("<wpt " + formatLocation(l) + ">");
pw.println("<ele>" + elevationFormatter.format(l.getAltitude()) + "</ele>");
pw.println("<time>" + FileUtils.FILE_TIMESTAMP_FORMAT.format(l.getTime()) + "</time>");
pw.println("<name>" + StringUtils.stringAsCData(waypoint.getName())
+ "</name>");
pw.println("<desc>"
+ StringUtils.stringAsCData(waypoint.getDescription()) + "</desc>");
pw.println("</wpt>");
if (printWriter != null) {
Location location = waypoint.getLocation();
if (location != null) {
printWriter.println("<wpt " + formatLocation(location) + ">");
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(
"<desc>" + StringUtils.formatCData(waypoint.getDescription()) + "</desc>");
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()) + "\"";
}
}
@@ -15,18 +15,25 @@
*/
package com.google.android.apps.mytracks.io.file;
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.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.TrackWriterFactory.TrackFileFormat;
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.location.Location;
import android.os.Build;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.util.Vector;
import java.util.ArrayList;
/**
* Write track as KML to a file.
@@ -35,28 +42,43 @@ import java.util.Vector;
*/
public class KmlTrackWriter implements TrackFormatWriter {
private final Vector<Double> distances = new Vector<Double>();
private final Vector<Double> elevations = new Vector<Double>();
private final StringUtils stringUtils;
private PrintWriter pw = null;
/**
* ID of the KML feature to play a tour.
*/
public static final String TOUR_FEATURE_ID = "tour";
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 BATTER_LEVEL = "battery_level";
private final Context context;
private final DescriptionGenerator descriptionGenerator;
private Track track;
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 ArrayList<Integer> batteryLevelList = new ArrayList<Integer>();
private boolean hasPower;
private boolean hasCadence;
private boolean hasHeartRate;
private boolean hasBatteryLevel;
public KmlTrackWriter(Context context) {
stringUtils = new StringUtils(context);
this(context, new DescriptionGeneratorImpl(context));
}
/**
* Testing constructor.
*/
KmlTrackWriter(StringUtils stringUtils) {
this.stringUtils = stringUtils;
}
@SuppressWarnings("hiding")
@Override
public void prepare(Track track, OutputStream out) {
this.track = track;
this.pw = new PrintWriter(out);
@VisibleForTesting
KmlTrackWriter(Context context, DescriptionGenerator descriptionGenerator) {
this.context = context;
this.descriptionGenerator = descriptionGenerator;
}
@Override
@@ -65,171 +87,279 @@ public class KmlTrackWriter implements TrackFormatWriter {
}
@Override
public void writeHeader() {
if (pw != null) {
pw.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
pw.print("<kml");
pw.print(" xmlns=\"http://earth.google.com/kml/2.0\"");
pw.println(" xmlns:atom=\"http://www.w3.org/2005/Atom\">");
pw.println("<Document>");
pw.format("<atom:author><atom:name>My Tracks running on %s"
+ "</atom:name></atom:author>\n", Build.MODEL);
public void prepare(Track aTrack, OutputStream outputStream) {
this.track = aTrack;
this.printWriter = new PrintWriter(outputStream);
}
pw.println("<name>" + StringUtils.stringAsCData(track.getName())
+ "</name>");
pw.println("<description>"
+ StringUtils.stringAsCData(track.getDescription())
+ "</description>");
writeStyles();
@Override
public void close() {
if (printWriter != null) {
printWriter.close();
printWriter = null;
}
}
@Override
public void writeHeader() {
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:atom=\"http://www.w3.org/2005/Atom\"");
printWriter.println("xmlns:gx=\"http://www.google.com/kml/ext/2.2\">");
printWriter.println("<Document>");
printWriter.println("<open>1</open>");
printWriter.println("<visibility>1</visibility>");
printWriter.println(
"<description>" + StringUtils.formatCData(track.getDescription()) + "</description>");
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, "http://maps.google.com/mapfiles/kml/paddle/grn-circle.png", 32, 1);
writePlacemarkerStyle(
END_STYLE, "http://maps.google.com/mapfiles/kml/paddle/red-circle.png", 32, 1);
writePlacemarkerStyle(
STATISTICS_STYLE, "http://maps.google.com/mapfiles/kml/pushpin/ylw-pushpin.png", 20, 2);
writePlacemarkerStyle(
WAYPOINT_STYLE, "http://maps.google.com/mapfiles/kml/pushpin/blue-pushpin.png", 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));
writeSensorStyle(BATTER_LEVEL, context.getString(R.string.description_sensor_battery_level));
printWriter.println("</Schema>");
}
}
@Override
public void writeFooter() {
if (pw != null) {
pw.println("</Document>");
pw.println("</kml>");
if (printWriter != null) {
printWriter.println("</Document>");
printWriter.println("</kml>");
}
}
@Override
public void writeBeginTrack(Location firstPoint) {
if (pw != null) {
writePlacemark("(Start)", track.getDescription(), "#sh_green-circle",
firstPoint);
pw.println("<Placemark>");
pw.println("<name>" + StringUtils.stringAsCData(track.getName())
+ "</name>");
pw.println("<description>"
+ StringUtils.stringAsCData(track.getDescription())
+ "</description>");
pw.println("<styleUrl>#track</styleUrl>");
pw.println("<MultiGeometry>");
public void writeBeginWaypoints() {
if (printWriter != null) {
printWriter.println(
"<Folder><name>" + StringUtils.formatCData(context.getString(R.string.menu_markers))
+ "</name>");
}
}
@Override
public void writeEndTrack(Location lastPoint) {
if (pw != null) {
pw.println("</MultiGeometry>");
pw.println("</Placemark>");
String description = stringUtils.generateTrackDescription(
track, distances, elevations);
writePlacemark("(End)", description, "#sh_red-circle", lastPoint);
public void writeEndWaypoints() {
if (printWriter != null) {
printWriter.println("</Folder>");
}
}
@Override
public void writeWaypoint(Waypoint waypoint) {
if (printWriter != null) {
String styleName = waypoint.getType() == Waypoint.TYPE_STATISTICS ? STATISTICS_STYLE
: WAYPOINT_STYLE;
writePlacemark(
waypoint.getName(), waypoint.getDescription(), styleName, waypoint.getLocation());
}
}
@Override
public void writeBeginTrack(Location firstLocation) {
if (printWriter != null) {
String name = context.getString(R.string.marker_label_start, track.getName());
writePlacemark(name, track.getDescription(), START_STYLE, firstLocation);
printWriter.println("<Placemark id=\"" + TOUR_FEATURE_ID + "\">");
printWriter.println(
"<description>" + StringUtils.formatCData(track.getDescription()) + "</description>");
printWriter.println("<name>" + StringUtils.formatCData(track.getName()) + "</name>");
printWriter.println("<styleUrl>#" + TRACK_STYLE + "</styleUrl>");
printWriter.println("<gx:MultiTrack>");
printWriter.println("<altitudeMode>absolute</altitudeMode>");
printWriter.println("<gx:interpolate>1</gx:interpolate>");
}
}
@Override
public void writeEndTrack(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);
writePlacemark(name, description, END_STYLE, lastLocation);
}
}
@Override
public void writeOpenSegment() {
if (pw != null) {
pw.print("<LineString><coordinates>");
if (printWriter != null) {
printWriter.println("<gx:Track>");
hasPower = false;
hasCadence = false;
hasHeartRate = false;
hasBatteryLevel = false;
powerList.clear();
cadenceList.clear();
heartRateList.clear();
batteryLevelList.clear();
}
}
@Override
public void writeCloseSegment() {
if (pw != null) {
pw.println("</coordinates></LineString>");
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);
}
if (hasBatteryLevel) {
writeSensorData(batteryLevelList, BATTER_LEVEL);
}
printWriter.println("</SchemaData>");
printWriter.println("</ExtendedData>");
printWriter.println("</gx:Track>");
}
}
@Override
public void writeLocation(Location l) {
if (pw != null) {
pw.print(l.getLongitude() + "," + l.getLatitude() + ","
+ l.getAltitude() + " ");
public void writeLocation(Location location) {
if (printWriter != null) {
printWriter.println("<when>" + StringUtils.formatDateTimeIso8601(location.getTime()) + "</when>");
printWriter.println(
"<gx:coord>" + location.getLongitude() + " " + location.getLatitude() + " "
+ location.getAltitude() + "</gx:coord>");
if (location instanceof MyTracksLocation) {
SensorDataSet sensorDataSet = ((MyTracksLocation) location).getSensorDataSet();
int power = -1;
int cadence = -1;
int heartRate = -1;
int batteryLevel = -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();
}
}
if (sensorDataSet.hasBatteryLevel()) {
SensorData sensorData = sensorDataSet.getBatteryLevel();
if (sensorData.hasValue() && sensorData.getState() == Sensor.SensorState.SENDING) {
hasBatteryLevel = true;
batteryLevel = sensorData.getValue();
}
}
}
powerList.add(power);
cadenceList.add(cadence);
heartRateList.add(heartRate);
batteryLevelList.add(batteryLevel);
}
}
}
private String getPinStyle(Waypoint waypoint) {
if (waypoint.getType() == Waypoint.TYPE_STATISTICS) {
return "#sh_ylw-pushpin";
/**
* 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>");
}
// Try to find the icon color.
// The string should be of the form:
// "http://maps.google.com/mapfiles/ms/micons/XXX.png"
int slash = waypoint.getIcon().lastIndexOf('/');
int png = waypoint.getIcon().lastIndexOf('.');
if ((slash != -1) && (slash < png)) {
String color = waypoint.getIcon().substring(slash + 1, png);
return "#sh_" + color + "-pushpin";
}
return "#sh_blue-pushpin";
printWriter.println("</gx:SimpleArrayData>");
}
@Override
public void writeWaypoint(Waypoint waypoint) {
if (pw != null) {
writePlacemark(
waypoint.getName(),
waypoint.getDescription(),
getPinStyle(waypoint),
waypoint.getLocation());
}
}
@Override
public void close() {
if (pw != null) {
pw.close();
pw = null;
}
}
private void writeStyles() {
pw.println("<Style id=\"track\"><LineStyle><color>7f0000ff</color>"
+ "<width>4</width></LineStyle></Style>");
pw.print("<Style id=\"sh_green-circle\"><IconStyle><scale>1.3</scale>");
pw.print("<Icon><href>http://maps.google.com/mapfiles/kml/paddle/"
+ "grn-circle.png</href></Icon>");
pw.println("<hotSpot x=\"32\" y=\"1\" xunits=\"pixels\" yunits=\"pixels\"/>"
+ "</IconStyle></Style>");
pw.print("<Style id=\"sh_red-circle\"><IconStyle><scale>1.3</scale>");
pw.print("<Icon><href>http://maps.google.com/mapfiles/kml/paddle/"
+ "red-circle.png</href></Icon>");
pw.println("<hotSpot x=\"32\" y=\"1\" xunits=\"pixels\" yunits=\"pixels\"/>"
+ "</IconStyle></Style>");
pw.print("<Style id=\"sh_ylw-pushpin\"><IconStyle><scale>1.3</scale>");
pw.print("<Icon><href>http://maps.google.com/mapfiles/kml/pushpin/"
+ "ylw-pushpin.png</href></Icon>");
pw.println("<hotSpot x=\"20\" y=\"2\" xunits=\"pixels\" yunits=\"pixels\"/>"
+ "</IconStyle></Style>");
pw.print("<Style id=\"sh_blue-pushpin\"><IconStyle><scale>1.3</scale>");
pw.print("<Icon><href>http://maps.google.com/mapfiles/kml/pushpin/"
+ "blue-pushpin.png</href></Icon>");
pw.println("<hotSpot x=\"20\" y=\"2\" xunits=\"pixels\" yunits=\"pixels\"/>"
+ "</IconStyle></Style>");
pw.print("<Style id=\"sh_green-pushpin\"><IconStyle><scale>1.3</scale>");
pw.print("<Icon><href>http://maps.google.com/mapfiles/kml/pushpin/"
+ "grn-pushpin.png</href></Icon>");
pw.println("<hotSpot x=\"20\" y=\"2\" xunits=\"pixels\" yunits=\"pixels\"/>"
+ "</IconStyle></Style>");
pw.print("<Style id=\"sh_red-pushpin\"><IconStyle><scale>1.3</scale>");
pw.print("<Icon><href>http://maps.google.com/mapfiles/kml/pushpin/"
+ "red-pushpin.png</href></Icon>");
pw.println("<hotSpot x=\"20\" y=\"2\" xunits=\"pixels\" yunits=\"pixels\"/>"
+ "</IconStyle></Style>");
}
private void writePlacemark(String name, String description, String style,
Location location) {
/**
* Writes a placemark.
*
* @param name the name of the placemark
* @param description the description
* @param styleName the style name
* @param location the location
*/
private void writePlacemark(
String name, String description, String styleName, Location location) {
if (location != null) {
pw.println("<Placemark>");
pw.println(" <name>" + StringUtils.stringAsCData(name) + "</name>");
pw.println(" <description>" + StringUtils.stringAsCData(description)
+ "</description>");
pw.println(" <styleUrl>" + style + "</styleUrl>");
pw.println(" <Point>");
pw.println(" <coordinates>" + location.getLongitude() + ","
+ location.getLatitude() + "</coordinates>");
pw.println(" </Point>");
pw.println("</Placemark>");
printWriter.println("<Placemark>");
printWriter.println(
"<description>" + StringUtils.formatCData(description) + "</description>");
printWriter.println("<name>" + StringUtils.formatCData(name) + "</name>");
printWriter.println("<styleUrl>#" + styleName + "</styleUrl>");
printWriter.println("<Point>");
printWriter.println(
"<coordinates>" + location.getLongitude() + "," + location.getLatitude() + ","
+ location.getAltitude() + "</coordinates>");
printWriter.println("</Point>");
printWriter.println("</Placemark>");
}
}
/**
* Writes the track style.
*/
private void writeTrackStyle() {
printWriter.println("<Style id=\"" + TRACK_STYLE + "\"><LineStyle>");
printWriter.println("<color>7f0000ff</color><width>4</width>");
printWriter.println("</LineStyle></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>");
}
}
@@ -22,6 +22,7 @@ import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.content.TracksColumns;
import com.google.android.apps.mytracks.io.file.TrackWriterFactory.TrackFileFormat;
import com.google.android.apps.mytracks.util.FileUtils;
import com.google.android.apps.mytracks.util.PlayTrackUtils;
import com.google.android.apps.mytracks.util.UriUtils;
import com.google.android.maps.mytracks.R;
@@ -46,8 +47,10 @@ import java.io.File;
* @author Rodrigo Damazio
*/
public class SaveActivity extends Activity {
public static final String EXTRA_SHARE_FILE = "share_file";
public static final String EXTRA_FILE_FORMAT = "file_format";
public static final String EXTRA_SHARE_FILE = "share_file";
public static final String EXTRA_PLAY_FILE = "play_file";
private static final int RESULT_DIALOG = 1;
/* VisibleForTesting */
static final int PROGRESS_DIALOG = 2;
@@ -56,6 +59,7 @@ public class SaveActivity extends Activity {
private long trackId;
private TrackWriter writer;
private boolean shareFile;
private boolean playFile;
private TrackFileFormat format;
private WriteProgressController controller;
@@ -87,7 +91,8 @@ public class SaveActivity extends Activity {
int formatIdx = intent.getIntExtra(EXTRA_FILE_FORMAT, -1);
format = TrackFileFormat.values()[formatIdx];
shareFile = intent.getBooleanExtra(EXTRA_SHARE_FILE, false);
playFile = intent.getBooleanExtra(EXTRA_PLAY_FILE, false);
writer = TrackWriterFactory.newWriter(this, providerUtils, trackId, format);
if (writer == null) {
Log.e(TAG, "Unable to build writer");
@@ -95,7 +100,7 @@ public class SaveActivity extends Activity {
return;
}
if (shareFile) {
if (shareFile || playFile) {
// If the file is for sending, save it to a temporary location instead.
FileUtils fileUtils = new FileUtils();
String extension = format.getExtension();
@@ -118,6 +123,8 @@ public class SaveActivity extends Activity {
private void onWriteComplete() {
if (shareFile) {
shareWrittenFile();
} if (playFile) {
playWrittenFile();
} else {
showResultDialog();
}
@@ -144,6 +151,22 @@ public class SaveActivity extends Activity {
getResources().getText(R.string.share_track_picker_title).toString()));
}
private void playWrittenFile() {
if (!writer.wasSuccess()) {
showResultDialog();
return;
}
Uri uri = Uri.fromFile(new File(writer.getAbsolutePath()));
Intent intent = new Intent()
.setClassName(PlayTrackUtils.GOOGLE_EARTH_PACKAGE, PlayTrackUtils.GOOGLE_EARTH_CLASS)
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
.setDataAndType(uri, PlayTrackUtils.KML_MIME_TYPE)
.putExtra(PlayTrackUtils.TOUR_FEATURE_ID, KmlTrackWriter.TOUR_FEATURE_ID);
startActivity(intent);
}
private void showResultDialog() {
removeDialog(RESULT_DIALOG);
showDialog(RESULT_DIALOG);
@@ -21,67 +21,90 @@ import com.google.android.apps.mytracks.content.Sensor.SensorDataSet;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.io.file.TrackWriterFactory.TrackFileFormat;
import com.google.android.apps.mytracks.util.FileUtils;
import com.google.android.apps.mytracks.lib.R;
import com.google.android.apps.mytracks.util.StringUtils;
import com.google.android.apps.mytracks.util.SystemUtils;
import android.content.Context;
import android.location.Location;
import android.os.Build;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.nio.charset.Charset;
import java.util.Date;
import java.util.Locale;
/**
* Write out a a track in the Garmin training center database, tcx format.
* As defined by:
* http://www.garmin.com/xmlschemas/TrainingCenterDatabase/v2
*
* The TCX file written by this class has been verified as compatible with
* Garmin Training Center 3.5.3.
* 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 TrackFormatWriter {
// These are the only sports allowed by the TCX v2 specification for fields
// of type Sport_t.
private static final String TCX_SPORT_BIKING = "Biking";
private static final String TCX_SPORT_RUNNING = "Running";
private static final String TCX_SPORT_OTHER = "Other";
/**
* TCX sport type. See the TCX spec.
*
* @author Jimmy Shih
*/
private enum SportType {
RUNNING("Running"),
BIKING("Biking"),
OTHER("Other");
// Values for fields of type Build_t/Type.
private static final String TCX_TYPE_RELEASE = "Release";
private static final String TCX_TYPE_INTERNAL = "Internal";
private final 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 pw = null;
private Track track;
// Determines whether to encode cadence value as running or cycling cadence.
private boolean sportIsCycling;
private PrintWriter printWriter;
private SportType sportType;
public TcxTrackWriter(Context context) {
this.context = context;
}
@SuppressWarnings("hiding")
@Override
public void prepare(Track track, OutputStream out) {
this.track = track;
this.pw = new PrintWriter(out);
this.sportIsCycling = categoryToTcxSport(track.getCategory()).equals(TCX_SPORT_BIKING);
public void prepare(Track aTrack, OutputStream out) {
this.track = aTrack;
this.printWriter = new PrintWriter(out);
this.sportType = getSportType(track.getCategory());
}
@Override
public void close() {
if (pw != null) {
pw.close();
pw = null;
if (printWriter != null) {
printWriter.close();
printWriter = null;
}
}
@@ -92,222 +115,214 @@ public class TcxTrackWriter implements TrackFormatWriter {
@Override
public void writeHeader() {
if (pw == null) {
return;
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>");
}
pw.format("<?xml version=\"1.0\" encoding=\"%s\" standalone=\"no\" ?>\n",
Charset.defaultCharset().name());
pw.print("<TrainingCenterDatabase ");
pw.print("xmlns=\"http://www.garmin.com/xmlschemas/TrainingCenterDatabase/v2\" ");
pw.print("xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" ");
pw.print("xsi:schemaLocation=\"http://www.garmin.com/xmlschemas/TrainingCenterDatabase/v2 ");
pw.println("http://www.garmin.com/xmlschemas/TrainingCenterDatabasev2.xsd\">");
pw.println();
}
@Override
public void writeBeginTrack(Location firstPoint) {
if (pw == null) {
return;
}
if (printWriter != null) {
String startTime = StringUtils.formatDateTimeIso8601(track.getStatistics().getStartTime());
long totalTimeInSeconds = track.getStatistics().getTotalTime() / 1000;
String startTime = FileUtils.FILE_TIMESTAMP_FORMAT.format(track.getStatistics().getStartTime());
pw.println(" <Activities>");
pw.format(" <Activity Sport=\"%s\">\n", categoryToTcxSport(track.getCategory()));
pw.format(" <Id>%s</Id>\n", startTime);
pw.format(" <Lap StartTime=\"%s\">\n", startTime);
pw.print(" <TotalTimeSeconds>");
pw.print(track.getStatistics().getTotalTime() / 1000);
pw.println("</TotalTimeSeconds>");
pw.print(" <DistanceMeters>");
pw.print(track.getStatistics().getTotalDistance());
pw.println("</DistanceMeters>");
// TODO max speed etc.
// Calories are a required element just put in 0.
pw.print("<Calories>0</Calories>");
pw.println("<Intensity>Active</Intensity>");
pw.println("<TriggerMethod>Manual</TriggerMethod>");
}
@Override
public void writeOpenSegment() {
if (pw != null) {
pw.println(" <Track>");
}
}
@Override
public void writeLocation(Location location) {
if (pw == null) {
return;
}
pw.println(" <Trackpoint>");
Date d = new Date(location.getTime());
pw.println(" <Time>" + FileUtils.FILE_TIMESTAMP_FORMAT.format(d) + "</Time>");
pw.println(" <Position>");
pw.print(" <LatitudeDegrees>");
pw.print(location.getLatitude());
pw.println("</LatitudeDegrees>");
pw.print(" <LongitudeDegrees>");
pw.print(location.getLongitude());
pw.println("</LongitudeDegrees>");
pw.println(" </Position>");
pw.print(" <AltitudeMeters>");
pw.print(location.getAltitude());
pw.println("</AltitudeMeters>");
if (location instanceof MyTracksLocation) {
SensorDataSet sensorData = ((MyTracksLocation) location).getSensorDataSet();
if (sensorData != null) {
if (sensorData.hasHeartRate()
&& sensorData.getHeartRate().getState() == Sensor.SensorState.SENDING
&& sensorData.getHeartRate().hasValue()) {
pw.print(" <HeartRateBpm>");
pw.print("<Value>");
pw.print(sensorData.getHeartRate().getValue());
pw.print("</Value>");
pw.println("</HeartRateBpm>");
}
boolean cadenceAvailable = sensorData.hasCadence()
&& sensorData.getCadence().getState() == Sensor.SensorState.SENDING
&& sensorData.getCadence().hasValue();
// TCX Trackpoint_t contains a sequence. Thus, the legacy XML element
// <Cadence> needs to be put before <Extensions>.
// This field should only be used for the case that activity was marked as biking.
// Otherwise cadence is interpreted as running cadence data which
// is written in the <Extensions> as <RunCadence>.
if (sportIsCycling && cadenceAvailable) {
pw.print(" <Cadence>");
pw.print(Math.min(254, sensorData.getCadence().getValue()));
pw.println("</Cadence>");
}
boolean powerAvailable = sensorData.hasPower()
&& sensorData.getPower().getState() == Sensor.SensorState.SENDING
&& sensorData.getPower().hasValue();
if(powerAvailable || (!sportIsCycling && cadenceAvailable)) {
pw.print(" <Extensions>");
pw.print("<TPX xmlns=\"http://www.garmin.com/xmlschemas/ActivityExtension/v2\">");
// RunCadence needs to be put before power in order to be understood
// by Garmin Training Center.
if (!sportIsCycling && cadenceAvailable) {
pw.print("<RunCadence>");
pw.print(Math.min(254, sensorData.getCadence().getValue()));
pw.print("</RunCadence>");
}
if (powerAvailable) {
pw.print("<Watts>");
pw.print(sensorData.getPower().getValue());
pw.print("</Watts>");
}
pw.println("</TPX></Extensions>");
}
}
}
pw.println(" </Trackpoint>");
}
@Override
public void writeCloseSegment() {
if (pw != null) {
pw.println(" </Track>");
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.getStatistics().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(Location lastPoint) {
if (pw == null) {
return;
if (printWriter != null) {
printWriter.println("</Lap>");
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>");
}
pw.println(" </Lap>");
pw.print(" <Creator xsi:type=\"Device_t\">");
pw.format("<Name>My Tracks running on %s</Name>\n", Build.MODEL);
// The following code is correct. ID is inconsistently capitalized in the
// TCX schema.
pw.println("<UnitId>0</UnitId>");
pw.println("<ProductID>0</ProductID>");
writeVersion();
pw.println("</Creator>");
pw.println(" </Activity>");
pw.println(" </Activities>");
}
@Override
public void writeFooter() {
if (pw == null) {
return;
public void writeOpenSegment() {
if (printWriter != null) {
printWriter.println("<Track>");
}
pw.println(" <Author xsi:type=\"Application_t\">");
}
// We put the version in the name because there isn't a better place for
// it. The TCX schema tightly defined the Version tag, so we can't put it
// there. They've similarly constrained the PartNumber tag, so it can't go
// there either.
pw.format("<Name>My Tracks %s by Google</Name>\n", SystemUtils.getMyTracksVersion(context));
@Override
public void writeCloseSegment() {
if (printWriter != null) {
printWriter.println("</Track>");
}
}
pw.println("<Build>");
@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>");
printWriter.println("<AltitudeMeters>" + location.getAltitude() + "</AltitudeMeters>");
writeVersion();
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>");
}
pw.format("<Type>%s</Type>\n", SystemUtils.isRelease(context) ? TCX_TYPE_RELEASE
: TCX_TYPE_INTERNAL);
pw.println("</Build>");
pw.format("<LangID>%s</LangID>\n", Locale.getDefault().getLanguage());
pw.println("<PartNumber>000-00000-00</PartNumber>");
pw.println("</Author>");
pw.println("</TrainingCenterDatabase>");
// <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) {
// TODO Write out the waypoints somewhere.
// Do nothing.
}
/**
* Writes the TCX Version.
*/
private void writeVersion() {
if (pw == null) {
return;
}
// Splitting the myTracks version code into VersionMajor, VersionMinor and BuildMajor
// to fit the integer type requirement for these fields in the TCX spec.
// Putting a string like "x.x.x" into VersionMajor breaks XML validation.
// We also set the BuildMinor version to 1 if this is a development build to
// signify that this build is newer than the one associated with the
// version code given in BuildMajor.
String[] myTracksVersionComponents = SystemUtils.getMyTracksVersion(context).split("\\.");
// 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;
pw.println("<Version>");
pw.format("<VersionMajor>%d</VersionMajor>\n", Integer.valueOf(myTracksVersionComponents[0]));
pw.format("<VersionMinor>%d</VersionMinor>\n", Integer.valueOf(myTracksVersionComponents[1]));
// TCX schema says these are optional but http://connect.garmin.com only accepts
// the TCX file when they are present.
pw.format("<BuildMajor>%d</BuildMajor>\n", Integer.valueOf(myTracksVersionComponents[2]));
pw.format("<BuildMinor>%d</BuildMinor>\n", SystemUtils.isRelease(context) ? 0 : 1);
pw.println("</Version>");
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>");
}
private String categoryToTcxSport(String category) {
/**
* Gets the sport type from the category.
*
* @param category the category
*/
private SportType getSportType(String category) {
category = category.trim();
if (category.equalsIgnoreCase(TCX_SPORT_RUNNING)) {
return TCX_SPORT_RUNNING;
} else if (category.equalsIgnoreCase(TCX_SPORT_BIKING)) {
return TCX_SPORT_BIKING;
// 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 TCX_SPORT_OTHER;
return SportType.OTHER;
}
}
}
@@ -1,78 +0,0 @@
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.io.file;
import static com.google.android.apps.mytracks.Constants.TAG;
import com.google.android.apps.mytracks.util.FileUtils;
import android.os.Environment;
import android.util.Log;
import java.io.File;
/**
* A class to clean up old temporary files.
* @author Sandor Dornbush
*/
public class TempFileCleaner {
private long currentTimeMillis;
public static void clean() {
(new TempFileCleaner(System.currentTimeMillis())).cleanImpl();
}
// @VisibleForTesting
TempFileCleaner(long time) {
currentTimeMillis = time;
}
private void cleanImpl() {
if (!Environment.getExternalStorageState().equals(
Environment.MEDIA_MOUNTED)) {
return; // Can't do anything now.
}
cleanTmpDirectory("csv");
cleanTmpDirectory("gpx");
cleanTmpDirectory("kml");
cleanTmpDirectory("tcx");
}
private void cleanTmpDirectory(String name) {
FileUtils fileUtils = new FileUtils();
String dirName = fileUtils.buildExternalDirectoryPath(name, "tmp");
cleanTmpDirectory(new File(dirName));
}
// @VisibleForTesting
int cleanTmpDirectory(File dir) {
if (!dir.exists()) {
return 0;
}
int count = 0;
long oldest = currentTimeMillis - 1000 * 3600;
for (File f : dir.listFiles()) {
if (f.lastModified() < oldest) {
if (!f.delete()) {
Log.w(TAG, "Failed to delete file " + f.getAbsolutePath());
}
count++;
}
}
return count;
}
}
@@ -23,12 +23,15 @@ import android.location.Location;
import java.io.OutputStream;
/**
* Interface for writing data to a specific track file format.
* Interface for writing a track to file.
*
* The expected sequence of calls is:
* <ol>
* <li>{@link #prepare}
* <li>{@link #writeHeader}
* <li>{@link #writeBeginWaypoints}
* <li>For each waypoint: {@link #writeWaypoint}
* <li>{@link #writeEndWaypoints}
* <li>{@link #writeBeginTrack}
* <li>For each segment:
* <ol>
@@ -37,8 +40,8 @@ import java.io.OutputStream;
* <li>{@link #writeCloseSegment}
* </ol>
* <li>{@link #writeEndTrack}
* <li>For each waypoint: {@link #writeWaypoint}
* <li>{@link #writeFooter}
* <li>{@link #close}
* </ol>
*
* @author Rodrigo Damazio
@@ -46,68 +49,78 @@ import java.io.OutputStream;
public interface TrackFormatWriter {
/**
* Sets up the writer to write the given track to the given output.
*
* @param track the track to write
* @param out the stream to write the track contents to
* Gets the file extension (i.e. gpx, kml, ...)
*/
void prepare(Track track, OutputStream out);
public String getExtension();
/**
* @return The file extension (i.e. gpx, kml, ...)
* Sets up the writer to write the given track.
*
* @param track the track to write
* @param outputStream the output stream to write the track to
*/
String getExtension();
public void prepare(Track track, OutputStream outputStream);
/**
* Closes the underlying file handler.
*/
public void close();
/**
* Writes the header.
* This is chance for classes to write out opening information.
*/
void writeHeader();
public void writeHeader();
/**
* Writes the footer.
* This is chance for classes to write out closing information.
*/
void writeFooter();
public void writeFooter();
/**
* Write the given location object.
* Writes the beginning of the waypoints.
*/
public void writeBeginWaypoints();
/**
* Writes the end of the waypoints.
*/
public void writeEndWaypoints();
/**
* Writes a waypoint.
*
* TODO Add some flexible handling of other sensor data.
* @param waypoint the waypoint
*/
public void writeWaypoint(Waypoint waypoint);
/**
* Writes the beginning of the track.
*
* @param firstLocation the first location
*/
public void writeBeginTrack(Location firstLocation);
/**
* Writes the end of the track.
*
* @param lastLocation the last location
*/
public void writeEndTrack(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 to write
* @param location the location
*/
void writeLocation(Location location) throws InterruptedException;
/**
* Write a way point.
*
* @param waypoint
*/
void writeWaypoint(Waypoint waypoint);
/**
* Write the beginning of a track.
*/
void writeBeginTrack(Location firstPoint);
/**
* Write the end of a track.
*/
void writeEndTrack(Location lastPoint);
/**
* Write the statements necessary to open a new segment.
*/
void writeOpenSegment();
/**
* Write the statements necessary to close a segment.
*/
void writeCloseSegment();
/**
* Close the underlying file handle.
*/
void close();
}
public void writeLocation(Location location);
}
@@ -38,7 +38,7 @@ public class TrackWriterFactory {
GPX {
@Override
TrackFormatWriter newFormatWriter(Context context) {
return new GpxTrackWriter();
return new GpxTrackWriter(context);
}
},
KML {
@@ -50,7 +50,7 @@ public class TrackWriterFactory {
CSV {
@Override
public TrackFormatWriter newFormatWriter(Context context) {
return new CsvTrackWriter();
return new CsvTrackWriter(context);
}
},
TCX {
@@ -269,12 +269,17 @@ class TrackWriterImpl implements TrackWriter {
Cursor cursor = null;
cursor = providerUtils.getWaypointsCursor(trackId, 0,
Constants.MAX_LOADED_WAYPOINTS_POINTS);
boolean hasWaypoints = false;
if (cursor != null) {
try {
if (cursor.moveToFirst()) {
// Yes, this will skip the 1st way point and that is intentional
// as the 1st points holds the stats for the current/last segment.
while (cursor.moveToNext()) {
if (!hasWaypoints) {
writer.writeBeginWaypoints();
hasWaypoints = true;
}
Waypoint wpt = providerUtils.createWaypoint(cursor);
writer.writeWaypoint(wpt);
}
@@ -283,6 +288,9 @@ class TrackWriterImpl implements TrackWriter {
cursor.close();
}
}
if (hasWaypoints) {
writer.writeEndWaypoints();
}
}
/**
@@ -0,0 +1,68 @@
/*
* Copyright 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.io.fusiontables;
import com.google.android.apps.mytracks.io.docs.SendDocsActivity;
import com.google.android.apps.mytracks.io.sendtogoogle.AbstractSendActivity;
import com.google.android.apps.mytracks.io.sendtogoogle.AbstractSendAsyncTask;
import com.google.android.apps.mytracks.io.sendtogoogle.SendRequest;
import com.google.android.apps.mytracks.io.sendtogoogle.UploadResultActivity;
import com.google.android.maps.mytracks.R;
import 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.send_google_fusion_tables);
}
@Override
protected void startNextActivity(boolean success, boolean isCancel) {
sendRequest.setFusionTablesSuccess(success);
Class<?> next = getNextClass(sendRequest, isCancel);
Intent intent = new Intent(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.isSendDocs()) {
return SendDocsActivity.class;
} else {
return UploadResultActivity.class;
}
}
}
}
@@ -0,0 +1,463 @@
// 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.io.sendtogoogle.AbstractSendAsyncTask;
import com.google.android.apps.mytracks.io.sendtogoogle.SendToGoogleUtils;
import com.google.android.apps.mytracks.stats.DoubleBuffer;
import com.google.android.apps.mytracks.stats.TripStatisticsBuilder;
import com.google.android.apps.mytracks.util.ApiAdapterFactory;
import com.google.android.apps.mytracks.util.LocationUtils;
import com.google.android.apps.mytracks.util.SystemUtils;
import com.google.android.apps.mytracks.util.UnitConversions;
import com.google.android.maps.mytracks.R;
import com.google.api.client.googleapis.GoogleHeaders;
import com.google.api.client.googleapis.MethodOverride;
import com.google.api.client.http.GenericUrl;
import com.google.api.client.http.HttpRequest;
import com.google.api.client.http.HttpRequestFactory;
import com.google.api.client.http.HttpResponse;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.http.InputStreamContent;
import com.google.api.client.util.Strings;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.accounts.AuthenticatorException;
import android.accounts.OperationCanceledException;
import android.content.Context;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.location.Location;
import android.util.Log;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.List;
import java.util.Vector;
/**
* AsyncTask to send a track to Google Fusion Tables.
*
* @author Jimmy Shih
*/
public class SendFusionTablesAsyncTask extends AbstractSendAsyncTask {
private static final String APP_NAME_PREFIX = "Google-MyTracks-";
private static final String SQL_KEY = "sql=";
private static final String CONTENT_TYPE = "application/x-www-form-urlencoded";
private static final String FUSION_TABLES_BASE_URL =
"https://www.google.com/fusiontables/api/query";
private static final int MAX_POINTS_PER_UPLOAD = 2048;
private static final String GDATA_VERSION = "2";
private static final int PROGRESS_CREATE_TABLE = 0;
private static final int PROGRESS_UNLIST_TABLE = 5;
private static final int PROGRESS_UPLOAD_DATA_MIN = 10;
private static final int PROGRESS_UPLOAD_DATA_MAX = 90;
private static final int PROGRESS_UPLOAD_WAYPOINTS = 95;
private static final int PROGRESS_COMPLETE = 100;
// See http://support.google.com/fusiontables/bin/answer.py?hl=en&answer=185991
private static final String MARKER_TYPE_START = "large_green";
private static final String MARKER_TYPE_END = "large_red";
private static final String MARKER_TYPE_WAYPOINT = "large_yellow";
private static final String TAG = SendFusionTablesAsyncTask.class.getSimpleName();
private final Context context;
private final long trackId;
private final Account account;
private final MyTracksProviderUtils myTracksProviderUtils;
private final HttpRequestFactory httpRequestFactory;
// The following variables are for per upload states
private String authToken;
private String tableId;
int currentSegment;
public SendFusionTablesAsyncTask(
SendFusionTablesActivity activity, long trackId, Account account) {
super(activity);
this.trackId = trackId;
this.account = account;
context = activity.getApplicationContext();
myTracksProviderUtils = MyTracksProviderUtils.Factory.get(context);
HttpTransport transport = ApiAdapterFactory.getApiAdapter().getHttpTransport();
httpRequestFactory = transport.createRequestFactory(new MethodOverride());
}
@Override
protected void closeConnection() {
// No action needed for Google Fusion Tables
}
@Override
protected void saveResult() {
Track track = myTracksProviderUtils.getTrack(trackId);
if (track != null) {
track.setTableId(tableId);
myTracksProviderUtils.updateTrack(track);
} else {
Log.d(TAG, "No track");
}
}
@Override
protected boolean performTask() {
// Reset the per upload states
authToken = null;
tableId = null;
currentSegment = 1;
try {
authToken = AccountManager.get(context).blockingGetAuthToken(
account, SendFusionTablesUtils.SERVICE, false);
} catch (OperationCanceledException e) {
Log.d(TAG, "Unable to get auth token", e);
return retryTask();
} catch (AuthenticatorException e) {
Log.d(TAG, "Unable to get auth token", e);
return retryTask();
} catch (IOException e) {
Log.d(TAG, "Unable to get auth token", e);
return retryTask();
}
Track track = myTracksProviderUtils.getTrack(trackId);
if (track == null) {
Log.d(TAG, "Track is null");
return false;
}
// Create a new table
publishProgress(PROGRESS_CREATE_TABLE);
if (!createNewTable(track)) {
// Retry upload in case the auth token is invalid
return retryTask();
}
// Unlist table
publishProgress(PROGRESS_UNLIST_TABLE);
if (!unlistTable()) {
return false;
}
// Upload all the track points plus the start and end markers
publishProgress(PROGRESS_UPLOAD_DATA_MIN);
if (!uploadAllTrackPoints(track)) {
return false;
}
// Upload all the waypoints
publishProgress(PROGRESS_UPLOAD_WAYPOINTS);
if (!uploadWaypoints()) {
return false;
}
publishProgress(PROGRESS_COMPLETE);
return true;
}
@Override
protected void invalidateToken() {
AccountManager.get(context).invalidateAuthToken(Constants.ACCOUNT_TYPE, authToken);
}
/**
* Creates a new table.
*
* @param track the track
* @return true if success.
*/
private boolean createNewTable(Track track) {
String query = "CREATE TABLE '" + SendFusionTablesUtils.escapeSqlString(track.getName())
+ "' (name:STRING,description:STRING,geometry:LOCATION,marker:STRING)";
return sendQuery(query, true);
}
/**
* Unlists a table.
*
* @return true if success.
*/
private boolean unlistTable() {
String query = "UPDATE TABLE " + tableId + " SET VISIBILITY = UNLISTED";
return sendQuery(query, false);
}
/**
* Uploads all the points in a track.
*
* @param track the track
* @return true if success.
*/
private boolean uploadAllTrackPoints(Track track) {
Cursor locationsCursor = null;
try {
SharedPreferences prefs = context.getSharedPreferences(
Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
boolean metricUnits = prefs.getBoolean(context.getString(R.string.metric_units_key), true);
locationsCursor = myTracksProviderUtils.getLocationsCursor(trackId, 0, -1, false);
if (locationsCursor == null) {
Log.d(TAG, "Location cursor is null");
return false;
}
int locationsCount = locationsCursor.getCount();
List<Location> locations = new ArrayList<Location>(MAX_POINTS_PER_UPLOAD);
Location lastLocation = null;
// For chart server, limit the number of elevation readings to 250.
int elevationSamplingFrequency = Math.max(1, (int) (locationsCount / 250.0));
TripStatisticsBuilder tripStatisticsBuilder = new TripStatisticsBuilder(
track.getStatistics().getStartTime());
DoubleBuffer elevationBuffer = new DoubleBuffer(Constants.ELEVATION_SMOOTHING_FACTOR);
Vector<Double> distances = new Vector<Double>();
Vector<Double> elevations = new Vector<Double>();
for (int i = 0; i < locationsCount; i++) {
locationsCursor.moveToPosition(i);
Location location = myTracksProviderUtils.createLocation(locationsCursor);
locations.add(location);
if (i == 0) {
// Create a start marker
String name = context.getString(R.string.marker_label_start, track.getName());
if (!createNewPoint(name, "", location, MARKER_TYPE_START)) {
Log.d(TAG, "Unable to create the start marker");
return false;
}
}
// Add to the distances and elevations vectors
if (LocationUtils.isValidLocation(location)) {
tripStatisticsBuilder.addLocation(location, location.getTime());
// All points go into the smoothing buffer
elevationBuffer.setNext(metricUnits ? location.getAltitude()
: location.getAltitude() * UnitConversions.M_TO_FT);
if (i % elevationSamplingFrequency == 0) {
distances.add(tripStatisticsBuilder.getStatistics().getTotalDistance());
elevations.add(elevationBuffer.getAverage());
}
lastLocation = location;
}
// Upload periodically
int readCount = i + 1;
if (readCount % MAX_POINTS_PER_UPLOAD == 0) {
if (!prepareAndUploadPoints(track, locations, false)) {
Log.d(TAG, "Unable to upload points");
return false;
}
updateProgress(readCount, locationsCount);
locations.clear();
}
}
// Do a final upload with the remaining locations
if (!prepareAndUploadPoints(track, locations, true)) {
Log.d(TAG, "Unable to upload points");
return false;
}
// Create an end marker
if (lastLocation != null) {
distances.add(tripStatisticsBuilder.getStatistics().getTotalDistance());
elevations.add(elevationBuffer.getAverage());
DescriptionGenerator descriptionGenerator = new DescriptionGeneratorImpl(context);
track.setDescription("<p>" + track.getDescription() + "</p><p>"
+ descriptionGenerator.generateTrackDescription(track, distances, elevations) + "</p>");
String name = context.getString(R.string.marker_label_end, track.getName());
if (!createNewPoint(name, track.getDescription(), lastLocation, MARKER_TYPE_END)) {
Log.d(TAG, "Unable to create the end marker");
return false;
}
}
return true;
} finally {
if (locationsCursor != null) {
locationsCursor.close();
}
}
}
/**
* Prepares and uploads a list of locations from a track.
*
* @param track the track
* @param locations the locations from the track
* @param lastBatch true if it is the last batch of locations
*/
private boolean prepareAndUploadPoints(Track track, List<Location> locations, boolean lastBatch) {
// Prepare locations
ArrayList<Track> splitTracks = SendToGoogleUtils.prepareLocations(track, locations);
// Upload segments
boolean onlyOneSegment = lastBatch && currentSegment == 1 && splitTracks.size() == 1;
for (Track splitTrack : splitTracks) {
if (!onlyOneSegment) {
splitTrack.setName(context.getString(
R.string.send_google_track_part_label, splitTrack.getName(), currentSegment));
}
if (!createNewLineString(splitTrack)) {
Log.d(TAG, "Upload points failed");
return false;
}
currentSegment++;
}
return true;
}
/**
* Uploads all the waypoints.
*
* @return true if success.
*/
private boolean uploadWaypoints() {
Cursor cursor = null;
try {
cursor = myTracksProviderUtils.getWaypointsCursor(
trackId, 0, Constants.MAX_LOADED_WAYPOINTS_POINTS);
if (cursor != null && cursor.moveToFirst()) {
// This will skip the first waypoint (it carries the stats for the
// track).
while (cursor.moveToNext()) {
Waypoint wpt = myTracksProviderUtils.createWaypoint(cursor);
if (!createNewPoint(
wpt.getName(), wpt.getDescription(), wpt.getLocation(), MARKER_TYPE_WAYPOINT)) {
Log.d(TAG, "Upload waypoints failed");
return false;
}
}
}
return true;
} finally {
if (cursor != null) {
cursor.close();
}
}
}
/**
* Creates a new row in Google Fusion Tables representing a marker as a
* point.
*
* @param name the marker name
* @param description the marker description
* @param location the marker location
* @param type the marker type
* @return true if success.
*/
private boolean createNewPoint(
String name, String description, Location location, String type) {
String query = "INSERT INTO " + tableId + " (name,description,geometry,marker) VALUES "
+ SendFusionTablesUtils.formatSqlValues(
name, description, SendFusionTablesUtils.getKmlPoint(location), type);
return sendQuery(query, false);
}
/**
* Creates a new row in Google Fusion Tables representing the track as a
* line segment.
*
* @param track the track
* @return true if success.
*/
private boolean createNewLineString(Track track) {
String query = "INSERT INTO " + tableId + " (name,description,geometry) VALUES "
+ SendFusionTablesUtils.formatSqlValues(track.getName(), track.getDescription(),
SendFusionTablesUtils.getKmlLineString(track.getLocations()));
return sendQuery(query, false);
}
/**
* Sends a query to Google Fusion Tables.
*
* @param query the Fusion Tables SQL query
* @param setTableId true to set the table id
* @return true if success.
*/
private boolean sendQuery(String query, boolean setTableId) {
Log.d(TAG, "SendQuery: " + query);
if (isCancelled()) {
return false;
}
GenericUrl url = new GenericUrl(FUSION_TABLES_BASE_URL);
String sql = SQL_KEY + URLEncoder.encode(query);
ByteArrayInputStream inputStream = new ByteArrayInputStream(Strings.toBytesUtf8(sql));
InputStreamContent inputStreamContent = new InputStreamContent(null, inputStream);
HttpRequest request;
try {
request = httpRequestFactory.buildPostRequest(url, inputStreamContent);
} catch (IOException e) {
Log.d(TAG, "Unable to build request", e);
return false;
}
GoogleHeaders headers = new GoogleHeaders();
headers.setApplicationName(APP_NAME_PREFIX + SystemUtils.getMyTracksVersion(context));
headers.gdataVersion = GDATA_VERSION;
headers.setGoogleLogin(authToken);
headers.setContentType(CONTENT_TYPE);
request.setHeaders(headers);
HttpResponse response;
try {
response = request.execute();
} catch (IOException e) {
Log.d(TAG, "Unable to execute request", e);
return false;
}
boolean isSuccess = response.isSuccessStatusCode();
if (isSuccess) {
InputStream content;
try {
content = response.getContent();
} catch (IOException e) {
Log.d(TAG, "Unable to get response", e);
return false;
}
if (setTableId) {
tableId = SendFusionTablesUtils.getTableId(content);
if (tableId == null) {
Log.d(TAG, "tableId is null");
return false;
}
}
} else {
Log.d(TAG,
"sendQuery failed: " + response.getStatusMessage() + ": " + response.getStatusCode());
return false;
}
return true;
}
/**
* Updates the progress based on the number of locations uploaded.
*
* @param uploaded the number of uploaded locations
* @param total the number of total locations
*/
private void updateProgress(int uploaded, int total) {
double totalPercentage = (double) uploaded / total;
double scaledPercentage = totalPercentage
* (PROGRESS_UPLOAD_DATA_MAX - PROGRESS_UPLOAD_DATA_MIN) + PROGRESS_UPLOAD_DATA_MIN;
publishProgress((int) scaledPercentage);
}
}
@@ -0,0 +1,196 @@
/*
* Copyright 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.io.fusiontables;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.stats.TripStatistics;
import com.google.api.client.util.Strings;
import com.google.common.annotations.VisibleForTesting;
import android.location.Location;
import android.util.Log;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Locale;
/**
* Utilities for sending a track to Google Fusion Tables.
*
* @author Jimmy Shih
*/
public class SendFusionTablesUtils {
public static final String SERVICE = "fusiontables";
private static final String UTF8 = "UTF8";
private static final String TABLE_ID = "tableid";
private static final String MAP_URL = "https://www.google.com/fusiontables/embedviz?"
+ "viz=MAP&q=select+col0,+col1,+col2,+col3+from+%s+&h=false&lat=%f&lng=%f&z=%d&t=1&l=col2";
private static final String TAG = SendFusionTablesUtils.class.getSimpleName();
private SendFusionTablesUtils() {}
/**
* Gets the url to visualize a fusion table on a map.
*
* @param track the track
* @return the url.
*/
public static String getMapUrl(Track track) {
if (track == null || track.getStatistics() == null || track.getTableId() == null) {
Log.e(TAG, "Invalid track");
return null;
}
// TODO(jshih): Determine the correct bounding box and zoom level that
// will show the entire track.
TripStatistics stats = track.getStatistics();
double latE6 = stats.getBottom() + (stats.getTop() - stats.getBottom()) / 2;
double lonE6 = stats.getLeft() + (stats.getRight() - stats.getLeft()) / 2;
int z = 15;
// We explicitly format with Locale.US because we need the latitude and
// longitude to be formatted in a locale-independent manner. Specifically,
// we need the decimal separator to be a period rather than a comma.
return String.format(
Locale.US, MAP_URL, track.getTableId(), latE6 / 1.E6, lonE6 / 1.E6, z);
}
/**
* Formats an array of values as a SQL VALUES like
* ('value1','value2',...,'value_n'). Escapes single quotes with two single
* quotes.
*
* @param values an array of values to format
* @return the formated SQL VALUES.
*/
public static String formatSqlValues(String... values) {
StringBuilder builder = new StringBuilder("(");
for (int i = 0; i < values.length; i++) {
if (i > 0) {
builder.append(',');
}
builder.append('\'');
builder.append(escapeSqlString(values[i]));
builder.append('\'');
}
builder.append(")");
return builder.toString();
}
/**
* Escapes a SQL string. Escapes single quotes with two single quotes.
*
* @param string the string
* @return the escaped string.
*/
public static String escapeSqlString(String string) {
return string.replaceAll("'", "''");
}
/**
* Gets a KML Point value representing a location.
*
* @param location the location
* @return the KML Point value.
*/
public static String getKmlPoint(Location location) {
StringBuilder builder = new StringBuilder("<Point><coordinates>");
if (location != null) {
appendLocation(location, builder);
}
builder.append("</coordinates></Point>");
return builder.toString();
}
/**
* Gets a KML LineString value representing an array of locations.
*
* @param locations the locations.
* @return the KML LineString value.
*/
public static String getKmlLineString(ArrayList<Location> locations) {
StringBuilder builder = new StringBuilder("<LineString><coordinates>");
if (locations != null) {
for (int i = 0; i < locations.size(); i++) {
if (i != 0) {
builder.append(' ');
}
appendLocation(locations.get(i), builder);
}
}
builder.append("</coordinates></LineString>");
return builder.toString();
}
/**
* Appends a location to a string builder using "longitude,latitude[,altitude]" format.
*
* @param location the location
* @param builder the string builder
*/
@VisibleForTesting
static void appendLocation(Location location, StringBuilder builder) {
builder.append(location.getLongitude()).append(",").append(location.getLatitude());
if (location.hasAltitude()) {
builder.append(",");
builder.append(location.getAltitude());
}
}
/**
* Gets the table id from an input streawm.
*
* @param inputStream input stream
* @return table id or null if not available.
*/
public static String getTableId(InputStream inputStream) {
if (inputStream == null) {
Log.d(TAG, "inputStream is null");
return null;
}
byte[] result = new byte[1024];
int read;
try {
read = inputStream.read(result);
} catch (IOException e) {
Log.d(TAG, "Unable to read result", e);
return null;
}
if (read == -1) {
Log.d(TAG, "no data read");
return null;
}
String s;
try {
s = new String(result, 0, read, UTF8);
} catch (UnsupportedEncodingException e) {
Log.d(TAG, "Unable to parse result", e);
return null;
}
String[] lines = s.split(Strings.LINE_SEPARATOR);
if (lines.length > 1 && lines[0].equals(TABLE_ID)) {
// returns the next line
return lines[1];
} else {
Log.d(TAG, "Response is not valid: " + s);
return null;
}
}
}
@@ -36,6 +36,7 @@ public class GDataClientFactory {
* @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.
@@ -1,309 +0,0 @@
/*
* 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.gdata;
import com.google.android.apps.mytracks.Constants;
import com.google.android.apps.mytracks.io.AuthManager;
import com.google.android.apps.mytracks.io.AuthManager.AuthCallback;
import android.util.Log;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* GDataWrapper provides a wrapper around GData operations that maintains the
* GData client, and provides a method to run GData queries with proper error
* handling. After a query is run, the wrapper can be queried about the error
* that occurred.
*
* @param <C> the GData service client
* @author Sandor Dornbush
*/
public class GDataWrapper<C> {
public static class AuthenticationException extends Exception {
private Exception exception;
private static final long serialVersionUID = 1L;
public AuthenticationException(Exception caught) {
this.exception = caught;
}
public Exception getException() {
return exception;
}
};
public static class ParseException extends Exception {
private Exception exception;
private static final long serialVersionUID = 1L;
public ParseException(Exception caught) {
this.exception = caught;
}
public Exception getException() {
return exception;
}
};
public static class ConflictDetectedException extends Exception {
private Exception exception;
private static final long serialVersionUID = 1L;
public ConflictDetectedException(Exception caught) {
this.exception = caught;
}
public Exception getException() {
return exception;
}
};
public static class HttpException extends Exception {
private static final long serialVersionUID = 1L;
private int statusCode;
private String statusMessage;
public HttpException(int statusCode, String statusMessage) {
super();
this.statusCode = statusCode;
this.statusMessage = statusMessage;
}
public int getStatusCode() {
return statusCode;
}
public String getStatusMessage() {
return statusMessage;
}
};
/**
* A QueryFunction is passed in when executing a query. The query function of
* the class is called with the GData client as a parameter. The function
* should execute whatever operations it desires on the client without concern
* for whether the client will throw an error.
*/
public interface QueryFunction<C> {
public abstract void query(C client)
throws AuthenticationException, IOException, ParseException,
ConflictDetectedException, HttpException;
}
/**
* A AuthenticatedFunction is passed in when executing the google
* authenticated service. The authenticated function of the class is called
* with the current authentication token for the service. The function should
* execute whatever operations with the google service without concern for
* whether the client will throw an error.
*/
public interface AuthenticatedFunction {
public abstract void run(String authenticationToken)
throws AuthenticationException, IOException;
}
// The types of error that may be encountered
// No error occurred.
public static final int ERROR_NO_ERROR = 0;
// There was an authentication error, the auth token may be invalid.
public static final int ERROR_AUTH = 1;
// There was an internal error on the server side.
public static final int ERROR_INTERNAL = 2;
// There was an error connecting to the server.
public static final int ERROR_CONNECTION = 3;
// The item queried did not exit.
public static final int ERROR_NOT_FOUND = 4;
// There was an error parsing or serializing locally.
public static final int ERROR_LOCAL = 5;
// There was a conflict, update the entry and try again.
public static final int ERROR_CONFLICT = 6;
// A query was run after cleaning up the wrapper, so the client was invalid.
public static final int ERROR_CLEANED_UP = 7;
// An unknown error occurred.
public static final int ERROR_UNKNOWN = 100;
private static final int AUTH_TOKEN_INVALIDATE_REFRESH_NUM_RETRIES = 1;
private static final int AUTH_TOKEN_INVALIDATE_REFRESH_TIMEOUT = 5000;
private String errorMessage;
private int errorType;
private C gdataServiceClient;
private AuthManager auth;
private boolean retryOnAuthFailure;
public GDataWrapper() {
errorType = ERROR_NO_ERROR;
errorMessage = null;
auth = null;
retryOnAuthFailure = false;
}
public void setClient(C gdataServiceClient) {
this.gdataServiceClient = gdataServiceClient;
}
public boolean runAuthenticatedFunction(
final AuthenticatedFunction function) {
return runCommon(function, null);
}
public boolean runQuery(final QueryFunction<C> query) {
return runCommon(null, query);
}
/**
* Runs an arbitrary piece of code.
*/
private boolean runCommon(final AuthenticatedFunction function,
final QueryFunction<C> query) {
for (int i = 0; i <= AUTH_TOKEN_INVALIDATE_REFRESH_NUM_RETRIES; i++) {
runOne(function, query);
if (errorType == ERROR_NO_ERROR) {
return true;
}
Log.d(Constants.TAG, "GData error encountered: " + errorMessage);
if (errorType == ERROR_AUTH && auth != null) {
if (!retryOnAuthFailure || !invalidateAndRefreshAuthToken()) {
return false;
}
}
Log.d(Constants.TAG, "retrying function/query");
}
return false;
}
/**
* Execute a given function or query. If one is executed, errorType and
* errorMessage will contain the result/status of the function/query.
*/
private void runOne(final AuthenticatedFunction function,
final QueryFunction<C> query) {
try {
if (function != null) {
function.run(this.auth.getAuthToken());
} else if (query != null) {
query.query(gdataServiceClient);
} else {
throw new IllegalArgumentException(
"invalid invocation of runOne; one of function/query " +
"must be non-null");
}
errorType = ERROR_NO_ERROR;
errorMessage = null;
} catch (AuthenticationException e) {
Log.e(Constants.TAG, "AuthenticationException", e);
errorType = ERROR_AUTH;
errorMessage = e.getMessage();
} catch (HttpException e) {
Log.e(Constants.TAG,
"HttpException, code " + e.getStatusCode() + " message " + e.getMessage(), e);
errorMessage = e.getMessage();
if (e.getStatusCode() == 401) {
errorType = ERROR_AUTH;
} else {
errorType = ERROR_CONNECTION;
}
} catch (FileNotFoundException e) {
Log.e(Constants.TAG, "Exception", e);
errorType = ERROR_AUTH;
errorMessage = e.getMessage();
} catch (IOException e) {
Log.e(Constants.TAG, "Exception", e);
errorMessage = e.getMessage();
if (errorMessage != null && errorMessage.contains("503")) {
errorType = ERROR_INTERNAL;
} else {
errorType = ERROR_CONNECTION;
}
} catch (ParseException e) {
Log.e(Constants.TAG, "Exception", e);
errorType = ERROR_LOCAL;
errorMessage = e.getMessage();
} catch (ConflictDetectedException e) {
Log.e(Constants.TAG, "Exception", e);
errorType = ERROR_CONFLICT;
errorMessage = e.getMessage();
}
}
/**
* Invalidates and refreshes the auth token. Blocks until the refresh has
* completed or until we deem the refresh as having timed out.
*
* @return true If the invalidate/refresh succeeds, false if it fails or
* times out.
*/
private boolean invalidateAndRefreshAuthToken() {
Log.d(Constants.TAG, "Retrying due to auth failure");
// This FutureTask doesn't do anything -- it exists simply to be
// blocked upon using get().
final FutureTask<?> whenFinishedFuture = new FutureTask<Object>(new Runnable() {
public void run() {}
}, null);
final AtomicBoolean finalSuccess = new AtomicBoolean(false);
auth.invalidateAndRefresh(new AuthCallback() {
@Override
public void onAuthResult(boolean success) {
finalSuccess.set(success);
whenFinishedFuture.run();
}
});
try {
Log.d(Constants.TAG, "waiting for invalidate");
whenFinishedFuture.get(AUTH_TOKEN_INVALIDATE_REFRESH_TIMEOUT,
TimeUnit.MILLISECONDS);
boolean success = finalSuccess.get();
Log.d(Constants.TAG, "invalidate finished, success = " + success);
return success;
} catch (InterruptedException e) {
Log.e(Constants.TAG, "Failed to invalidate", e);
} catch (ExecutionException e) {
Log.e(Constants.TAG, "Failed to invalidate", e);
} catch (TimeoutException e) {
Log.e(Constants.TAG, "Invalidate didn't complete in time", e);
} finally {
whenFinishedFuture.cancel(false);
}
return false;
}
public int getErrorType() {
return errorType;
}
public String getErrorMessage() {
return errorMessage;
}
public void setAuthManager(AuthManager auth) {
this.auth = auth;
}
public AuthManager getAuthManager() {
return auth;
}
public void setRetryOnAuthFailure(boolean retry) {
retryOnAuthFailure = retry;
}
}
@@ -0,0 +1,46 @@
/*
* Copyright 2009 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.io.gdata.docs;
import com.google.wireless.gdata.client.GDataClient;
import com.google.wireless.gdata.client.GDataParserFactory;
import com.google.wireless.gdata.client.GDataServiceClient;
/**
* GDataServiceClient for accessing Google Documents. This is not a full
* implementation.
*/
public class DocumentsClient extends GDataServiceClient {
/** The name of the service, dictated to be 'wise' by the protocol. */
public static final String SERVICE = "writely";
/**
* Creates a new DocumentsClient.
*
* @param client The GDataClient that should be used to authenticate requests,
* retrieve feeds, etc
* @param parserFactory The GDataParserFactory that should be used to obtain
* GDataParsers used by this client
*/
public DocumentsClient(GDataClient client, GDataParserFactory parserFactory) {
super(client, parserFactory);
}
@Override
public String getServiceName() {
return SERVICE;
}
}
@@ -0,0 +1,269 @@
/*
* Copyright 2007 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.io.gdata.docs;
import com.google.wireless.gdata.client.GDataClient;
import com.google.wireless.gdata.client.GDataParserFactory;
import com.google.wireless.gdata.client.GDataServiceClient;
import com.google.wireless.gdata.client.HttpException;
import com.google.wireless.gdata.data.Entry;
import com.google.wireless.gdata.data.StringUtils;
import com.google.wireless.gdata.parser.GDataParser;
import com.google.wireless.gdata.parser.ParseException;
import com.google.wireless.gdata.serializer.GDataSerializer;
import com.google.wireless.gdata2.client.AuthenticationException;
import java.io.IOException;
import java.io.InputStream;
/**
* GDataServiceClient for accessing Google Spreadsheets. This client can access
* and parse all of the Spreadsheets feed types: Spreadsheets feed, Worksheets
* feed, List feed, and Cells feed. Read operations are supported on all feed
* types, but only the List and Cells feeds support write operations. (This is a
* limitation of the protocol, not this API. Such write access may be added to
* the protocol in the future, requiring changes to this implementation.)
*
* Only 'private' visibility and 'full' projections are currently supported.
*/
public class SpreadsheetsClient extends GDataServiceClient {
/** The name of the service, dictated to be 'wise' by the protocol. */
public static final String SERVICE = "wise";
/** Standard base feed url for spreadsheets. */
public static final String SPREADSHEETS_BASE_FEED_URL =
"http://spreadsheets.google.com/feeds/spreadsheets/private/full";
/**
* Represents an entry in a GData Spreadsheets meta-feed.
*/
public static class SpreadsheetEntry extends Entry { }
/**
* Represents an entry in a GData Worksheets meta-feed.
*/
public static class WorksheetEntry extends Entry { }
/**
* Creates a new SpreadsheetsClient. Uses the standard base URL for
* spreadsheets feeds.
*
* @param client The GDataClient that should be used to authenticate requests,
* retrieve feeds, etc
*/
public SpreadsheetsClient(GDataClient client,
GDataParserFactory spreadsheetFactory) {
super(client, spreadsheetFactory);
}
@Override
public String getServiceName() {
return SERVICE;
}
/**
* Returns a parser for the specified feed type.
*
* @param feedEntryClass the Class of entry type that will be parsed, which
* lets this method figure out which parser to create
* @param feedUri the URI of the feed to be fetched and parsed
* @param authToken the current authToken to use for the request
* @return a parser for the indicated feed
* @throws AuthenticationException if the authToken is not valid
* @throws ParseException if the response from the server could not be parsed
*/
private GDataParser getParserForTypedFeed(
Class<? extends Entry> feedEntryClass, String feedUri, String authToken)
throws AuthenticationException, ParseException, IOException {
GDataClient gDataClient = getGDataClient();
GDataParserFactory gDataParserFactory = getGDataParserFactory();
try {
InputStream is = gDataClient.getFeedAsStream(feedUri, authToken);
return gDataParserFactory.createParser(feedEntryClass, is);
} catch (HttpException e) {
convertHttpExceptionForReads("Could not fetch parser feed.", e);
return null; // never reached
}
}
/**
* Converts an HTTP exception that happened while reading into the equivalent
* local exception.
*/
public void convertHttpExceptionForReads(String message, HttpException cause)
throws AuthenticationException, IOException {
switch (cause.getStatusCode()) {
case HttpException.SC_FORBIDDEN:
case HttpException.SC_UNAUTHORIZED:
throw new AuthenticationException(message, cause);
case HttpException.SC_GONE:
default:
throw new IOException(message + ": " + cause.getMessage());
}
}
@Override
public Entry createEntry(String feedUri, String authToken, Entry entry)
throws ParseException, IOException {
GDataParserFactory factory = getGDataParserFactory();
GDataSerializer serializer = factory.createSerializer(entry);
InputStream is;
try {
is = getGDataClient().createEntry(feedUri, authToken, serializer);
} catch (HttpException e) {
convertHttpExceptionForWrites(entry.getClass(),
"Could not update entry.", e);
return null; // never reached.
}
GDataParser parser = factory.createParser(entry.getClass(), is);
try {
return parser.parseStandaloneEntry();
} finally {
parser.close();
}
}
/**
* Fetches a GDataParser for the indicated feed. The parser can be used to
* access the contents of URI. WARNING: because we cannot reliably infer the
* feed type from the URI alone, this method assumes the default feed type!
* This is probably NOT what you want. Please use the getParserFor[Type]Feed
* methods.
*
* @param feedEntryClass
* @param feedUri the URI of the feed to be fetched and parsed
* @param authToken the current authToken to use for the request
* @return a parser for the indicated feed
* @throws ParseException if the response from the server could not be parsed
*/
@SuppressWarnings("unchecked")
@Override
public GDataParser getParserForFeed(
Class feedEntryClass, String feedUri, String authToken)
throws ParseException, IOException {
try {
return getParserForTypedFeed(SpreadsheetEntry.class, feedUri, authToken);
} catch (AuthenticationException e) {
throw new IOException("Authentication Failure: " + e.getMessage());
}
}
/**
* Returns a parser for a Worksheets meta-feed.
*
* @param feedUri the URI of the feed to be fetched and parsed
* @param authToken the current authToken to use for the request
* @return a parser for the indicated feed
* @throws AuthenticationException if the authToken is not valid
* @throws ParseException if the response from the server could not be parsed
*/
public GDataParser getParserForWorksheetsFeed(
String feedUri, String authToken)
throws AuthenticationException, ParseException, IOException {
return getParserForTypedFeed(WorksheetEntry.class, feedUri, authToken);
}
/**
* Updates an entry. The URI to be updated is taken from <code>entry</code>.
* Note that only entries in List and Cells feeds can be updated, so
* <code>entry</code> must be of the corresponding type; other types will
* result in an exception.
*
* @param entry the entry to be updated; must include its URI
* @param authToken the current authToken to be used for the operation
* @return An Entry containing the re-parsed version of the entry returned by
* the server in response to the update
* @throws ParseException if the server returned an error, if the server's
* response was unparseable (unlikely), or if <code>entry</code> is of
* a read-only type
* @throws IOException on network error
*/
@Override
public Entry updateEntry(Entry entry, String authToken)
throws ParseException, IOException {
GDataParserFactory factory = getGDataParserFactory();
GDataSerializer serializer = factory.createSerializer(entry);
String editUri = entry.getEditUri();
if (StringUtils.isEmpty(editUri)) {
throw new ParseException("No edit URI -- cannot update.");
}
InputStream is;
try {
is = getGDataClient().updateEntry(editUri, authToken, serializer);
} catch (HttpException e) {
convertHttpExceptionForWrites(entry.getClass(),
"Could not update entry.", e);
return null; // never reached
}
GDataParser parser = factory.createParser(entry.getClass(), is);
try {
return parser.parseStandaloneEntry();
} finally {
parser.close();
}
}
/**
* Converts an HTTP exception which happened while writing to the equivalent
* local exception.
*/
@SuppressWarnings("unchecked")
private void convertHttpExceptionForWrites(
Class entryClass, String message, HttpException cause)
throws ParseException, IOException {
switch (cause.getStatusCode()) {
case HttpException.SC_CONFLICT:
if (entryClass != null) {
InputStream is = cause.getResponseStream();
if (is != null) {
parseEntry(entryClass, cause.getResponseStream());
}
}
throw new IOException(message);
case HttpException.SC_BAD_REQUEST:
throw new ParseException(message + ": " + cause);
case HttpException.SC_FORBIDDEN:
case HttpException.SC_UNAUTHORIZED:
throw new IOException(message);
default:
throw new IOException(message + ": " + cause.getMessage());
}
}
/**
* Parses one entry from the input stream.
*/
@SuppressWarnings("unchecked")
private Entry parseEntry(Class entryClass, InputStream is)
throws ParseException, IOException {
GDataParser parser = null;
try {
parser = getGDataParserFactory().createParser(entryClass, is);
return parser.parseStandaloneEntry();
} finally {
if (parser != null) {
parser.close();
}
}
}
}
@@ -0,0 +1,72 @@
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.io.gdata.docs;
import com.google.wireless.gdata.client.GDataParserFactory;
import com.google.wireless.gdata.data.Entry;
import com.google.wireless.gdata.parser.GDataParser;
import com.google.wireless.gdata.parser.ParseException;
import com.google.wireless.gdata.parser.xml.XmlGDataParser;
import com.google.wireless.gdata.parser.xml.XmlParserFactory;
import com.google.wireless.gdata.serializer.GDataSerializer;
import com.google.wireless.gdata.serializer.xml.XmlEntryGDataSerializer;
import java.io.InputStream;
import org.xmlpull.v1.XmlPullParserException;
/**
* Factory of Xml parsers for gdata maps data.
*/
public class XmlDocsGDataParserFactory implements GDataParserFactory {
private XmlParserFactory xmlFactory;
public XmlDocsGDataParserFactory(XmlParserFactory xmlFactory) {
this.xmlFactory = xmlFactory;
}
@Override
public GDataParser createParser(InputStream is) throws ParseException {
try {
return new XmlGDataParser(is, xmlFactory.createParser());
} catch (XmlPullParserException e) {
e.printStackTrace();
return null;
}
}
@SuppressWarnings("unchecked")
@Override
public GDataParser createParser(Class cls, InputStream is)
throws ParseException {
try {
return createParserForClass(is);
} catch (XmlPullParserException e) {
e.printStackTrace();
return null;
}
}
private GDataParser createParserForClass(InputStream is)
throws ParseException, XmlPullParserException {
return new XmlGDataParser(is, xmlFactory.createParser());
}
@Override
public GDataSerializer createSerializer(Entry en) {
return new XmlEntryGDataSerializer(xmlFactory, en);
}
}
@@ -1,5 +1,5 @@
// Copyright 2010 Google Inc. All Rights Reserved.
package com.google.android.apps.mytracks.io.maps;
package com.google.android.apps.mytracks.io.gdata.maps;
import com.google.wireless.gdata.data.Entry;
@@ -9,7 +9,7 @@ import java.util.Map;
/**
* GData entry for a map feature.
*/
class MapFeatureEntry extends Entry {
public class MapFeatureEntry extends Entry {
private String mPrivacy = null;
private Map<String, String> mAttributes = new HashMap<String, String>();
@@ -1,5 +1,5 @@
// Copyright 2010 Google Inc. All Rights Reserved.
package com.google.android.apps.mytracks.io.maps;
package com.google.android.apps.mytracks.io.gdata.maps;
import com.google.wireless.gdata.client.GDataClient;
import com.google.wireless.gdata.client.GDataParserFactory;
@@ -10,7 +10,7 @@ import android.util.Log;
/**
* Client to talk to Google Maps via GData.
*/
class MapsClient extends GDataServiceClient {
public class MapsClient extends GDataServiceClient {
private static final boolean DEBUG = false;
public static final boolean LOG_COMMUNICATION = false;
@@ -1,11 +1,10 @@
// Copyright 2009 Google Inc. All Rights Reserved.
package com.google.android.apps.mytracks.io.maps;
package com.google.android.apps.mytracks.io.gdata.maps;
/**
* Constants for Google Maps.
*/
public class MapsConstants {
static final String TAG = "MapsApi";
static final String MAPSHOP_BASE_URL =
"https://maps.google.com/maps/ms";
public static final String SERVICE_NAME = "local";
@@ -1,5 +1,5 @@
// Copyright 2009 Google Inc. All Rights Reserved.
package com.google.android.apps.mytracks.io.maps;
package com.google.android.apps.mytracks.io.gdata.maps;
import com.google.android.maps.GeoPoint;
@@ -12,7 +12,7 @@ import java.util.Vector;
* in a {@link MapsFeatureMetadata} object so that it can be more efficiently
* transmitted to other activities.
*/
class MapsFeature {
public class MapsFeature {
private static final long serialVersionUID = 8439035544430497236L;
@@ -73,7 +73,7 @@ class MapsFeature {
* Generates a new local id for this feature based on the current time and
* a random number.
*/
void generateAndroidId() {
public void generateAndroidId() {
long time = System.currentTimeMillis();
int rand = random.nextInt(10000);
androidId = time + "." + rand;
@@ -84,7 +84,7 @@ class MapsFeature {
*
* @return The local id for this feature
*/
String getAndroidId() {
public String getAndroidId() {
return androidId;
}
@@ -1,5 +1,6 @@
// Copyright 2009 Google Inc. All Rights Reserved.
package com.google.android.apps.mytracks.io.maps;
package com.google.android.apps.mytracks.io.gdata.maps;
/**
* Metadata about a maps feature.
@@ -1,5 +1,5 @@
// Copyright 2009 Google Inc. All Rights Reserved.
package com.google.android.apps.mytracks.io.maps;
package com.google.android.apps.mytracks.io.gdata.maps;
import com.google.wireless.gdata.data.Entry;
import com.google.wireless.gdata.data.StringUtils;
@@ -17,7 +17,7 @@ import org.xmlpull.v1.XmlSerializer;
/**
* Converter from GData objects to Maps objects.
*/
class MapsGDataConverter {
public class MapsGDataConverter {
private final XmlSerializer xmlSerializer;
@@ -34,11 +34,7 @@ class MapsGDataConverter {
metadata.setSearchable(false);
}
metadata.setTitle(entry.getTitle());
String desc = entry.getContent();
if (desc.length() > 12) {
metadata.setDescription(desc.substring(9, desc.length() - 3));
}
metadata.setDescription(entry.getSummary());
String editUri = entry.getEditUri();
if (editUri != null) {
@@ -56,7 +52,7 @@ class MapsGDataConverter {
MapFeatureEntry entry = new MapFeatureEntry();
entry.setEditUri(metadata.getGDataEditUri());
entry.setTitle(metadata.getTitle());
entry.setContent(metadata.getDescription());
entry.setSummary(metadata.getDescription());
entry.setPrivacy(metadata.getSearchable() ? "public" : "unlisted");
entry.setAuthor("android");
entry.setEmail("nobody@google.com");
@@ -1,10 +1,10 @@
// Copyright 2009 Google Inc. All Rights Reserved.
package com.google.android.apps.mytracks.io.maps;
package com.google.android.apps.mytracks.io.gdata.maps;
/**
* Metadata about a Google Maps map.
*/
class MapsMapMetadata {
public class MapsMapMetadata {
private String title;
private String description;
@@ -1,5 +1,5 @@
// Copyright 2009 Google Inc. All Rights Reserved.
package com.google.android.apps.mytracks.io.maps;
package com.google.android.apps.mytracks.io.gdata.maps;
import com.google.wireless.gdata.data.Entry;
import com.google.wireless.gdata.data.Feed;
@@ -1,5 +1,5 @@
// Copyright 2010 Google Inc. All Rights Reserved.
package com.google.android.apps.mytracks.io.maps;
package com.google.android.apps.mytracks.io.gdata.maps;
import com.google.wireless.gdata.client.GDataParserFactory;
import com.google.wireless.gdata.data.Entry;
@@ -21,7 +21,7 @@ import org.xmlpull.v1.XmlPullParserException;
/**
* Factory of Xml parsers for gdata maps data.
*/
class XmlMapsGDataParserFactory implements GDataParserFactory {
public class XmlMapsGDataParserFactory implements GDataParserFactory {
private XmlParserFactory xmlFactory;
public XmlMapsGDataParserFactory(XmlParserFactory xmlFactory) {
@@ -1,5 +1,5 @@
// Copyright 2010 Google Inc. All Rights Reserved.
package com.google.android.apps.mytracks.io.maps;
package com.google.android.apps.mytracks.io.gdata.maps;
import com.google.wireless.gdata.data.StringUtils;
import com.google.wireless.gdata.parser.ParseException;
@@ -0,0 +1,256 @@
/*
* Copyright 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.io.maps;
import com.google.android.apps.mytracks.io.gdata.maps.MapsMapMetadata;
import com.google.android.apps.mytracks.io.sendtogoogle.SendRequest;
import com.google.android.maps.mytracks.R;
import com.google.common.annotations.VisibleForTesting;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.ProgressDialog;
import android.content.DialogInterface;
import android.content.DialogInterface.OnCancelListener;
import android.content.DialogInterface.OnClickListener;
import android.content.Intent;
import android.graphics.Color;
import android.os.Bundle;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.TextView;
import java.util.ArrayList;
/**
* An activity to choose a Google Map.
*
* @author Jimmy Shih
*/
public class ChooseMapActivity extends Activity {
private static final int PROGRESS_DIALOG = 1;
@VisibleForTesting
static final int ERROR_DIALOG = 2;
private SendRequest sendRequest;
private ChooseMapAsyncTask asyncTask;
private ProgressDialog progressDialog;
@VisibleForTesting
ArrayAdapter<ListItem> arrayAdapter;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
sendRequest = getIntent().getParcelableExtra(SendRequest.SEND_REQUEST_KEY);
setContentView(R.layout.choose_map);
arrayAdapter = new ArrayAdapter<ListItem>(this, R.layout.choose_map_item, new ArrayList<
ListItem>()) {
@Override
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null) {
convertView = getLayoutInflater().inflate(R.layout.choose_map_item, parent, false);
}
MapsMapMetadata mapData = getItem(position).getMapData();
TextView title = (TextView) convertView.findViewById(R.id.choose_map_list_item_title);
title.setText(mapData.getTitle());
TextView description = (TextView) convertView.findViewById(
R.id.choose_map_list_item_description);
String descriptionText = mapData.getDescription();
if (descriptionText == null || descriptionText.equals("")) {
description.setVisibility(View.GONE);
} else {
description.setVisibility(View.VISIBLE);
description.setText(descriptionText);
}
TextView searchStatus = (TextView) convertView.findViewById(
R.id.choose_map_list_item_search_status);
searchStatus.setTextColor(mapData.getSearchable() ? Color.RED : Color.GREEN);
searchStatus.setText(mapData.getSearchable() ? R.string.maps_list_public_label
: R.string.maps_list_unlisted_label);
return convertView;
}
};
ListView list = (ListView) findViewById(R.id.choose_map_list_view);
list.setEmptyView(findViewById(R.id.choose_map_empty_view));
list.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
startNextActivity(arrayAdapter.getItem(position).getMapId());
}
});
list.setAdapter(arrayAdapter);
Object retained = getLastNonConfigurationInstance();
if (retained instanceof ChooseMapAsyncTask) {
asyncTask = (ChooseMapAsyncTask) retained;
asyncTask.setActivity(this);
} else {
asyncTask = new ChooseMapAsyncTask(this, sendRequest.getAccount());
asyncTask.execute();
}
}
@Override
public Object onRetainNonConfigurationInstance() {
asyncTask.setActivity(null);
return asyncTask;
}
@Override
protected Dialog onCreateDialog(int id) {
switch (id) {
case PROGRESS_DIALOG:
progressDialog = new ProgressDialog(this);
progressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER);
progressDialog.setMessage(getString(R.string.maps_list_loading));
progressDialog.setCancelable(true);
progressDialog.setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
asyncTask.cancel(true);
finish();
}
});
progressDialog.setIcon(android.R.drawable.ic_dialog_info);
progressDialog.setTitle(R.string.generic_progress_title);
return progressDialog;
case ERROR_DIALOG:
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setIcon(android.R.drawable.ic_dialog_alert);
builder.setTitle(R.string.generic_error_title);
builder.setMessage(R.string.maps_list_error);
builder.setPositiveButton(R.string.generic_ok, new OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int arg1) {
finish();
}
});
builder.setOnCancelListener(new OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
finish();
}
});
return builder.create();
default:
return null;
}
}
/**
* Invokes when the associated AsyncTask completes.
*
* @param success true if success
* @param mapIds an array of map ids
* @param mapData an array of map data
*/
public void onAsyncTaskCompleted(
boolean success, ArrayList<String> mapIds, ArrayList<MapsMapMetadata> mapData) {
removeProgressDialog();
if (success) {
arrayAdapter.clear();
// To prevent displaying the emptyView message momentarily before the
// arrayAdapter is set, don't set the emptyView message in the xml layout.
// Instead, set it only when needed.
if (mapIds.size() == 0) {
TextView emptyView = (TextView) findViewById(R.id.choose_map_empty_view);
emptyView.setText(R.string.maps_list_no_maps);
} else {
for (int i = 0; i < mapIds.size(); i++) {
arrayAdapter.add(new ListItem(mapIds.get(i), mapData.get(i)));
}
}
} else {
showErrorDialog();
}
}
/**
* Shows the progress dialog.
*/
public void showProgressDialog() {
showDialog(PROGRESS_DIALOG);
}
/**
* Shows the error dialog.
*/
@VisibleForTesting
void showErrorDialog() {
showDialog(ERROR_DIALOG);
}
/**
* Remove the progress dialog.
*/
@VisibleForTesting
void removeProgressDialog() {
removeDialog(PROGRESS_DIALOG);
}
/**
* Starts the next activity, {@link SendMapsActivity}.
*
* @param mapId the chosen map id
*/
private void startNextActivity(String mapId) {
sendRequest.setMapId(mapId);
Intent intent = new Intent(this, SendMapsActivity.class)
.putExtra(SendRequest.SEND_REQUEST_KEY, sendRequest);
startActivity(intent);
finish();
}
/**
* A class containing {@link ChooseMapActivity} list item.
*
* @author Jimmy Shih
*/
@VisibleForTesting
class ListItem {
private String mapId;
private MapsMapMetadata mapData;
private ListItem(String mapId, MapsMapMetadata mapData) {
this.mapId = mapId;
this.mapData = mapData;
}
/**
* Gets the map id.
*/
public String getMapId() {
return mapId;
}
/**
* Gets the map data.
*/
public MapsMapMetadata getMapData() {
return mapData;
}
}
}
@@ -0,0 +1,209 @@
/*
* Copyright 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.io.maps;
import com.google.android.apps.mytracks.Constants;
import com.google.android.apps.mytracks.io.gdata.GDataClientFactory;
import com.google.android.apps.mytracks.io.gdata.maps.MapFeatureEntry;
import com.google.android.apps.mytracks.io.gdata.maps.MapsClient;
import com.google.android.apps.mytracks.io.gdata.maps.MapsConstants;
import com.google.android.apps.mytracks.io.gdata.maps.MapsGDataConverter;
import com.google.android.apps.mytracks.io.gdata.maps.MapsMapMetadata;
import com.google.android.apps.mytracks.io.gdata.maps.XmlMapsGDataParserFactory;
import com.google.android.common.gdata.AndroidXmlParserFactory;
import com.google.wireless.gdata.client.GDataClient;
import com.google.wireless.gdata.client.HttpException;
import com.google.wireless.gdata.parser.GDataParser;
import com.google.wireless.gdata.parser.ParseException;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.accounts.AuthenticatorException;
import android.accounts.OperationCanceledException;
import android.content.Context;
import android.os.AsyncTask;
import android.util.Log;
import java.io.IOException;
import java.util.ArrayList;
/**
* AsyncTask for {@link ChooseMapActivity} to get all the maps from Google Maps.
*
* @author Jimmy Shih
*/
public class ChooseMapAsyncTask extends AsyncTask<Void, Integer, Boolean> {
private static final String TAG = ChooseMapAsyncTask.class.getSimpleName();
private ChooseMapActivity activity;
private final Account account;
private final Context context;
private final GDataClient gDataClient;
private final MapsClient mapsClient;
/**
* True if can retry sending to Google Fusion Tables.
*/
private boolean canRetry;
/**
* True if the AsyncTask has completed.
*/
private boolean completed;
/**
* True if the result is success.
*/
private boolean success;
// The following variables are for per request states
private String authToken;
private ArrayList<String> mapIds;
private ArrayList<MapsMapMetadata> mapData;
public ChooseMapAsyncTask(ChooseMapActivity activity, Account account) {
this.activity = activity;
this.account = account;
context = activity.getApplicationContext();
gDataClient = GDataClientFactory.getGDataClient(context);
mapsClient = new MapsClient(
gDataClient, new XmlMapsGDataParserFactory(new AndroidXmlParserFactory()));
canRetry = true;
completed = false;
success = false;
}
/**
* Sets the activity associated with this AyncTask.
*
* @param activity the activity.
*/
public void setActivity(ChooseMapActivity activity) {
this.activity = activity;
if (completed && activity != null) {
activity.onAsyncTaskCompleted(success, mapIds, mapData);
}
}
@Override
protected void onPreExecute() {
activity.showProgressDialog();
}
@Override
protected Boolean doInBackground(Void... params) {
return getMaps();
}
@Override
protected void onCancelled() {
closeClient();
}
@Override
protected void onPostExecute(Boolean result) {
closeClient();
success = result;
completed = true;
if (activity != null) {
activity.onAsyncTaskCompleted(success, mapIds, mapData);
}
}
/**
* Closes the gdata client.
*/
private void closeClient() {
if (gDataClient != null) {
gDataClient.close();
}
}
/**
* Gets all the maps from Google Maps.
*
* @return true if success.
*/
private boolean getMaps() {
// Reset the per request states
authToken = null;
mapIds = new ArrayList<String>();
mapData = new ArrayList<MapsMapMetadata>();
try {
authToken = AccountManager.get(context).blockingGetAuthToken(
account, MapsConstants.SERVICE_NAME, false);
} catch (OperationCanceledException e) {
Log.d(TAG, "Unable to get auth token", e);
return retryUpload();
} catch (AuthenticatorException e) {
Log.d(TAG, "Unable to get auth token", e);
return retryUpload();
} catch (IOException e) {
Log.d(TAG, "Unable to get auth token", e);
return retryUpload();
}
if (isCancelled()) {
return false;
}
GDataParser gDataParser = null;
try {
gDataParser = mapsClient.getParserForFeed(
MapFeatureEntry.class, MapsClient.getMapsFeed(), authToken);
gDataParser.init();
while (gDataParser.hasMoreData()) {
MapFeatureEntry entry = (MapFeatureEntry) gDataParser.readNextEntry(null);
mapIds.add(MapsGDataConverter.getMapidForEntry(entry));
mapData.add(MapsGDataConverter.getMapMetadataForEntry(entry));
}
} catch (ParseException e) {
Log.d(TAG, "Unable to get maps", e);
return retryUpload();
} catch (IOException e) {
Log.d(TAG, "Unable to get maps", e);
return retryUpload();
} catch (HttpException e) {
Log.d(TAG, "Unable to get maps", e);
return retryUpload();
} finally {
if (gDataParser != null) {
gDataParser.close();
}
}
return true;
}
/**
* Retries upload. Invalidates the authToken. If can retry, invokes
* {@link ChooseMapAsyncTask#getMaps()}. Returns false if cannot retry.
*/
private boolean retryUpload() {
if (isCancelled()) {
return false;
}
AccountManager.get(context).invalidateAuthToken(Constants.ACCOUNT_TYPE, authToken);
if (canRetry) {
canRetry = false;
return getMaps();
}
return false;
}
}
@@ -1,50 +0,0 @@
// Copyright 2009 Google Inc. All Rights Reserved.
package com.google.android.apps.mytracks.io.maps;
import com.google.android.maps.GeoPoint;
/**
* A rectangle in geographical space.
*/
class GeoRect {
public int top;
public int left;
public int bottom;
public int right;
public GeoRect() {
top = 0;
left = 0;
bottom = 0;
right = 0;
}
public GeoRect(GeoPoint center, int latSpan, int longSpan) {
top = center.getLatitudeE6() - latSpan / 2;
left = center.getLongitudeE6() - longSpan / 2;
bottom = center.getLatitudeE6() + latSpan / 2;
right = center.getLongitudeE6() + longSpan / 2;
}
public GeoPoint getCenter() {
return new GeoPoint(top / 2 + bottom / 2, left / 2 + right / 2);
}
public int getLatSpan() {
return bottom - top;
}
public int getLongSpan() {
return right - left;
}
public boolean contains(GeoPoint geoPoint) {
if (geoPoint.getLatitudeE6() >= top
&& geoPoint.getLatitudeE6() <= bottom
&& geoPoint.getLongitudeE6() >= left
&& geoPoint.getLongitudeE6() <= right) {
return true;
}
return false;
}
}
@@ -1,379 +0,0 @@
// Copyright 2011 Google Inc. All Rights Reserved.
package com.google.android.apps.mytracks.io.maps;
import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.io.AuthManager;
import com.google.android.apps.mytracks.util.StringUtils;
import com.google.android.maps.GeoPoint;
import com.google.android.maps.mytracks.R;
import com.google.wireless.gdata.client.HttpException;
import com.google.wireless.gdata.data.Entry;
import com.google.wireless.gdata.parser.GDataParser;
import com.google.wireless.gdata.parser.ParseException;
import android.content.Context;
import android.location.Location;
import android.text.TextUtils;
import android.util.Log;
import java.io.IOException;
import java.util.Collection;
import org.xmlpull.v1.XmlPullParserException;
/**
* Single interface which abstracts all access to the Google Maps service.
*
* @author Rodrigo Damazio
*/
public class MapsFacade {
/**
* Interface for receiving data back from getMapsList.
* All calls to the interface will happen before getMapsList returns.
*/
public interface MapsListCallback {
void onReceivedMapListing(String mapId, String title, String description,
boolean isPublic);
}
private static final String END_ICON_URL =
"http://maps.google.com/mapfiles/ms/micons/red-dot.png";
private static final String START_ICON_URL =
"http://maps.google.com/mapfiles/ms/micons/green-dot.png";
private final Context context;
private final MapsGDataWrapper wrapper;
private final MapsGDataConverter gdataConverter;
private final String authToken;
public MapsFacade(Context context, AuthManager auth) {
this.context = context;
this.authToken = auth.getAuthToken();
wrapper = new MapsGDataWrapper(context, auth);
wrapper.setRetryOnAuthFailure(true);
try {
gdataConverter = new MapsGDataConverter();
} catch (XmlPullParserException e) {
throw new IllegalStateException("Unable to create maps data converter", e);
}
}
public static String buildMapUrl(String mapId) {
return MapsClient.buildMapUrl(mapId);
}
/**
* Returns a list of all maps for the current user.
*
* @param callback callback to call for each map returned
* @return true on success, false otherwise
*/
public boolean getMapsList(final MapsListCallback callback) {
return wrapper.runQuery(new MapsGDataWrapper.QueryFunction() {
@Override
public void query(MapsClient client) throws IOException, Exception {
GDataParser listParser = client.getParserForFeed(
MapFeatureEntry.class, MapsClient.getMapsFeed(), authToken);
listParser.init();
while (listParser.hasMoreData()) {
MapFeatureEntry entry =
(MapFeatureEntry) listParser.readNextEntry(null);
MapsMapMetadata metadata =
MapsGDataConverter.getMapMetadataForEntry(entry);
String mapId = MapsGDataConverter.getMapidForEntry(entry);
callback.onReceivedMapListing(
mapId, metadata.getTitle(), metadata.getDescription(), metadata.getSearchable());
}
listParser.close();
listParser = null;
}
});
}
/**
* Creates a new map for the current user.
*
* @param title title of the map
* @param category category of the map
* @param description description for the map
* @param isPublic whether the map should be public
* @param mapIdBuilder builder to append the map ID to
* @return true on success, false otherwise
*/
public boolean createNewMap(
final String title, final String category, final String description,
final boolean isPublic, final StringBuilder mapIdBuilder) {
if (mapIdBuilder.length() > 0) {
throw new IllegalArgumentException("mapIdBuilder should be empty");
}
return wrapper.runQuery(new MapsGDataWrapper.QueryFunction() {
@Override
public void query(MapsClient client) throws IOException, Exception {
Log.d(MapsConstants.TAG, "Creating a new map.");
String mapFeed = MapsClient.getMapsFeed();
Log.d(MapsConstants.TAG, "Map feed is " + mapFeed);
MapsMapMetadata metaData = new MapsMapMetadata();
metaData.setTitle(title);
metaData.setDescription(description + " - "
+ category + " - " + StringUtils.getCreatedByMyTracks(context, false));
metaData.setSearchable(isPublic);
Entry entry = MapsGDataConverter.getMapEntryForMetadata(metaData);
Log.d(MapsConstants.TAG, "Title: " + entry.getTitle());
Entry map = client.createEntry(mapFeed, authToken, entry);
String mapId = MapsClient.getMapIdFromMapEntryId(map.getId());
mapIdBuilder.append(mapId);
Log.d(MapsConstants.TAG, "New map id is: " + mapId);
}
});
}
/**
* Uploads a single start or end marker to the given map.
*
* @param mapId ID of the map to upload to
* @param trackName name of the track being started/ended
* @param trackDescription description of the track being started/ended
* @param loc the location of the marker
* @param isStart true to add a start marker, false to add an end marker
* @return true on success, false otherwise
*/
public boolean uploadMarker(final String mapId, final String trackName,
final String trackDescription, final Location loc, final boolean isStart) {
return wrapper.runQuery(new MapsGDataWrapper.QueryFunction() {
@Override
public void query(MapsClient client)
throws IOException, Exception {
String featureFeed = MapsClient.getFeaturesFeed(mapId);
GeoPoint geoPoint = getGeoPoint(loc);
insertMarker(client, featureFeed, trackName, trackDescription, geoPoint, isStart);
}
});
}
/**
* Inserts a place mark. Second try if 1st try fails. Will throw exception on
* 2nd failure.
*/
private void insertMarker(MapsClient client,
String featureFeed,
String trackName, String trackDescription,
GeoPoint geoPoint,
boolean isStart) throws IOException, Exception {
MapsFeature feature =
buildMapsPlacemarkFeature(trackName, trackDescription, geoPoint, isStart);
Entry entry = gdataConverter.getEntryForFeature(feature);
Log.d(MapsConstants.TAG, "SendToMaps: Creating placemark "
+ entry.getTitle());
try {
client.createEntry(featureFeed, authToken, entry);
Log.d(MapsConstants.TAG, "SendToMaps: createEntry success!");
} catch (IOException e) {
Log.w(MapsConstants.TAG,
"SendToMaps: createEntry 1st try failed. Trying again.");
// Retry once (often IOException is thrown on a timeout):
client.createEntry(featureFeed, authToken, entry);
Log.d(MapsConstants.TAG,
"SendToMaps: createEntry success on 2nd try!");
}
}
/**
* Builds a placemark MapsFeature from a track.
*
* @param trackName the track
* @param trackDescription the track description
* @param geoPoint the geo point
* @param isStart true if it's the start of the track, or false for end
* @return a MapsFeature
*/
private MapsFeature buildMapsPlacemarkFeature(
String trackName, String trackDescription,
GeoPoint geoPoint, boolean isStart) {
String iconUrl;
if (isStart) {
iconUrl = START_ICON_URL;
} else {
iconUrl = END_ICON_URL;
}
String title = trackName + " "
+ (isStart ? context.getString(R.string.marker_label_start)
: context.getString(R.string.marker_label_end));
String description = isStart ? "" : trackDescription;
return buildMapsPlacemarkFeature(title, description, iconUrl, geoPoint);
}
/**
* Builds a MapsFeature from a waypoint.
*
* @param title the title
* @param description the description
* @param iconUrl the icon url
* @param geoPoint the waypoint
* @return a MapsFeature
*/
private static MapsFeature buildMapsPlacemarkFeature(
String title, String description, String iconUrl, GeoPoint geoPoint) {
MapsFeature mapsFeature = new MapsFeature();
mapsFeature.generateAndroidId();
mapsFeature.setType(MapsFeature.MARKER);
mapsFeature.setIconUrl(iconUrl);
mapsFeature.setDescription(description);
mapsFeature.addPoint(geoPoint);
if (TextUtils.isEmpty(title)) {
// Features must have a name (otherwise GData upload may fail):
mapsFeature.setTitle("-");
} else {
mapsFeature.setTitle(title);
}
mapsFeature.setDescription(description.replaceAll("\n", "<br>"));
return mapsFeature;
}
/**
* Uploads a series of waypoints to the given map.
*
* @param mapId ID of the map to upload to
* @param waypoints the waypoints to upload
* @return true on success, false otherwise
*/
public boolean uploadWaypoints(
final String mapId, final Iterable<Waypoint> waypoints) {
return wrapper.runQuery(new MapsGDataWrapper.QueryFunction() {
public void query(MapsClient client) {
// TODO(rdamazio): 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.
String featureFeed = MapsClient.getFeaturesFeed(mapId);
try {
for (Waypoint waypoint : waypoints) {
MapsFeature feature = buildMapsPlacemarkFeature(
waypoint.getName(), waypoint.getDescription(), waypoint.getIcon(),
getGeoPoint(waypoint.getLocation()));
Entry entry = gdataConverter.getEntryForFeature(feature);
Log.d(MapsConstants.TAG,
"SendToMaps: Creating waypoint.");
try {
client.createEntry(featureFeed, authToken, entry);
Log.d(MapsConstants.TAG,
"SendToMaps: createEntry success!");
} catch (IOException e) {
Log.w(MapsConstants.TAG,
"SendToMaps: createEntry 1st try failed. Retrying.");
// Retry once (often IOException is thrown on a timeout):
client.createEntry(featureFeed, authToken, entry);
Log.d(MapsConstants.TAG,
"SendToMaps: createEntry success on 2nd try!");
}
}
} catch (ParseException e) {
Log.w(MapsConstants.TAG, "ParseException caught.", e);
} catch (HttpException e) {
Log.w(MapsConstants.TAG, "HttpException caught.", e);
} catch (IOException e) {
Log.w(MapsConstants.TAG, "IOException caught.", e);
}
}
});
}
/**
* Uploads a series of points to the given map.
*
* @param mapId ID of the map to upload to
* @param trackName the name of the track
* @param locations the locations to upload
* @return true on success, false otherwise
*/
public boolean uploadTrackPoints(
final String mapId, final String trackName, final Collection<Location> locations) {
return wrapper.runQuery(new MapsGDataWrapper.QueryFunction() {
@Override
public void query(MapsClient client)
throws IOException, Exception {
String featureFeed = MapsClient.getFeaturesFeed(mapId);
Log.d(MapsConstants.TAG, "Feature feed url: " + featureFeed);
uploadTrackPoints(client, featureFeed, trackName, locations);
}
});
}
private boolean uploadTrackPoints(
MapsClient client,
String featureFeed,
String trackName, Collection<Location> locations)
throws IOException, Exception {
Entry entry = null;
int numLocations = locations.size();
if (numLocations < 2) {
// Need at least two points for a polyline:
Log.w(MapsConstants.TAG, "Not uploading too few points");
return true;
}
// Put the line:
entry = gdataConverter.getEntryForFeature(
buildMapsLineFeature(trackName, locations));
Log.d(MapsConstants.TAG,
"SendToMaps: Creating line " + entry.getTitle());
try {
client.createEntry(featureFeed, authToken, entry);
Log.d(MapsConstants.TAG, "SendToMaps: createEntry success!");
} catch (IOException e) {
Log.w(MapsConstants.TAG,
"SendToMaps: createEntry 1st try failed. Trying again.");
// Retry once (often IOException is thrown on a timeout):
client.createEntry(featureFeed, authToken, entry);
Log.d(MapsConstants.TAG,
"SendToMaps: createEntry success on 2nd try!");
}
return true;
}
/**
* Builds a MapsFeature from a track.
*
* @param trackName the track name
* @param locations locations on the track
* @return a MapsFeature
*/
private static MapsFeature buildMapsLineFeature(String trackName,
Iterable<Location> locations) {
MapsFeature mapsFeature = new MapsFeature();
mapsFeature.generateAndroidId();
mapsFeature.setType(MapsFeature.LINE);
if (TextUtils.isEmpty(trackName)) {
// Features must have a name (otherwise GData upload may fail):
mapsFeature.setTitle("-");
} else {
mapsFeature.setTitle(trackName);
}
mapsFeature.setColor(0x80FF0000);
for (Location loc : locations) {
mapsFeature.addPoint(getGeoPoint(loc));
}
return mapsFeature;
}
/**
* Cleans up after a series of uploads.
* This closes the connection to Maps and resets retry counters.
*/
public void cleanUp() {
wrapper.cleanUp();
}
private static GeoPoint getGeoPoint(Location location) {
return new GeoPoint((int) (location.getLatitude() * 1E6),
(int) (location.getLongitude() * 1E6));
}
}
@@ -1,171 +0,0 @@
// Copyright 2010 Google Inc. All Rights Reserved.
package com.google.android.apps.mytracks.io.maps;
import com.google.android.apps.mytracks.io.AuthManager;
import com.google.android.apps.mytracks.io.AuthManager.AuthCallback;
import com.google.android.apps.mytracks.io.gdata.GDataClientFactory;
import com.google.android.common.gdata.AndroidXmlParserFactory;
import com.google.wireless.gdata.client.GDataClient;
import com.google.wireless.gdata.client.HttpException;
import com.google.wireless.gdata.parser.xml.SimplePullParser.ParseException;
import com.google.wireless.gdata2.ConflictDetectedException;
import com.google.wireless.gdata2.client.AuthenticationException;
import android.content.Context;
import android.util.Log;
import java.io.IOException;
/**
* MapsGDataWrapper provides a wrapper around GData operations that maintains
* the GData client, and provides a method to run gdata queries with proper
* error handling. After a query is run, the wrapper can be queried about the
* error that occurred.
*/
class MapsGDataWrapper {
/**
* A QueryFunction is passed in when executing a query. The query function
* of the class is called with the GData client as a parameter. The
* function should execute whatever operations it desires on the client
* without concern for whether the client will throw an error.
*/
interface QueryFunction {
public abstract void query(MapsClient client)
throws AuthenticationException, IOException, ParseException,
ConflictDetectedException, Exception;
}
// The types of error that may be encountered
/** No error occurred. */
public static final int ERROR_NO_ERROR = 0;
/** There was an authentication error, the auth token may be invalid. */
public static final int ERROR_AUTH = 1;
/** There was an internal error on the server side. */
public static final int ERROR_INTERNAL = 2;
/** There was an error connecting to the server. */
public static final int ERROR_CONNECTION = 3;
/** The item queried did not exit. */
public static final int ERROR_NOT_FOUND = 4;
/** There was an error parsing or serializing locally. */
public static final int ERROR_LOCAL = 5;
/** There was a conflict, update the entry and try again. */
public static final int ERROR_CONFLICT = 6;
/**
* A query was run after cleaning up the wrapper, so the client was invalid.
*/
public static final int ERROR_CLEANED_UP = 7;
/** An unknown error occurred. */
public static final int ERROR_UNKNOWN = 100;
private final GDataClient androidGdataClient;
private final AuthManager auth;
private final MapsClient client;
private String errorMessage;
private int errorType;
private boolean retryOnAuthFailure;
private int retriesPending;
private boolean cleanupCalled;
public MapsGDataWrapper(Context context, AuthManager auth) {
androidGdataClient = GDataClientFactory.getGDataClient(context);
this.auth = auth;
client =
new MapsClient(androidGdataClient, new XmlMapsGDataParserFactory(
new AndroidXmlParserFactory()));
errorType = ERROR_NO_ERROR;
errorMessage = null;
retryOnAuthFailure = false;
retriesPending = 0;
cleanupCalled = false;
}
public boolean runQuery(final QueryFunction query) {
if (client == null) {
errorType = ERROR_CLEANED_UP;
errorMessage = "GData Wrapper has already been cleaned up!";
return false;
}
try {
query.query(client);
errorType = ERROR_NO_ERROR;
errorMessage = null;
return true;
} catch (AuthenticationException e) {
Log.e(MapsConstants.TAG, "Exception", e);
errorType = ERROR_AUTH;
errorMessage = e.getMessage();
} catch (HttpException e) {
Log.e(MapsConstants.TAG, "HttpException", e);
errorMessage = e.getMessage();
if (errorMessage != null && errorMessage.contains("401")) {
errorType = ERROR_AUTH;
} else {
errorType = ERROR_CONNECTION;
}
} catch (IOException e) {
Log.e(MapsConstants.TAG, "Exception", e);
errorMessage = e.getMessage();
if (errorMessage != null && errorMessage.contains("503")) {
errorType = ERROR_INTERNAL;
} else {
errorType = ERROR_CONNECTION;
}
} catch (ParseException e) {
Log.e(MapsConstants.TAG, "Exception", e);
errorType = ERROR_LOCAL;
errorMessage = e.getMessage();
} catch (ConflictDetectedException e) {
Log.e(MapsConstants.TAG, "Exception", e);
errorType = ERROR_CONFLICT;
errorMessage = e.getMessage();
} catch (Exception e) {
Log.e(MapsConstants.TAG, "Exception", e);
errorType = ERROR_UNKNOWN;
errorMessage = e.getMessage();
e.printStackTrace();
}
Log.d(MapsConstants.TAG, "GData error encountered: " + errorMessage);
if (errorType == ERROR_AUTH && auth != null) {
AuthCallback whenFinished = null;
if (retryOnAuthFailure) {
retriesPending++;
whenFinished = new AuthCallback() {
@Override
public void onAuthResult(boolean success) {
retriesPending--;
retryOnAuthFailure = false;
runQuery(query);
if (cleanupCalled && retriesPending == 0) {
cleanUp();
}
}
};
}
auth.invalidateAndRefresh(whenFinished);
}
return false;
}
public int getErrorType() {
return errorType;
}
public String getErrorMessage() {
return errorMessage;
}
// cleanUp must be called when done using this wrapper to close the client.
// Note that the cleanup will be delayed if auth failure retries were
// requested and there is a pending retry.
public void cleanUp() {
if (retriesPending == 0 && !cleanupCalled) {
androidGdataClient.close();
}
cleanupCalled = true;
}
public void setRetryOnAuthFailure(boolean retry) {
retryOnAuthFailure = retry;
}
}
@@ -0,0 +1,72 @@
/*
* Copyright 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.io.maps;
import com.google.android.apps.mytracks.io.docs.SendDocsActivity;
import com.google.android.apps.mytracks.io.fusiontables.SendFusionTablesActivity;
import com.google.android.apps.mytracks.io.sendtogoogle.AbstractSendActivity;
import com.google.android.apps.mytracks.io.sendtogoogle.AbstractSendAsyncTask;
import com.google.android.apps.mytracks.io.sendtogoogle.SendRequest;
import com.google.android.apps.mytracks.io.sendtogoogle.UploadResultActivity;
import com.google.android.maps.mytracks.R;
import 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(), sendRequest.getMapId());
}
@Override
protected String getServiceName() {
return getString(R.string.send_google_maps);
}
@Override
protected void startNextActivity(boolean success, boolean isCancel) {
sendRequest.setMapsSuccess(success);
Class<?> next = getNextClass(sendRequest, isCancel);
Intent intent = new Intent(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.isSendDocs()) {
return SendDocsActivity.class;
} else {
return UploadResultActivity.class;
}
}
}
}
@@ -0,0 +1,480 @@
/*
* 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.DoubleBuffer;
import com.google.android.apps.mytracks.stats.TripStatisticsBuilder;
import com.google.android.apps.mytracks.util.LocationUtils;
import com.google.android.apps.mytracks.util.UnitConversions;
import com.google.android.common.gdata.AndroidXmlParserFactory;
import com.google.android.maps.mytracks.R;
import com.google.wireless.gdata.client.GDataClient;
import com.google.wireless.gdata.client.HttpException;
import com.google.wireless.gdata.parser.ParseException;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.accounts.AuthenticatorException;
import android.accounts.OperationCanceledException;
import android.content.Context;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.location.Location;
import android.util.Log;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Vector;
import org.xmlpull.v1.XmlPullParserException;
/**
* AsyncTask to send a track to Google Maps.
* <p>
* IMPORTANT: While this code is Apache-licensed, please notice that usage of
* the Google Maps servers through this API is only allowed for the My Tracks
* application. Other applications looking to upload maps data should look into
* using the Google Fusion Tables API.
*
* @author Jimmy Shih
*/
public class SendMapsAsyncTask extends AbstractSendAsyncTask {
private static final String START_ICON_URL =
"http://maps.google.com/mapfiles/ms/micons/green-dot.png";
private static final String END_ICON_URL =
"http://maps.google.com/mapfiles/ms/micons/red-dot.png";
private static final int MAX_POINTS_PER_UPLOAD = 500;
private static final int PROGRESS_FETCH_MAP_ID = 5;
private static final int PROGRESS_UPLOAD_DATA_MIN = 10;
private static final int PROGRESS_UPLOAD_DATA_MAX = 90;
private static final int PROGRESS_UPLOAD_WAYPOINTS = 95;
private static final int PROGRESS_COMPLETE = 100;
private static final String TAG = SendMapsAsyncTask.class.getSimpleName();
private final long trackId;
private final Account account;
private final String chooseMapId;
private final Context context;
private final MyTracksProviderUtils myTracksProviderUtils;
private final GDataClient gDataClient;
private final MapsClient mapsClient;
// The following variables are for per upload states
private MapsGDataConverter mapsGDataConverter;
private String authToken;
private String mapId;
int currentSegment;
public SendMapsAsyncTask (
SendMapsActivity activity, long trackId, Account account, String chooseMapId) {
super(activity);
this.trackId = trackId;
this.account = account;
this.chooseMapId = chooseMapId;
context = activity.getApplicationContext();
myTracksProviderUtils = MyTracksProviderUtils.Factory.get(context);
gDataClient = GDataClientFactory.getGDataClient(context);
mapsClient = new MapsClient(
gDataClient, new XmlMapsGDataParserFactory(new AndroidXmlParserFactory()));
}
@Override
protected void closeConnection() {
if (gDataClient != null) {
gDataClient.close();
}
}
@Override
protected void saveResult() {
Track track = myTracksProviderUtils.getTrack(trackId);
if (track != null) {
track.setMapId(mapId);
myTracksProviderUtils.updateTrack(track);
} else {
Log.d(TAG, "No track");
}
}
@Override
protected boolean performTask() {
// Reset the per upload states
mapsGDataConverter = null;
authToken = null;
mapId = null;
currentSegment = 1;
// Create a maps gdata converter
try {
mapsGDataConverter = new MapsGDataConverter();
} catch (XmlPullParserException e) {
Log.d(TAG, "Unable to create a maps gdata converter", e);
return false;
}
// Get auth token
try {
authToken = AccountManager.get(context).blockingGetAuthToken(
account, MapsConstants.SERVICE_NAME, false);
} catch (OperationCanceledException e) {
Log.d(TAG, "Unable to get auth token", e);
return retryTask();
} catch (AuthenticatorException e) {
Log.d(TAG, "Unable to get auth token", e);
return retryTask();
} catch (IOException e) {
Log.d(TAG, "Unable to get auth token", e);
return retryTask();
}
// Get the track
Track track = myTracksProviderUtils.getTrack(trackId);
if (track == null) {
Log.d(TAG, "Track is null");
return false;
}
// Fetch the mapId, create a new map if necessary
publishProgress(PROGRESS_FETCH_MAP_ID);
if (!fetchSendMapId(track)) {
Log.d("TAG", "Unable to upload all track points");
return retryTask();
}
// Upload all the track points plus the start and end markers
publishProgress(PROGRESS_UPLOAD_DATA_MIN);
if (!uploadAllTrackPoints(track)) {
Log.d("TAG", "Unable to upload all track points");
return retryTask();
}
// Upload all the waypoints
publishProgress(PROGRESS_UPLOAD_WAYPOINTS);
if (!uploadWaypoints()) {
Log.d("TAG", "Unable to upload waypoints");
return false;
}
publishProgress(PROGRESS_COMPLETE);
return true;
}
@Override
protected void invalidateToken() {
AccountManager.get(context).invalidateAuthToken(Constants.ACCOUNT_TYPE, authToken);
}
/**
* Fetches the {@link SendMapsAsyncTask#mapId} instance variable for
* sending a track to Google Maps.
*
* @param track the Track
* @return true if able to fetch the mapId variable.
*/
private boolean fetchSendMapId(Track track) {
if (isCancelled()) {
return false;
}
if (chooseMapId != null) {
mapId = chooseMapId;
return true;
} else {
SharedPreferences sharedPreferences = context.getSharedPreferences(
Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
boolean mapPublic = sharedPreferences.getBoolean(
context.getString(R.string.default_map_public_key), true);
try {
String description = track.getCategory() + "\n" + track.getDescription() + "\n"
+ context.getString(R.string.send_google_by_my_tracks, "", "");
mapId = SendMapsUtils.createNewMap(
track.getName(), description, mapPublic, mapsClient, authToken);
} catch (ParseException e) {
Log.d(TAG, "Unable to create a new map", e);
return false;
} catch (HttpException e) {
Log.d(TAG, "Unable to create a new map", e);
return false;
} catch (IOException e) {
Log.d(TAG, "Unable to create a new map", e);
return false;
}
return mapId != null;
}
}
/**
* Uploads all the points in a track.
*
* @param track the track
* @return true if success.
*/
private boolean uploadAllTrackPoints(Track track) {
Cursor locationsCursor = null;
try {
SharedPreferences prefs = context.getSharedPreferences(
Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
boolean metricUnits = prefs.getBoolean(context.getString(R.string.metric_units_key), true);
locationsCursor = myTracksProviderUtils.getLocationsCursor(trackId, 0, -1, false);
if (locationsCursor == null) {
Log.d(TAG, "Location cursor is null");
return false;
}
int locationsCount = locationsCursor.getCount();
List<Location> locations = new ArrayList<Location>(MAX_POINTS_PER_UPLOAD);
Location lastLocation = null;
// For chart server, limit the number of elevation readings to 250.
int elevationSamplingFrequency = Math.max(1, (int) (locationsCount / 250.0));
TripStatisticsBuilder tripStatisticsBuilder = new TripStatisticsBuilder(
track.getStatistics().getStartTime());
DoubleBuffer elevationBuffer = new DoubleBuffer(Constants.ELEVATION_SMOOTHING_FACTOR);
Vector<Double> distances = new Vector<Double>();
Vector<Double> elevations = new Vector<Double>();
for (int i = 0; i < locationsCount; i++) {
locationsCursor.moveToPosition(i);
Location location = myTracksProviderUtils.createLocation(locationsCursor);
locations.add(location);
if (i == 0) {
// Create a start marker
if (!uploadMarker(context.getString(R.string.marker_label_start, track.getName()), "",
START_ICON_URL, location)) {
Log.d(TAG, "Unable to create a start marker");
return false;
}
}
// Add to the distances and elevations vectors
if (LocationUtils.isValidLocation(location)) {
tripStatisticsBuilder.addLocation(location, location.getTime());
// All points go into the smoothing buffer
elevationBuffer.setNext(metricUnits ? location.getAltitude()
: location.getAltitude() * UnitConversions.M_TO_FT);
if (i % elevationSamplingFrequency == 0) {
distances.add(tripStatisticsBuilder.getStatistics().getTotalDistance());
elevations.add(elevationBuffer.getAverage());
}
lastLocation = location;
}
// Upload periodically
int readCount = i + 1;
if (readCount % MAX_POINTS_PER_UPLOAD == 0) {
if (!prepareAndUploadPoints(track, locations, false)) {
Log.d(TAG, "Unable to upload points");
return false;
}
updateProgress(readCount, locationsCount);
locations.clear();
}
}
// Do a final upload with the remaining locations
if (!prepareAndUploadPoints(track, locations, true)) {
Log.d(TAG, "Unable to upload points");
return false;
}
// Create an end marker
if (lastLocation != null) {
distances.add(tripStatisticsBuilder.getStatistics().getTotalDistance());
elevations.add(elevationBuffer.getAverage());
DescriptionGenerator descriptionGenerator = new DescriptionGeneratorImpl(context);
track.setDescription("<p>" + track.getDescription() + "</p><p>"
+ descriptionGenerator.generateTrackDescription(track, distances, elevations) + "</p>");
if (!uploadMarker(context.getString(R.string.marker_label_end, track.getName()),
track.getDescription(), END_ICON_URL, lastLocation)) {
Log.d(TAG, "Unable to create an end marker");
return false;
}
}
return true;
} finally {
if (locationsCursor != null) {
locationsCursor.close();
}
}
}
/**
* Prepares and uploads a list of locations from a track.
*
* @param track the track
* @param locations the locations from the track
* @param lastBatch true if it is the last batch of locations
* @return true if success.
*/
private boolean prepareAndUploadPoints(Track track, List<Location> locations, boolean lastBatch) {
// Prepare locations
ArrayList<Track> splitTracks = SendToGoogleUtils.prepareLocations(track, locations);
// Upload segments
boolean onlyOneSegment = lastBatch && currentSegment == 1 && splitTracks.size() == 1;
for (Track segment : splitTracks) {
if (!onlyOneSegment) {
segment.setName(context.getString(
R.string.send_google_track_part_label, segment.getName(), currentSegment));
}
if (!uploadSegment(segment.getName(), segment.getLocations())) {
Log.d(TAG, "Unable to upload segment");
return false;
}
currentSegment++;
}
return true;
}
/**
* Uploads a marker.
*
* @param title marker title
* @param description marker description
* @param iconUrl marker marker icon
* @param location marker location
* @return true if success.
*/
private boolean uploadMarker(
String title, String description, String iconUrl, Location location) {
if (isCancelled()) {
return false;
}
try {
if (!SendMapsUtils.uploadMarker(mapId, title, description, iconUrl, location, mapsClient,
authToken, mapsGDataConverter)) {
Log.d(TAG, "Unable to upload marker");
return false;
}
} catch (ParseException e) {
Log.d(TAG, "Unable to upload marker", e);
return false;
} catch (HttpException e) {
Log.d(TAG, "Unable to upload marker", e);
return false;
} catch (IOException e) {
Log.d(TAG, "Unable to upload marker", e);
return false;
}
return true;
}
/**
* Uploads a segment
*
* @param title segment title
* @param locations segment locations
* @return true if success
*/
private boolean uploadSegment(String title, ArrayList<Location> locations) {
if (isCancelled()) {
return false;
}
try {
if (!SendMapsUtils.uploadSegment(
mapId, title, locations, mapsClient, authToken, mapsGDataConverter)) {
Log.d(TAG, "Unable to upload track points");
return false;
}
} catch (ParseException e) {
Log.d(TAG, "Unable to upload track points", e);
return false;
} catch (HttpException e) {
Log.d(TAG, "Unable to upload track points", e);
return false;
} catch (IOException e) {
Log.d(TAG, "Unable to upload track points", e);
return false;
}
return true;
}
/**
* Uploads all the waypoints.
*
* @return true if success.
*/
private boolean uploadWaypoints() {
Cursor cursor = null;
try {
cursor = myTracksProviderUtils.getWaypointsCursor(
trackId, 0, Constants.MAX_LOADED_WAYPOINTS_POINTS);
if (cursor != null && cursor.moveToFirst()) {
// This will skip the first waypoint (it carries the stats for the
// track).
while (cursor.moveToNext()) {
if (isCancelled()) {
return false;
}
Waypoint waypoint = myTracksProviderUtils.createWaypoint(cursor);
try {
if (!SendMapsUtils.uploadWaypoint(
mapId, waypoint, mapsClient, authToken, mapsGDataConverter)) {
Log.d(TAG, "Unable to upload waypoint");
return false;
}
} catch (ParseException e) {
Log.d(TAG, "Unable to upload waypoint", e);
return false;
} catch (HttpException e) {
Log.d(TAG, "Unable to upload waypoint", e);
return false;
} catch (IOException e) {
Log.d(TAG, "Unable to upload waypoint", e);
return false;
}
}
}
return true;
} finally {
if (cursor != null) {
cursor.close();
}
}
}
/**
* Updates the progress based on the number of locations uploaded.
*
* @param uploaded the number of uploaded locations
* @param total the number of total locations
*/
private void updateProgress(int uploaded, int total) {
double totalPercentage = (double) uploaded / total;
double scaledPercentage = totalPercentage
* (PROGRESS_UPLOAD_DATA_MAX - PROGRESS_UPLOAD_DATA_MIN) + PROGRESS_UPLOAD_DATA_MIN;
publishProgress((int) scaledPercentage);
}
}
@@ -0,0 +1,227 @@
/*
* Copyright 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.io.maps;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.io.gdata.maps.MapsClient;
import com.google.android.apps.mytracks.io.gdata.maps.MapsFeature;
import com.google.android.apps.mytracks.io.gdata.maps.MapsGDataConverter;
import com.google.android.apps.mytracks.io.gdata.maps.MapsMapMetadata;
import com.google.android.maps.GeoPoint;
import com.google.common.annotations.VisibleForTesting;
import com.google.wireless.gdata.client.HttpException;
import com.google.wireless.gdata.data.Entry;
import com.google.wireless.gdata.parser.ParseException;
import android.location.Location;
import android.text.TextUtils;
import android.util.Log;
import java.io.IOException;
import java.util.ArrayList;
/**
* Utilities for sending a track to Google Maps.
*
* @author Jimmy Shih
*/
public class SendMapsUtils {
private static final String EMPTY_TITLE = "-";
private static final int LINE_COLOR = 0x80FF0000;
private static final String TAG = SendMapsUtils.class.getSimpleName();
private SendMapsUtils() {}
/**
* Gets the Google Maps url for a track.
*
* @param track the track
* @return the url if available.
*/
public static String getMapUrl(Track track) {
if (track == null || track.getMapId() == null) {
Log.e(TAG, "Invalid track");
return null;
}
return MapsClient.buildMapUrl(track.getMapId());
}
/**
* Creates a new Google Map.
*
* @param title title of the map
* @param description description of the map
* @param isPublic true if the map can be public
* @param mapsClient the maps client
* @param authToken the auth token
* @return map id of the created map if successful.
*/
public static String createNewMap(
String title, String description, boolean isPublic, MapsClient mapsClient, String authToken)
throws ParseException, HttpException, IOException {
String mapFeed = MapsClient.getMapsFeed();
MapsMapMetadata metaData = new MapsMapMetadata();
metaData.setTitle(title);
metaData.setDescription(description);
metaData.setSearchable(isPublic);
Entry entry = MapsGDataConverter.getMapEntryForMetadata(metaData);
Entry result = mapsClient.createEntry(mapFeed, authToken, entry);
if (result == null) {
Log.d(TAG, "No result when creating a new map");
return null;
}
return MapsClient.getMapIdFromMapEntryId(result.getId());
}
/**
* Uploads a start/end marker to Google Maps.
*
* @param mapId the map id
* @param title the marker title
* @param description the marker description
* @param iconUrl the marker icon URL
* @param location the marker location
* @param mapsClient the maps client
* @param authToken the auth token
* @param mapsGDataConverter the maps gdata converter
* @return true if success.
*/
public static boolean uploadMarker(String mapId, String title, String description, String iconUrl,
Location location, MapsClient mapsClient, String authToken,
MapsGDataConverter mapsGDataConverter) throws ParseException, HttpException, IOException {
String featuresFeed = MapsClient.getFeaturesFeed(mapId);
MapsFeature mapsFeature = buildMapsMarkerFeature(
title, description, iconUrl, getGeoPoint(location));
Entry entry = mapsGDataConverter.getEntryForFeature(mapsFeature);
try {
mapsClient.createEntry(featuresFeed, authToken, entry);
} catch (IOException e) {
// Retry once (often IOException is thrown on a timeout)
Log.d(TAG, "Retry upload marker", e);
mapsClient.createEntry(featuresFeed, authToken, entry);
}
return true;
}
/**
* Uploads a waypoint as a marker feature to Google Maps.
*
* @param mapId the map id
* @param waypoint the waypoint
* @param mapsClient the maps client
* @param authToken the auth token
* @param mapsGDataConverter the maps gdata converter
* @return true if success.
*/
public static boolean uploadWaypoint(String mapId, Waypoint waypoint, MapsClient mapsClient,
String authToken, MapsGDataConverter mapsGDataConverter)
throws ParseException, HttpException, IOException {
String featuresFeed = MapsClient.getFeaturesFeed(mapId);
MapsFeature feature = buildMapsMarkerFeature(waypoint.getName(), waypoint.getDescription(),
waypoint.getIcon(), getGeoPoint(waypoint.getLocation()));
Entry entry = mapsGDataConverter.getEntryForFeature(feature);
try {
mapsClient.createEntry(featuresFeed, authToken, entry);
} catch (IOException e) {
// Retry once (often IOException is thrown on a timeout)
Log.d(TAG, "Retry upload waypoint", e);
mapsClient.createEntry(featuresFeed, authToken, entry);
}
return true;
}
/**
* Uploads a segment as a line feature to Google Maps.
*
* @param mapId the map id
* @param title the segment title
* @param locations the segment locations
* @param mapsClient the maps client
* @param authToken the auth token
* @param mapsGDataConverter the maps gdata converter
* @return true if success.
*/
public static boolean uploadSegment(String mapId, String title, ArrayList<Location> locations,
MapsClient mapsClient, String authToken, MapsGDataConverter mapsGDataConverter)
throws ParseException, HttpException, IOException {
String featuresFeed = MapsClient.getFeaturesFeed(mapId);
Entry entry = mapsGDataConverter.getEntryForFeature(buildMapsLineFeature(title, locations));
try {
mapsClient.createEntry(featuresFeed, authToken, entry);
} catch (IOException e) {
// Retry once (often IOException is thrown on a timeout)
Log.d(TAG, "Retry upload track points", e);
mapsClient.createEntry(featuresFeed, authToken, entry);
}
return true;
}
/**
* Builds a map marker feature.
*
* @param title feature title
* @param description the feature description
* @param iconUrl the feature icon URL
* @param geoPoint the marker
*/
@VisibleForTesting
static MapsFeature buildMapsMarkerFeature(
String title, String description, String iconUrl, GeoPoint geoPoint) {
MapsFeature mapsFeature = new MapsFeature();
mapsFeature.setType(MapsFeature.MARKER);
mapsFeature.generateAndroidId();
// Feature must have a name (otherwise GData upload may fail)
mapsFeature.setTitle(TextUtils.isEmpty(title) ? EMPTY_TITLE : title);
mapsFeature.setDescription(description.replaceAll("\n", "<br>"));
mapsFeature.setIconUrl(iconUrl);
mapsFeature.addPoint(geoPoint);
return mapsFeature;
}
/**
* Builds a maps line feature from a set of locations.
*
* @param title the feature title
* @param locations set of locations
*/
@VisibleForTesting
static MapsFeature buildMapsLineFeature(String title, ArrayList<Location> locations) {
MapsFeature mapsFeature = new MapsFeature();
mapsFeature.setType(MapsFeature.LINE);
mapsFeature.generateAndroidId();
// Feature must have a name (otherwise GData upload may fail)
mapsFeature.setTitle(TextUtils.isEmpty(title) ? EMPTY_TITLE : title);
mapsFeature.setColor(LINE_COLOR);
for (Location location : locations) {
mapsFeature.addPoint(getGeoPoint(location));
}
return mapsFeature;
}
/**
* Gets a {@link GeoPoint} from a {@link Location}.
*
* @param location the location
*/
@VisibleForTesting
static GeoPoint getGeoPoint(Location location) {
return new GeoPoint(
(int) (location.getLatitude() * 1E6), (int) (location.getLongitude() * 1E6));
}
}
@@ -0,0 +1,139 @@
/*
* Copyright 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.io.sendtogoogle;
import com.google.android.maps.mytracks.R;
import android.app.Activity;
import android.app.Dialog;
import android.app.ProgressDialog;
import android.content.DialogInterface;
import android.os.Bundle;
/**
* The abstract class for activities sending a track to Google.
* <p>
* The activity gets recreated when the screen rotates. To support the activity
* displaying a progress dialog, we do the following:
* <ul>
* <li>use one instance of an AyncTask to send the track</li>
* <li>save that instance as the last non configuration instance of the activity
* </li>
* <li>when a new activity is created, pass the activity to the AsyncTask so
* that the AsyncTask can update the progress dialog of the activity</li>
* </ul>
*
* @author Jimmy Shih
*/
public abstract class AbstractSendActivity extends Activity {
private static final int PROGRESS_DIALOG = 1;
protected SendRequest sendRequest;
private AbstractSendAsyncTask asyncTask;
private ProgressDialog progressDialog;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
sendRequest = getIntent().getParcelableExtra(SendRequest.SEND_REQUEST_KEY);
Object retained = getLastNonConfigurationInstance();
if (retained instanceof AbstractSendAsyncTask) {
asyncTask = (AbstractSendAsyncTask) retained;
asyncTask.setActivity(this);
} else {
asyncTask = createAsyncTask();
asyncTask.execute();
}
}
@Override
public Object onRetainNonConfigurationInstance() {
asyncTask.setActivity(null);
return asyncTask;
}
@Override
protected Dialog onCreateDialog(int id) {
switch (id) {
case PROGRESS_DIALOG:
progressDialog = new ProgressDialog(this);
progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
progressDialog.setIcon(android.R.drawable.ic_dialog_info);
progressDialog.setTitle(getString(R.string.send_google_progress_title, getServiceName()));
progressDialog.setMax(100);
progressDialog.setProgress(0);
progressDialog.setCancelable(true);
progressDialog.setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
asyncTask.cancel(true);
startNextActivity(false, true);
}
});
return progressDialog;
default:
return null;
}
}
/**
* Invokes when the associated AsyncTask completes.
*
* @param success true if the AsyncTask is successful
*/
public void onAsyncTaskCompleted(boolean success) {
startNextActivity(success, false);
}
/**
* Shows the progress dialog.
*/
public void showProgressDialog() {
showDialog(PROGRESS_DIALOG);
}
/**
* Sets the progress dialog value.
*
* @param value the dialog value
*/
public void setProgressDialogValue(int value) {
if (progressDialog != null) {
progressDialog.setProgress(value);
}
}
/**
* Creates the AsyncTask.
*/
protected abstract AbstractSendAsyncTask createAsyncTask();
/**
* Gets the service name.
*/
protected abstract String getServiceName();
/**
* Starts the next activity.
*
* @param success true if this activity is successful
* @param isCancel true if it is a cancel request
*/
protected abstract void startNextActivity(boolean success, boolean isCancel);
}
@@ -0,0 +1,147 @@
/*
* Copyright 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.io.sendtogoogle;
import android.os.AsyncTask;
/**
* The abstract class for AsyncTasks sending a track to Google.
*
* @author Jimmy Shih
*/
public abstract class AbstractSendAsyncTask extends AsyncTask<Void, Integer, Boolean> {
/**
* The activity associated with this AsyncTask.
*/
private AbstractSendActivity activity;
/**
* True if the AsyncTask result is success.
*/
private boolean success;
/**
* True if the AsyncTask has completed.
*/
private boolean completed;
/**
* True if can retry the AsyncTask.
*/
private boolean canRetry;
/**
* Creates an AsyncTask.
*
* @param activity the activity currently associated with this AsyncTask
*/
public AbstractSendAsyncTask(AbstractSendActivity activity) {
this.activity = activity;
success = false;
completed = false;
canRetry = true;
}
/**
* Sets the current activity associated with this AyncTask.
*
* @param activity the current activity, can be null
*/
public void setActivity(AbstractSendActivity activity) {
this.activity = activity;
if (completed && activity != null) {
activity.onAsyncTaskCompleted(success);
}
}
@Override
protected void onPreExecute() {
activity.showProgressDialog();
}
@Override
protected Boolean doInBackground(Void... params) {
return performTask();
}
@Override
protected void onProgressUpdate(Integer... values) {
if (activity != null) {
activity.setProgressDialogValue(values[0]);
}
}
@Override
protected void onPostExecute(Boolean result) {
success = result;
if (success) {
saveResult();
}
completed = true;
closeConnection();
if (activity != null) {
activity.onAsyncTaskCompleted(success);
}
}
@Override
protected void onCancelled() {
closeConnection();
}
/**
* Retries the task. First, invalidates the auth token. If can retry, invokes
* {@link #performTask()}. Returns false if cannot retry.
*
* @return the result of the retry.
*/
protected boolean retryTask() {
if (isCancelled()) {
return false;
}
invalidateToken();
if (canRetry) {
canRetry = false;
return performTask();
}
return false;
}
/**
* Closes any AsyncTask connection.
*/
protected abstract void closeConnection();
/**
* Saves any AsyncTask result.
*/
protected abstract void saveResult();
/**
* Performs the AsyncTask.
*
* @return true if success
*/
protected abstract boolean performTask();
/**
* Invalidates the auth token.
*/
protected abstract void invalidateToken();
}
@@ -16,11 +16,23 @@
package com.google.android.apps.mytracks.io.sendtogoogle;
import com.google.android.apps.mytracks.Constants;
import com.google.android.apps.mytracks.util.ApiFeatures;
import com.google.android.apps.mytracks.io.docs.SendDocsActivity;
import com.google.android.apps.mytracks.io.fusiontables.SendFusionTablesActivity;
import com.google.android.apps.mytracks.io.fusiontables.SendFusionTablesUtils;
import com.google.android.apps.mytracks.io.gdata.docs.DocumentsClient;
import com.google.android.apps.mytracks.io.gdata.docs.SpreadsheetsClient;
import com.google.android.apps.mytracks.io.gdata.maps.MapsConstants;
import com.google.android.apps.mytracks.io.maps.ChooseMapActivity;
import com.google.android.apps.mytracks.io.maps.SendMapsActivity;
import com.google.android.apps.mytracks.util.ApiAdapterFactory;
import com.google.android.maps.mytracks.R;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.accounts.AccountManagerCallback;
import android.accounts.AccountManagerFuture;
import android.accounts.AuthenticatorException;
import android.accounts.OperationCanceledException;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
@@ -30,48 +42,60 @@ import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.os.Bundle;
import android.util.Log;
import java.io.IOException;
/**
* A chooser to select an account. To be called with
* {@link Activity#startActivityForResult(Intent, int)}. Returns
* {@link Activity#RESULT_CANCELED} if the user cancels the activity. Otherwise,
* returns {@link Activity#RESULT_OK} with an intent containing the selected
* account. The selected account is stored as an {@link Account} in the
* {@link AccountChooserActivity#ACCOUNT} item of the intent. The selected
* account can be null.
* A chooser to select an account.
*
* @author jshih@google.com (Jimmy Shih)
* @author Jimmy Shih
*/
public class AccountChooserActivity extends Activity {
/**
* Key for storing an {@link Account} in an {@link Intent}.
*/
public static final String ACCOUNT = "account";
private static final String TAG = AccountChooserActivity.class.getSimpleName();
private static final int NO_ACCOUNT_DIALOG = 1;
private static final int CHOOSE_ACCOUNT_DIALOG = 2;
/**
* A callback after getting the permission to access a Google service.
*
* @author Jimmy Shih
*/
private interface PermissionCallback {
/**
* To be invoked when the permission is granted.
*/
public void onSuccess();
/**
* To be invoked when the permission is not granted.
*/
public void onFailure();
}
private SendRequest sendRequest;
private Account[] accounts;
private int selectedAccountIndex;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
sendRequest = getIntent().getParcelableExtra(SendRequest.SEND_REQUEST_KEY);
accounts = AccountManager.get(this).getAccountsByType(Constants.ACCOUNT_TYPE);
if (accounts.length == 1) {
Intent intent = new Intent();
intent.putExtra(ACCOUNT, accounts[0]);
setResult(RESULT_OK, intent);
finish();
sendRequest.setAccount(accounts[0]);
getPermission(MapsConstants.SERVICE_NAME, sendRequest.isSendMaps(), mapsCallback);
return;
}
SharedPreferences prefs = getSharedPreferences(Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
String preferredAccount = prefs.getString(getString(R.string.preferred_account_key), "");
selectedAccountIndex = -1;
selectedAccountIndex = 0;
for (int i = 0; i < accounts.length; i++) {
if (accounts[i].name.equals(preferredAccount)) {
selectedAccountIndex = i;
@@ -85,7 +109,7 @@ public class AccountChooserActivity extends Activity {
super.onResume();
if (accounts.length == 0) {
showDialog(NO_ACCOUNT_DIALOG);
} else {
} else if (accounts.length > 1 ) {
showDialog(CHOOSE_ACCOUNT_DIALOG);
}
}
@@ -102,15 +126,11 @@ public class AccountChooserActivity extends Activity {
builder.setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
setResult(RESULT_CANCELED);
finish();
}
});
builder.setPositiveButton(R.string.generic_ok, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
Intent intent = new Intent();
intent.putExtra(ACCOUNT, (Account) null);
setResult(RESULT_OK, intent);
finish();
}
});
@@ -134,31 +154,25 @@ public class AccountChooserActivity extends Activity {
builder.setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
setResult(RESULT_CANCELED);
finish();
}
});
builder.setNegativeButton(R.string.generic_cancel, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
setResult(RESULT_CANCELED);
finish();
}
});
builder.setPositiveButton(R.string.generic_ok, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
Account account = null;
if (selectedAccountIndex != -1) {
account = accounts[selectedAccountIndex];
SharedPreferences prefs = getSharedPreferences(
Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
Editor editor = prefs.edit();
editor.putString(getString(R.string.preferred_account_key), account.name);
ApiFeatures.getInstance().getApiAdapter().applyPreferenceChanges(editor);
}
Intent intent = new Intent();
intent.putExtra(ACCOUNT, account);
setResult(RESULT_OK, intent);
finish();
Account account = accounts[selectedAccountIndex];
SharedPreferences prefs = getSharedPreferences(
Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
Editor editor = prefs.edit();
editor.putString(getString(R.string.preferred_account_key), account.name);
ApiAdapterFactory.getApiAdapter().applyPreferenceChanges(editor);
sendRequest.setAccount(account);
getPermission(MapsConstants.SERVICE_NAME, sendRequest.isSendMaps(), mapsCallback);
}
});
return builder.create();
@@ -166,4 +180,118 @@ public class AccountChooserActivity extends Activity {
return null;
}
}
}
private PermissionCallback spreadsheetsCallback = new PermissionCallback() {
@Override
public void onSuccess() {
startNextActivity();
}
@Override
public void onFailure() {
finish();
}
};
private PermissionCallback docsCallback = new PermissionCallback() {
@Override
public void onSuccess() {
getPermission(SpreadsheetsClient.SERVICE, sendRequest.isSendDocs(), spreadsheetsCallback);
}
@Override
public void onFailure() {
finish();
}
};
private PermissionCallback fusionTablesCallback = new PermissionCallback() {
@Override
public void onSuccess() {
getPermission(DocumentsClient.SERVICE, sendRequest.isSendDocs(), docsCallback);
}
@Override
public void onFailure() {
finish();
}
};
private PermissionCallback mapsCallback = new PermissionCallback() {
@Override
public void onSuccess() {
getPermission(
SendFusionTablesUtils.SERVICE, sendRequest.isSendFusionTables(), fusionTablesCallback);
}
@Override
public void onFailure() {
finish();
}
};
/**
* Gets the user permission to access a service.
*
* @param authTokenType the auth token type of the service
* @param needPermission true if need the permission
* @param callback callback after getting the permission
*/
private void getPermission(
String authTokenType, boolean needPermission, final PermissionCallback callback) {
if (needPermission) {
AccountManager.get(this).getAuthToken(sendRequest.getAccount(), authTokenType, null, this,
new AccountManagerCallback<Bundle>() {
@Override
public void run(AccountManagerFuture<Bundle> future) {
try {
if (future.getResult().getString(AccountManager.KEY_AUTHTOKEN) != null) {
callback.onSuccess();
} else {
Log.d(TAG, "auth token is null");
callback.onFailure();
}
} catch (OperationCanceledException e) {
Log.d(TAG, "Unable to get auth token", e);
callback.onFailure();
} catch (AuthenticatorException e) {
Log.d(TAG, "Unable to get auth token", e);
callback.onFailure();
} catch (IOException e) {
Log.d(TAG, "Unable to get auth token", e);
callback.onFailure();
}
}
}, null);
} else {
callback.onSuccess();
}
}
/**
* Starts the next activity. If
* <p>
* sendMaps and newMap -> {@link SendMapsActivity}
* <p>
* sendMaps and !newMap -> {@link ChooseMapActivity}
* <p>
* !sendMaps && sendFusionTables -> {@link SendFusionTablesActivity}
* <p>
* !sendMaps && !sendFusionTables && sendDocs -> {@link SendDocsActivity}
* <p>
* !sendMaps && !sendFusionTables && !sendDocs -> {@link UploadResultActivity}
*
*/
private void startNextActivity() {
Class<?> next;
if (sendRequest.isSendMaps()) {
next = sendRequest.isNewMap() ? SendMapsActivity.class : ChooseMapActivity.class;
} else if (sendRequest.isSendFusionTables()) {
next = SendFusionTablesActivity.class;
} else if (sendRequest.isSendDocs()) {
next = SendDocsActivity.class;
} else {
next = UploadResultActivity.class;
}
Intent intent = new Intent(this, next)
.putExtra(SendRequest.SEND_REQUEST_KEY, sendRequest);
startActivity(intent);
finish();
}
}
@@ -1,109 +0,0 @@
/*
* 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.sendtogoogle;
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.view.View;
import android.widget.ListView;
import java.util.List;
/**
* Creates the dialog used to display the results of sending track data to
* Google. The dialog lists the services to which data was uploaded, along
* with an indicator of success/failure. If possible, a button is displayed
* offering to share the uploaded track.
*
* Implementation note: This class is a factory, rather than a {@link Dialog}
* subclass, because alert dialogs have to be created with
* {@link AlertDialog.Builder}. Attempts to subclass {@link AlertDialog}
* directly have been unsuccessful, as {@link AlertDialog.Builder} uses a
* private subclass to implement most of the interesting behavior.
*
* @author Matthew Simmons
*/
public class ResultDialogFactory {
private ResultDialogFactory() {}
/**
* Create a send-to-Google result dialog. The caller is responsible for
* showing it.
*
* @param activity the activity associated with the dialog
* @param results the results to be displayed in the dialog
* @param onOkClickListener the listener to invoke if the OK button is
* clicked
* @param onShareClickListener the listener to invoke if the Share button is
* clicked. If no share listener is provided, the Share button will not
* be displayed.
* @return the created dialog
*/
public static AlertDialog makeDialog(Activity activity, List<SendResult> results,
DialogInterface.OnClickListener onOkClickListener,
DialogInterface.OnClickListener onShareClickListener,
DialogInterface.OnCancelListener onCancelListener) {
boolean success = true;
for (SendResult result : results) {
if (!result.isSuccess()) {
success = false;
break;
}
}
AlertDialog.Builder builder = new AlertDialog.Builder(activity)
.setView(makeDialogContent(activity, results, success));
if (success) {
builder.setTitle(R.string.generic_success_title);
builder.setIcon(android.R.drawable.ic_dialog_info);
} else {
builder.setTitle(R.string.generic_error_title);
builder.setIcon(android.R.drawable.ic_dialog_alert);
}
builder.setPositiveButton(activity.getString(R.string.generic_ok), onOkClickListener);
if (onShareClickListener != null) {
builder.setNegativeButton(activity.getString(R.string.send_google_result_share_url),
onShareClickListener);
}
builder.setOnCancelListener(onCancelListener);
return builder.create();
}
private static View makeDialogContent(Activity activity, List<SendResult> results,
boolean success) {
ResultListAdapter resultListAdapter = new ResultListAdapter(activity,
R.layout.send_to_google_result_list_item, results);
View content = activity.getLayoutInflater().inflate(R.layout.send_to_google_result, null);
ListView resultList = (ListView) content.findViewById(R.id.send_to_google_result_list);
resultList.setAdapter(resultListAdapter);
content.findViewById(R.id.send_to_google_result_comment)
.setVisibility(success ? View.VISIBLE : View.GONE);
content.findViewById(R.id.send_to_google_result_error)
.setVisibility(success ? View.GONE : View.VISIBLE);
return content;
}
}
@@ -1,85 +0,0 @@
/*
* 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.sendtogoogle;
import com.google.android.maps.mytracks.R;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.ListAdapter;
import android.widget.TextView;
import java.util.List;
/**
* This class implements the {@link ListAdapter} used by the send-to-Google
* result dialog created by {@link ResultDialogFactory}. It generates views
* for each entry in an array of {@link SendResult} instances.
* @author Matthew Simmons
*/
class ResultListAdapter extends ArrayAdapter<SendResult> {
public ResultListAdapter(Context context, int textViewResourceId, List<SendResult> results) {
super(context, textViewResourceId, results);
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
View view = convertView;
if (view == null) {
LayoutInflater inflater
= (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
view = inflater.inflate(R.layout.send_to_google_result_list_item, null);
}
SendResult result = getItem(position);
setImage(view, result.isSuccess() ? R.drawable.success : R.drawable.failure);
setName(view, result.getType().getServiceName());
setUrl(view, result.getType().getServiceUrl());
return view;
}
@Override
public boolean areAllItemsEnabled() {
// We don't want the displayed items to be clickable
return false;
}
// The following protected methods exist to be overridden for testing
// purposes. Doing so insulates the test class from the details of the
// layout.
protected void setImage(View content, int drawableId) {
ImageView imageView = (ImageView) content.findViewById(R.id.send_to_google_result_icon);
imageView.setImageDrawable(getContext().getResources().getDrawable(drawableId));
}
protected void setName(View content, int nameId) {
setTextViewText(content, R.id.send_to_google_result_name, nameId);
}
protected void setUrl(View content, int urlId) {
setTextViewText(content, R.id.send_to_google_result_url, urlId);
}
private void setTextViewText(View content, int viewId, int textId) {
TextView textView = (TextView) content.findViewById(viewId);
textView.setText(getContext().getString(textId));
}
};
@@ -1,814 +0,0 @@
/*
* 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.sendtogoogle;
import static com.google.android.apps.mytracks.Constants.TAG;
import com.google.android.apps.analytics.GoogleAnalyticsTracker;
import com.google.android.apps.mytracks.Constants;
import com.google.android.apps.mytracks.MapsList;
import com.google.android.apps.mytracks.ProgressIndicator;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.io.AuthManager;
import com.google.android.apps.mytracks.io.AuthManager.AuthCallback;
import com.google.android.apps.mytracks.io.AuthManagerFactory;
import com.google.android.apps.mytracks.io.SendToDocs;
import com.google.android.apps.mytracks.io.SendToFusionTables;
import com.google.android.apps.mytracks.io.SendToFusionTables.OnSendCompletedListener;
import com.google.android.apps.mytracks.io.SendToMaps;
import com.google.android.apps.mytracks.io.maps.MapsConstants;
import com.google.android.apps.mytracks.io.maps.MapsFacade;
import com.google.android.apps.mytracks.util.SystemUtils;
import com.google.android.maps.mytracks.R;
import android.accounts.Account;
import android.app.Activity;
import android.app.Dialog;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnCancelListener;
import android.content.DialogInterface.OnClickListener;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.util.Log;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
/**
* Helper activity for managing the sending of tracks to Google services.
*
* @author Rodrigo Damazio
*/
public class SendActivity extends Activity implements ProgressIndicator {
// Items in the intent that starts the activity.
public static final String TRACK_ID = "trackId";
public static final String SHARE_URL = "shareUrl";
public static final String SEND_MAPS = "sendMaps";
public static final String SEND_FUSION_TABLES = "sendFusionTables";
public static final String SEND_DOCS = "sendDocs";
public static final String CREATE_MAP = "createMap";
// Keys for saved state variables.
private static final String STATE_ACCOUNT = "account";
private static final String STATE_STATE = "state";
private static final String STATE_DOCS_SUCCESS = "docsSuccess";
private static final String STATE_FUSION_SUCCESS = "fusionSuccess";
private static final String STATE_MAPS_SUCCESS = "mapsSuccess";
private static final String STATE_TABLE_ID = "tableId";
private static final String STATE_MAP_ID = "mapId";
/** States for the state machine that defines the upload process. */
private enum SendState {
CHOOSE_ACCOUNT,
START,
AUTHENTICATE_MAPS,
PICK_MAP,
SEND_TO_MAPS,
SEND_TO_MAPS_DONE,
AUTHENTICATE_FUSION_TABLES,
SEND_TO_FUSION_TABLES,
SEND_TO_FUSION_TABLES_DONE,
AUTHENTICATE_DOCS,
AUTHENTICATE_TRIX,
SEND_TO_DOCS,
SEND_TO_DOCS_DONE,
SHOW_RESULTS,
SHARE_LINK,
FINISH,
DONE,
NOT_READY
}
private static final int PROGRESS_DIALOG = 1;
/* @VisibleForTesting */
static final int DONE_DIALOG = 2;
// UI
private ProgressDialog progressDialog;
// Set in Activity.onCreate
private MyTracksProviderUtils providerUtils;
private SharedPreferences sharedPreferences;
private GoogleAnalyticsTracker tracker;
// Send request information. Set by the intent that starts the activity.
private long sendTrackId;
private boolean shareRequest;
private boolean sendToMaps;
private boolean sendToMapsNewMap;
private boolean sendToFusionTables;
private boolean sendToDocs;
// Authentication information.
private Account account;
private HashMap<String, AuthManager> authMap = new HashMap<String, AuthManager>();
// Current sending state.
private SendState currentState;
// Send result information. Used by the results dialog.
private boolean sendToMapsSuccess;
private boolean sendToFusionTablesSuccess;
private boolean sendToDocsSuccess;
private String sendToMapsMapId;
private String sendToFusionTablesTableId;
private final OnCancelListener finishOnCancelListener = new OnCancelListener() {
@Override
public void onCancel(DialogInterface arg0) {
onAllDone();
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
Log.d(TAG, "SendActivity.onCreate");
super.onCreate(savedInstanceState);
providerUtils = MyTracksProviderUtils.Factory.get(this);
sharedPreferences = getSharedPreferences(Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
tracker = GoogleAnalyticsTracker.getInstance();
// Start the tracker in manual dispatch mode...
tracker.start(getString(R.string.my_tracks_analytics_id),
getApplicationContext());
tracker.setProductVersion("android-mytracks",
SystemUtils.getMyTracksVersion(this));
resetState();
if (savedInstanceState != null) {
restoreInstanceState(savedInstanceState);
}
// If we had the instance restored after it was done, reset it.
if (currentState == SendState.DONE) {
resetState();
}
// Only consider the intent if we're not restoring from a previous state.
if (currentState == SendState.CHOOSE_ACCOUNT) {
if (!handleIntent()) {
finish();
return;
}
}
// Execute the state machine, at the start or restored state.
Log.w(TAG, "Starting at state " + currentState);
executeStateMachine(currentState);
}
private boolean handleIntent() {
Intent intent = getIntent();
sendTrackId = intent.getLongExtra(TRACK_ID, -1L);
if (sendTrackId == -1L) {
return false;
}
sendToMaps = intent.getBooleanExtra(SEND_MAPS, false);
sendToFusionTables = intent.getBooleanExtra(SEND_FUSION_TABLES, false);
sendToDocs = intent.getBooleanExtra(SEND_DOCS, false);
if (!sendToMaps && !sendToFusionTables && !sendToDocs) {
return false;
}
sendToMapsNewMap = intent.getBooleanExtra(CREATE_MAP, true);
return true;
}
@Override
protected Dialog onCreateDialog(int id) {
switch (id) {
case PROGRESS_DIALOG:
return createProgressDialog();
case DONE_DIALOG:
return createDoneDialog();
default:
return null;
}
}
private void restoreInstanceState(Bundle savedInstanceState) {
account = savedInstanceState.getParcelable(STATE_ACCOUNT);
currentState = SendState.values()[savedInstanceState.getInt(STATE_STATE)];
sendToMapsSuccess = savedInstanceState.getBoolean(STATE_MAPS_SUCCESS);
sendToFusionTablesSuccess = savedInstanceState.getBoolean(STATE_FUSION_SUCCESS);
sendToDocsSuccess = savedInstanceState.getBoolean(STATE_DOCS_SUCCESS);
sendToMapsMapId = savedInstanceState.getString(STATE_MAP_ID);
sendToFusionTablesTableId = savedInstanceState.getString(STATE_TABLE_ID);
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putParcelable(STATE_ACCOUNT, account);
outState.putInt(STATE_STATE, currentState.ordinal());
outState.putBoolean(STATE_MAPS_SUCCESS, sendToMapsSuccess);
outState.putBoolean(STATE_FUSION_SUCCESS, sendToFusionTablesSuccess);
outState.putBoolean(STATE_DOCS_SUCCESS, sendToDocsSuccess);
outState.putString(STATE_MAP_ID, sendToMapsMapId);
outState.putString(STATE_TABLE_ID, sendToFusionTablesTableId);
}
@Override
protected void onStop() {
Log.d(TAG, "SendActivity.onStop, state=" + currentState);
tracker.dispatch();
tracker.stop();
super.onStop();
}
@Override
protected void onDestroy() {
Log.d(TAG, "SendActivity.onDestroy, state=" + currentState);
super.onDestroy();
}
private void executeStateMachine(SendState startState) {
currentState = startState;
// If a state handler returns NOT_READY, it means it's waiting for some
// event, and will call this method again when it happens.
while (currentState != SendState.DONE &&
currentState != SendState.NOT_READY) {
Log.d(TAG, "Executing state " + currentState);
currentState = executeState(currentState);
Log.d(TAG, "New state is " + currentState);
}
}
private SendState executeState(SendState state) {
switch (state) {
case CHOOSE_ACCOUNT:
return chooseAccount();
case START:
return startSend();
case AUTHENTICATE_MAPS:
return authenticateToGoogleMaps();
case PICK_MAP:
return pickMap();
case SEND_TO_MAPS:
return sendToGoogleMaps();
case SEND_TO_MAPS_DONE:
return onSendToGoogleMapsDone();
case AUTHENTICATE_FUSION_TABLES:
return authenticateToFusionTables();
case SEND_TO_FUSION_TABLES:
return sendToFusionTables();
case SEND_TO_FUSION_TABLES_DONE:
return onSendToFusionTablesDone();
case AUTHENTICATE_DOCS:
return authenticateToGoogleDocs();
case AUTHENTICATE_TRIX:
return authenticateToGoogleTrix();
case SEND_TO_DOCS:
return sendToGoogleDocs();
case SEND_TO_DOCS_DONE:
return onSendToGoogleDocsDone();
case SHOW_RESULTS:
return onSendToGoogleDone();
case SHARE_LINK:
return shareLink();
case FINISH:
return onAllDone();
default:
Log.e(TAG, "Reached a non-executable state");
return null;
}
}
private SendState chooseAccount() {
Intent intent = new Intent(this, AccountChooserActivity.class);
startActivityForResult(intent, Constants.CHOOSE_ACCOUNT);
return SendState.NOT_READY;
}
/**
* Initiates the process to send tracks to google.
* This is called once the user has selected sending options via the
* SendToGoogleDialog.
*/
private SendState startSend() {
showDialog(PROGRESS_DIALOG);
if (sendToMaps) {
return SendState.AUTHENTICATE_MAPS;
} else if (sendToFusionTables) {
return SendState.AUTHENTICATE_FUSION_TABLES;
} else if (sendToDocs) {
return SendState.AUTHENTICATE_DOCS;
} else {
Log.w(TAG, "Nowhere to upload to");
return SendState.FINISH;
}
}
private Dialog createProgressDialog() {
progressDialog = new ProgressDialog(this);
progressDialog.setCancelable(false);
progressDialog.setIcon(android.R.drawable.ic_dialog_info);
progressDialog.setTitle(R.string.generic_progress_title);
progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
progressDialog.setMax(100);
progressDialog.setProgress(0);
return progressDialog;
}
private SendState authenticateToGoogleMaps() {
Log.d(TAG, "SendActivity.authenticateToGoogleMaps");
progressDialog.setProgress(0);
progressDialog.setMessage(getAuthenticatingProgressMessage(SendType.MAPS));
authenticate(Constants.AUTHENTICATE_TO_MAPS, MapsConstants.SERVICE_NAME);
// AUTHENTICATE_TO_MAPS callback calls sendToGoogleMaps
return SendState.NOT_READY;
}
private SendState pickMap() {
if (!sendToMapsNewMap) {
// Ask the user to choose a map to upload into
Intent listIntent = new Intent(this, MapsList.class);
listIntent.putExtra(MapsList.EXTRA_ACCOUNT_NAME, account.name);
listIntent.putExtra(MapsList.EXTRA_ACCOUNT_TYPE, account.type);
startActivityForResult(listIntent, Constants.GET_MAP);
// The callback for GET_MAP calls authenticateToGoogleMaps
return SendState.NOT_READY;
} else {
return SendState.SEND_TO_MAPS;
}
}
private SendState sendToGoogleMaps() {
tracker.trackPageView("/send/maps");
SendToMaps.OnSendCompletedListener onCompletion = new SendToMaps.OnSendCompletedListener() {
@Override
public void onSendCompleted(String mapId, boolean success) {
sendToMapsSuccess = success;
if (sendToMapsSuccess) {
sendToMapsMapId = mapId;
// Update the map id for this track:
try {
Track track = providerUtils.getTrack(sendTrackId);
if (track != null) {
track.setMapId(mapId);
providerUtils.updateTrack(track);
} else {
Log.w(TAG, "Updating map id failed.");
}
} catch (RuntimeException e) {
// If that fails whatever reasons we'll just log an error, but
// continue.
Log.w(TAG, "Updating map id failed.", e);
}
}
executeStateMachine(SendState.SEND_TO_MAPS_DONE);
}
};
if (sendToMapsMapId == null) {
sendToMapsMapId = SendToMaps.NEW_MAP_ID;
}
final SendToMaps sender = new SendToMaps(this, sendToMapsMapId,
getAuthManager(MapsConstants.SERVICE_NAME), sendTrackId, this /*progressIndicator*/,
onCompletion);
new Thread(sender, "SendToMaps").start();
return SendState.NOT_READY;
}
private SendState onSendToGoogleMapsDone() {
if (sendToFusionTables) {
return SendState.AUTHENTICATE_FUSION_TABLES;
} else if (sendToDocs) {
return SendState.AUTHENTICATE_DOCS;
} else {
return SendState.SHOW_RESULTS;
}
}
private SendState authenticateToFusionTables() {
progressDialog.setProgress(0);
progressDialog.setMessage(getAuthenticatingProgressMessage(SendType.FUSION_TABLES));
authenticate(Constants.AUTHENTICATE_TO_FUSION_TABLES, SendToFusionTables.SERVICE_ID);
// AUTHENTICATE_TO_FUSION_TABLES callback calls sendToFusionTables
return SendState.NOT_READY;
}
private SendState sendToFusionTables() {
tracker.trackPageView("/send/fusion_tables");
OnSendCompletedListener onCompletion = new OnSendCompletedListener() {
@Override
public void onSendCompleted(String tableId, boolean success) {
sendToFusionTablesSuccess = success;
if (sendToFusionTablesSuccess) {
sendToFusionTablesTableId = tableId;
// Update the table id for this track:
try {
Track track = providerUtils.getTrack(sendTrackId);
if (track != null) {
track.setTableId(tableId);
providerUtils.updateTrack(track);
} else {
Log.w(TAG, "Updating table id failed.");
}
} catch (RuntimeException e) {
// If that fails whatever reasons we'll just log an error, but
// continue.
Log.w(TAG, "Updating table id failed.", e);
}
}
executeStateMachine(SendState.SEND_TO_FUSION_TABLES_DONE);
}
};
final SendToFusionTables sender = new SendToFusionTables(
this, getAuthManager(SendToFusionTables.SERVICE_ID), sendTrackId,
this /* progressIndicator */, onCompletion);
new Thread(sender, "SendToFusionTables").start();
return SendState.NOT_READY;
}
private SendState onSendToFusionTablesDone() {
if (sendToDocs) {
return SendState.AUTHENTICATE_DOCS;
} else {
return SendState.SHOW_RESULTS;
}
}
private SendState authenticateToGoogleDocs() {
setProgressValue(0);
setProgressMessage(getAuthenticatingProgressMessage(SendType.DOCS));
authenticate(Constants.AUTHENTICATE_TO_DOCLIST, SendToDocs.GDATA_SERVICE_NAME_DOCLIST);
// AUTHENTICATE_TO_DOCLIST callback calls authenticateToGoogleTrix
return SendState.NOT_READY;
}
private SendState authenticateToGoogleTrix() {
setProgressValue(30);
setProgressMessage(getAuthenticatingProgressMessage(SendType.DOCS));
authenticate(Constants.AUTHENTICATE_TO_TRIX, SendToDocs.GDATA_SERVICE_NAME_TRIX);
// AUTHENTICATE_TO_TRIX callback calls sendToGoogleDocs
return SendState.NOT_READY;
}
private SendState sendToGoogleDocs() {
Log.d(TAG, "Sending to Docs....");
tracker.trackPageView("/send/docs");
setProgressValue(50);
String format = getString(R.string.send_google_progress_sending);
String serviceName = getString(SendType.DOCS.getServiceName());
setProgressMessage(String.format(format, serviceName));
final SendToDocs sender = new SendToDocs(this,
getAuthManager(SendToDocs.GDATA_SERVICE_NAME_TRIX),
getAuthManager(SendToDocs.GDATA_SERVICE_NAME_DOCLIST),
this);
Runnable onCompletion = new Runnable() {
public void run() {
setProgressValue(100);
sendToDocsSuccess = sender.wasSuccess();
executeStateMachine(SendState.SEND_TO_DOCS_DONE);
}
};
sender.setOnCompletion(onCompletion);
sender.sendToDocs(sendTrackId);
return SendState.NOT_READY;
}
private SendState onSendToGoogleDocsDone() {
return SendState.SHOW_RESULTS;
}
private SendState onSendToGoogleDone() {
tracker.dispatch();
runOnUiThread(new Runnable() {
@Override
public void run() {
Log.d(TAG, "Sending to Google done.");
dismissDialog(PROGRESS_DIALOG);
// Ensure a new done dialog is created each time.
// This is required because the send results must be available at the
// time the dialog is created.
removeDialog(DONE_DIALOG);
showDialog(DONE_DIALOG);
}
});
return SendState.NOT_READY;
}
private Dialog createDoneDialog() {
Log.d(TAG, "Creating done dialog");
// We've finished sending the track to the user-selected services. Now
// we tell them the results of the upload, and optionally share the track.
// There are a few different paths through this code:
//
// 1. The user pre-requested a share (shareRequested == true). We're going
// to display the result dialog *without* the share button (the share
// listener will be null). The OK button listener will initiate the
// share.
//
// 2. The user did not pre-request a share, and the set of services to
// which we succeeded in uploading the track are compatible with
// sharing. We'll display a share button (the share listener will be
// non-null), and will share the link if the user clicks it.
//
// 3. The user did not pre-request a share, and the set of services to
// which we succeeded in uploading the track are incompatible with
// sharing. We won't display a share button.
List<SendResult> results = makeSendToGoogleResults();
final boolean canShare = sendToFusionTablesTableId != null || sendToMapsMapId != null;
final OnClickListener finishListener = new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
executeStateMachine(SendState.FINISH);
}
};
DialogInterface.OnClickListener doShareListener = null;
if (canShare) {
doShareListener = new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
executeStateMachine(SendState.SHARE_LINK);
}
};
}
DialogInterface.OnClickListener onOkListener = (canShare && shareRequest)
? doShareListener : finishListener;
DialogInterface.OnClickListener onShareListener = (canShare && !shareRequest)
? doShareListener : null;
return ResultDialogFactory.makeDialog(
this, results, onOkListener, onShareListener, finishOnCancelListener);
}
private SendState onAllDone() {
Log.d(TAG, "All sending done.");
removeDialog(PROGRESS_DIALOG);
removeDialog(DONE_DIALOG);
progressDialog = null;
finish();
return SendState.DONE;
}
private SendState shareLink() {
String url = null;
if (sendToMaps && sendToMapsSuccess) {
// Prefer a link to Maps
url = MapsFacade.buildMapUrl(sendToMapsMapId);
} else if (sendToFusionTables && sendToFusionTablesSuccess) {
// Otherwise try using the link to fusion tables
url = getFusionTablesUrl(sendTrackId);
}
if (url != null) {
shareLinkToMap(url);
} else {
Log.w(TAG, "Failed to share link");
}
return SendState.FINISH;
}
/**
* Shares a link to a Map or Fusion Table via external app (email, gmail, ...)
* A chooser with apps that support text/plain will be shown to the user.
*/
private void shareLinkToMap(String url) {
boolean shareUrlOnly = sharedPreferences.getBoolean(
getString(R.string.share_url_only_key), false);
String msg = shareUrlOnly ? url : String.format(
getResources().getText(R.string.share_track_url_body_format).toString(), url);
Intent shareIntent = new Intent(Intent.ACTION_SEND);
shareIntent.setType("text/plain");
shareIntent.putExtra(Intent.EXTRA_SUBJECT,
getResources().getText(R.string.share_track_subject).toString());
shareIntent.putExtra(Intent.EXTRA_TEXT, msg);
startActivity(Intent.createChooser(shareIntent,
getResources().getText(R.string.share_track_picker_title).toString()));
}
protected String getFusionTablesUrl(long trackId) {
Track track = providerUtils.getTrack(trackId);
return SendToFusionTables.getMapVisualizationUrl(track);
}
/**
* Creates a list of {@link SendResult} instances based on the set of
* services selected in {@link SendDialog} and the results as known to
* this class.
*/
private List<SendResult> makeSendToGoogleResults() {
List<SendResult> results = new ArrayList<SendResult>();
if (sendToMaps) {
results.add(new SendResult(SendType.MAPS, sendToMapsSuccess));
}
if (sendToFusionTables) {
results.add(new SendResult(SendType.FUSION_TABLES,
sendToFusionTablesSuccess));
}
if (sendToDocs) {
results.add(new SendResult(SendType.DOCS, sendToDocsSuccess));
}
return results;
}
/**
* Initializes the authentication manager which obtains an authentication
* token, prompting the user for a login and password if needed.
*/
private void authenticate(final int requestCode, final String service) {
AuthManager authManager = getAuthManager(service);
authManager.doLogin(new AuthCallback() {
@Override
public void onAuthResult(boolean success) {
Log.i(TAG, "Login success for " + service + ": " + success);
if (!success) {
executeStateMachine(SendState.SHOW_RESULTS);
return;
}
onLoginSuccess(requestCode);
}
}, account);
}
@Override
public void onActivityResult(int requestCode, int resultCode,
final Intent results) {
SendState nextState = null;
switch (requestCode) {
case Constants.GET_MAP: {
// User picked a map to upload to
Log.d(TAG, "Get map result: " + resultCode);
if (resultCode == RESULT_OK) {
if (results.hasExtra("mapid")) {
sendToMapsMapId = results.getStringExtra("mapid");
}
nextState = SendState.SEND_TO_MAPS;
} else {
nextState = SendState.FINISH;
}
break;
}
case Constants.CHOOSE_ACCOUNT: {
if (resultCode == RESULT_CANCELED) {
nextState = SendState.FINISH;
break;
}
account = results.getParcelableExtra(AccountChooserActivity.ACCOUNT);
if (account == null) {
nextState = SendState.FINISH;
break;
}
nextState = SendState.START;
break;
}
default: {
Log.e(TAG, "Unrequested result: " + requestCode);
return;
}
}
if (nextState != null) {
executeStateMachine(nextState);
}
}
private void onLoginSuccess(int requestCode) {
SendState nextState;
switch (requestCode) {
case Constants.AUTHENTICATE_TO_MAPS:
// Authenticated with Google Maps
nextState = SendState.PICK_MAP;
break;
case Constants.AUTHENTICATE_TO_FUSION_TABLES:
// Authenticated with Google Fusion Tables
nextState = SendState.SEND_TO_FUSION_TABLES;
break;
case Constants.AUTHENTICATE_TO_DOCLIST:
// Authenticated with Google Docs
nextState = SendState.AUTHENTICATE_TRIX;
break;
case Constants.AUTHENTICATE_TO_TRIX:
// Authenticated with Trix
nextState = SendState.SEND_TO_DOCS;
break;
default: {
Log.e(TAG, "Unrequested login code: " + requestCode);
return;
}
}
executeStateMachine(nextState);
}
private AuthManager getAuthManager(String service) {
AuthManager authManager = authMap.get(service);
if (authManager == null) {
authManager = AuthManagerFactory.getAuthManager(this,
Constants.GET_LOGIN,
null,
true,
service);
authMap.put(service, authManager);
}
return authManager;
}
/**
* Resets status information for sending to Maps/Fusion Tables/Docs.
*/
private void resetState() {
account = null;
authMap.clear();
currentState = SendState.CHOOSE_ACCOUNT;
sendToMapsSuccess = false;
sendToFusionTablesSuccess = false;
sendToDocsSuccess = false;
sendToMapsMapId = null;
sendToFusionTablesTableId = null;
}
/**
* Gets a progress message indicating My Tracks is authenticating to a
* service.
*
* @param type the type of service
*/
private String getAuthenticatingProgressMessage(SendType type) {
String format = getString(R.string.send_google_progress_authenticating);
String serviceName = getString(type.getServiceName());
return String.format(format, serviceName);
}
@Override
public void setProgressMessage(final String message) {
runOnUiThread(new Runnable() {
public void run() {
if (progressDialog != null) {
progressDialog.setMessage(message);
}
}
});
}
@Override
public void setProgressValue(final int percent) {
runOnUiThread(new Runnable() {
public void run() {
if (progressDialog != null) {
progressDialog.setProgress(percent);
}
}
});
}
@Override
public void clearProgressMessage() {
progressDialog.setMessage("");
}
}
@@ -0,0 +1,294 @@
/*
* Copyright 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.io.sendtogoogle;
import android.accounts.Account;
import android.os.Parcel;
import android.os.Parcelable;
/**
* Send request states for sending a track to Google Maps, Google Fusion Tables,
* and Google Docs.
*
* @author Jimmy Shih
*/
public class SendRequest implements Parcelable {
public static final String SEND_REQUEST_KEY = "sendRequest";
private long trackId = -1L;
private boolean showMaps = false;
private boolean showFusionTables = false;
private boolean showDocs = false;
private boolean sendMaps = false;
private boolean sendFusionTables = false;
private boolean sendDocs = false;
private boolean newMap = false;
private Account account = null;
private String mapId = null;
private boolean mapsSuccess = false;
private boolean docsSuccess = false;
private boolean fusionTablesSuccess = false;
/**
* Creates a new send request.
*
* @param trackId the track id
* @param showMaps true to show the Google Maps option
* @param showFusionTables true to show the Google Fusion Tables option
* @param showDocs true to show the Google Docs option
*/
public SendRequest(long trackId, boolean showMaps, boolean showFusionTables, boolean showDocs) {
this.trackId = trackId;
this.showMaps = showMaps;
this.showFusionTables = showFusionTables;
this.showDocs = showDocs;
}
/**
* Get the track id.
*/
public long getTrackId() {
return trackId;
}
/**
* True if showing the send to Google Maps option.
*/
public boolean isShowMaps() {
return showMaps;
}
/**
* True if showing the send to Google Fusion Tables option.
*/
public boolean isShowFusionTables() {
return showFusionTables;
}
/**
* True if showing the send to Google Docs option.
*/
public boolean isShowDocs() {
return showDocs;
}
/**
* True if showing all the send options.
*/
public boolean isShowAll() {
return showMaps && showFusionTables && showDocs;
}
/**
* True if the user has selected the send to Google Maps option.
*/
public boolean isSendMaps() {
return sendMaps;
}
/**
* Sets the send to Google Maps option.
*
* @param sendMaps true if the user has selected the send to Google Maps
* option
*/
public void setSendMaps(boolean sendMaps) {
this.sendMaps = sendMaps;
}
/**
* True if the user has selected the send to Google Fusion Tables option.
*/
public boolean isSendFusionTables() {
return sendFusionTables;
}
/**
* Sets the send to Google Fusion Tables option.
*
* @param sendFusionTables true if the user has selected the send to Google
* Fusion Tables option
*/
public void setSendFusionTables(boolean sendFusionTables) {
this.sendFusionTables = sendFusionTables;
}
/**
* True if the user has selected the send to Google Docs option.
*/
public boolean isSendDocs() {
return sendDocs;
}
/**
* Sets the send to Google Docs option.
*
* @param sendDocs true if the user has selected the send to Google Docs
* option
*/
public void setSendDocs(boolean sendDocs) {
this.sendDocs = sendDocs;
}
/**
* True if the user has selected to create a new Google Maps.
*/
public boolean isNewMap() {
return newMap;
}
/**
* Sets the new map option.
*
* @param newMap true if the user has selected to create a new Google Maps.
*/
public void setNewMap(boolean newMap) {
this.newMap = newMap;
}
/**
* Gets the account.
*/
public Account getAccount() {
return account;
}
/**
* Sets the account.
*
* @param account the account
*/
public void setAccount(Account account) {
this.account = account;
}
/**
* Gets the selected map id if the user has selected to send a track to an
* existing Google Maps.
*/
public String getMapId() {
return mapId;
}
/**
* Sets the map id.
*
* @param mapId the map id
*/
public void setMapId(String mapId) {
this.mapId = mapId;
}
/**
* True if sending to Google Maps is success.
*/
public boolean isMapsSuccess() {
return mapsSuccess;
}
/**
* Sets the Google Maps result.
*
* @param mapsSuccess true if sending to Google Maps is success
*/
public void setMapsSuccess(boolean mapsSuccess) {
this.mapsSuccess = mapsSuccess;
}
/**
* True if sending to Google Fusion Tables is success.
*/
public boolean isFusionTablesSuccess() {
return fusionTablesSuccess;
}
/**
* Sets the Google Fusion Tables result.
*
* @param fusionTablesSuccess true if sending to Google Fusion Tables is
* success
*/
public void setFusionTablesSuccess(boolean fusionTablesSuccess) {
this.fusionTablesSuccess = fusionTablesSuccess;
}
/**
* True if sending to Google Docs is success.
*/
public boolean isDocsSuccess() {
return docsSuccess;
}
/**
* Sets the Google Docs result.
*
* @param docsSuccess true if sending to Google Docs is success
*/
public void setDocsSuccess(boolean docsSuccess) {
this.docsSuccess = docsSuccess;
}
private SendRequest(Parcel in) {
trackId = in.readLong();
showMaps = in.readByte() == 1;
showFusionTables = in.readByte() == 1;
showDocs = in.readByte() == 1;
sendMaps = in.readByte() == 1;
sendFusionTables = in.readByte() == 1;
sendDocs = in.readByte() == 1;
newMap = in.readByte() == 1;
account = in.readParcelable(null);
mapId = in.readString();
mapsSuccess = in.readByte() == 1;
fusionTablesSuccess = in.readByte() == 1;
docsSuccess = in.readByte() == 1;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel out, int flags) {
out.writeLong(trackId);
out.writeByte((byte) (showMaps ? 1 : 0));
out.writeByte((byte) (showFusionTables ? 1 : 0));
out.writeByte((byte) (showDocs ? 1 : 0));
out.writeByte((byte) (sendMaps ? 1 : 0));
out.writeByte((byte) (sendFusionTables ? 1 : 0));
out.writeByte((byte) (sendDocs ? 1 : 0));
out.writeByte((byte) (newMap ? 1 : 0));
out.writeParcelable(account, 0);
out.writeString(mapId);
out.writeByte((byte) (mapsSuccess ? 1 : 0));
out.writeByte((byte) (fusionTablesSuccess ? 1 : 0));
out.writeByte((byte) (docsSuccess ? 1 : 0));
}
public static final Parcelable.Creator<SendRequest> CREATOR = new Parcelable.Creator<
SendRequest>() {
public SendRequest createFromParcel(Parcel in) {
return new SendRequest(in);
}
public SendRequest[] newArray(int size) {
return new SendRequest[size];
}
};
}
@@ -1,46 +0,0 @@
/*
* 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.sendtogoogle;
/**
* This class represents the the result of the uploading of track data to a
* single service.
*
* @author Matthew Simmons
*/
public class SendResult {
private final SendType type;
private final boolean success;
/**
* @param type the service to which track data was uploaded
* @param success true if the uploading succeeded
*/
public SendResult(SendType type, boolean success) {
this.type = type;
this.success = success;
}
/** Returns the service to which the track data was uploaded */
public SendType getType() {
return type;
}
/** Returns true if the uploading succeeded */
public boolean isSuccess() {
return success;
}
}
@@ -0,0 +1,132 @@
/*
* Copyright 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.io.sendtogoogle;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.stats.TripStatistics;
import com.google.android.apps.mytracks.util.LocationUtils;
import android.location.Location;
import android.util.Log;
import java.util.ArrayList;
import java.util.List;
/**
* Commons utilities for sending a track to Google.
*
* @author Jimmy Shih
*/
public class SendToGoogleUtils {
private static final String TAG = SendToGoogleUtils.class.getSimpleName();
private SendToGoogleUtils() {}
/**
* Prepares a list of locations to send to Google Maps or Google Fusion
* Tables. Splits the locations into segments if necessary.
*
* @param track the track
* @param locations the list of locations
* @return an array of split segments.
*/
public static ArrayList<Track> prepareLocations(Track track, List<Location> locations) {
ArrayList<Track> splitTracks = new ArrayList<Track>();
// Create a new segment
Track segment = new Track();
segment.setId(track.getId());
segment.setName(track.getName());
segment.setDescription("");
segment.setCategory(track.getCategory());
TripStatistics segmentStats = segment.getStatistics();
TripStatistics trackStats = track.getStatistics();
segmentStats.setStartTime(trackStats.getStartTime());
segmentStats.setStopTime(trackStats.getStopTime());
boolean startNewTrackSegment = false;
for (Location loc : locations) {
// Latitude is greater than 90 if the location is invalid. Do not add to
// the segment.
if (loc.getLatitude() > 90) {
startNewTrackSegment = true;
}
if (startNewTrackSegment) {
// Close the last segment
prepareTrackSegment(segment, splitTracks);
startNewTrackSegment = false;
segment = new Track();
segment.setId(track.getId());
segment.setName(track.getName());
segment.setDescription("");
segment.setCategory(track.getCategory());
segmentStats = segment.getStatistics();
}
if (loc.getLatitude() <= 90) {
segment.addLocation(loc);
// For a new segment, sets its start time using the first available
// location time.
if (segmentStats.getStartTime() < 0) {
segmentStats.setStartTime(loc.getTime());
}
}
}
prepareTrackSegment(segment, splitTracks);
return splitTracks;
}
/**
* Prepares a track segment for sending to Google Maps or Google Fusion
* Tables. The main steps are:
* <ul>
* <li>make sure the segment has at least 2 points</li>
* <li>set the segment stop time if necessary</li>
* <li>decimate locations precision</li>
* </ul>
* The prepared track will be added to the splitTracks.
*
* @param segment the track segment
* @param splitTracks an array of track segments
*/
private static void prepareTrackSegment(Track segment, ArrayList<Track> splitTracks) {
// Make sure the segment has at least 2 points
if (segment.getLocations().size() < 2) {
Log.d(TAG, "segment has less than 2 points");
return;
}
// For a new segment, sets it stop time
TripStatistics segmentStats = segment.getStatistics();
if (segmentStats.getStopTime() < 0) {
Location lastLocation = segment.getLocations().get(segment.getLocations().size() - 1);
segmentStats.setStopTime(lastLocation.getTime());
}
// Decimate to 2 meter precision. Google Maps and Google Fusion Tables do
// not like the locations to be too precise.
LocationUtils.decimate(segment, 2.0);
splitTracks.add(segment);
}
}
@@ -1,76 +0,0 @@
/*
* 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.sendtogoogle;
import com.google.android.maps.mytracks.R;
import android.os.Parcel;
import android.os.Parcelable;
/**
* Enumerates the services to which we can upload track data.
*
* @author Matthew Simmons
*/
public enum SendType implements Parcelable {
MAPS(R.string.send_google_maps,
R.string.send_google_maps_url),
FUSION_TABLES(R.string.send_google_fusion_tables,
R.string.send_google_fusion_tables_url),
DOCS(R.string.send_google_docs,
R.string.send_google_docs_url);
private int serviceName;
private int serviceUrl;
private SendType(int serviceName, int serviceUrl) {
this.serviceName = serviceName;
this.serviceUrl = serviceUrl;
}
/** Returns the resource ID for the printable (short) name of the service */
public int getServiceName() {
return serviceName;
}
/** Returns the resource ID for the service's URL */
public int getServiceUrl() {
return serviceUrl;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(ordinal());
}
public static final Creator<SendType> CREATOR = new Creator<SendType>() {
@Override
public SendType createFromParcel(Parcel source) {
return SendType.values()[source.readInt()];
}
@Override
public SendType[] newArray(int size) {
return new SendType[size];
}
};
}
@@ -0,0 +1,194 @@
/*
* Copyright 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.io.sendtogoogle;
import com.google.android.apps.mytracks.Constants;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.io.fusiontables.SendFusionTablesUtils;
import com.google.android.apps.mytracks.io.maps.SendMapsUtils;
import com.google.android.maps.mytracks.R;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.view.View;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
/**
* A dialog to show the result of uploading to Google services.
*
* @author Jimmy Shih
*/
public class UploadResultActivity extends Activity {
private static final String TEXT_PLAIN_TYPE = "text/plain";
private static final int RESULT_DIALOG = 1;
private SendRequest sendRequest;
private Track track;
private String shareUrl;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
sendRequest = getIntent().getParcelableExtra(SendRequest.SEND_REQUEST_KEY);
track = null;
shareUrl = null;
if (sendRequest.isSendMaps() && sendRequest.isMapsSuccess()) {
shareUrl = SendMapsUtils.getMapUrl(getTrack());
}
if (shareUrl == null && sendRequest.isSendFusionTables()
&& sendRequest.isFusionTablesSuccess()) {
shareUrl = SendFusionTablesUtils.getMapUrl(getTrack());
}
}
private Track getTrack() {
if (track == null) {
track = MyTracksProviderUtils.Factory.get(this).getTrack(sendRequest.getTrackId());
}
return track;
}
@Override
protected void onResume() {
super.onResume();
showDialog(RESULT_DIALOG);
}
@Override
protected Dialog onCreateDialog(int id) {
switch (id) {
case RESULT_DIALOG:
AlertDialog.Builder builder = new AlertDialog.Builder(this);
View view = getLayoutInflater().inflate(R.layout.upload_result, null);
builder.setView(view);
LinearLayout mapsResult = (LinearLayout) view.findViewById(R.id.upload_result_maps_result);
LinearLayout fusionTablesResult = (LinearLayout) view.findViewById(
R.id.upload_result_fusion_tables_result);
LinearLayout docsResult = (LinearLayout) view.findViewById(R.id.upload_result_docs_result);
ImageView mapsResultIcon = (ImageView) view.findViewById(
R.id.upload_result_maps_result_icon);
ImageView fusionTablesResultIcon = (ImageView) view.findViewById(
R.id.upload_result_fusion_tables_result_icon);
ImageView docsResultIcon = (ImageView) view.findViewById(
R.id.upload_result_docs_result_icon);
TextView successFooter = (TextView) view.findViewById(R.id.upload_result_success_footer);
TextView errorFooter = (TextView) view.findViewById(R.id.upload_result_error_footer);
boolean hasError = false;
if (!sendRequest.isSendMaps()) {
mapsResult.setVisibility(View.GONE);
} else {
if (!sendRequest.isMapsSuccess()) {
mapsResultIcon.setImageResource(R.drawable.failure);
hasError = true;
}
}
if (!sendRequest.isSendFusionTables()) {
fusionTablesResult.setVisibility(View.GONE);
} else {
if (!sendRequest.isFusionTablesSuccess()) {
fusionTablesResultIcon.setImageResource(R.drawable.failure);
hasError = true;
}
}
if (!sendRequest.isSendDocs()) {
docsResult.setVisibility(View.GONE);
} else {
if (!sendRequest.isDocsSuccess()) {
docsResultIcon.setImageResource(R.drawable.failure);
hasError = true;
}
}
if (hasError) {
builder.setTitle(R.string.generic_error_title);
builder.setIcon(android.R.drawable.ic_dialog_alert);
successFooter.setVisibility(View.GONE);
} else {
builder.setTitle(R.string.generic_success_title);
builder.setIcon(android.R.drawable.ic_dialog_info);
errorFooter.setVisibility(View.GONE);
}
builder.setCancelable(true);
builder.setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
finish();
}
});
builder.setPositiveButton(
getString(R.string.generic_ok), new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
if (!sendRequest.isShowAll() && shareUrl != null) {
startShareUrlActivity(shareUrl);
}
finish();
}
});
// Add a Share URL button if showing all the options and a shareUrl
// exists
if (sendRequest.isShowAll() && shareUrl != null) {
builder.setNegativeButton(getString(R.string.send_google_result_share_url),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
startShareUrlActivity(shareUrl);
finish();
}
});
}
return builder.create();
default:
return null;
}
}
/**
* Starts an activity to share the url.
*
* @param url the url
*/
private void startShareUrlActivity(String url) {
SharedPreferences prefs = getSharedPreferences(Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
boolean shareUrlOnly = prefs.getBoolean(getString(R.string.share_url_only_key), false);
Intent intent = new Intent(Intent.ACTION_SEND);
intent.setType(TEXT_PLAIN_TYPE);
intent.putExtra(Intent.EXTRA_SUBJECT, getString(R.string.share_track_subject));
intent.putExtra(Intent.EXTRA_TEXT,
shareUrlOnly ? url : getString(R.string.share_track_url_body_format, url));
startActivity(Intent.createChooser(intent, getString(R.string.share_track_picker_title)));
}
}
@@ -16,8 +16,10 @@
package com.google.android.apps.mytracks.io.sendtogoogle;
import com.google.android.apps.mytracks.Constants;
import com.google.android.apps.mytracks.util.ApiFeatures;
import com.google.android.apps.mytracks.util.AnalyticsUtils;
import com.google.android.apps.mytracks.util.ApiAdapterFactory;
import com.google.android.maps.mytracks.R;
import com.google.common.annotations.VisibleForTesting;
import android.app.Activity;
import android.app.Dialog;
@@ -36,27 +38,19 @@ import android.widget.CompoundButton.OnCheckedChangeListener;
import android.widget.RadioButton;
import android.widget.TableRow;
import java.util.ArrayList;
/**
* A chooser to select the Google services to upload a track to.
*
* @author jshih@google.com (Jimmy Shih)
* @author Jimmy Shih
*/
public class UploadServiceChooserActivity extends Activity {
/**
* The track id.
*/
public static final String TRACK_ID = "trackId";
/**
* The send type. Null to send to all Google services.
*/
public static final String SEND_TYPE = "sendType";
private static final int SERVICE_PICKER_DIALOG = 1;
private long trackId;
private SendType sendType;
private SendRequest sendRequest;
private Dialog dialog;
private TableRow mapsTableRow;
private TableRow fusionTablesTableRow;
@@ -76,10 +70,7 @@ public class UploadServiceChooserActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Intent intent = getIntent();
trackId = intent.getLongExtra(TRACK_ID, -1L);
sendType = intent.getParcelableExtra(SEND_TYPE);
sendRequest = getIntent().getParcelableExtra(SendRequest.SEND_REQUEST_KEY);
}
@Override
@@ -92,7 +83,7 @@ public class UploadServiceChooserActivity extends Activity {
protected Dialog onCreateDialog(int id) {
switch (id) {
case SERVICE_PICKER_DIALOG:
final Dialog dialog = new Dialog(this);
dialog = new Dialog(this);
dialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
dialog.setContentView(R.layout.upload_service_chooser);
dialog.setCancelable(true);
@@ -136,23 +127,15 @@ public class UploadServiceChooserActivity extends Activity {
send.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
saveState();
Intent intent = new Intent(UploadServiceChooserActivity.this, SendActivity.class);
intent.putExtra(SendActivity.TRACK_ID, trackId);
intent.putExtra(SendActivity.SHARE_URL, sendType != null);
intent.putExtra(SendActivity.SEND_MAPS, sendMaps());
intent.putExtra(SendActivity.SEND_FUSION_TABLES, sendFusionTables());
intent.putExtra(SendActivity.SEND_DOCS, sendDocs());
intent.putExtra(SendActivity.CREATE_MAP, !existingMapRadioButton.isChecked());
startActivity(intent);
finish();
startNextActivity();
}
});
// Setup initial state
initState();
// Update state based on sendType
updateStateBySendType();
// Update state based on sendRequest
updateStateBySendRequest();
// Update state based on current user selection
updateStateBySelection();
@@ -166,7 +149,8 @@ public class UploadServiceChooserActivity extends Activity {
/**
* Initializes the UI state based on the shared preferences.
*/
private void initState() {
@VisibleForTesting
void initState() {
SharedPreferences prefs = getSharedPreferences(Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
boolean pickExistingMap = prefs.getBoolean(getString(R.string.pick_existing_map_key), false);
@@ -180,21 +164,21 @@ public class UploadServiceChooserActivity extends Activity {
}
/**
* Updates the UI state based on the sendType.
* Updates the UI state based on sendRequest.
*/
private void updateStateBySendType() {
if (sendType == SendType.MAPS) {
mapsCheckBox.setChecked(true);
} else if (sendType == SendType.FUSION_TABLES) {
fusionTablesCheckBox.setChecked(true);
} else if (sendType == SendType.DOCS) {
docsCheckBox.setChecked(true);
} else {
// sendType == null
private void updateStateBySendRequest() {
if (!sendRequest.isShowAll()) {
if (sendRequest.isShowMaps()) {
mapsCheckBox.setChecked(true);
} else if (sendRequest.isShowFusionTables()) {
fusionTablesCheckBox.setChecked(true);
} else if (sendRequest.isShowDocs()) {
docsCheckBox.setChecked(true);
}
}
mapsTableRow.setVisibility(showMaps() ? View.VISIBLE : View.GONE);
fusionTablesTableRow.setVisibility(showFusionTables() ? View.VISIBLE : View.GONE);
docsTableRow.setVisibility(showDocs() ? View.VISIBLE : View.GONE);
mapsTableRow.setVisibility(sendRequest.isShowMaps() ? View.VISIBLE : View.GONE);
fusionTablesTableRow.setVisibility(sendRequest.isShowFusionTables() ? View.VISIBLE : View.GONE);
docsTableRow.setVisibility(sendRequest.isShowDocs() ? View.VISIBLE : View.GONE);
}
/**
@@ -208,58 +192,81 @@ public class UploadServiceChooserActivity extends Activity {
/**
* Saves the UI state to the shared preferences.
*/
private void saveState() {
@VisibleForTesting
void saveState() {
SharedPreferences prefs = getSharedPreferences(Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
Editor editor = prefs.edit();
editor.putBoolean(
getString(R.string.pick_existing_map_key), existingMapRadioButton.isChecked());
if (sendType == null) {
if (sendRequest.isShowAll()) {
editor.putBoolean(getString(R.string.send_to_maps_key), sendMaps());
editor.putBoolean(getString(R.string.send_to_fusion_tables_key), sendFusionTables());
editor.putBoolean(getString(R.string.send_to_docs_key), sendDocs());
}
ApiFeatures.getInstance().getApiAdapter().applyPreferenceChanges(editor);
}
/**
* Returns true to show the Google Maps option.
*/
private boolean showMaps() {
return sendType == null || sendType == SendType.MAPS;
}
/**
* Returns true to show the Google Fusion Tables option.
*/
private boolean showFusionTables() {
return sendType == null || sendType == SendType.FUSION_TABLES;
}
/**
* Returns true to show the Google Docs option.
*/
private boolean showDocs() {
return sendType == null || sendType == SendType.DOCS;
ApiAdapterFactory.getApiAdapter().applyPreferenceChanges(editor);
}
/**
* Returns true to send to Google Maps.
*/
private boolean sendMaps() {
return showMaps() && mapsCheckBox.isChecked();
return sendRequest.isShowMaps() && mapsCheckBox.isChecked();
}
/**
* Returns true to send to Google Fusion Tables.
*/
private boolean sendFusionTables() {
return showFusionTables() && fusionTablesCheckBox.isChecked();
return sendRequest.isShowFusionTables() && fusionTablesCheckBox.isChecked();
}
/**
* Returns true to send to Google Docs.
*/
private boolean sendDocs() {
return showDocs() && docsCheckBox.isChecked();
return sendRequest.isShowDocs() && docsCheckBox.isChecked();
}
/**
* Starts the next activity, {@link AccountChooserActivity}.
*/
@VisibleForTesting
protected void startNextActivity() {
sendStats();
sendRequest.setSendMaps(sendMaps());
sendRequest.setSendFusionTables(sendFusionTables());
sendRequest.setSendDocs(sendDocs());
sendRequest.setNewMap(!existingMapRadioButton.isChecked());
Intent intent = new Intent(this, AccountChooserActivity.class)
.putExtra(SendRequest.SEND_REQUEST_KEY, sendRequest);
startActivity(intent);
finish();
}
/**
* Sends stats to Google Analytics.
*/
private void sendStats() {
ArrayList<String> pages = new ArrayList<String>();
if (sendRequest.isSendMaps()) {
pages.add("/send/maps");
}
if (sendRequest.isSendFusionTables()) {
pages.add("/send/fusion_tables");
}
if (sendRequest.isSendDocs()) {
pages.add("/send/docs");
}
AnalyticsUtils.sendPageViews(this, pages.toArray(new String[pages.size()]));
}
@VisibleForTesting
Dialog getDialog() {
return dialog;
}
@VisibleForTesting
SendRequest getSendRequest() {
return sendRequest;
}
}
@@ -21,35 +21,38 @@ import com.google.android.apps.mytracks.Constants;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.stats.TripStatistics;
import com.google.android.apps.mytracks.util.UnitConversions;
import com.google.android.maps.mytracks.R;
import com.google.common.annotations.VisibleForTesting;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.util.Log;
/**
* A dynamic speed path descriptor.
*
*
* @author Vangelis S.
*/
public class DynamicSpeedTrackPathDescriptor
implements TrackPathDescriptor, OnSharedPreferenceChangeListener {
public class DynamicSpeedTrackPathDescriptor implements TrackPathDescriptor,
OnSharedPreferenceChangeListener {
private int slowSpeed;
private int normalSpeed;
private int speedMargin;
private final int speedMarginDefault;
private double averageMovingSpeed;
private final Context context;
public DynamicSpeedTrackPathDescriptor(Context context){
@VisibleForTesting
static final int CRITICAL_DIFFERENCE_PERCENTAGE = 20;
public DynamicSpeedTrackPathDescriptor(Context context) {
this.context = context;
speedMarginDefault = Integer.parseInt(
context.getString(R.string.color_mode_dynamic_percentage_default));
SharedPreferences prefs = context.getSharedPreferences(
Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
speedMarginDefault = Integer.parseInt(context
.getString(R.string.color_mode_dynamic_percentage_default));
SharedPreferences prefs = context.getSharedPreferences(Constants.SETTINGS_NAME,
Context.MODE_PRIVATE);
if (prefs == null) {
speedMargin = speedMarginDefault;
@@ -60,7 +63,8 @@ public class DynamicSpeedTrackPathDescriptor
speedMargin = getSpeedMargin(prefs);
}
private int getSpeedMargin(SharedPreferences sharedPreferences) {
@VisibleForTesting
int getSpeedMargin(SharedPreferences sharedPreferences) {
try {
return Integer.parseInt(sharedPreferences.getString(
context.getString(R.string.track_color_mode_dynamic_speed_variation_key),
@@ -72,34 +76,32 @@ public class DynamicSpeedTrackPathDescriptor
/**
* Get the slow speed calculated based on the % below the average speed.
*
* @return The speed limit considered as slow.
*/
public int getSlowSpeed()
{
slowSpeed = (int) (averageMovingSpeed - (averageMovingSpeed * speedMargin / 100));
public int getSlowSpeed() {
slowSpeed = (int) (averageMovingSpeed - (averageMovingSpeed * speedMargin / 100));
return slowSpeed;
}
/**
* Get the medium speed calculated based on the % above the average speed.
* Gets the medium speed calculated based on the % above the average speed.
*
* @return The speed limit considered as normal.
*/
public int getNormalSpeed()
{
normalSpeed = (int) (averageMovingSpeed + (averageMovingSpeed * speedMargin / 100));
public int getNormalSpeed() {
normalSpeed = (int) (averageMovingSpeed + (averageMovingSpeed * speedMargin / 100));
return normalSpeed;
}
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
Log.d(TAG, "DynamicSpeedTrackPathDescriptor: onSharedPreferences changed " + key);
if (key == null
|| !key.equals(context.getString(R.string.track_color_mode_dynamic_speed_variation_key))) {
return;
}
SharedPreferences prefs = context.getSharedPreferences(
Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
if (key == null
|| !key.equals(context.getString(R.string.track_color_mode_dynamic_speed_variation_key))) { return; }
SharedPreferences prefs = context.getSharedPreferences(Constants.SETTINGS_NAME,
Context.MODE_PRIVATE);
if (prefs == null) {
speedMargin = speedMarginDefault;
return;
@@ -110,33 +112,70 @@ public class DynamicSpeedTrackPathDescriptor
@Override
public boolean needsRedraw() {
SharedPreferences prefs = context.getSharedPreferences(
Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
SharedPreferences prefs = context.getSharedPreferences(Constants.SETTINGS_NAME,
Context.MODE_PRIVATE);
long currentTrackId = prefs.getLong(context.getString(R.string.selected_track_key), -1);
if(currentTrackId == -1) {
// Could not find track.
return false;
if (currentTrackId == -1) {
// Could not find track.
return false;
}
Track track = MyTracksProviderUtils.Factory.get(context).getTrack(currentTrackId);
TripStatistics stats = track.getStatistics();
double newaverageSpeed = (int) Math.floor(stats.getAverageMovingSpeed() * 3.6);
if(averageMovingSpeed == 0) {
averageMovingSpeed = newaverageSpeed;
return true;
}
double difference = Math.max(averageMovingSpeed, newaverageSpeed);
double newAverageMovingSpeed = (int) Math.floor(
stats.getAverageMovingSpeed() * UnitConversions.MS_TO_KMH);
if (difference == 0.0) {
difference = 0.0;
} else {
difference = Math.abs(averageMovingSpeed - newaverageSpeed) / difference * 100;
return isDifferenceSignificant(averageMovingSpeed, newAverageMovingSpeed);
}
/**
* Checks whether the old speed and the new speed differ significantly or not.
*/
public boolean isDifferenceSignificant(double oldAverageMovingSpeed, double newAverageMovingSpeed) {
if (oldAverageMovingSpeed == 0) {
if (newAverageMovingSpeed == 0) {
return false;
} else {
averageMovingSpeed = newAverageMovingSpeed;
return true;
}
}
if(difference >= 20) {
averageMovingSpeed = newaverageSpeed;
// Here, both oldAverageMovingSpeed and newAverageMovingSpeed are not zero.
double maxValue = Math.max(oldAverageMovingSpeed, newAverageMovingSpeed);
double differencePercentage = Math.abs(oldAverageMovingSpeed - newAverageMovingSpeed)
/ maxValue * 100;
if (differencePercentage >= CRITICAL_DIFFERENCE_PERCENTAGE) {
averageMovingSpeed = newAverageMovingSpeed;
return true;
}
return false;
}
/**
* Gets the value of variable speedMargin to check the result of test.
* @return the value of speedMargin.
*/
@VisibleForTesting
int getSpeedMargin() {
return speedMargin;
}
/**
* Sets the value of newAverageMovingSpeed to test the method isDifferenceSignificant.
* @param newAverageMovingSpeed the value to set.
*/
@VisibleForTesting
void setAverageMovingSpeed(double newAverageMovingSpeed) {
averageMovingSpeed = newAverageMovingSpeed;
}
/**
* Gets the value of averageMovingSpeed to check the result of test.
*
* @return the value of averageMovingSpeed
*/
@VisibleForTesting
double getAverageMovingSpeed() {
return averageMovingSpeed;
}
}
@@ -37,7 +37,7 @@ import android.util.Log;
* application delegates starting and stopping a recording to
* {@link TrackRecordingService} using RPC calls.
*
* @author jshih@google.com (Jimmy Shih)
* @author Jimmy Shih
*/
public class ControlRecordingService extends IntentService implements ServiceConnection {
@@ -16,7 +16,7 @@
package com.google.android.apps.mytracks.services;
import com.google.android.apps.mytracks.Constants;
import com.google.android.apps.mytracks.util.ApiFeatures;
import com.google.android.apps.mytracks.util.ApiAdapterFactory;
import com.google.android.maps.mytracks.R;
import android.content.Context;
@@ -174,19 +174,19 @@ public class PreferenceManager implements OnSharedPreferenceChangeListener {
public void setAutoResumeTrackCurrentRetry(int retryAttempts) {
Editor editor = sharedPreferences.edit();
editor.putInt(autoResumeTrackCurrentRetryKey, retryAttempts);
ApiFeatures.getInstance().getApiAdapter().applyPreferenceChanges(editor);
ApiAdapterFactory.getApiAdapter().applyPreferenceChanges(editor);
}
public void setRecordingTrack(long id) {
Editor editor = sharedPreferences.edit();
editor.putLong(recordingTrackKey, id);
ApiFeatures.getInstance().getApiAdapter().applyPreferenceChanges(editor);
ApiAdapterFactory.getApiAdapter().applyPreferenceChanges(editor);
}
public void setSelectedTrack(long id) {
Editor editor = sharedPreferences.edit();
editor.putLong(selectedTrackKey, id);
ApiFeatures.getInstance().getApiAdapter().applyPreferenceChanges(editor);
ApiAdapterFactory.getApiAdapter().applyPreferenceChanges(editor);
}
public void shutdown() {
@@ -0,0 +1,124 @@
/*
* 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.services;
import com.google.android.apps.mytracks.io.file.TrackWriterFactory.TrackFileFormat;
import com.google.android.apps.mytracks.util.FileUtils;
import com.google.common.annotations.VisibleForTesting;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.Environment;
import android.os.IBinder;
import android.util.Log;
import java.io.File;
/**
* A service to remove My Tracks temp files older than one hour on the SD card.
*
* @author Jimmy Shih
*/
public class RemoveTempFilesService extends Service {
private static final String TAG = RemoveTempFilesService.class.getSimpleName();
private static final int ONE_HOUR_IN_MILLISECONDS = 60 * 60 * 1000;
private RemoveTempFilesAsyncTask removeTempFilesAsyncTask = null;
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
// Setup an alarm to repeatedly call this service
Intent alarmIntent = new Intent(this, RemoveTempFilesService.class);
PendingIntent pendingIntent = PendingIntent.getService(
this, 0, alarmIntent, PendingIntent.FLAG_CANCEL_CURRENT);
AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
alarmManager.setInexactRepeating(AlarmManager.RTC_WAKEUP,
System.currentTimeMillis() + ONE_HOUR_IN_MILLISECONDS, AlarmManager.INTERVAL_HOUR,
pendingIntent);
// Invoke the AsyncTask to cleanup the temp files
if (removeTempFilesAsyncTask == null
|| removeTempFilesAsyncTask.getStatus().equals(AsyncTask.Status.FINISHED)) {
removeTempFilesAsyncTask = new RemoveTempFilesAsyncTask();
removeTempFilesAsyncTask.execute((Void[]) null);
}
return START_NOT_STICKY;
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
private class RemoveTempFilesAsyncTask extends AsyncTask<Void, Void, Void> {
@Override
protected Void doInBackground(Void... params) {
if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
// Can't do anything
return null;
}
cleanTempDirectory(TrackFileFormat.GPX.getExtension());
cleanTempDirectory(TrackFileFormat.KML.getExtension());
cleanTempDirectory(TrackFileFormat.CSV.getExtension());
cleanTempDirectory(TrackFileFormat.TCX.getExtension());
return null;
}
@Override
protected void onPostExecute(Void result) {
stopSelf();
}
}
private void cleanTempDirectory(String name) {
FileUtils fileUtils = new FileUtils();
String dirName = fileUtils.buildExternalDirectoryPath(name, "tmp");
File dir = new File(dirName);
cleanTempDirectory(dir);
}
/**
* Removes temp files in a directory older than one hour.
*
* @param dir the directory
* @return the number of files removed.
*/
@VisibleForTesting
int cleanTempDirectory(File dir) {
if (!dir.exists()) {
return 0;
}
int count = 0;
long oneHourAgo = System.currentTimeMillis() - ONE_HOUR_IN_MILLISECONDS;
for (File f : dir.listFiles()) {
if (f.lastModified() < oneHourAgo) {
if (!f.delete()) {
Log.e(TAG, "Unable to delete file: " + f.getAbsolutePath());
} else {
count++;
}
}
}
return count;
}
}
@@ -20,6 +20,8 @@ import static com.google.android.apps.mytracks.Constants.TAG;
import com.google.android.apps.mytracks.Constants;
import com.google.android.apps.mytracks.MyTracks;
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;
@@ -36,14 +38,11 @@ import com.google.android.apps.mytracks.services.tasks.SplitTask;
import com.google.android.apps.mytracks.services.tasks.StatusAnnouncerFactory;
import com.google.android.apps.mytracks.stats.TripStatistics;
import com.google.android.apps.mytracks.stats.TripStatisticsBuilder;
import com.google.android.apps.mytracks.util.ApiFeatures;
import com.google.android.apps.mytracks.util.ApiLevelAdapter;
import com.google.android.apps.mytracks.util.LocationUtils;
import com.google.android.apps.mytracks.util.StringUtils;
import com.google.android.maps.mytracks.R;
import com.google.common.annotations.VisibleForTesting;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.ContentValues;
@@ -80,7 +79,6 @@ public class TrackRecordingService extends Service {
static final int MAX_AUTO_RESUME_TRACK_RETRY_ATTEMPTS = 3;
private NotificationManager notificationManager;
private LocationManager locationManager;
private WakeLock wakeLock;
@@ -243,8 +241,6 @@ public class TrackRecordingService extends Service {
super.onCreate();
Log.d(TAG, "TrackRecordingService.onCreate");
providerUtils = MyTracksProviderUtils.Factory.get(this);
notificationManager =
(NotificationManager) getSystemService(NOTIFICATION_SERVICE);
locationManager = (LocationManager) getSystemService(LOCATION_SERVICE);
setUpTaskExecutors();
@@ -301,7 +297,7 @@ public class TrackRecordingService extends Service {
handleStartCommand(intent, startId);
return START_STICKY;
}
private void handleStartCommand(Intent intent, int startId) {
Log.d(TAG, "TrackRecordingService.handleStartCommand: " + startId);
@@ -318,7 +314,7 @@ public class TrackRecordingService extends Service {
private boolean isTrackInProgress() {
return recordingTrackId != -1 || isRecording;
}
private void resumeTrack(int startId) {
Log.d(TAG, "TrackRecordingService: requested resume");
@@ -363,13 +359,12 @@ public class TrackRecordingService extends Service {
unregisterLocationListener();
shutdownTaskExecutors();
if (sensorManager != null) {
sensorManager.shutdown();
SensorManagerFactory.getInstance().releaseSensorManager(sensorManager);
sensorManager = null;
}
// Make sure we have no indirect references to this service.
locationManager = null;
notificationManager = null;
providerUtils = null;
binder.detachFromService();
binder = null;
@@ -485,8 +480,6 @@ public class TrackRecordingService extends Service {
* Shows the notification message and icon in the notification bar.
*/
private void showNotification() {
final ApiLevelAdapter apiLevelAdapter =
ApiFeatures.getInstance().getApiAdapter();
if (isRecording) {
Notification notification = new Notification(
R.drawable.arrow_320, null /* tickerText */,
@@ -497,16 +490,24 @@ public class TrackRecordingService extends Service {
notification.setLatestEventInfo(this, getString(R.string.my_tracks_app_name),
getString(R.string.track_record_notification), contentIntent);
notification.flags += Notification.FLAG_NO_CLEAR;
apiLevelAdapter.startForeground(this, notificationManager, 1,
notification);
startForegroundService(notification);
} else {
apiLevelAdapter.stopForeground(this, notificationManager, 1);
stopForegroundService();
}
}
@VisibleForTesting
protected void startForegroundService(Notification notification) {
startForeground(1, notification);
}
@VisibleForTesting
protected void stopForegroundService() {
stopForeground(true);
}
private void setUpTaskExecutors() {
announcementExecutor = new PeriodicTaskExecutor(
this, new StatusAnnouncerFactory(ApiFeatures.getInstance()));
announcementExecutor = new PeriodicTaskExecutor(this, new StatusAnnouncerFactory());
splitExecutor = new PeriodicTaskExecutor(this, new SplitTask.Factory());
}
@@ -559,7 +560,7 @@ public class TrackRecordingService extends Service {
Log.d(TAG,
"Location listener now unregistered w/ TrackRecordingService.");
}
private String getDefaultActivityType(Context context) {
SharedPreferences prefs = context.getSharedPreferences(
Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
@@ -601,10 +602,7 @@ public class TrackRecordingService extends Service {
length = 0;
showNotification();
registerLocationListener();
sensorManager = SensorManagerFactory.getSensorManager(this);
if (sensorManager != null) {
sensorManager.onStartTrack();
}
sensorManager = SensorManagerFactory.getInstance().getSensorManager(this);
// Reset the number of auto-resume retries.
setAutoResumeTrackRetries(0);
@@ -985,7 +983,7 @@ public class TrackRecordingService extends Service {
* @param waypoint The waypoint which will be populated with stats data.
*/
private void buildStatisticsMarker(Waypoint waypoint) {
StringUtils utils = new StringUtils(TrackRecordingService.this);
DescriptionGenerator descriptionGenerator = new DescriptionGeneratorImpl(this);
// Set stop and total time in the stats data
final long time = System.currentTimeMillis();
@@ -999,7 +997,7 @@ public class TrackRecordingService extends Service {
waypoint.setType(Waypoint.TYPE_STATISTICS);
waypoint.setName(getString(R.string.marker_type_statistics));
waypoint.setStatistics(waypointStatsBuilder.getStatistics());
waypoint.setDescription(utils.generateWaypointDescription(waypoint));
waypoint.setDescription(descriptionGenerator.generateWaypointDescription(waypoint));
waypoint.setIcon(getString(R.string.marker_statistics_icon_url));
waypoint.setStartId(providerUtils.getLastLocationId(recordingTrackId));
@@ -1038,7 +1036,7 @@ public class TrackRecordingService extends Service {
prefManager.setRecordingTrack(recordingTrackId = -1);
if (sensorManager != null) {
sensorManager.shutdown();
SensorManagerFactory.getInstance().releaseSensorManager(sensorManager);
sensorManager = null;
}
@@ -1056,7 +1054,7 @@ public class TrackRecordingService extends Service {
.setAction(getString(actionResId))
.putExtra(getString(R.string.track_id_broadcast_extra), trackId);
sendBroadcast(broadcastIntent, getString(R.string.permission_notification_value));
SharedPreferences sharedPreferences = getSharedPreferences(
Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
if (sharedPreferences.getBoolean(getString(R.string.allow_access_key), false)) {
@@ -1202,17 +1200,17 @@ public class TrackRecordingService extends Service {
throw new IllegalStateException("The service has been already detached!");
}
}
/**
* Returns true if the RPC caller is from the same application or if the
* "Allow access" setting indicates that another app can invoke this service's
* RPCs.
* RPCs.
*/
private boolean canAccess() {
// As a precondition for access, must check if the service is available.
checkService();
if (Process.myPid() == Binder.getCallingPid()) {
return true;
} else {
@@ -1297,7 +1295,7 @@ public class TrackRecordingService extends Service {
public int getSensorState() {
if (!canAccess()) {
return Sensor.SensorState.NONE.getNumber();
}
}
if (service.sensorManager == null) {
Log.d(TAG, "No sensor manager for data.");
return Sensor.SensorState.NONE.getNumber();
@@ -18,7 +18,7 @@ package com.google.android.apps.mytracks.services.sensors;
import com.google.android.apps.mytracks.Constants;
import com.google.android.apps.mytracks.content.Sensor;
import com.google.android.apps.mytracks.util.ApiFeatures;
import com.google.android.apps.mytracks.util.ApiAdapterFactory;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
@@ -31,8 +31,6 @@ import android.util.Log;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.UUID;
/**
@@ -44,8 +42,9 @@ import java.util.UUID;
* @author Sandor Dornbush
*/
public class BluetoothConnectionManager {
// Unique UUID for this application
private static final UUID SPP_UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB");
// Unique Bluetooth UUID for My Tracks
public static final UUID SPP_UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB");
private MessageParser parser;
@@ -254,41 +253,13 @@ public class BluetoothConnectionManager {
// Get a BluetoothSocket for a connection with the
// given BluetoothDevice
try {
tmp = getSocket();
tmp = ApiAdapterFactory.getApiAdapter().getBluetoothSocket(device);
} catch (IOException e) {
Log.e(Constants.TAG, "create() failed", e);
}
socket = tmp;
}
private BluetoothSocket getSocket() throws IOException {
if (ApiFeatures.getInstance().hasBluetoothDeviceCreateInsecureRfcommSocketToServiceRecord()) {
try {
return device.createInsecureRfcommSocketToServiceRecord(SPP_UUID);
} catch (IOException e) {
Log.e(Constants.TAG, "Unable to get insecure connect.", e);
}
} else {
try {
Class<? extends BluetoothDevice> c = device.getClass();
Method insecure = c.getMethod("createInsecureRfcommSocket", Integer.class);
insecure.setAccessible(true);
return (BluetoothSocket) insecure.invoke(device, 1);
} catch (SecurityException e) {
Log.e(Constants.TAG, "Unable to get insecure connect.", e);
} catch (NoSuchMethodException e) {
Log.e(Constants.TAG, "Unable to get insecure connect.", e);
} catch (IllegalArgumentException e) {
Log.e(Constants.TAG, "Unable to get insecure connect.", e);
} catch (IllegalAccessException e) {
Log.e(Constants.TAG, "Unable to get insecure connect.", e);
} catch (InvocationTargetException e) {
Log.e(Constants.TAG, "Unable to get insecure connect.", e);
}
}
return device.createRfcommSocketToServiceRecord(SPP_UUID);
}
@Override
public void run() {
Log.d(Constants.TAG, "BEGIN mConnectThread");

Some files were not shown because too many files have changed in this diff Show More