forked from upstream-mirrors/OpenTracks
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:
@@ -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 Rttsches
|
||||
*/
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+68
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+463
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+196
@@ -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;
|
||||
}
|
||||
}
|
||||
+269
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+72
@@ -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);
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -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>();
|
||||
+2
-2
@@ -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
-2
@@ -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";
|
||||
+4
-4
@@ -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;
|
||||
}
|
||||
|
||||
+2
-1
@@ -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.
|
||||
+4
-8
@@ -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");
|
||||
+2
-2
@@ -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
-1
@@ -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;
|
||||
+2
-2
@@ -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
-1
@@ -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));
|
||||
}
|
||||
}
|
||||
+139
@@ -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);
|
||||
}
|
||||
+147
@@ -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();
|
||||
}
|
||||
+168
-40
@@ -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();
|
||||
}
|
||||
}
|
||||
-109
@@ -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];
|
||||
}
|
||||
};
|
||||
}
|
||||
+194
@@ -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)));
|
||||
}
|
||||
}
|
||||
+78
-71
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+86
-47
@@ -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();
|
||||
|
||||
+5
-34
@@ -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
Reference in New Issue
Block a user