Add a class EndToEndTestRunner which extends InstrumentationTestRunner and can get the port of emulator. Then test can be run on more than one emulators at the same time.

This commit is contained in:
youtaol
2012-08-01 17:25:21 +08:00
commit 8796318335
690 changed files with 64632 additions and 0 deletions
@@ -0,0 +1,55 @@
/*
* 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;
import com.google.android.apps.mytracks.util.ApiAdapterFactory;
import android.os.Bundle;
import android.speech.tts.TextToSpeech;
import android.support.v4.app.FragmentActivity;
import android.view.MenuItem;
/**
* An abstract class for all My Tracks activities.
*
* @author Jimmy Shih
*/
public class AbstractMyTracksActivity extends FragmentActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setVolumeControlStream(TextToSpeech.Engine.DEFAULT_STREAM);
ApiAdapterFactory.getApiAdapter().configureActionBarHomeAsUp(this);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() != android.R.id.home) {
return super.onOptionsItemSelected(item);
}
onHomeSelected();
return true;
}
/**
* Callback when the home menu item is selected.
*/
protected void onHomeSelected() {
finish();
}
}
@@ -0,0 +1,59 @@
/*
* 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;
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.StatsUtils;
import com.google.android.maps.mytracks.R;
import android.os.Bundle;
import java.util.List;
/**
* An activity to view aggregated stats from all recorded tracks.
*
* @author Fergus Nelson
*/
public class AggregatedStatsActivity extends AbstractMyTracksActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.aggregated_stats);
StatsUtils.setTripStatisticsValues(this, getTripStatistics());
StatsUtils.setLocationValues(this, null, false);
}
/**
* Gets the aggregated trip statistics for all the recorded tracks or null if
* there is no track.
*/
private TripStatistics getTripStatistics() {
List<Track> tracks = MyTracksProviderUtils.Factory.get(this).getAllTracks();
TripStatistics tripStatistics = null;
if (!tracks.isEmpty()) {
tripStatistics = new TripStatistics(tracks.iterator().next().getTripStatistics());
for (int i = 1; i < tracks.size(); i++) {
tripStatistics.merge(tracks.get(i).getTripStatistics());
}
}
return tripStatistics;
}
}
@@ -0,0 +1,65 @@
package com.google.android.apps.mytracks;
import com.google.android.apps.mytracks.util.PreferencesUtils;
import com.google.android.maps.mytracks.R;
import android.content.Context;
import android.preference.EditTextPreference;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.widget.ArrayAdapter;
import android.widget.AutoCompleteTextView;
/**
* The {@link AutoCompleteTextPreference} class is a preference that allows for
* string input using auto complete . It is a subclass of
* {@link EditTextPreference} and shows the {@link AutoCompleteTextView} in a
* dialog.
* <p>
* This preference will store a string into the SharedPreferences.
*
* @author Rimas Trumpa (with Matt Levan)
*/
public class AutoCompleteTextPreference extends EditTextPreference {
private AutoCompleteTextView mEditText = null;
public AutoCompleteTextPreference(Context context, AttributeSet attrs) {
super(context, attrs);
mEditText = new AutoCompleteTextView(context, attrs);
mEditText.setThreshold(0);
// Gets autocomplete values for 'Default Activity' preference
if (PreferencesUtils.getKey(context, R.string.default_activity_key).equals(getKey())) {
ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(
context, R.array.activity_types, android.R.layout.simple_dropdown_item_1line);
mEditText.setAdapter(adapter);
}
}
@Override
protected void onBindDialogView(View view) {
AutoCompleteTextView editText = mEditText;
editText.setText(getText());
ViewParent oldParent = editText.getParent();
if (oldParent != view) {
if (oldParent != null) {
((ViewGroup) oldParent).removeView(editText);
}
onAddEditTextToDialogView(view, editText);
}
}
@Override
protected void onDialogClosed(boolean positiveResult) {
if (positiveResult) {
String value = mEditText.getText().toString();
if (callChangeListener(value)) {
setText(value);
}
}
}
}
@@ -0,0 +1,61 @@
/*
* 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 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;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
/**
* This class handles MyTracks related broadcast messages.
*
* One example of a broadcast message that this class is interested in,
* is notification about the phone boot. We may want to resume a previously
* started tracking session if the phone crashed (hopefully not), or the user
* decided to swap the battery or some external event occurred which forced
* a phone reboot.
*
* This class simply delegates to {@link TrackRecordingService} to make a
* decision whether to continue with the previous track (if any), or just
* abandon it.
*
* @author Bartlomiej Niechwiej
*/
public class BootReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Log.d(TAG, "BootReceiver.onReceive: " + intent.getAction());
if (ACTION_BOOT_COMPLETED.equals(intent.getAction())) {
Intent startIntent = new Intent(context, TrackRecordingService.class)
.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");
}
}
}
@@ -0,0 +1,264 @@
/*
* 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;
import com.google.android.apps.mytracks.stats.ExtremityMonitor;
import com.google.common.annotations.VisibleForTesting;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Paint.Align;
import android.graphics.Paint.Style;
import android.graphics.Path;
import java.text.NumberFormat;
/**
* This class encapsulates the meta data for one series of the chart values.
*
* @author Sandor Dornbush
*/
public class ChartValueSeries {
private static final float STROKE_WIDTH = 2f;
private final int absoluteMin;
private final int absoluteMax;
private final int[] intervalValues;
private final int metricTitleId;
private final int imperialTitleId;
private final Paint fillPaint;
private final Paint strokePaint;
private final Paint titlePaint;
private final Paint markerPaint;
private final ExtremityMonitor extremityMonitor;
private final NumberFormat numberFormat;
private final Path path;
private int interval = 1;
private int minMarkerValue = 0;
private int maxMarkerValue = interval * ChartView.Y_AXIS_INTERVALS;
private boolean enabled = true;
/**
* Constructor.
*
* @param context the context
* @param absoluteMin the absolute min value
* @param absoluteMax the absolute max value
* @param intervalValues the list of interval values
* @param metricTitleId the metric title id
* @param imperialTitleId the imperial title id
* @param fillColor the fill color
* @param strokeColor the stroke color
*/
public ChartValueSeries(Context context, int absoluteMin, int absoluteMax, int[] intervalValues,
int metricTitleId, int imperialTitleId, int fillColor, int strokeColor) {
this.absoluteMin = absoluteMin;
this.absoluteMax = absoluteMax;
this.intervalValues = intervalValues;
this.metricTitleId = metricTitleId;
this.imperialTitleId = imperialTitleId;
fillPaint = new Paint();
fillPaint.setStyle(Style.FILL);
fillPaint.setColor(context.getResources().getColor(fillColor));
fillPaint.setAntiAlias(true);
strokePaint = new Paint();
strokePaint.setStyle(Style.STROKE);
strokePaint.setColor(context.getResources().getColor(strokeColor));
strokePaint.setAntiAlias(true);
float scale = context.getResources().getDisplayMetrics().density;
// Make copies of the stroke paint with the default thickness
titlePaint = new Paint(strokePaint);
titlePaint.setTextSize(ChartView.MEDIUM_TEXT_SIZE * scale);
titlePaint.setTextAlign(Align.CENTER);
markerPaint = new Paint(strokePaint);
markerPaint.setTextSize(ChartView.SMALL_TEXT_SIZE * scale);
markerPaint.setTextAlign(Align.RIGHT);
// Set stroke paint thickness
strokePaint.setStrokeWidth(STROKE_WIDTH);
extremityMonitor = new ExtremityMonitor();
numberFormat = NumberFormat.getIntegerInstance();
path = new Path();
}
/**
* Sets the series enabled value.
*
* @param enabled true to enable
*/
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
/**
* Returns true if the series is enabled.
*/
public boolean isEnabled() {
return enabled;
}
/**
* Returns true if the series has data.
*/
public boolean hasData() {
return extremityMonitor.hasData();
}
/**
* Updates the series with a new value.
*
* @param value the new value
*/
public void update(double value) {
extremityMonitor.update(value);
}
/**
* Gets the path.
*/
public Path getPath() {
return path;
}
/**
* Draws the path on canvas.
*
* @param canvas the canvas
*/
public void drawPath(Canvas canvas) {
canvas.drawPath(path, fillPaint);
canvas.drawPath(path, strokePaint);
}
/**
* Updates the y axis dimension.
*/
public void updateDimension() {
double min = hasData() ? extremityMonitor.getMin() : 0.0;
double max = hasData() ? extremityMonitor.getMax() : 1.0;
min = Math.max(min, absoluteMin);
max = Math.min(max, absoluteMax);
interval = getInterval(min, max);
minMarkerValue = getMinMarkerValue(min, interval);
maxMarkerValue = minMarkerValue + interval * ChartView.Y_AXIS_INTERVALS;
}
/**
* Gets the interval value.
*
* @param min the min value
* @param max the max value
*/
private int getInterval(double min, double max) {
for (int i = 0; i < intervalValues.length; i++) {
int intervalValue = intervalValues[i];
int minValue = getMinMarkerValue(min, intervalValue);
double targetInterval = (max - minValue) / ChartView.Y_AXIS_INTERVALS;
if (intervalValue >= targetInterval) {
return intervalValue;
}
}
// Return the largest interval
return intervalValues[intervalValues.length - 1];
}
/**
* Gets the min marker value.
*
* @param min the min series value
* @param intervalValue the interval value
*/
private int getMinMarkerValue(double min, int intervalValue) {
// Round down to the nearest intervalValue
int value = ((int) (min / intervalValue)) * intervalValue;
// value > min if min is negative
if (value > min) {
return value - intervalValue;
}
return value;
}
/**
* Gets the interval value.
*/
public int getInterval() {
return interval;
}
/**
* Gets the min marker value.
*/
public int getMinMarkerValue() {
return minMarkerValue;
}
/**
* Gets the max marker value.
*/
@VisibleForTesting
int getMaxMarkerValue() {
return maxMarkerValue;
}
/**
* Gets the title id.
*/
public int getTitleId(boolean metricUnits) {
return metricUnits ? metricTitleId : imperialTitleId;
}
/**
* Gets the title paint.
*/
public Paint getTitlePaint() {
return titlePaint;
}
/**
* Gets the marker paint.
*/
public Paint getMarkerPaint() {
return markerPaint;
}
/**
* Gets the largest marker.
*/
public String getLargestMarker() {
String minMarker = numberFormat.format(getMinMarkerValue());
String maxMarker = numberFormat.format(getMaxMarkerValue());
return minMarker.length() >= maxMarker.length() ? minMarker : maxMarker;
}
/**
* Formats a marker value.
*
* @param value the value
*/
public String formatMarker(int value) {
return numberFormat.format(value);
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,62 @@
/*
* 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 android.graphics.Paint;
import android.graphics.Path;
/**
* Represents a colored {@code Path} to save its relative color for drawing.
* @author Vangelis S.
*/
public class ColoredPath {
private final Path path;
private final Paint pathPaint;
/**
* Constructor for a ColoredPath by color.
*/
public ColoredPath(int color) {
path = new Path();
pathPaint = new Paint();
pathPaint.setColor(color);
pathPaint.setStrokeWidth(3);
pathPaint.setStyle(Paint.Style.STROKE);
pathPaint.setAntiAlias(true);
}
/**
* Constructor for a ColoredPath by Paint.
*/
public ColoredPath(Paint paint) {
path = new Path();
pathPaint = paint;
}
/**
* @return the path
*/
public Path getPath() {
return path;
}
/**
* @return the pathPaint
*/
public Paint getPathPaint() {
return pathPaint;
}
}
@@ -0,0 +1,137 @@
/*
* 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;
/**
* Constants used by the MyTracks application.
*
* @author Leif Hendrik Wilden
*/
public abstract class Constants {
/**
* Should be used by all log statements
*/
public static final String TAG = "MyTracks";
/**
* Name of the top-level directory inside the SD card where our files will
* be read from/written to.
*/
public static final String SDCARD_TOP_DIR = "MyTracks";
/**
* The number of distance readings to smooth to get a stable signal.
*/
public static final int DISTANCE_SMOOTHING_FACTOR = 25;
/**
* The number of elevation readings to smooth to get a somewhat accurate
* signal.
*/
public static final int ELEVATION_SMOOTHING_FACTOR = 25;
/**
* The number of grade readings to smooth to get a somewhat accurate signal.
*/
public static final int GRADE_SMOOTHING_FACTOR = 5;
/**
* The number of speed reading to smooth to get a somewhat accurate signal.
*/
public static final int SPEED_SMOOTHING_FACTOR = 25;
/**
* Maximum number of track points displayed by the map overlay.
*/
public static final int MAX_DISPLAYED_TRACK_POINTS = 10000;
/**
* Target number of track points displayed by the map overlay.
* We may display more than this number of points.
*/
public static final int TARGET_DISPLAYED_TRACK_POINTS = 5000;
/**
* Maximum number of track points ever loaded at once from the provider into
* memory.
* With a recording frequency of 2 seconds, 15000 corresponds to 8.3 hours.
*/
public static final int MAX_LOADED_TRACK_POINTS = 20000;
/**
* Maximum number of track points ever loaded at once from the provider into
* memory in a single call to read points.
*/
public static final int MAX_LOADED_TRACK_POINTS_PER_BATCH = 1000;
/**
* Maximum number of way points displayed by the map overlay.
*/
public static final int MAX_DISPLAYED_WAYPOINTS_POINTS = 128;
/**
* Maximum number of way points that will be loaded at one time.
*/
public static final int MAX_LOADED_WAYPOINTS_POINTS = 10000;
/**
* Any time segment where the distance traveled is less than this value will
* not be considered moving.
*/
public static final double MAX_NO_MOVEMENT_DISTANCE = 2;
/**
* Anything faster than that (in meters per second) will be considered moving.
*/
public static final double MAX_NO_MOVEMENT_SPEED = 0.224;
/**
* Ignore any acceleration faster than this.
* Will ignore any speeds that imply accelaration greater than 2g's
* 2g = 19.6 m/s^2 = 0.0002 m/ms^2 = 0.02 m/(m*ms)
*/
public static final double MAX_ACCELERATION = 0.02;
/** Maximum age of a GPS location to be considered current. */
public static final long MAX_LOCATION_AGE_MS = 60 * 1000; // 1 minute
/** Maximum age of a network location to be considered current. */
public static final long MAX_NETWORK_AGE_MS = 1000 * 60 * 10; // 10 minutes
/**
* The type of account that we can use for gdata uploads.
*/
public static final String ACCOUNT_TYPE = "com.google";
/**
* The name of extra intent property to indicate whether we want to resume
* a previously recorded track.
*/
public static final String
RESUME_TRACK_EXTRA_NAME = "com.google.android.apps.mytracks.RESUME_TRACK";
public static final String SETTINGS_NAME = "SettingsActivity";
public static final double WAYPOINT_X_OFFSET_PERCENTAGE = 13 / 48.0;
public static final double MARKER_Y_OFFSET_PERCENTAGE = 91 / 96.0;
/**
* This is an abstract utility class.
*/
protected Constants() { }
}
@@ -0,0 +1,34 @@
/*
* 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;
/**
* Callback when an item in the contextual action mode is selected.
*
* @author Jimmy Shih
*/
public interface ContextualActionModeCallback {
/**
* Invoked when an item is selected.
*
* @param itemId the context menu item id
* @param position the position of the selected row
* @param id the id of the selected row, if available
*/
public boolean onClick(int itemId, int position, long id);
}
@@ -0,0 +1,61 @@
/*
* 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;
import com.google.android.apps.mytracks.fragments.AboutDialogFragment;
import com.google.android.maps.mytracks.R;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.TextView;
/**
* An activity that displays the help page.
*
* @author Sandor Dornbush
*/
public class HelpActivity extends AbstractMyTracksActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.help);
TextView mapsPublicUnlisted = (TextView) findViewById(R.id.help_maps_public_unlisted_answer);
mapsPublicUnlisted.setText(getString(
R.string.help_maps_public_unlisted_answer, getString(R.string.maps_public_unlisted_url)));
TextView sendTrack = (TextView) findViewById(R.id.help_send_track_answer);
sendTrack.setText(getString(R.string.help_send_track_answer,
getString(R.string.send_google_maps_url),
getString(R.string.send_google_fusion_tables_url),
getString(R.string.send_google_docs_url)));
findViewById(R.id.help_ok).setOnClickListener(new OnClickListener() {
public void onClick(View v) {
finish();
}
});
findViewById(R.id.help_about).setOnClickListener(new OnClickListener() {
public void onClick(View v) {
new AboutDialogFragment().show(
getSupportFragmentManager(), AboutDialogFragment.ABOUT_DIALOG_TAG);
}
});
}
}
@@ -0,0 +1,203 @@
/*
* 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 com.google.android.apps.mytracks.util.DialogUtils;
import com.google.android.apps.mytracks.util.FileUtils;
import com.google.android.apps.mytracks.util.IntentUtils;
import com.google.android.apps.mytracks.util.UriUtils;
import com.google.android.maps.mytracks.R;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.ProgressDialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.support.v4.app.TaskStackBuilder;
import android.util.Log;
import android.widget.Toast;
/**
* An activity to import GPX files from the SD card. Optionally to import one
* GPX file and display it in My Tracks.
*
* @author Rodrigo Damazio
*/
public class ImportActivity extends Activity {
public static final String EXTRA_IMPORT_ALL = "import_all";
private static final String TAG = ImportActivity.class.getSimpleName();
private static final int DIALOG_PROGRESS_ID = 0;
private static final int DIALOG_RESULT_ID = 1;
private ImportAsyncTask importAsyncTask;
private ProgressDialog progressDialog;
private boolean importAll;
// path on the SD card to import
private String path;
// number of succesfully imported files
private int successCount;
// number of files to import
private int totalCount;
// last successfully imported track id
private long trackId;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Intent intent = getIntent();
importAll = intent.getBooleanExtra(EXTRA_IMPORT_ALL, false);
if (importAll) {
path = FileUtils.buildExternalDirectoryPath("gpx");
} else {
String action = intent.getAction();
if (!(Intent.ACTION_ATTACH_DATA.equals(action) || Intent.ACTION_VIEW.equals(action))) {
Log.d(TAG, "Invalid action: " + intent);
finish();
return;
}
Uri data = intent.getData();
if (!UriUtils.isFileUri(data)) {
Log.d(TAG, "Invalid data: " + intent);
finish();
return;
}
path = data.getPath();
}
Object retained = getLastNonConfigurationInstance();
if (retained instanceof ImportAsyncTask) {
importAsyncTask = (ImportAsyncTask) retained;
importAsyncTask.setActivity(this);
} else {
importAsyncTask = new ImportAsyncTask(this, importAll, path);
importAsyncTask.execute();
}
}
@Override
public Object onRetainNonConfigurationInstance() {
importAsyncTask.setActivity(null);
return importAsyncTask;
}
@Override
protected Dialog onCreateDialog(int id) {
switch (id) {
case DIALOG_PROGRESS_ID:
progressDialog = DialogUtils.createHorizontalProgressDialog(
this, R.string.sd_card_import_progress_message, new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
importAsyncTask.cancel(true);
finish();
}
});
return progressDialog;
case DIALOG_RESULT_ID:
String message;
if (successCount == 0) {
message = getString(R.string.sd_card_import_error_no_file, path);
} else {
String totalFiles = getResources()
.getQuantityString(R.plurals.importGpxFiles, totalCount, totalCount);
message = getString(
R.string.sd_card_import_success_count, successCount, totalFiles, path);
}
return new AlertDialog.Builder(this)
.setCancelable(true)
.setMessage(message)
.setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
finish();
}
})
.setPositiveButton(R.string.generic_ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
if (!importAll && trackId != -1L) {
Intent intent = IntentUtils
.newIntent(ImportActivity.this, TrackDetailActivity.class)
.putExtra(TrackDetailActivity.EXTRA_TRACK_ID, trackId);
TaskStackBuilder taskStackBuilder = TaskStackBuilder.from(ImportActivity.this);
taskStackBuilder.addNextIntent(intent);
taskStackBuilder.startActivities();
}
finish();
}
})
.create();
default:
return null;
}
}
/**
* Invokes when the associated AsyncTask completes.
*
* @param success true if the AsyncTask is successful
* @param imported the number of files successfully imported
* @param total the total number of files to import
* @param id the last successfully imported track id
*/
public void onAsyncTaskCompleted(boolean success, int imported, int total, long id) {
successCount = imported;
totalCount = total;
trackId = id;
removeDialog(DIALOG_PROGRESS_ID);
if (success) {
showDialog(DIALOG_RESULT_ID);
} else {
Toast.makeText(this, R.string.sd_card_import_error, Toast.LENGTH_LONG).show();
finish();
}
}
/**
* Shows the progress dialog.
*/
public void showProgressDialog() {
showDialog(DIALOG_PROGRESS_ID);
}
/**
* Sets the progress dialog value.
*
* @param number the number of files imported
* @param max the maximum number of files
*/
public void setProgressDialogValue(int number, int max) {
if (progressDialog != null) {
progressDialog.setIndeterminate(false);
progressDialog.setMax(max);
progressDialog.setProgress(Math.min(number, max));
}
}
}
@@ -0,0 +1,216 @@
/*
* 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;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.io.file.GpxImporter;
import com.google.android.apps.mytracks.util.FileUtils;
import com.google.android.apps.mytracks.util.PreferencesUtils;
import com.google.android.apps.mytracks.util.SystemUtils;
import com.google.android.maps.mytracks.R;
import android.os.AsyncTask;
import android.os.PowerManager.WakeLock;
import android.util.Log;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import javax.xml.parsers.ParserConfigurationException;
import org.xml.sax.SAXException;
/**
* AsyncTask to import GPX files from the SD card.
*
* @author Jimmy Shih
*/
public class ImportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
private static final String TAG = ImportAsyncTask.class.getSimpleName();
private ImportActivity importActivity;
private final boolean importAll;
private final String path;
private final MyTracksProviderUtils myTracksProviderUtils;
private WakeLock wakeLock;
// true if the AsyncTask result is success
private boolean success;
// true if the AsyncTask has completed
private boolean completed;
// number of files successfully imported
private int successCount;
// number of files to import
private int totalCount;
// the last successfully imported track id
private long trackId;
/**
* Creates an AsyncTask.
*
* @param importActivity the activity currently associated with this AsyncTask
* @param importAll true to import all GPX files
* @param path path to import GPX files
*/
public ImportAsyncTask(ImportActivity importActivity, boolean importAll, String path) {
this.importActivity = importActivity;
this.importAll = importAll;
this.path = path;
myTracksProviderUtils = MyTracksProviderUtils.Factory.get(importActivity);
// Get the wake lock if not recording
if (PreferencesUtils.getLong(importActivity, R.string.recording_track_id_key)
== PreferencesUtils.RECORDING_TRACK_ID_DEFAULT) {
wakeLock = SystemUtils.acquireWakeLock(importActivity, wakeLock);
}
success = false;
completed = false;
successCount = 0;
totalCount = 0;
trackId = -1L;
}
/**
* Sets the current {@link ImportActivity} associated with this AyncTask.
*
* @param importActivity the current {@link ImportActivity}, can be null
*/
public void setActivity(ImportActivity importActivity) {
this.importActivity = importActivity;
if (completed && importActivity != null) {
importActivity.onAsyncTaskCompleted(success, successCount, totalCount, trackId);
}
}
@Override
protected void onPreExecute() {
if (importActivity != null) {
importActivity.showProgressDialog();
}
}
@Override
protected Boolean doInBackground(Void... params) {
try {
if (!FileUtils.isSdCardAvailable()) {
return false;
}
List<File> files = getFiles();
totalCount = files.size();
if (totalCount == 0) {
return true;
}
for (int i = 0; i < totalCount; i++) {
if (isCancelled()) {
// If cancelled, return true to show the number of files imported
return true;
}
File file = files.get(i);
if (importFile(file)) {
successCount++;
}
publishProgress(i + 1, totalCount);
}
return true;
} finally {
// Release the wake lock if obtained
if (wakeLock != null && wakeLock.isHeld()) {
wakeLock.release();
}
}
}
@Override
protected void onProgressUpdate(Integer... values) {
if (importActivity != null) {
importActivity.setProgressDialogValue(values[0], values[1]);
}
}
@Override
protected void onPostExecute(Boolean result) {
success = result;
completed = true;
if (importActivity != null) {
importActivity.onAsyncTaskCompleted(success, successCount, totalCount, trackId);
}
}
/**
* Imports a GPX file.
*
* @param file the file
*/
private boolean importFile(final File file) {
try {
long trackIds[] = GpxImporter.importGPXFile(new FileInputStream(file), myTracksProviderUtils);
int length = trackIds.length;
if (length > 0) {
trackId = trackIds[length - 1];
}
return true;
} catch (FileNotFoundException e) {
Log.d(TAG, "file: " + file.getAbsolutePath(), e);
return false;
} catch (ParserConfigurationException e) {
Log.d(TAG, "file: " + file.getAbsolutePath(), e);
return false;
} catch (SAXException e) {
Log.d(TAG, "file: " + file.getAbsolutePath(), e);
return false;
} catch (IOException e) {
Log.d(TAG, "file: " + file.getAbsolutePath(), e);
return false;
}
}
/**
* Gets a list of GPX files. If importAll is true, returns a list of GPX files
* under the path directory. If importAll is false, returns a list containing
* just the path file.
*/
private List<File> getFiles() {
List<File> files = new ArrayList<File>();
File file = new File(path);
if (importAll) {
File[] candidates = file.listFiles();
if (candidates != null) {
for (File candidate : candidates) {
if (!candidate.isDirectory() && candidate.getName().endsWith(".gpx")) {
files.add(candidate);
}
}
}
} else {
files.add(file);
}
return files;
}
}
@@ -0,0 +1,95 @@
/*
* 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 android.content.Context;
import android.preference.ListPreference;
import android.util.AttributeSet;
/**
* A list preference which persists its values as integers instead of strings.
* Code reading the values should use
* {@link android.content.SharedPreferences#getInt}.
* When using XML-declared arrays for entry values, the arrays should be regular
* string arrays containing valid integer values.
*
* @author Rodrigo Damazio
*/
public class IntegerListPreference extends ListPreference {
public IntegerListPreference(Context context) {
super(context);
verifyEntryValues(null);
}
public IntegerListPreference(Context context, AttributeSet attrs) {
super(context, attrs);
verifyEntryValues(null);
}
@Override
public void setEntryValues(CharSequence[] entryValues) {
CharSequence[] oldValues = getEntryValues();
super.setEntryValues(entryValues);
verifyEntryValues(oldValues);
}
@Override
public void setEntryValues(int entryValuesResId) {
CharSequence[] oldValues = getEntryValues();
super.setEntryValues(entryValuesResId);
verifyEntryValues(oldValues);
}
@Override
protected String getPersistedString(String defaultReturnValue) {
// During initial load, there's no known default value
int defaultIntegerValue = Integer.MIN_VALUE;
if (defaultReturnValue != null) {
defaultIntegerValue = Integer.parseInt(defaultReturnValue);
}
// When the list preference asks us to read a string, instead read an
// integer.
int value = getPersistedInt(defaultIntegerValue);
return Integer.toString(value);
}
@Override
protected boolean persistString(String value) {
// When asked to save a string, instead save an integer
return persistInt(Integer.parseInt(value));
}
private void verifyEntryValues(CharSequence[] oldValues) {
CharSequence[] entryValues = getEntryValues();
if (entryValues == null) {
return;
}
for (CharSequence entryValue : entryValues) {
try {
Integer.parseInt(entryValue.toString());
} catch (NumberFormatException nfe) {
super.setEntryValues(oldValues);
throw nfe;
}
}
}
}
@@ -0,0 +1,579 @@
/*
* 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;
import static com.google.android.apps.mytracks.Constants.TAG;
import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.maps.TrackPathPainter;
import com.google.android.apps.mytracks.maps.TrackPathPainterFactory;
import com.google.android.apps.mytracks.maps.TrackPathUtils;
import com.google.android.apps.mytracks.util.IntentUtils;
import com.google.android.apps.mytracks.util.LocationUtils;
import com.google.android.apps.mytracks.util.PreferencesUtils;
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;
import com.google.android.maps.Projection;
import com.google.android.maps.mytracks.R;
import com.google.common.annotations.VisibleForTesting;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.location.Location;
import android.util.Log;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
/**
* A map overlay that displays my location arrow, error circle, and track info.
*
* @author Leif Hendrik Wilden
*/
public class MapOverlay extends Overlay implements OnSharedPreferenceChangeListener {
private final Context context;
private final List<Waypoint> waypoints;
private final List<CachedLocation> points;
private final BlockingQueue<CachedLocation> pendingPoints;
private final Drawable[] arrows;
private final int arrowWidth;
private final int arrowHeight;
private final Drawable statsMarker;
private final Drawable waypointMarker;
private final Drawable startMarker;
private final Drawable endMarker;
private final int markerWidth;
private final int markerHeight;
private final Paint errorCirclePaint;
private TrackPathPainter trackPathPainter;
private boolean trackDrawingEnabled;
private boolean showEndMarker = true;
private int headingIndex = 0;
private Location myLocation;
private GeoPoint lastReferencePoint;
private Rect lastViewRect;
/**
* A pre-processed {@link Location} to speed up drawing.
*
* @author Jimmy Shih
*/
public static class CachedLocation {
private final boolean valid;
private final GeoPoint geoPoint;
private final int speed;
/**
* Constructor for an invalid cached location.
*/
public CachedLocation() {
this.valid = false;
this.geoPoint = null;
this.speed = -1;
}
/**
* Constructor for a potentially valid cached location.
*/
public CachedLocation(Location location) {
this.valid = LocationUtils.isValidLocation(location);
this.geoPoint = valid ? LocationUtils.getGeoPoint(location) : null;
this.speed = (int) Math.floor(location.getSpeed() * UnitConversions.MS_TO_KMH);
}
/**
* Returns true if the location is valid.
*/
public boolean isValid() {
return valid;
}
/**
* Gets the {@link GeoPoint}.
*/
public GeoPoint getGeoPoint() {
return geoPoint;
}
/**
* Gets the speed in kilometers per hour.
*/
public int getSpeed() {
return speed;
}
};
public MapOverlay(Context context) {
this.context = context;
this.waypoints = new ArrayList<Waypoint>();
this.points = new ArrayList<CachedLocation>(1024);
this.pendingPoints = new ArrayBlockingQueue<CachedLocation>(
Constants.MAX_DISPLAYED_TRACK_POINTS, true);
/*
* TODO: Use animation rather than individual resources for each arrow
* direction.
*/
final Resources resources = context.getResources();
arrows = new Drawable[] { resources.getDrawable(R.drawable.arrow_0),
resources.getDrawable(R.drawable.arrow_20), resources.getDrawable(R.drawable.arrow_40),
resources.getDrawable(R.drawable.arrow_60), resources.getDrawable(R.drawable.arrow_80),
resources.getDrawable(R.drawable.arrow_100), resources.getDrawable(R.drawable.arrow_120),
resources.getDrawable(R.drawable.arrow_140), resources.getDrawable(R.drawable.arrow_160),
resources.getDrawable(R.drawable.arrow_180), resources.getDrawable(R.drawable.arrow_200),
resources.getDrawable(R.drawable.arrow_220), resources.getDrawable(R.drawable.arrow_240),
resources.getDrawable(R.drawable.arrow_260), resources.getDrawable(R.drawable.arrow_280),
resources.getDrawable(R.drawable.arrow_300), resources.getDrawable(R.drawable.arrow_320),
resources.getDrawable(R.drawable.arrow_340) };
arrowWidth = arrows[headingIndex].getIntrinsicWidth();
arrowHeight = arrows[headingIndex].getIntrinsicHeight();
for (Drawable arrow : arrows) {
arrow.setBounds(0, 0, arrowWidth, arrowHeight);
}
statsMarker = resources.getDrawable(R.drawable.yellow_pushpin);
markerWidth = statsMarker.getIntrinsicWidth();
markerHeight = statsMarker.getIntrinsicHeight();
statsMarker.setBounds(0, 0, markerWidth, markerHeight);
waypointMarker = resources.getDrawable(R.drawable.blue_pushpin);
waypointMarker.setBounds(0, 0, markerWidth, markerHeight);
startMarker = resources.getDrawable(R.drawable.green_dot);
startMarker.setBounds(0, 0, markerWidth, markerHeight);
endMarker = resources.getDrawable(R.drawable.red_dot);
endMarker.setBounds(0, 0, markerWidth, markerHeight);
errorCirclePaint = TrackPathUtils.getPaint(context, R.color.blue);
errorCirclePaint.setAlpha(127);
trackPathPainter = TrackPathPainterFactory.getTrackPathPainter(context);
context.getSharedPreferences(Constants.SETTINGS_NAME, Context.MODE_PRIVATE)
.registerOnSharedPreferenceChangeListener(this);
}
/**
* Add a location to the map overlay.
* <p>
* NOTE: This method doesn't take ownership of the given location, so it is
* safe to reuse the same location while calling this method.
*
* @param location the location
*/
public void addLocation(Location location) {
// Queue up in the pendingPoints until it's merged with points.
if (!pendingPoints.offer(new CachedLocation(location))) {
Log.e(TAG, "Unable to add to pendingPoints.");
}
}
/**
* Adds a segment split to the map overlay.
*/
public void addSegmentSplit() {
// Queue up in the pendingPoints until it's merged with points.
if (!pendingPoints.offer(new CachedLocation())) {
Log.e(TAG, "Unable to add to pendingPoints");
}
}
/**
* Clears the locations.
*/
public void clearPoints() {
synchronized (points) {
points.clear();
pendingPoints.clear();
trackPathPainter.clearPath();
lastReferencePoint = null;
lastViewRect = null;
}
}
/**
* Adds a waypoint to the map overlay.
*
* @param waypoint the waypoint
*/
public void addWaypoint(Waypoint waypoint) {
// Note: We don't cache waypoints because it's not worth the effort.
if (waypoint != null && waypoint.getLocation() != null) {
synchronized (waypoints) {
waypoints.add(waypoint);
}
}
}
/**
* Clears the waypoints.
*/
public void clearWaypoints() {
synchronized (waypoints) {
waypoints.clear();
}
}
/**
* Sets whether to draw the track or not.
*
* @param trackDrawingEnabled true to draw track
*/
public void setTrackDrawingEnabled(boolean trackDrawingEnabled) {
this.trackDrawingEnabled = trackDrawingEnabled;
}
/**
* Sets whether to draw the end maker or not.
*
* @param showEndMarker true to draw end marker
*/
public void setShowEndMarker(boolean showEndMarker) {
this.showEndMarker = showEndMarker;
}
/**
* Sets my location.
*
* @param myLocation my location
*/
public void setMyLocation(Location myLocation) {
this.myLocation = myLocation;
}
/**
* Sets the heading.
*
* @param heading the heading
* @return true if the visible heading has changed.
*/
public boolean setHeading(float heading) {
/*
* Use -heading because the arrow images are counter-clockwise rather than
* clockwise.
*/
int index = Math.round(-heading / 360 * 18);
while (index < 0) {
index += 18;
}
while (index > 17) {
index -= 18;
}
if (index != headingIndex) {
headingIndex = index;
return true;
} else {
return false;
}
}
@Override
public void draw(Canvas canvas, MapView mapView, boolean shadow) {
if (shadow) {
return;
}
// It's safe to keep projection within a single draw operation
Projection projection = getMapProjection(mapView);
if (projection == null) {
Log.w(TAG, "No projection, unable to draw.");
return;
}
if (trackDrawingEnabled) {
// Get the current viewing Rect
Rect viewRect = getMapViewRect(mapView);
// Draw the selected track
drawTrack(canvas, projection, viewRect);
// Draw the start and end markers
drawMarkers(canvas, projection);
// Draw the waypoints
drawWaypoints(canvas, projection);
}
// Draw the current location
drawMyLocation(canvas, projection);
}
@Override
public boolean onTap(GeoPoint geoPoint, MapView mapView) {
if (geoPoint.equals(mapView.getMapCenter())) {
/*
* There is (unfortunately) no good way to determine whether the tap was
* caused by an actual tap on the screen or the track ball. If the
* location is equal to the map center,then it was a track ball press with
* very high likelihood.
*/
return false;
}
final Location tapLocation = LocationUtils.getLocation(geoPoint);
double minDistance = Double.MAX_VALUE;
Waypoint waypoint = null;
synchronized (waypoints) {
for (int i = 0; i < waypoints.size(); i++) {
final Location waypointLocation = waypoints.get(i).getLocation();
if (waypointLocation == null) {
continue;
}
final double distance = waypointLocation.distanceTo(tapLocation);
if (distance < minDistance) {
minDistance = distance;
waypoint = waypoints.get(i);
}
}
}
if (waypoint != null && minDistance < 15000000 / Math.pow(2, mapView.getZoomLevel())) {
Intent intent = IntentUtils.newIntent(context, MarkerDetailActivity.class)
.putExtra(MarkerDetailActivity.EXTRA_MARKER_ID, waypoint.getId());
context.startActivity(intent);
return true;
}
return super.onTap(geoPoint, mapView);
}
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
if (PreferencesUtils.getKey(context, R.string.track_color_mode_key).equals(key)) {
trackPathPainter = TrackPathPainterFactory.getTrackPathPainter(context);
}
}
/**
* Gets the points.
*/
@VisibleForTesting
public List<CachedLocation> getPoints() {
return points;
}
/**
* Gets the track path painter.
*/
@VisibleForTesting
public TrackPathPainter getTrackPathPainter() {
return trackPathPainter;
}
/**
* Sets the track path painter.
*
* @param trackPathPainter the track path painter
*/
@VisibleForTesting
public void setTrackPathPainter(TrackPathPainter trackPathPainter) {
this.trackPathPainter = trackPathPainter;
}
/**
* Gets the map view projection.
*
* @param mapView the map view
*/
@VisibleForTesting
protected Projection getMapProjection(MapView mapView) {
return mapView.getProjection();
}
/**
* Gets the map view Rect.
*
* @param mapView the map view
*/
@VisibleForTesting
protected Rect getMapViewRect(MapView mapView) {
int width = mapView.getLongitudeSpan();
int height = mapView.getLatitudeSpan();
int centerX = mapView.getMapCenter().getLongitudeE6();
int centerY = mapView.getMapCenter().getLatitudeE6();
return new Rect(centerX - width / 2, centerY - height / 2, centerX + width / 2, centerY + height
/ 2);
}
/**
* Gets number of locations.
*/
@VisibleForTesting
int getNumLocations() {
synchronized (points) {
return points.size() + pendingPoints.size();
}
}
/**
* Gets number of waypoints.
*/
@VisibleForTesting
int getNumWaypoints() {
synchronized (waypoints) {
return waypoints.size();
}
}
/**
* Draws the track.
*
* @param canvas the canvas
* @param projection the projection
* @param viewRect the view rect
*/
private void drawTrack(Canvas canvas, Projection projection, Rect viewRect) {
boolean draw;
synchronized (points) {
// Merge the pending points with the list of cached locations.
GeoPoint referencePoint = projection.fromPixels(0, 0);
int newPoints = pendingPoints.drainTo(points);
boolean newProjection = !viewRect.equals(lastViewRect)
|| !referencePoint.equals(lastReferencePoint);
// Call updateState first to trigger its side effects.
boolean currentPathValid = !trackPathPainter.updateState() && !newProjection
&& trackPathPainter.hasPath();
if (newPoints == 0 && currentPathValid) {
// No need to update
draw = true;
} else {
int numPoints = points.size();
if (numPoints < 2) {
// Not enough points to draw a path
draw = false;
} else if (currentPathValid) {
// Incremental update of the path
draw = true;
trackPathPainter.updatePath(projection, viewRect, numPoints - newPoints, points);
} else {
// Reload the path
draw = true;
trackPathPainter.clearPath();
trackPathPainter.updatePath(projection, viewRect, 0, points);
}
}
lastReferencePoint = referencePoint;
lastViewRect = viewRect;
}
if (draw) {
trackPathPainter.drawPath(canvas);
}
}
/**
* Draws the start and end markers.
*
* @param canvas the canvas
* @param projection the projection
*/
private void drawMarkers(Canvas canvas, Projection projection) {
int offsetY = (int) (markerHeight * Constants.MARKER_Y_OFFSET_PERCENTAGE);
// Draw the end marker
if (showEndMarker) {
for (int i = points.size() - 1; i >= 0; i--) {
if (points.get(i).valid) {
drawElement(
canvas, projection, points.get(i).geoPoint, endMarker, -markerWidth / 2, -offsetY);
break;
}
}
}
// Draw the start marker
for (int i = 0; i < points.size(); i++) {
if (points.get(i).valid) {
drawElement(
canvas, projection, points.get(i).geoPoint, startMarker, -markerWidth / 2, -offsetY);
break;
}
}
}
/**
* Draws the waypoints.
*
* @param canvas the canvas
* @param projection the projection
*/
private void drawWaypoints(Canvas canvas, Projection projection) {
synchronized (waypoints) {
int offsetX = (int) (markerWidth * Constants.WAYPOINT_X_OFFSET_PERCENTAGE);
for (Waypoint waypoint : waypoints) {
Location location = waypoint.getLocation();
Drawable drawable = waypoint.getType() == Waypoint.TYPE_STATISTICS ? statsMarker
: waypointMarker;
drawElement(canvas, projection, LocationUtils.getGeoPoint(location), drawable, -offsetX,
-markerHeight);
}
}
}
/**
* Draws my location.
*
* @param canvas the canvas
* @param projection the projection
*/
private void drawMyLocation(Canvas canvas, Projection projection) {
if (myLocation == null) {
return;
}
Point point = drawElement(canvas, projection, LocationUtils.getGeoPoint(myLocation),
arrows[headingIndex], -(arrowWidth / 2), -(arrowHeight / 2));
// Draw the error circle
float radius = projection.metersToEquatorPixels(myLocation.getAccuracy());
canvas.drawCircle(point.x, point.y, radius, errorCirclePaint);
}
/**
* Draws an element.
*
* @param canvas the canvas
* @param projection the projection
* @param geoPoint the geo point
* @param drawable the drawable
* @param offsetX the x offset
* @param offsetY the y offset
* @return the point of the drawing.
*/
private Point drawElement(Canvas canvas, Projection projection, GeoPoint geoPoint,
Drawable drawable, int offsetX, int offsetY) {
Point point = new Point();
projection.toPixels(geoPoint, point);
canvas.save();
canvas.translate(point.x + offsetX, point.y + offsetY);
drawable.draw(canvas);
canvas.restore();
return point;
}
}
@@ -0,0 +1,131 @@
/*
* 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;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.fragments.DeleteOneMarkerDialogFragment;
import com.google.android.apps.mytracks.util.IntentUtils;
import com.google.android.apps.mytracks.util.StatsUtils;
import com.google.android.maps.mytracks.R;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.TextView;
/**
* An activity to display marker detail info.
*
* @author Leif Hendrik Wilden
*/
public class MarkerDetailActivity extends AbstractMyTracksActivity {
public static final String EXTRA_MARKER_ID = "marker_id";
private static final String TAG = MarkerDetailActivity.class.getSimpleName();
private long markerId;
private Waypoint waypoint;
private TextView name;
private View waypointSection;
private View statisticsSection;
@Override
protected void onCreate(Bundle bundle) {
super.onCreate(bundle);
setContentView(R.layout.marker_detail);
markerId = getIntent().getLongExtra(EXTRA_MARKER_ID, -1L);
if (markerId == -1L) {
Log.d(TAG, "invalid marker id");
finish();
return;
}
name = (TextView) findViewById(R.id.marker_detail_name);
waypointSection = findViewById(R.id.marker_detail_waypoint_section);
statisticsSection = findViewById(R.id.marker_detail_statistics_section);
}
@Override
protected void onResume() {
super.onResume();
waypoint = MyTracksProviderUtils.Factory.get(this).getWaypoint(markerId);
if (waypoint == null) {
Log.d(TAG, "waypoint is null");
finish();
return;
}
name.setText(getString(R.string.generic_name_line, waypoint.getName()));
if (waypoint.getType() == Waypoint.TYPE_WAYPOINT) {
waypointSection.setVisibility(View.VISIBLE);
statisticsSection.setVisibility(View.GONE);
TextView markerType = (TextView) findViewById(R.id.marker_detail_waypoint_marker_type);
markerType.setText(getString(
R.string.marker_detail_waypoint_marker_type, waypoint.getCategory()));
TextView description = (TextView) findViewById(R.id.marker_detail_waypoint_description);
description.setText(getString(R.string.generic_description_line, waypoint.getDescription()));
} else {
waypointSection.setVisibility(View.GONE);
statisticsSection.setVisibility(View.VISIBLE);
StatsUtils.setTripStatisticsValues(this, waypoint.getTripStatistics());
StatsUtils.setLocationValues(this, waypoint.getLocation(), false);
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.marker_detail, menu);
return true;
}
@Override
protected void onHomeSelected() {
Intent intent = IntentUtils.newIntent(this, MarkerListActivity.class)
.putExtra(MarkerListActivity.EXTRA_TRACK_ID, waypoint.getTrackId());
startActivity(intent);
finish();
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
Intent intent;
switch (item.getItemId()) {
case R.id.marker_detail_show_on_map:
intent = IntentUtils.newIntent(this, TrackDetailActivity.class)
.putExtra(TrackDetailActivity.EXTRA_MARKER_ID, markerId);
startActivity(intent);
return true;
case R.id.marker_detail_edit:
intent = IntentUtils.newIntent(this, MarkerEditActivity.class)
.putExtra(MarkerEditActivity.EXTRA_MARKER_ID, markerId);
startActivity(intent);
return true;
case R.id.marker_detail_delete:
DeleteOneMarkerDialogFragment.newInstance(markerId, waypoint.getTrackId()).show(
getSupportFragmentManager(),
DeleteOneMarkerDialogFragment.DELETE_ONE_MARKER_DIALOG_TAG);
return true;
default:
return super.onOptionsItemSelected(item);
}
}
}
@@ -0,0 +1,187 @@
/*
* 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;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.content.WaypointCreationRequest;
import com.google.android.apps.mytracks.content.WaypointCreationRequest.WaypointType;
import com.google.android.apps.mytracks.services.TrackRecordingServiceConnection;
import com.google.android.apps.mytracks.util.TrackRecordingServiceConnectionUtils;
import com.google.android.maps.mytracks.R;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.ArrayAdapter;
import android.widget.AutoCompleteTextView;
import android.widget.Button;
import android.widget.EditText;
/**
* An activity to add/edit a marker.
*
* @author Jimmy Shih
*/
public class MarkerEditActivity extends AbstractMyTracksActivity {
private static final String TAG = MarkerEditActivity.class.getSimpleName();
public static final String EXTRA_TRACK_ID = "track_id";
public static final String EXTRA_MARKER_ID = "marker_id";
private long trackId;
private long markerId;
private TrackRecordingServiceConnection trackRecordingServiceConnection;
private Waypoint waypoint;
// UI elements
private View statisticsSection;
private EditText statisticsName;
private View waypointSection;
private EditText waypointName;
private AutoCompleteTextView waypointMarkerType;
private EditText waypointDescription;
private Button done;
@Override
protected void onCreate(Bundle bundle) {
super.onCreate(bundle);
setContentView(R.layout.marker_edit);
trackId = getIntent().getLongExtra(EXTRA_TRACK_ID, -1L);
markerId = getIntent().getLongExtra(EXTRA_MARKER_ID, -1L);
trackRecordingServiceConnection = new TrackRecordingServiceConnection(this, null);
// Setup UI elements
statisticsSection = findViewById(R.id.marker_edit_statistics_section);
statisticsName = (EditText) findViewById(R.id.marker_edit_statistics_name);
waypointSection = findViewById(R.id.marker_edit_waypoint_section);
waypointName = (EditText) findViewById(R.id.marker_edit_waypoint_name);
waypointMarkerType = (AutoCompleteTextView) findViewById(R.id.marker_edit_waypoint_marker_type);
ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(
this, R.array.waypoint_types, android.R.layout.simple_dropdown_item_1line);
waypointMarkerType.setAdapter(adapter);
waypointDescription = (EditText) findViewById(R.id.marker_edit_waypoint_description);
Button cancel = (Button) findViewById(R.id.marker_edit_cancel);
cancel.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
finish();
}
});
done = (Button) findViewById(R.id.marker_edit_done);
updateUiByMarkerId();
}
/**
* Updates the UI based on the marker id.
*/
private void updateUiByMarkerId() {
final boolean newMarker = markerId == -1L;
setTitle(newMarker ? R.string.menu_insert_marker : R.string.menu_edit);
done.setText(newMarker ? R.string.generic_add : R.string.generic_save);
done.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (newMarker) {
addMarker();
} else {
saveMarker();
}
finish();
}
});
if (newMarker) {
statisticsSection.setVisibility(View.GONE);
waypointSection.setVisibility(View.VISIBLE);
int nextMarkerNumber = trackId == -1L ? -1
: MyTracksProviderUtils.Factory.get(this).getNextMarkerNumber(trackId, false);
if (nextMarkerNumber == -1) {
nextMarkerNumber = 0;
}
waypointName.setText(getString(R.string.marker_name_format, nextMarkerNumber));
waypointName.selectAll();
waypointMarkerType.setText("");
waypointDescription.setText("");
} else {
waypoint = MyTracksProviderUtils.Factory.get(this).getWaypoint(markerId);
if (waypoint == null) {
Log.d(TAG, "waypoint is null");
finish();
return;
}
boolean statistics = waypoint.getType() == Waypoint.TYPE_STATISTICS;
statisticsSection.setVisibility(statistics ? View.VISIBLE : View.GONE);
waypointSection.setVisibility(statistics ? View.GONE : View.VISIBLE);
if (statistics) {
statisticsName.setText(waypoint.getName());
} else {
waypointName.setText(waypoint.getName());
waypointMarkerType.setText(waypoint.getCategory());
waypointDescription.setText(waypoint.getDescription());
}
}
}
@Override
protected void onResume() {
super.onResume();
TrackRecordingServiceConnectionUtils.resume(this, trackRecordingServiceConnection);
}
@Override
protected void onDestroy() {
super.onDestroy();
trackRecordingServiceConnection.unbind();
}
/**
* Adds a marker.
*/
private void addMarker() {
WaypointCreationRequest waypointCreationRequest = new WaypointCreationRequest(
WaypointType.WAYPOINT,
false,
waypointName.getText().toString(),
waypointMarkerType.getText().toString(),
waypointDescription.getText().toString(),
getString(R.string.marker_waypoint_icon_url));
TrackRecordingServiceConnectionUtils.addMarker(
this, trackRecordingServiceConnection, waypointCreationRequest);
}
/**
* Saves a marker.
*/
private void saveMarker() {
boolean statistics = waypoint.getType() == Waypoint.TYPE_STATISTICS;
if (statistics) {
waypoint.setName(statisticsName.getText().toString());
} else {
waypoint.setName(waypointName.getText().toString());
waypoint.setCategory(waypointMarkerType.getText().toString());
waypoint.setDescription(waypointDescription.getText().toString());
}
MyTracksProviderUtils.Factory.get(this).updateWaypoint(waypoint);
}
}
@@ -0,0 +1,262 @@
/*
* 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;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.content.WaypointsColumns;
import com.google.android.apps.mytracks.fragments.DeleteOneMarkerDialogFragment;
import com.google.android.apps.mytracks.util.ApiAdapterFactory;
import com.google.android.apps.mytracks.util.IntentUtils;
import com.google.android.apps.mytracks.util.ListItemUtils;
import com.google.android.apps.mytracks.util.PreferencesUtils;
import com.google.android.maps.mytracks.R;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.database.Cursor;
import android.os.Bundle;
import android.support.v4.app.LoaderManager.LoaderCallbacks;
import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader;
import android.support.v4.widget.ResourceCursorAdapter;
import android.util.Log;
import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.AdapterView;
import android.widget.AdapterView.AdapterContextMenuInfo;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ListView;
/**
* Activity to show a list of markers in a track.
*
* @author Leif Hendrik Wilden
*/
public class MarkerListActivity extends AbstractMyTracksActivity {
public static final String EXTRA_TRACK_ID = "track_id";
private static final String TAG = MarkerListActivity.class.getSimpleName();
private static final String[] PROJECTION = new String[] {
WaypointsColumns._ID,
WaypointsColumns.NAME,
WaypointsColumns.DESCRIPTION,
WaypointsColumns.CATEGORY,
WaypointsColumns.TYPE,
WaypointsColumns.TIME};
// Callback when an item is selected in the contextual action mode
private ContextualActionModeCallback contextualActionModeCallback =
new ContextualActionModeCallback() {
@Override
public boolean onClick(int itemId, int position, long id) {
return handleContextItem(itemId, id);
}
};
/*
* Note that sharedPreferenceChangeListener cannot be an anonymous inner
* class. Anonymous inner class will get garbage collected.
*/
private final OnSharedPreferenceChangeListener
sharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() {
@Override
public void onSharedPreferenceChanged(SharedPreferences preferences, String key) {
// Note that key can be null
if (PreferencesUtils.getKey(MarkerListActivity.this, R.string.recording_track_id_key)
.equals(key)) {
updateMenu();
}
}
};
private long trackId = -1;
private ResourceCursorAdapter resourceCursorAdapter;
// UI elements
private MenuItem insertMarkerMenuItem;
private MenuItem searchMenuItem;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
trackId = getIntent().getLongExtra(EXTRA_TRACK_ID, -1L);
if (trackId == -1L) {
Log.d(TAG, "invalid track id");
finish();
return;
}
setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL);
setContentView(R.layout.marker_list);
getSharedPreferences(Constants.SETTINGS_NAME, Context.MODE_PRIVATE)
.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
ListView listView = (ListView) findViewById(R.id.marker_list);
listView.setEmptyView(findViewById(R.id.marker_list_empty));
listView.setOnItemClickListener(new OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Intent intent = IntentUtils.newIntent(MarkerListActivity.this, MarkerDetailActivity.class)
.putExtra(MarkerDetailActivity.EXTRA_MARKER_ID, id);
startActivity(intent);
}
});
resourceCursorAdapter = new ResourceCursorAdapter(this, R.layout.list_item, null, 0) {
@Override
public void bindView(View view, Context context, Cursor cursor) {
int nameIndex = cursor.getColumnIndex(WaypointsColumns.NAME);
int descriptionIndex = cursor.getColumnIndex(WaypointsColumns.DESCRIPTION);
int categoryIndex = cursor.getColumnIndex(WaypointsColumns.CATEGORY);
int typeIndex = cursor.getColumnIndex(WaypointsColumns.TYPE);
int timeIndex = cursor.getColumnIndexOrThrow(WaypointsColumns.TIME);
boolean statistics = cursor.getInt(typeIndex) == Waypoint.TYPE_STATISTICS;
String name = cursor.getString(nameIndex);
int iconId = statistics ? R.drawable.yellow_pushpin : R.drawable.blue_pushpin;
String contentDescription = getString(R.string.icon_marker);
String category = statistics ? null : cursor.getString(categoryIndex);
long time = cursor.getLong(timeIndex);
String description = statistics ? null : cursor.getString(descriptionIndex);
ListItemUtils.setListItem(MarkerListActivity.this,
view, name, iconId, contentDescription, category, null, null, time, description);
}
};
listView.setAdapter(resourceCursorAdapter);
ApiAdapterFactory.getApiAdapter()
.configureListViewContextualMenu(this, listView, contextualActionModeCallback);
final long firstWaypointId = MyTracksProviderUtils.Factory.get(this)
.getFirstWaypointId(trackId);
getSupportLoaderManager().initLoader(0, null, new LoaderCallbacks<Cursor>() {
@Override
public Loader<Cursor> onCreateLoader(int arg0, Bundle arg1) {
return new CursorLoader(MarkerListActivity.this,
WaypointsColumns.CONTENT_URI,
PROJECTION,
WaypointsColumns.TRACKID + "=? AND " + WaypointsColumns._ID + "!=?",
new String[] { String.valueOf(trackId), String.valueOf(firstWaypointId) },
null);
}
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
resourceCursorAdapter.swapCursor(cursor);
}
@Override
public void onLoaderReset(Loader<Cursor> loader) {
resourceCursorAdapter.swapCursor(null);
}
});
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.marker_list, menu);
insertMarkerMenuItem = menu.findItem(R.id.marker_list_insert_marker);
searchMenuItem = menu.findItem(R.id.marker_list_search);
ApiAdapterFactory.getApiAdapter().configureSearchWidget(this, searchMenuItem);
updateMenu();
return true;
}
private void updateMenu() {
if (insertMarkerMenuItem != null) {
insertMarkerMenuItem.setVisible(
trackId == PreferencesUtils.getLong(this, R.string.recording_track_id_key));
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.marker_list_insert_marker:
Intent intent = IntentUtils.newIntent(this, MarkerEditActivity.class)
.putExtra(MarkerEditActivity.EXTRA_TRACK_ID, trackId);
startActivity(intent);
return true;
case R.id.marker_list_search:
return ApiAdapterFactory.getApiAdapter().handleSearchMenuSelection(this);
default:
return super.onOptionsItemSelected(item);
}
}
@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
super.onCreateContextMenu(menu, v, menuInfo);
getMenuInflater().inflate(R.menu.list_context_menu, menu);
}
@Override
public boolean onContextItemSelected(MenuItem item) {
if (handleContextItem(
item.getItemId(), ((AdapterContextMenuInfo) item.getMenuInfo()).id)) {
return true;
}
return super.onContextItemSelected(item);
}
/**
* Handles a context item selection.
*
* @param itemId the menu item id
* @param markerId the marker id
* @return true if handled.
*/
private boolean handleContextItem(int itemId, long markerId) {
Intent intent;
switch (itemId) {
case R.id.list_context_menu_show_on_map:
intent = IntentUtils.newIntent(this, TrackDetailActivity.class)
.putExtra(TrackDetailActivity.EXTRA_MARKER_ID, markerId);
startActivity(intent);
return true;
case R.id.list_context_menu_edit:
intent = IntentUtils.newIntent(this, MarkerEditActivity.class)
.putExtra(MarkerEditActivity.EXTRA_MARKER_ID, markerId);
startActivity(intent);
return true;
case R.id.list_context_menu_delete:
DeleteOneMarkerDialogFragment.newInstance(markerId, trackId).show(
getSupportFragmentManager(),
DeleteOneMarkerDialogFragment.DELETE_ONE_MARKER_DIALOG_TAG);
return true;
default:
return false;
}
}
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_SEARCH) {
if (ApiAdapterFactory.getApiAdapter().handleSearchKey(searchMenuItem)) { return true; }
}
return super.onKeyUp(keyCode, event);
}
}
@@ -0,0 +1,43 @@
/*
* 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;
import com.google.android.apps.mytracks.services.RemoveTempFilesService;
import com.google.android.apps.mytracks.util.AnalyticsUtils;
import com.google.android.apps.mytracks.util.ApiAdapterFactory;
import com.google.android.maps.mytracks.BuildConfig;
import android.app.Application;
import android.content.Intent;
/**
* MyTracksApplication for keeping global state.
*
* @author Jimmy Shih
*/
public class MyTracksApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
if (BuildConfig.DEBUG) {
ApiAdapterFactory.getApiAdapter().enableStrictMode();
}
AnalyticsUtils.sendPageViews(getApplicationContext(), "/appstart");
Intent intent = new Intent(this, RemoveTempFilesService.class);
startService(intent);
}
}
@@ -0,0 +1,444 @@
/*
* 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 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.Waypoint;
import com.google.android.apps.mytracks.fragments.DeleteOneMarkerDialogFragment;
import com.google.android.apps.mytracks.fragments.DeleteOneTrackDialogFragment;
import com.google.android.apps.mytracks.fragments.DeleteOneTrackDialogFragment.DeleteOneTrackCaller;
import com.google.android.apps.mytracks.services.TrackRecordingServiceConnection;
import com.google.android.apps.mytracks.stats.TripStatistics;
import com.google.android.apps.mytracks.util.ApiAdapterFactory;
import com.google.android.apps.mytracks.util.IntentUtils;
import com.google.android.apps.mytracks.util.ListItemUtils;
import com.google.android.apps.mytracks.util.PreferencesUtils;
import com.google.android.apps.mytracks.util.StringUtils;
import com.google.android.apps.mytracks.util.TrackIconUtils;
import com.google.android.apps.mytracks.util.TrackRecordingServiceConnectionUtils;
import com.google.android.maps.mytracks.R;
import android.app.SearchManager;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.location.Location;
import android.location.LocationManager;
import android.os.Bundle;
import android.provider.SearchRecentSuggestions;
import android.util.Log;
import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.AdapterView;
import android.widget.AdapterView.AdapterContextMenuInfo;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.SortedSet;
/**
* An activity to display a list of search results.
*
* @author Rodrigo Damazio
*/
public class SearchListActivity extends AbstractMyTracksActivity implements DeleteOneTrackCaller {
private static final String TAG = SearchListActivity.class.getSimpleName();
private static final String NAME_FIELD = "name";
private static final String ICON_FIELD = "icon";
private static final String ICON_CONTENT_DESCRIPTION_FIELD = "iconContentDescription";
private static final String CATEGORY_FIELD = "category";
private static final String TOTAL_TIME_FIELD = "totalTime";
private static final String TOTAL_DISTANCE_FIELD = "totalDistance";
private static final String START_TIME_FIELD = "startTime";
private static final String DESCRIPTION_FIELD = "description";
private static final String TRACK_ID_FIELD = "trackId";
private static final String MARKER_ID_FIELD = "markerId";
private final OnSharedPreferenceChangeListener
sharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() {
@Override
public void onSharedPreferenceChanged(SharedPreferences preferences, String key) {
if (PreferencesUtils.getKey(SearchListActivity.this, R.string.recording_track_id_key)
.equals(key)) {
recordingTrackId = PreferencesUtils.getLong(
SearchListActivity.this, R.string.recording_track_id_key);
arrayAdapter.notifyDataSetChanged();
}
}
};
// Callback when an item is selected in the contextual action mode
private ContextualActionModeCallback
contextualActionModeCallback = new ContextualActionModeCallback() {
@Override
public boolean onClick(int itemId, int position, long id) {
return handleContextItem(itemId, position);
}
};
private TrackRecordingServiceConnection trackRecordingServiceConnection;
private MyTracksProviderUtils myTracksProviderUtils;
private SearchEngine searchEngine;
private SearchRecentSuggestions searchRecentSuggestions;
private LocationManager locationManager;
private long recordingTrackId;
private boolean metricUnits;
private ArrayAdapter<Map<String, Object>> arrayAdapter;
// UI elements
private ListView listView;
private MenuItem searchMenuItem;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL);
setContentView(R.layout.search_list);
trackRecordingServiceConnection = new TrackRecordingServiceConnection(this, null);
myTracksProviderUtils = MyTracksProviderUtils.Factory.get(this);
searchEngine = new SearchEngine(myTracksProviderUtils);
searchRecentSuggestions = SearchEngineProvider.newHelper(this);
locationManager = (LocationManager) getSystemService(LOCATION_SERVICE);
getSharedPreferences(Constants.SETTINGS_NAME, Context.MODE_PRIVATE)
.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
recordingTrackId = PreferencesUtils.getLong(this, R.string.recording_track_id_key);
listView = (ListView) findViewById(R.id.search_list);
listView.setEmptyView(findViewById(R.id.search_list_empty));
listView.setOnItemClickListener(new OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Map<String, Object> item = arrayAdapter.getItem(position);
Long trackId = (Long) item.get(TRACK_ID_FIELD);
Long markerId = (Long) item.get(MARKER_ID_FIELD);
Intent intent;
if (markerId != null) {
intent = IntentUtils.newIntent(SearchListActivity.this, TrackDetailActivity.class)
.putExtra(TrackDetailActivity.EXTRA_MARKER_ID, markerId);
} else {
intent = IntentUtils.newIntent(SearchListActivity.this, TrackDetailActivity.class)
.putExtra(TrackDetailActivity.EXTRA_TRACK_ID, trackId);
}
startActivity(intent);
}
});
arrayAdapter = new ArrayAdapter<Map<String, Object>>(
this, R.layout.list_item, R.id.list_item_name) {
@Override
public View getView(int position, View convertView, android.view.ViewGroup parent) {
View view;
if (convertView == null) {
view = getLayoutInflater().inflate(R.layout.list_item, parent, false);
} else {
view = convertView;
}
Map<String, Object> resultMap = getItem(position);
String name = (String) resultMap.get(NAME_FIELD);
int iconId = (Integer) resultMap.get(ICON_FIELD);
String iconContentDescription = (String) resultMap.get(ICON_CONTENT_DESCRIPTION_FIELD);
String category = (String) resultMap.get(CATEGORY_FIELD);
String totalTime = (String) resultMap.get(TOTAL_TIME_FIELD);
String totalDistance = (String) resultMap.get(TOTAL_DISTANCE_FIELD);
long startTime = (Long) resultMap.get(START_TIME_FIELD);
String description = (String) resultMap.get(DESCRIPTION_FIELD);
ListItemUtils.setListItem(SearchListActivity.this,
view,
name,
iconId,
iconContentDescription,
category,
totalTime,
totalDistance,
startTime,
description);
return view;
}
};
listView.setAdapter(arrayAdapter);
ApiAdapterFactory.getApiAdapter()
.configureListViewContextualMenu(this, listView, contextualActionModeCallback);
handleIntent(getIntent());
}
@Override
protected void onResume() {
super.onResume();
TrackRecordingServiceConnectionUtils.resume(this, trackRecordingServiceConnection);
metricUnits = PreferencesUtils.getBoolean(
this, R.string.metric_units_key, PreferencesUtils.METRIC_UNITS_DEFAULT);
}
@Override
protected void onDestroy() {
super.onDestroy();
trackRecordingServiceConnection.unbind();
}
@Override
public void onNewIntent(Intent intent) {
setIntent(intent);
handleIntent(intent);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.search_list, menu);
searchMenuItem = menu.findItem(R.id.search_list_search);
ApiAdapterFactory.getApiAdapter().configureSearchWidget(this, searchMenuItem);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.search_list_search:
return ApiAdapterFactory.getApiAdapter().handleSearchMenuSelection(this);
default:
return super.onOptionsItemSelected(item);
}
}
@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
super.onCreateContextMenu(menu, v, menuInfo);
getMenuInflater().inflate(R.menu.list_context_menu, menu);
}
@Override
public boolean onContextItemSelected(MenuItem item) {
AdapterContextMenuInfo adapterContextMenuInfo = (AdapterContextMenuInfo) item.getMenuInfo();
if (handleContextItem(item.getItemId(), adapterContextMenuInfo.position)) {
return true;
}
return super.onContextItemSelected(item);
}
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_SEARCH) {
if (ApiAdapterFactory.getApiAdapter().handleSearchKey(searchMenuItem)) {
return true;
}
}
return super.onKeyUp(keyCode, event);
}
/**
* Handles a context item selection.
*
* @param itemId the menu item id
* @param position the position of the selected row
* @return true if handled.
*/
private boolean handleContextItem(int itemId, int position) {
Map<String, Object> item = arrayAdapter.getItem(position);
Long trackId = (Long) item.get(TRACK_ID_FIELD);
Long markerId = (Long) item.get(MARKER_ID_FIELD);
Intent intent;
switch (itemId) {
case R.id.list_context_menu_show_on_map:
if (markerId != null) {
intent = IntentUtils.newIntent(this, TrackDetailActivity.class)
.putExtra(TrackDetailActivity.EXTRA_MARKER_ID, markerId);
} else {
intent = IntentUtils.newIntent(this, TrackDetailActivity.class)
.putExtra(TrackDetailActivity.EXTRA_TRACK_ID, trackId);
}
startActivity(intent);
return true;
case R.id.list_context_menu_edit:
if (markerId != null) {
intent = IntentUtils.newIntent(this, MarkerEditActivity.class)
.putExtra(MarkerEditActivity.EXTRA_MARKER_ID, markerId);
} else {
intent = IntentUtils.newIntent(this, TrackEditActivity.class)
.putExtra(TrackEditActivity.EXTRA_TRACK_ID, trackId);
}
startActivity(intent);
// Close the search result since its content can change after edit.
finish();
return true;
case R.id.list_context_menu_delete:
if (markerId != null) {
DeleteOneMarkerDialogFragment.newInstance(markerId, trackId).show(
getSupportFragmentManager(),
DeleteOneMarkerDialogFragment.DELETE_ONE_MARKER_DIALOG_TAG);
} else {
DeleteOneTrackDialogFragment.newInstance(trackId).show(getSupportFragmentManager(),
DeleteOneTrackDialogFragment.DELETE_ONE_TRACK_DIALOG_TAG);
}
return true;
default:
return false;
}
}
/**
* Handles the intent.
*
* @param intent the intent
*/
private void handleIntent(Intent intent) {
if (!Intent.ACTION_SEARCH.equals(intent.getAction())) {
Log.e(TAG, "Invalid intent action: " + intent);
finish();
return;
}
String textQuery = intent.getStringExtra(SearchManager.QUERY);
setTitle(textQuery);
Location currentLocation = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER);
final SearchQuery query = new SearchQuery(textQuery, currentLocation, -1L, System
.currentTimeMillis());
new Thread() {
@Override
public void run() {
doSearch(query);
}
}.start();
}
/**
* Do the search.
*
* @param query the query
*/
private void doSearch(SearchQuery query) {
SortedSet<ScoredResult> scoredResults = searchEngine.search(query);
final List<Map<String, Object>> displayResults = prepareResultsforDisplay(scoredResults);
// Use the UI thread to display the results
runOnUiThread(new Runnable() {
@Override
public void run() {
arrayAdapter.clear();
ApiAdapterFactory.getApiAdapter().addAllToArrayAdapter(arrayAdapter, displayResults);
}
});
// Save the query as a suggestion for the future
searchRecentSuggestions.saveRecentQuery(query.textQuery, null);
}
/**
* Prepares the result for display.
*
* @param scoredResults a list of score results
* @return a list of result maps
*/
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 {
prepareMarkerForDisplay(result.waypoint, resultMap);
}
output.add(resultMap);
}
return output;
}
/**
* Prepares a marker for display by filling in a result map.
*
* @param waypoint the marker
* @param resultMap the result map
*/
private void prepareMarkerForDisplay(Waypoint waypoint, Map<String, Object> resultMap) {
/*
* TODO: It may be more appropriate to obtain the track name as a join in
* the retrieval phase of the search.
*/
String trackName = null;
long trackId = waypoint.getTrackId();
if (trackId != -1L) {
Track track = myTracksProviderUtils.getTrack(trackId);
if (track != null) {
trackName = track.getName();
}
}
boolean statistics = waypoint.getType() == Waypoint.TYPE_STATISTICS;
resultMap.put(NAME_FIELD, waypoint.getName());
resultMap.put(ICON_FIELD, statistics ? R.drawable.yellow_pushpin : R.drawable.blue_pushpin);
resultMap.put(ICON_CONTENT_DESCRIPTION_FIELD, getString(R.string.icon_marker));
resultMap.put(CATEGORY_FIELD, statistics ? null : waypoint.getCategory());
resultMap.put(START_TIME_FIELD, waypoint.getLocation().getTime());
// Display the marker's track name in the total time field
resultMap.put(TOTAL_TIME_FIELD, trackName == null ? null
: getString(R.string.search_list_marker_track_location, trackName));
resultMap.put(TOTAL_DISTANCE_FIELD, null);
resultMap.put(DESCRIPTION_FIELD, statistics ? null : waypoint.getDescription());
resultMap.put(TRACK_ID_FIELD, waypoint.getTrackId());
resultMap.put(MARKER_ID_FIELD, waypoint.getId());
}
/**
* Prepares a track for display by filling in a result map.
*
* @param track the track
* @param resultMap the result map
*/
private void prepareTrackForDisplay(Track track, Map<String, Object> resultMap) {
boolean isRecording = track.getId() == recordingTrackId;
TripStatistics tripStatitics = track.getTripStatistics();
resultMap.put(NAME_FIELD, track.getName());
resultMap.put(ICON_FIELD, isRecording
? R.drawable.menu_record_track : TrackIconUtils.getIconDrawable(track.getIcon()));
resultMap.put(ICON_CONTENT_DESCRIPTION_FIELD, getString(isRecording ? R.string.icon_recording
: R.string.icon_track));
resultMap.put(CATEGORY_FIELD, track.getCategory());
resultMap.put(TOTAL_TIME_FIELD, isRecording ? null : StringUtils.formatElapsedTime(
tripStatitics.getTotalTime()));
resultMap.put(TOTAL_DISTANCE_FIELD, isRecording ? null : StringUtils.formatDistance(
this, tripStatitics.getTotalDistance(), metricUnits));
resultMap.put(START_TIME_FIELD, tripStatitics.getStartTime());
resultMap.put(DESCRIPTION_FIELD, track.getDescription());
resultMap.put(TRACK_ID_FIELD, track.getId());
resultMap.put(MARKER_ID_FIELD, null);
}
@Override
public TrackRecordingServiceConnection getTrackRecordingServiceConnection() {
return trackRecordingServiceConnection;
}
}
@@ -0,0 +1,299 @@
/*
* 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.apps.mytracks.content.Sensor;
import com.google.android.apps.mytracks.services.ITrackRecordingService;
import com.google.android.apps.mytracks.services.TrackRecordingServiceConnection;
import com.google.android.apps.mytracks.services.sensors.SensorManager;
import com.google.android.apps.mytracks.services.sensors.SensorManagerFactory;
import com.google.android.apps.mytracks.services.sensors.SensorUtils;
import com.google.android.apps.mytracks.util.TrackRecordingServiceConnectionUtils;
import com.google.android.maps.mytracks.R;
import com.google.protobuf.InvalidProtocolBufferException;
import android.os.Bundle;
import android.os.RemoteException;
import android.text.format.DateFormat;
import android.util.Log;
import android.widget.TextView;
import java.util.Timer;
import java.util.TimerTask;
/**
* An activity that displays information about sensors.
*
* @author Sandor Dornbush
*/
public class SensorStateActivity extends AbstractMyTracksActivity {
private static final String TAG = SensorStateActivity.class.getName();
private static final long REFRESH_PERIOD_MS = 250;
/**
* A Runnable to update the UI.
*/
private final Runnable updateRunnable = new Runnable() {
public void run() {
ITrackRecordingService trackRecordingService = trackRecordingServiceConnection
.getServiceIfBound();
// Check if service is available and recording
boolean isRecording = false;
if (trackRecordingService != null) {
try {
isRecording = trackRecordingService.isRecording();
} catch (RemoteException e) {
Log.e(TAG, "Unable to determine if the track recording service is recording.", e);
}
}
if (!isRecording) {
updateFromTempSensorManager();
} else {
stopTempSensorManager();
updateFromSystemSensorManager();
}
}
};
/**
* A TimeTask to update the UI.
*/
private class UpdateTimerTask extends TimerTask {
@Override
public void run() {
if (isVisible) {
runOnUiThread(updateRunnable);
}
}
};
private TrackRecordingServiceConnection trackRecordingServiceConnection;
private boolean isVisible = false;
private Timer timer = null;
private SensorManager tempSensorManager = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.sensor_state);
trackRecordingServiceConnection = new TrackRecordingServiceConnection(this, null);
}
@Override
protected void onResume() {
super.onResume();
TrackRecordingServiceConnectionUtils.resume(this, trackRecordingServiceConnection);
isVisible = true;
timer = new Timer();
timer.schedule(new UpdateTimerTask(), 0, REFRESH_PERIOD_MS);
}
@Override
protected void onPause() {
super.onPause();
isVisible = false;
timer.cancel();
timer.purge();
timer = null;
stopTempSensorManager();
}
@Override
protected void onDestroy() {
super.onDestroy();
trackRecordingServiceConnection.unbind();
}
/**
* Stops the temp sensor manager.
*/
private void stopTempSensorManager() {
if (tempSensorManager != null) {
SensorManagerFactory.releaseTempSensorManager();
tempSensorManager = null;
}
}
/**
* Updates from a temp sensor manager.
*/
private void updateFromTempSensorManager() {
Sensor.SensorState sensorState = Sensor.SensorState.NONE;
Sensor.SensorDataSet sensorDataSet = null;
if (tempSensorManager == null) {
tempSensorManager = SensorManagerFactory.getTempSensorManager(this);
}
if (tempSensorManager != null) {
sensorState = tempSensorManager.getSensorState();
sensorDataSet = tempSensorManager.getSensorDataSet();
}
updateSensorStateAndDataSet(sensorState, sensorDataSet);
}
/**
* Updates from the system sensor manager.
*/
private void updateFromSystemSensorManager() {
Sensor.SensorState sensorState = Sensor.SensorState.NONE;
Sensor.SensorDataSet sensorDataSet = null;
ITrackRecordingService trackRecordingService = trackRecordingServiceConnection
.getServiceIfBound();
// Get sensor details from the service.
if (trackRecordingService == null) {
Log.d(TAG, "Cannot get teh track recording service.");
} else {
try {
sensorState = Sensor.SensorState.valueOf(trackRecordingService.getSensorState());
} catch (RemoteException e) {
Log.e(TAG, "Cannote read sensor state.", e);
sensorState = Sensor.SensorState.NONE;
}
try {
byte[] buff = trackRecordingService.getSensorData();
if (buff != null) {
sensorDataSet = Sensor.SensorDataSet.parseFrom(buff);
}
} catch (RemoteException e) {
Log.e(TAG, "Cannot read sensor data set.", e);
} catch (InvalidProtocolBufferException e) {
Log.e(TAG, "Cannot read sensor data set.", e);
}
}
updateSensorStateAndDataSet(sensorState, sensorDataSet);
}
/**
* Updates the sensor state and data set.
*
* @param sensorState sensor state
* @param sensorDataSet sensor data set
*/
private void updateSensorStateAndDataSet(
Sensor.SensorState sensorState, Sensor.SensorDataSet sensorDataSet) {
((TextView) findViewById(R.id.sensor_state)).setText(
SensorUtils.getStateAsString(sensorState, this));
String lastSensorTime = sensorDataSet == null ? getString(R.string.value_unknown)
: getLastSensorTime(sensorDataSet);
String heartRate = sensorDataSet == null ? getString(R.string.value_unknown)
: getHeartRate(sensorDataSet);
String cadence = sensorDataSet == null ? getString(R.string.value_unknown)
: getCadence(sensorDataSet);
String power = sensorDataSet == null ? getString(R.string.value_unknown)
: getPower(sensorDataSet);
String battery = sensorDataSet == null ? getString(R.string.value_unknown)
: getBattery(sensorDataSet);
((TextView) findViewById(R.id.sensor_state_last_sensor_time)).setText(lastSensorTime);
((TextView) findViewById(R.id.sensor_state_heart_rate)).setText(heartRate);
((TextView) findViewById(R.id.sensor_state_cadence)).setText(cadence);
((TextView) findViewById(R.id.sensor_state_power)).setText(power);
((TextView) findViewById(R.id.sensor_state_battery)).setText(battery);
}
/**
* Gets the last sensor time.
*
* @param sensorDataSet sensor data set
*/
private String getLastSensorTime(Sensor.SensorDataSet sensorDataSet) {
return DateFormat.format("h:mm:ss aa", sensorDataSet.getCreationTime()).toString();
}
/**
* Gets the heart rate.
*
* @param sensorDataSet sensor data set
*/
private String getHeartRate(Sensor.SensorDataSet sensorDataSet) {
String value;
if (sensorDataSet.hasHeartRate() && sensorDataSet.getHeartRate().hasValue()
&& sensorDataSet.getHeartRate().getState() == Sensor.SensorState.SENDING) {
value = getString(
R.string.sensor_state_heart_rate_value, sensorDataSet.getHeartRate().getValue());
} else {
value = SensorUtils.getStateAsString(
sensorDataSet.hasHeartRate() ? sensorDataSet.getHeartRate().getState()
: Sensor.SensorState.NONE, this);
}
return value;
}
/**
* Gets the cadence.
*
* @param sensorDataSet sensor data set
*/
private String getCadence(Sensor.SensorDataSet sensorDataSet) {
String value;
if (sensorDataSet.hasCadence() && sensorDataSet.getCadence().hasValue()
&& sensorDataSet.getCadence().getState() == Sensor.SensorState.SENDING) {
value = getString(R.string.sensor_state_cadence_value, sensorDataSet.getCadence().getValue());
} else {
value = SensorUtils.getStateAsString(
sensorDataSet.hasCadence() ? sensorDataSet.getCadence().getState()
: Sensor.SensorState.NONE, this);
}
return value;
}
/**
* Gets the power.
*
* @param sensorDataSet sensor data set
*/
private String getPower(Sensor.SensorDataSet sensorDataSet) {
String value;
if (sensorDataSet.hasPower() && sensorDataSet.getPower().hasValue()
&& sensorDataSet.getPower().getState() == Sensor.SensorState.SENDING) {
value = getString(R.string.sensor_state_power_value, sensorDataSet.getPower().getValue());
} else {
value = SensorUtils.getStateAsString(
sensorDataSet.hasPower() ? sensorDataSet.getPower().getState() : Sensor.SensorState.NONE,
this);
}
return value;
}
/**
* Gets the battery.
*
* @param sensorDataSet sensor data set
*/
private String getBattery(Sensor.SensorDataSet sensorDataSet) {
String value;
if (sensorDataSet.hasBatteryLevel() && sensorDataSet.getBatteryLevel().hasValue()
&& sensorDataSet.getBatteryLevel().getState() == Sensor.SensorState.SENDING) {
value = getString(R.string.value_integer_percent, sensorDataSet.getBatteryLevel().getValue());
} else {
value = SensorUtils.getStateAsString(
sensorDataSet.hasBatteryLevel() ? sensorDataSet.getBatteryLevel().getState()
: Sensor.SensorState.NONE, this);
}
return value;
}
}
@@ -0,0 +1,150 @@
/*
* 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;
import android.content.Context;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentActivity;
import android.support.v4.app.FragmentTransaction;
import android.view.View;
import android.widget.TabHost;
import android.widget.TabHost.TabContentFactory;
import android.widget.TabHost.TabSpec;
import java.util.HashMap;
/**
* This is a helper class that implements a generic mechanism for associating
* fragments with the tabs in a tab host. It relies on a trick. Normally a tab
* host has a simple API for supplying a View or Intent that each tab will show.
* This is not sufficient for switching between fragments. So instead we make
* the content part of the tab host 0dp high (it is not shown) and the
* TabManager supplies its own dummy view to show as the tab content. It listens
* to changes in tabs, and takes care of switch to the correct fragment shown in
* a separate content area whenever the selected tab changes.
* <p>
* Copied from the Fragment Tabs example in the API 4+ Support Demos.
*
* @author Jimmy Shih
*/
public class TabManager implements TabHost.OnTabChangeListener {
private final FragmentActivity fragmentActivity;
private final TabHost tabHost;
private final int containerId;
private final HashMap<String, TabInfo> tabs = new HashMap<String, TabInfo>();
private TabInfo lastTabInfo;
/**
* An object to hold a tab's info.
*
* @author Jimmy Shih
*/
private static final class TabInfo {
private final String tag;
private final Class<?> clss;
private final Bundle bundle;
private Fragment fragment;
public TabInfo(String tag, Class<?> clss, Bundle bundle) {
this.tag = tag;
this.clss = clss;
this.bundle = bundle;
}
}
/**
* A dummy {@link TabContentFactory} that creates an empty view to satisfy the
* {@link TabHost} API.
*
* @author Jimmy Shih
*/
private static class DummyTabContentFactory implements TabContentFactory {
private final Context context;
public DummyTabContentFactory(Context context) {
this.context = context;
}
@Override
public View createTabContent(String tag) {
View view = new View(context);
view.setMinimumWidth(0);
view.setMinimumHeight(0);
return view;
}
}
public TabManager(FragmentActivity fragmentActivity, TabHost tabHost, int containerId) {
this.fragmentActivity = fragmentActivity;
this.tabHost = tabHost;
this.containerId = containerId;
tabHost.setOnTabChangedListener(this);
}
public void addTab(TabSpec tabSpec, Class<?> clss, Bundle bundle) {
tabSpec.setContent(new DummyTabContentFactory(fragmentActivity));
String tag = tabSpec.getTag();
TabInfo tabInfo = new TabInfo(tag, clss, bundle);
/*
* Check to see if we already have a fragment for this tab, probably from a
* previously saved state. If so, deactivate it, because our initial state
* is that a tab isn't shown.
*/
tabInfo.fragment = fragmentActivity.getSupportFragmentManager().findFragmentByTag(tag);
if (tabInfo.fragment != null && !tabInfo.fragment.isDetached()) {
FragmentTransaction fragmentTransaction = fragmentActivity.getSupportFragmentManager()
.beginTransaction();
fragmentTransaction.detach(tabInfo.fragment);
fragmentTransaction.commit();
}
tabs.put(tag, tabInfo);
tabHost.addTab(tabSpec);
}
@Override
public void onTabChanged(String tabId) {
TabInfo newTabInfo = tabs.get(tabId);
if (lastTabInfo != newTabInfo) {
FragmentTransaction fragmentTransaction = fragmentActivity.getSupportFragmentManager()
.beginTransaction();
if (lastTabInfo != null) {
if (lastTabInfo.fragment != null) {
fragmentTransaction.detach(lastTabInfo.fragment);
}
}
if (newTabInfo != null) {
if (newTabInfo.fragment == null) {
newTabInfo.fragment = Fragment.instantiate(
fragmentActivity, newTabInfo.clss.getName(), newTabInfo.bundle);
fragmentTransaction.add(containerId, newTabInfo.fragment, newTabInfo.tag);
} else {
fragmentTransaction.attach(newTabInfo.fragment);
}
}
lastTabInfo = newTabInfo;
fragmentTransaction.commit();
fragmentActivity.getSupportFragmentManager().executePendingTransactions();
}
}
}
@@ -0,0 +1,498 @@
/*
* 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;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.content.TrackDataHub;
import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.content.WaypointCreationRequest;
import com.google.android.apps.mytracks.fragments.ChartFragment;
import com.google.android.apps.mytracks.fragments.ChooseActivityDialogFragment;
import com.google.android.apps.mytracks.fragments.ChooseUploadServiceDialogFragment;
import com.google.android.apps.mytracks.fragments.DeleteOneTrackDialogFragment;
import com.google.android.apps.mytracks.fragments.DeleteOneTrackDialogFragment.DeleteOneTrackCaller;
import com.google.android.apps.mytracks.fragments.FrequencyDialogFragment;
import com.google.android.apps.mytracks.fragments.InstallEarthDialogFragment;
import com.google.android.apps.mytracks.fragments.MapFragment;
import com.google.android.apps.mytracks.fragments.StatsFragment;
import com.google.android.apps.mytracks.io.file.SaveActivity;
import com.google.android.apps.mytracks.io.file.TrackWriterFactory.TrackFileFormat;
import com.google.android.apps.mytracks.io.sendtogoogle.SendRequest;
import com.google.android.apps.mytracks.services.TrackRecordingServiceConnection;
import com.google.android.apps.mytracks.settings.SettingsActivity;
import com.google.android.apps.mytracks.util.AnalyticsUtils;
import com.google.android.apps.mytracks.util.ApiAdapterFactory;
import com.google.android.apps.mytracks.util.IntentUtils;
import com.google.android.apps.mytracks.util.PreferencesUtils;
import com.google.android.apps.mytracks.util.TrackRecordingServiceConnectionUtils;
import com.google.android.maps.mytracks.R;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.os.Bundle;
import android.os.Parcelable;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.widget.TabHost;
import android.widget.TabHost.TabSpec;
import java.util.List;
/**
* An activity to show the track detail.
*
* @author Leif Hendrik Wilden
* @author Rodrigo Damazio
*/
public class TrackDetailActivity extends AbstractMyTracksActivity implements DeleteOneTrackCaller {
public static final String EXTRA_TRACK_ID = "track_id";
public static final String EXTRA_MARKER_ID = "marker_id";
private static final String TAG = TrackDetailActivity.class.getSimpleName();
private static final String CURRENT_TAG_KEY = "tab";
private TrackDataHub trackDataHub;
private TrackRecordingServiceConnection trackRecordingServiceConnection;
private TabHost tabHost;
private TabManager tabManager;
private long trackId;
private long markerId;
private MenuItem stopRecordingMenuItem;
private MenuItem insertMarkerMenuItem;
private MenuItem playMenuItem;
private MenuItem shareMenuItem;
private MenuItem voiceFrequencyMenuItem;
private MenuItem splitFrequencyMenuItem;
private MenuItem sendGoogleMenuItem;
private MenuItem saveMenuItem;
private View mapViewContainer;
/*
* Note that sharedPreferenceChangeListener cannot be an anonymous inner
* class. Anonymous inner class will get garbage collected.
*/
private final OnSharedPreferenceChangeListener
sharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() {
@Override
public void onSharedPreferenceChanged(SharedPreferences preferences, String key) {
// Note that key can be null
if (PreferencesUtils.getKey(TrackDetailActivity.this, R.string.recording_track_id_key)
.equals(key)) {
updateMenu();
}
}
};
/**
* We are not displaying driving directions. Just an arbitrary track that is
* not associated to any licensed mapping data. Therefore it should be okay to
* return false here and still comply with the terms of service.
*/
@Override
protected boolean isRouteDisplayed() {
return false;
}
/**
* We are displaying a location. This needs to return true in order to comply
* with the terms of service.
*/
@Override
protected boolean isLocationDisplayed() {
return true;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
handleIntent(getIntent());
ApiAdapterFactory.getApiAdapter().hideTitle(this);
setContentView(R.layout.track_detail);
getSharedPreferences(Constants.SETTINGS_NAME, Context.MODE_PRIVATE)
.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
trackRecordingServiceConnection = new TrackRecordingServiceConnection(this, null);
trackDataHub = TrackDataHub.newInstance(this);
mapViewContainer = getLayoutInflater().inflate(R.layout.map, null);
ApiAdapterFactory.getApiAdapter().disableHardwareAccelerated(mapViewContainer);
tabHost = (TabHost) findViewById(android.R.id.tabhost);
tabHost.setup();
tabManager = new TabManager(this, tabHost, R.id.realtabcontent);
TabSpec mapTabSpec = tabHost.newTabSpec(MapFragment.MAP_FRAGMENT_TAG).setIndicator(
getString(R.string.track_detail_map_tab), getResources().getDrawable(R.drawable.tab_map));
tabManager.addTab(mapTabSpec, MapFragment.class, null);
TabSpec chartTabSpec = tabHost.newTabSpec(ChartFragment.CHART_FRAGMENT_TAG).setIndicator(
getString(R.string.track_detail_chart_tab),
getResources().getDrawable(R.drawable.tab_chart));
tabManager.addTab(chartTabSpec, ChartFragment.class, null);
TabSpec statsTabSpec = tabHost.newTabSpec(StatsFragment.STATS_FRAGMENT_TAG).setIndicator(
getString(R.string.track_detail_stats_tab),
getResources().getDrawable(R.drawable.tab_stats));
tabManager.addTab(statsTabSpec, StatsFragment.class, null);
if (savedInstanceState != null) {
tabHost.setCurrentTabByTag(savedInstanceState.getString(CURRENT_TAG_KEY));
}
showMarker();
}
@Override
public void onNewIntent(Intent intent) {
setIntent(intent);
handleIntent(intent);
showMarker();
}
@Override
protected void onStart() {
super.onStart();
trackDataHub.start();
}
@Override
protected void onResume() {
super.onResume();
trackDataHub.loadTrack(trackId);
TrackRecordingServiceConnectionUtils.resume(this, trackRecordingServiceConnection);
setTitle(trackId == PreferencesUtils.getLong(this, R.string.recording_track_id_key));
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putString(CURRENT_TAG_KEY, tabHost.getCurrentTabTag());
}
@Override
protected void onStop() {
super.onStop();
trackDataHub.stop();
}
@Override
protected void onDestroy() {
super.onDestroy();
trackRecordingServiceConnection.unbind();
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.track_detail, menu);
String fileTypes[] = getResources().getStringArray(R.array.file_types);
menu.findItem(R.id.track_detail_save_gpx)
.setTitle(getString(R.string.menu_save_format, fileTypes[0]));
menu.findItem(R.id.track_detail_save_kml)
.setTitle(getString(R.string.menu_save_format, fileTypes[1]));
menu.findItem(R.id.track_detail_save_csv)
.setTitle(getString(R.string.menu_save_format, fileTypes[2]));
menu.findItem(R.id.track_detail_save_tcx)
.setTitle(getString(R.string.menu_save_format, fileTypes[3]));
stopRecordingMenuItem = menu.findItem(R.id.track_detail_stop_recording);
insertMarkerMenuItem = menu.findItem(R.id.track_detail_insert_marker);
playMenuItem = menu.findItem(R.id.track_detail_play);
shareMenuItem = menu.findItem(R.id.track_detail_share);
voiceFrequencyMenuItem = menu.findItem(R.id.track_detail_voice_frequency);
splitFrequencyMenuItem = menu.findItem(R.id.track_detail_split_frequency);
sendGoogleMenuItem = menu.findItem(R.id.track_detail_send_google);
saveMenuItem = menu.findItem(R.id.track_detail_save);
updateMenu();
return true;
}
@Override
protected void onHomeSelected() {
Intent intent = IntentUtils.newIntent(this, TrackListActivity.class);
startActivity(intent);
finish();
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
String sensorTypeValueNone = getString(R.string.sensor_type_value_none);
boolean showSensorState = !sensorTypeValueNone.equals(
PreferencesUtils.getString(this, R.string.sensor_type_key, sensorTypeValueNone));
menu.findItem(R.id.track_detail_sensor_state).setVisible(showSensorState);
return super.onPrepareOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
Intent intent;
switch (item.getItemId()) {
case R.id.track_detail_stop_recording:
updateMenuItems(false);
setTitle(false);
TrackRecordingServiceConnectionUtils.stop(this, trackRecordingServiceConnection, true);
return true;
case R.id.track_detail_insert_marker:
AnalyticsUtils.sendPageViews(this, "/action/insert_marker");
intent = IntentUtils.newIntent(this, MarkerEditActivity.class)
.putExtra(MarkerEditActivity.EXTRA_TRACK_ID, trackId);
startActivity(intent);
return true;
case R.id.track_detail_play:
if (isEarthInstalled()) {
AnalyticsUtils.sendPageViews(this, "/action/play");
intent = IntentUtils.newIntent(this, SaveActivity.class)
.putExtra(SaveActivity.EXTRA_TRACK_ID, trackId)
.putExtra(SaveActivity.EXTRA_TRACK_FILE_FORMAT, (Parcelable) TrackFileFormat.KML)
.putExtra(SaveActivity.EXTRA_PLAY_TRACK, true);
startActivity(intent);
} else {
new InstallEarthDialogFragment().show(
getSupportFragmentManager(), InstallEarthDialogFragment.INSTALL_EARTH_DIALOG_TAG);
}
return true;
case R.id.track_detail_share:
AnalyticsUtils.sendPageViews(this, "/action/share");
ChooseActivityDialogFragment.newInstance(trackId, null).show(
getSupportFragmentManager(), ChooseActivityDialogFragment.CHOOSE_ACTIVITY_DIALOG_TAG);
return true;
case R.id.track_detail_markers:
intent = IntentUtils.newIntent(this, MarkerListActivity.class)
.putExtra(MarkerListActivity.EXTRA_TRACK_ID, trackId);
startActivity(intent);
return true;
case R.id.track_detail_voice_frequency:
FrequencyDialogFragment.newInstance(R.string.announcement_frequency_key,
PreferencesUtils.ANNOUNCEMENT_FREQUENCY_DEFAULT,
R.string.settings_voice_frequency_title)
.show(getSupportFragmentManager(), FrequencyDialogFragment.FREQUENCY_DIALOG_TAG);
return true;
case R.id.track_detail_split_frequency:
FrequencyDialogFragment.newInstance(R.string.split_frequency_key,
PreferencesUtils.SPLIT_FREQUENCY_DEFAULT, R.string.settings_split_frequency_title)
.show(getSupportFragmentManager(), FrequencyDialogFragment.FREQUENCY_DIALOG_TAG);
return true;
case R.id.track_detail_send_google:
AnalyticsUtils.sendPageViews(this, "/action/send_google");
ChooseUploadServiceDialogFragment.newInstance(new SendRequest(trackId)).show(
getSupportFragmentManager(),
ChooseUploadServiceDialogFragment.CHOOSE_UPLOAD_SERVICE_DIALOG_TAG);
return true;
case R.id.track_detail_save_gpx:
startSaveActivity(TrackFileFormat.GPX);
return true;
case R.id.track_detail_save_kml:
startSaveActivity(TrackFileFormat.KML);
return true;
case R.id.track_detail_save_csv:
startSaveActivity(TrackFileFormat.CSV);
return true;
case R.id.track_detail_save_tcx:
startSaveActivity(TrackFileFormat.TCX);
return true;
case R.id.track_detail_edit:
intent = IntentUtils.newIntent(this, TrackEditActivity.class)
.putExtra(TrackEditActivity.EXTRA_TRACK_ID, trackId);
startActivity(intent);
return true;
case R.id.track_detail_delete:
DeleteOneTrackDialogFragment.newInstance(trackId).show(
getSupportFragmentManager(), DeleteOneTrackDialogFragment.DELETE_ONE_TRACK_DIALOG_TAG);
return true;
case R.id.track_detail_sensor_state:
intent = IntentUtils.newIntent(this, SensorStateActivity.class);
startActivity(intent);
return true;
case R.id.track_detail_settings:
intent = IntentUtils.newIntent(this, SettingsActivity.class);
startActivity(intent);
return true;
case R.id.track_detail_help:
intent = IntentUtils.newIntent(this, HelpActivity.class);
startActivity(intent);
return true;
default:
return super.onOptionsItemSelected(item);
}
}
@Override
public boolean onTrackballEvent(MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
if (TrackRecordingServiceConnectionUtils.isRecording(this, trackRecordingServiceConnection)) {
TrackRecordingServiceConnectionUtils.addMarker(
this, trackRecordingServiceConnection, WaypointCreationRequest.DEFAULT_WAYPOINT);
return true;
}
}
return super.onTrackballEvent(event);
}
/**
* Gets the map view container.
*/
public View getMapViewContainer() {
return mapViewContainer;
}
/**
* Gets the {@link TrackDataHub}.
*/
public TrackDataHub getTrackDataHub() {
return trackDataHub;
}
/**
* Handles the data in the intent.
*/
private void handleIntent(Intent intent) {
trackId = intent.getLongExtra(EXTRA_TRACK_ID, -1L);
markerId = intent.getLongExtra(EXTRA_MARKER_ID, -1L);
if (markerId != -1L) {
Waypoint waypoint = MyTracksProviderUtils.Factory.get(this).getWaypoint(markerId);
if (waypoint == null) {
exit();
return;
}
trackId = waypoint.getTrackId();
}
if (trackId == -1L) {
exit();
return;
}
}
/**
* Exists and returns to {@link TrackListActivity}.
*/
private void exit() {
Intent newIntent = IntentUtils.newIntent(this, TrackListActivity.class);
startActivity(newIntent);
finish();
}
/**
* Shows marker.
*/
private void showMarker() {
if (markerId != -1L) {
MapFragment mapFragmet = (MapFragment) getSupportFragmentManager()
.findFragmentByTag(MapFragment.MAP_FRAGMENT_TAG);
if (mapFragmet != null) {
tabHost.setCurrentTabByTag(MapFragment.MAP_FRAGMENT_TAG);
mapFragmet.showMarker(trackId, markerId);
} else {
Log.e(TAG, "MapFragment is null");
}
}
}
/**
* Sets the title.
*
* @param isRecording true if recording
*/
private void setTitle(boolean isRecording) {
String title;
if (isRecording) {
title = getString(R.string.track_detail_title_recording);
} else {
Track track = MyTracksProviderUtils.Factory.get(this).getTrack(trackId);
title = track != null ? track.getName() : getString(R.string.my_tracks_app_name);
}
setTitle(title);
}
/**
* Updates the menu.
*/
private void updateMenu() {
updateMenuItems(trackId == PreferencesUtils.getLong(this, R.string.recording_track_id_key));
}
/**
* Updates the menu items.
*
* @param isRecording true if recording
*/
private void updateMenuItems(boolean isRecording) {
if (stopRecordingMenuItem != null) {
stopRecordingMenuItem.setVisible(isRecording);
}
if (insertMarkerMenuItem != null) {
insertMarkerMenuItem.setVisible(isRecording);
}
if (playMenuItem != null) {
playMenuItem.setVisible(!isRecording);
}
if (shareMenuItem != null) {
shareMenuItem.setVisible(!isRecording);
}
if (voiceFrequencyMenuItem != null) {
voiceFrequencyMenuItem.setVisible(isRecording);
}
if (splitFrequencyMenuItem != null) {
splitFrequencyMenuItem.setVisible(isRecording);
}
if (sendGoogleMenuItem != null) {
sendGoogleMenuItem.setVisible(!isRecording);
}
if (saveMenuItem != null) {
saveMenuItem.setVisible(!isRecording);
}
}
/**
* Starts the {@link SaveActivity} to save a track.
*
* @param trackFileFormat the track file format
*/
private void startSaveActivity(TrackFileFormat trackFileFormat) {
AnalyticsUtils.sendPageViews(this, "/action/save");
Intent intent = IntentUtils.newIntent(this, SaveActivity.class)
.putExtra(SaveActivity.EXTRA_TRACK_ID, trackId)
.putExtra(SaveActivity.EXTRA_TRACK_FILE_FORMAT, (Parcelable) trackFileFormat);
startActivity(intent);
}
/**
* Returns true if Google Earth app is installed.
*/
private boolean isEarthInstalled() {
List<ResolveInfo> infos = getPackageManager().queryIntentActivities(
new Intent().setType(SaveActivity.GOOGLE_EARTH_KML_MIME_TYPE),
PackageManager.MATCH_DEFAULT_ONLY);
for (ResolveInfo info : infos) {
if (info.activityInfo != null && info.activityInfo.packageName != null
&& info.activityInfo.packageName.equals(SaveActivity.GOOGLE_EARTH_PACKAGE)) {
return true;
}
}
return false;
}
@Override
public TrackRecordingServiceConnection getTrackRecordingServiceConnection() {
return trackRecordingServiceConnection;
}
}
@@ -0,0 +1,155 @@
/*
* 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;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.util.TrackIconUtils;
import com.google.android.apps.mytracks.util.TrackNameUtils;
import com.google.android.maps.mytracks.R;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.AutoCompleteTextView;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
/**
* An activity that let's the user see and edit the user editable track meta
* data such as track name, activity type, and track description.
*
* @author Leif Hendrik Wilden
*/
public class TrackEditActivity extends AbstractMyTracksActivity {
public static final String EXTRA_TRACK_ID = "track_id";
public static final String EXTRA_NEW_TRACK = "new_track";
private static final String TAG = TrackEditActivity.class.getSimpleName();
private Long trackId;
private MyTracksProviderUtils myTracksProviderUtils;
private Track track;
private EditText name;
private TextView activityTypeLabel;
private AutoCompleteTextView activityType;
private EditText description;
@Override
protected void onCreate(Bundle bundle) {
super.onCreate(bundle);
setContentView(R.layout.track_edit);
trackId = getIntent().getLongExtra(EXTRA_TRACK_ID, -1L);
if (trackId == -1L) {
Log.e(TAG, "invalid trackId");
finish();
return;
}
myTracksProviderUtils = MyTracksProviderUtils.Factory.get(this);
track = myTracksProviderUtils.getTrack(trackId);
if (track == null) {
Log.e(TAG, "No track for " + trackId);
finish();
return;
}
name = (EditText) findViewById(R.id.track_edit_name);
name.setText(track.getName());
activityType = (AutoCompleteTextView) findViewById(R.id.track_edit_activity_type);
activityType.setText(track.getCategory());
activityTypeLabel = (TextView) findViewById(R.id.track_edit_activity_type_label);
setActivityTypeIcon(track.getIcon());
ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(
this, R.array.activity_types, android.R.layout.simple_dropdown_item_1line);
activityType.setAdapter(adapter);
activityType.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
String iconValue = TrackIconUtils.getIconValue(
TrackEditActivity.this, (String) activityType.getAdapter().getItem(position));
setActivityTypeIcon(iconValue);
}
});
activityType.setOnFocusChangeListener(new View.OnFocusChangeListener() {
@Override
public void onFocusChange(View v, boolean hasFocus) {
if (!hasFocus) {
String iconValue = TrackIconUtils.getIconValue(
TrackEditActivity.this, activityType.getText().toString());
setActivityTypeIcon(iconValue);
}
}
});
description = (EditText) findViewById(R.id.track_edit_description);
description.setText(track.getDescription());
Button save = (Button) findViewById(R.id.track_edit_save);
save.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
track.setName(name.getText().toString());
String category = activityType.getText().toString();
track.setCategory(category);
track.setIcon(TrackIconUtils.getIconValue(TrackEditActivity.this, category));
track.setDescription(description.getText().toString());
myTracksProviderUtils.updateTrack(track);
finish();
}
});
Button cancel = (Button) findViewById(R.id.track_edit_cancel);
if (getIntent().getBooleanExtra(EXTRA_NEW_TRACK, false)) {
String trackName = TrackNameUtils.getTrackName(
this, -1L, -1L, myTracksProviderUtils.getLastLocation());
if (trackName != null) {
name.setText(trackName);
}
setTitle(R.string.track_edit_new_track_title);
cancel.setVisibility(View.GONE);
} else {
setTitle(R.string.menu_edit);
cancel.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
finish();
}
});
cancel.setVisibility(View.VISIBLE);
}
}
/**
* Sets the activity type icon.
*
* @param iconValue the icon value
*/
private void setActivityTypeIcon(String iconValue) {
activityTypeLabel.setCompoundDrawablesWithIntrinsicBounds(
TrackIconUtils.getIconDrawable(iconValue), 0, 0, 0);
}
}
@@ -0,0 +1,524 @@
/*
* 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;
import com.google.android.apps.mytracks.content.TracksColumns;
import com.google.android.apps.mytracks.fragments.CheckUnitsDialogFragment;
import com.google.android.apps.mytracks.fragments.DeleteAllTrackDialogFragment;
import com.google.android.apps.mytracks.fragments.DeleteOneTrackDialogFragment;
import com.google.android.apps.mytracks.fragments.DeleteOneTrackDialogFragment.DeleteOneTrackCaller;
import com.google.android.apps.mytracks.fragments.EulaDialogFragment;
import com.google.android.apps.mytracks.fragments.WelcomeDialogFragment;
import com.google.android.apps.mytracks.io.file.SaveActivity;
import com.google.android.apps.mytracks.io.file.TrackWriterFactory.TrackFileFormat;
import com.google.android.apps.mytracks.services.ITrackRecordingService;
import com.google.android.apps.mytracks.services.TrackRecordingServiceConnection;
import com.google.android.apps.mytracks.settings.SettingsActivity;
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.IntentUtils;
import com.google.android.apps.mytracks.util.ListItemUtils;
import com.google.android.apps.mytracks.util.PreferencesUtils;
import com.google.android.apps.mytracks.util.StringUtils;
import com.google.android.apps.mytracks.util.TrackIconUtils;
import com.google.android.apps.mytracks.util.TrackRecordingServiceConnectionUtils;
import com.google.android.maps.mytracks.R;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.database.Cursor;
import android.os.Bundle;
import android.os.Parcelable;
import android.speech.tts.TextToSpeech;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentActivity;
import android.support.v4.app.LoaderManager.LoaderCallbacks;
import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader;
import android.support.v4.widget.ResourceCursorAdapter;
import android.util.Log;
import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.AdapterView;
import android.widget.AdapterView.AdapterContextMenuInfo;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ImageButton;
import android.widget.ListView;
import android.widget.Toast;
/**
* An activity displaying a list of tracks.
*
* @author Leif Hendrik Wilden
*/
public class TrackListActivity extends FragmentActivity implements DeleteOneTrackCaller {
private static final String TAG = TrackListActivity.class.getSimpleName();
private static final String[] PROJECTION = new String[] {
TracksColumns._ID,
TracksColumns.NAME,
TracksColumns.DESCRIPTION,
TracksColumns.CATEGORY,
TracksColumns.STARTTIME,
TracksColumns.TOTALDISTANCE,
TracksColumns.TOTALTIME,
TracksColumns.ICON};
// Callback when the trackRecordingServiceConnection binding changes.
private final Runnable bindChangedCallback = new Runnable() {
@Override
public void run() {
if (!startNewRecording) {
return;
}
ITrackRecordingService service = trackRecordingServiceConnection.getServiceIfBound();
if (service == null) {
Log.d(TAG, "service not available to start a new recording");
return;
}
try {
recordingTrackId = service.startNewTrack();
startNewRecording = false;
Intent intent = IntentUtils.newIntent(TrackListActivity.this, TrackDetailActivity.class)
.putExtra(TrackDetailActivity.EXTRA_TRACK_ID, recordingTrackId);
startActivity(intent);
Toast.makeText(
TrackListActivity.this, R.string.track_list_record_success, Toast.LENGTH_SHORT).show();
} catch (Exception e) {
Toast.makeText(TrackListActivity.this, R.string.track_list_record_error, Toast.LENGTH_LONG)
.show();
Log.e(TAG, "Unable to start a new recording.", e);
}
}
};
/*
* Note that sharedPreferenceChangeListenr cannot be an anonymous inner class.
* Anonymous inner class will get garbage collected.
*/
private final OnSharedPreferenceChangeListener
sharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() {
@Override
public void onSharedPreferenceChanged(SharedPreferences preferences, String key) {
boolean updateList = false;
// Note that key can be null
if (PreferencesUtils.getKey(TrackListActivity.this, R.string.metric_units_key)
.equals(key)) {
metricUnits = PreferencesUtils.getBoolean(TrackListActivity.this,
R.string.metric_units_key, PreferencesUtils.METRIC_UNITS_DEFAULT);
updateList = true;
}
if (PreferencesUtils.getKey(TrackListActivity.this, R.string.recording_track_id_key)
.equals(key)) {
recordingTrackId = PreferencesUtils.getLong(
TrackListActivity.this, R.string.recording_track_id_key);
if (TrackRecordingServiceConnectionUtils.isRecording(
TrackListActivity.this, trackRecordingServiceConnection)) {
trackRecordingServiceConnection.startAndBind();
}
updateMenu();
updateList = true;
}
if (updateList) {
resourceCursorAdapter.notifyDataSetChanged();
}
}
};
// Callback when an item is selected in the contextual action mode
private ContextualActionModeCallback contextualActionModeCallback =
new ContextualActionModeCallback() {
@Override
public boolean onClick(int itemId, int position, long id) {
return handleContextItem(itemId, id);
}
};
private TrackRecordingServiceConnection trackRecordingServiceConnection;
private boolean metricUnits;
private long recordingTrackId;
private ListView listView;
private ResourceCursorAdapter resourceCursorAdapter;
// True to start a new recording.
private boolean startNewRecording = false;
private MenuItem recordTrackMenuItem;
private MenuItem stopRecordingMenuItem;
private MenuItem searchMenuItem;
private MenuItem importMenuItem;
private MenuItem saveAllMenuItem;
private MenuItem deleteAllMenuItem;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setVolumeControlStream(TextToSpeech.Engine.DEFAULT_STREAM);
setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL);
setContentView(R.layout.track_list);
trackRecordingServiceConnection = new TrackRecordingServiceConnection(
this, bindChangedCallback);
getSharedPreferences(Constants.SETTINGS_NAME, Context.MODE_PRIVATE)
.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
metricUnits = PreferencesUtils.getBoolean(
this, R.string.metric_units_key, PreferencesUtils.METRIC_UNITS_DEFAULT);
recordingTrackId = PreferencesUtils.getLong(this, R.string.recording_track_id_key);
ImageButton recordImageButton = (ImageButton) findViewById(R.id.track_list_record_button);
recordImageButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
updateMenuItems(true);
startRecording();
}
});
listView = (ListView) findViewById(R.id.track_list);
listView.setEmptyView(findViewById(R.id.track_list_empty));
listView.setOnItemClickListener(new OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Intent intent = IntentUtils.newIntent(TrackListActivity.this, TrackDetailActivity.class)
.putExtra(TrackDetailActivity.EXTRA_TRACK_ID, id);
startActivity(intent);
}
});
resourceCursorAdapter = new ResourceCursorAdapter(this, R.layout.list_item, null, 0) {
@Override
public void bindView(View view, Context context, Cursor cursor) {
int idIndex = cursor.getColumnIndex(TracksColumns._ID);
int nameIndex = cursor.getColumnIndex(TracksColumns.NAME);
int descriptionIndex = cursor.getColumnIndex(TracksColumns.DESCRIPTION);
int categoryIndex = cursor.getColumnIndex(TracksColumns.CATEGORY);
int startTimeIndex = cursor.getColumnIndexOrThrow(TracksColumns.STARTTIME);
int totalDistanceIndex = cursor.getColumnIndexOrThrow(TracksColumns.TOTALDISTANCE);
int totalTimeIndex = cursor.getColumnIndexOrThrow(TracksColumns.TOTALTIME);
int iconIndex = cursor.getColumnIndex(TracksColumns.ICON);
boolean isRecording = cursor.getLong(idIndex) == recordingTrackId;
String name = cursor.getString(nameIndex);
int iconId = isRecording ? R.drawable.menu_record_track
: TrackIconUtils.getIconDrawable(cursor.getString(iconIndex));
String iconContentDescription = getString(isRecording ? R.string.icon_recording
: R.string.icon_track);
String category = cursor.getString(categoryIndex);
String totalTime = isRecording
? null : StringUtils.formatElapsedTime(cursor.getLong(totalTimeIndex));
String totalDistance = isRecording ? null : StringUtils.formatDistance(
TrackListActivity.this, cursor.getDouble(totalDistanceIndex), metricUnits);
long startTime = cursor.getLong(startTimeIndex);
String description = cursor.getString(descriptionIndex);
ListItemUtils.setListItem(TrackListActivity.this,
view,
name,
iconId,
iconContentDescription,
category,
totalTime,
totalDistance,
startTime,
description);
}
};
listView.setAdapter(resourceCursorAdapter);
ApiAdapterFactory.getApiAdapter()
.configureListViewContextualMenu(this, listView, contextualActionModeCallback);
getSupportLoaderManager().initLoader(0, null, new LoaderCallbacks<Cursor>() {
@Override
public Loader<Cursor> onCreateLoader(int arg0, Bundle arg1) {
return new CursorLoader(TrackListActivity.this,
TracksColumns.CONTENT_URI,
PROJECTION,
null,
null,
TracksColumns._ID + " DESC");
}
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
resourceCursorAdapter.swapCursor(cursor);
}
@Override
public void onLoaderReset(Loader<Cursor> loader) {
resourceCursorAdapter.swapCursor(null);
}
});
showStartupDialogs();
}
/**
* Shows start up dialogs.
*/
public void showStartupDialogs() {
if (!EulaUtils.getAcceptEula(this)) {
Fragment fragment = getSupportFragmentManager()
.findFragmentByTag(EulaDialogFragment.EULA_DIALOG_TAG);
if (fragment == null) {
EulaDialogFragment.newInstance(false)
.show(getSupportFragmentManager(), EulaDialogFragment.EULA_DIALOG_TAG);
}
} else if (EulaUtils.getShowWelcome(this)) {
Fragment fragment = getSupportFragmentManager()
.findFragmentByTag(WelcomeDialogFragment.WELCOME_DIALOG_TAG);
if (fragment == null) {
new WelcomeDialogFragment().show(
getSupportFragmentManager(), WelcomeDialogFragment.WELCOME_DIALOG_TAG);
}
} else if (EulaUtils.getShowCheckUnits(this)) {
Fragment fragment = getSupportFragmentManager()
.findFragmentByTag(CheckUnitsDialogFragment.CHECK_UNITS_DIALOG_TAG);
if (fragment == null) {
new CheckUnitsDialogFragment().show(
getSupportFragmentManager(), CheckUnitsDialogFragment.CHECK_UNITS_DIALOG_TAG);
}
} else {
enableEmptyView();
}
}
/**
* Enables the content of the empty view.
*/
private void enableEmptyView() {
View emptyMessage = findViewById(R.id.track_list_empty_message);
emptyMessage.setVisibility(View.VISIBLE);
View recordButton = findViewById(R.id.track_list_record_button);
recordButton.setVisibility(View.VISIBLE);
}
@Override
protected void onResume() {
super.onResume();
TrackRecordingServiceConnectionUtils.resume(this, trackRecordingServiceConnection);
}
@Override
protected void onDestroy() {
super.onDestroy();
trackRecordingServiceConnection.unbind();
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.track_list, menu);
String fileTypes[] = getResources().getStringArray(R.array.file_types);
menu.findItem(R.id.track_list_save_all_gpx)
.setTitle(getString(R.string.menu_save_format, fileTypes[0]));
menu.findItem(R.id.track_list_save_all_kml)
.setTitle(getString(R.string.menu_save_format, fileTypes[1]));
menu.findItem(R.id.track_list_save_all_csv)
.setTitle(getString(R.string.menu_save_format, fileTypes[2]));
menu.findItem(R.id.track_list_save_all_tcx)
.setTitle(getString(R.string.menu_save_format, fileTypes[3]));
recordTrackMenuItem = menu.findItem(R.id.track_list_record_track);
stopRecordingMenuItem = menu.findItem(R.id.track_list_stop_recording);
searchMenuItem = menu.findItem(R.id.track_list_search);
importMenuItem = menu.findItem(R.id.track_list_import);
saveAllMenuItem = menu.findItem(R.id.track_list_save_all);
deleteAllMenuItem = menu.findItem(R.id.track_list_delete_all);
ApiAdapterFactory.getApiAdapter().configureSearchWidget(this, searchMenuItem);
updateMenu();
return true;
}
/**
* Updates the menu based on whether My Tracks is recording or not.
*/
private void updateMenu() {
updateMenuItems(
TrackRecordingServiceConnectionUtils.isRecording(this, trackRecordingServiceConnection));
}
/**
* Updates the menu items.
*
* @param isRecording true if recording
*/
private void updateMenuItems(boolean isRecording) {
if (recordTrackMenuItem != null) {
recordTrackMenuItem.setVisible(!isRecording);
}
if (stopRecordingMenuItem != null) {
stopRecordingMenuItem.setVisible(isRecording);
}
if (importMenuItem != null) {
importMenuItem.setVisible(!isRecording);
}
if (saveAllMenuItem != null) {
saveAllMenuItem.setVisible(!isRecording);
}
if (deleteAllMenuItem != null) {
deleteAllMenuItem.setVisible(!isRecording);
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
Intent intent;
switch (item.getItemId()) {
case R.id.track_list_record_track:
AnalyticsUtils.sendPageViews(this, "/action/record_track");
updateMenuItems(true);
startRecording();
return true;
case R.id.track_list_stop_recording:
updateMenuItems(false);
TrackRecordingServiceConnectionUtils.stop(this, trackRecordingServiceConnection, true);
return true;
case R.id.track_list_search:
return ApiAdapterFactory.getApiAdapter().handleSearchMenuSelection(this);
case R.id.track_list_import:
AnalyticsUtils.sendPageViews(this, "/action/import");
intent = IntentUtils.newIntent(this, ImportActivity.class)
.putExtra(ImportActivity.EXTRA_IMPORT_ALL, true);
startActivity(intent);
return true;
case R.id.track_list_save_all_gpx:
startSaveActivity(TrackFileFormat.GPX);
return true;
case R.id.track_list_save_all_kml:
startSaveActivity(TrackFileFormat.KML);
return true;
case R.id.track_list_save_all_csv:
startSaveActivity(TrackFileFormat.CSV);
return true;
case R.id.track_list_save_all_tcx:
startSaveActivity(TrackFileFormat.TCX);
return true;
case R.id.track_list_delete_all:
new DeleteAllTrackDialogFragment().show(
getSupportFragmentManager(), DeleteAllTrackDialogFragment.DELETE_ALL_TRACK_DIALOG_TAG);
return true;
case R.id.track_list_aggregated_statistics:
intent = IntentUtils.newIntent(this, AggregatedStatsActivity.class);
startActivity(intent);
return true;
case R.id.track_list_settings:
intent = IntentUtils.newIntent(this, SettingsActivity.class);
startActivity(intent);
return true;
case R.id.track_list_help:
intent = IntentUtils.newIntent(this, HelpActivity.class);
startActivity(intent);
return true;
default:
return super.onOptionsItemSelected(item);
}
}
/**
* Starts the {@link SaveActivity} to save all tracks.
*
* @param trackFileFormat the track file format
*/
private void startSaveActivity(TrackFileFormat trackFileFormat) {
AnalyticsUtils.sendPageViews(this, "/action/save_all");
Intent intent = IntentUtils.newIntent(this, SaveActivity.class)
.putExtra(SaveActivity.EXTRA_TRACK_FILE_FORMAT, (Parcelable) trackFileFormat);
startActivity(intent);
}
@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
super.onCreateContextMenu(menu, v, menuInfo);
getMenuInflater().inflate(R.menu.list_context_menu, menu);
}
@Override
public boolean onContextItemSelected(MenuItem item) {
if (handleContextItem(item.getItemId(), ((AdapterContextMenuInfo) item.getMenuInfo()).id)) {
return true;
}
return super.onContextItemSelected(item);
}
/**
* Handles a context item selection.
*
* @param itemId the menu item id
* @param trackId the track id
* @return true if handled.
*/
private boolean handleContextItem(int itemId, long trackId) {
Intent intent;
switch (itemId) {
case R.id.list_context_menu_show_on_map:
intent = IntentUtils.newIntent(this, TrackDetailActivity.class)
.putExtra(TrackDetailActivity.EXTRA_TRACK_ID, trackId);
startActivity(intent);
return true;
case R.id.list_context_menu_edit:
intent = IntentUtils.newIntent(this, TrackEditActivity.class)
.putExtra(TrackEditActivity.EXTRA_TRACK_ID, trackId);
startActivity(intent);
return true;
case R.id.list_context_menu_delete:
DeleteOneTrackDialogFragment.newInstance(trackId).show(
getSupportFragmentManager(), DeleteOneTrackDialogFragment.DELETE_ONE_TRACK_DIALOG_TAG);
return true;
default:
return false;
}
}
/**
* Starts a new recording.
*/
private void startRecording() {
startNewRecording = true;
trackRecordingServiceConnection.startAndBind();
/*
* If the binding has happened, then invoke the callback to start a new
* recording. If the binding hasn't happened, then invoking the callback
* will have no effect. But when the binding occurs, the callback will get
* invoked.
*/
bindChangedCallback.run();
}
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_SEARCH) {
if (ApiAdapterFactory.getApiAdapter().handleSearchKey(searchMenuItem)) {
return true;
}
}
return super.onKeyUp(keyCode, event);
}
@Override
public TrackRecordingServiceConnection getTrackRecordingServiceConnection() {
return trackRecordingServiceConnection;
}
}
@@ -0,0 +1,192 @@
/*
* 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.content;
import static com.google.android.apps.mytracks.Constants.MAX_LOCATION_AGE_MS;
import com.google.android.apps.mytracks.Constants;
import com.google.android.maps.mytracks.R;
import android.content.ContentResolver;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.database.ContentObserver;
import android.hardware.Sensor;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.net.Uri;
import android.util.Log;
import android.widget.Toast;
/**
* Data source on the phone.
*
* @author Rodrigo Damazio
*/
public class DataSource {
private static final int NETWORK_PROVIDER_MIN_TIME = 5 * 60 * 1000; // 5 minutes
private static final String TAG = DataSource.class.getSimpleName();
private final Context context;
private final ContentResolver contentResolver;
private final LocationManager locationManager;
private final SensorManager sensorManager;
private final SharedPreferences sharedPreferences;
public DataSource(Context context) {
this.context = context;
contentResolver = context.getContentResolver();
locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
sensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
sharedPreferences = context.getSharedPreferences(Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
}
/**
* Registers a content observer.
*
* @param uri the uri
* @param observer the observer
*/
public void registerContentObserver(Uri uri, ContentObserver observer) {
contentResolver.registerContentObserver(uri, false, observer);
}
/**
* Unregisters a content observer.
*
* @param observer the observer
*/
public void unregisterContentObserver(ContentObserver observer) {
contentResolver.unregisterContentObserver(observer);
}
/**
* Registers a location listener.
*
* @param listener the listener
*/
public void registerLocationListener(LocationListener listener) {
// Check if the GPS provider exists
if (locationManager.getProvider(LocationManager.GPS_PROVIDER) == null) {
listener.onProviderDisabled(LocationManager.GPS_PROVIDER);
unregisterLocationListener(listener);
return;
}
// Listen for GPS location
locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0, listener);
// Update the listener with the current provider state
if (locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) {
listener.onProviderEnabled(LocationManager.GPS_PROVIDER);
} else {
listener.onProviderDisabled(LocationManager.GPS_PROVIDER);
}
// Listen for network location
try {
locationManager.requestLocationUpdates(
LocationManager.NETWORK_PROVIDER, NETWORK_PROVIDER_MIN_TIME, 0, listener);
} catch (RuntimeException e) {
// Network location is optional, so just log the exception
Log.w(TAG, "Could not register for network location.", e);
}
}
/**
* Unregisters a location listener.
*
* @param listener the listener
*/
public void unregisterLocationListener(LocationListener listener) {
locationManager.removeUpdates(listener);
}
/**
* Gets the last known location.
*/
public Location getLastKnownLocation() {
Location location = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER);
if (!isLocationRecent(location)) {
// Try network location
location = locationManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER);
int id = isLocationRecent(location)
? R.string.my_location_approximate_location : R.string.my_location_no_location;
Toast.makeText(context, id, Toast.LENGTH_LONG).show();
}
return location;
}
/**
* Returns true if the location is recent.
*
* @param location the location
*/
private boolean isLocationRecent(Location location) {
if (location == null) {
return false;
}
return location.getTime() > System.currentTimeMillis() - MAX_LOCATION_AGE_MS;
}
/**
* Registers a heading listener.
*
* @param listener the listener
*/
public void registerHeadingListener(SensorEventListener listener) {
Sensor heading = sensorManager.getDefaultSensor(Sensor.TYPE_ORIENTATION);
if (heading == null) {
Log.d(TAG, "No heading sensor.");
return;
}
sensorManager.registerListener(listener, heading, SensorManager.SENSOR_DELAY_UI);
}
/**
* Unregisters a heading listener.
*
* @param listener the listener
*/
public void unregisterHeadingListener(SensorEventListener listener) {
sensorManager.unregisterListener(listener);
}
/**
* Registers a shared preference change listener.
*
* @param listener the listener
*/
public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
sharedPreferences.registerOnSharedPreferenceChangeListener(listener);
}
/**
* Unregisters a shared preference change listener.
*
* @param listener the listener
*/
public void unregisterOnSharedPreferenceChangeListener(
OnSharedPreferenceChangeListener listener) {
sharedPreferences.unregisterOnSharedPreferenceChangeListener(listener);
}
}
@@ -0,0 +1,78 @@
/*
* 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.content;
import android.location.Location;
/**
* Listener to be invoked when {@link DataSource} changes.
*
* @author Jimmy Shih
*/
public interface DataSourceListener {
/**
* Notifies when the tracks table is updated.
*/
public void notifyTracksTableUpdated();
/**
* Notifies when the waypoints table is updated.
*/
public void notifyWaypointsTableUpdated();
/**
* Notifies when the track points table is updated.
*/
public void notifyTrackPointsTableUpdated();
/**
* Notifies when the current location changes.
*
* @param location the location
*/
public void notifyLocationChanged(Location location);
/**
* Notifies when the location provider enabled status changes.
*
* @param enabled true if enabled, false if not
*/
public void notifyLocationProviderEnabled(boolean enabled);
/**
* Notifies when the location provider availability status changes.
*
* @param available true if available, false if not
*/
public void notifyLocationProviderAvailable(boolean available);
/**
* Notifies when the compass heading changes.
*
* @param heading the new heading
*/
public void notifyHeadingChanged(float heading);
/**
* Notifies when a preference changes.
*
* @param key the preference key
*/
public void notifyPreferenceChanged(String key);
}
@@ -0,0 +1,316 @@
/*
* 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.content;
import static com.google.android.apps.mytracks.Constants.TAG;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.database.ContentObserver;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.location.LocationProvider;
import android.os.Bundle;
import android.os.Handler;
import android.util.Log;
import java.util.EnumSet;
import java.util.Set;
/**
* Data source manager. Creates observers/listeners and manages their
* registration with {@link DataSource}. The observers/listeners calls
* {@link DataSourceListener} when data changes.
*
* @author Rodrigo Damazio
*/
public class DataSourceManager {
/**
* Observer when the tracks table is updated.
*
* @author Jimmy Shih
*/
private class TracksTableObserver extends ContentObserver {
public TracksTableObserver() {
super(handler);
}
@Override
public void onChange(boolean selfChange) {
dataSourceListener.notifyTracksTableUpdated();
}
}
/**
* Observer when the waypoints table is updated.
*
* @author Jimmy Shih
*/
private class WaypointsTableObserver extends ContentObserver {
public WaypointsTableObserver() {
super(handler);
}
@Override
public void onChange(boolean selfChange) {
dataSourceListener.notifyWaypointsTableUpdated();
}
}
/**
* Observer when the track points table is updated.
*
* @author Jimmy Shih
*/
private class TrackPointsTableObserver extends ContentObserver {
public TrackPointsTableObserver() {
super(handler);
}
@Override
public void onChange(boolean selfChange) {
dataSourceListener.notifyTrackPointsTableUpdated();
}
}
/**
* Listener for location changes.
*
* @author Jimmy Shih
*/
private class CurrentLocationListener implements LocationListener {
@Override
public void onLocationChanged(Location location) {
dataSourceListener.notifyLocationChanged(location);
}
@Override
public void onProviderDisabled(String provider) {
if (!LocationManager.GPS_PROVIDER.equals(provider)) {
return;
}
dataSourceListener.notifyLocationProviderEnabled(false);
}
@Override
public void onProviderEnabled(String provider) {
if (!LocationManager.GPS_PROVIDER.equals(provider)) {
return;
}
dataSourceListener.notifyLocationProviderEnabled(true);
}
@Override
public void onStatusChanged(String provider, int status, Bundle extras) {
if (!LocationManager.GPS_PROVIDER.equals(provider)) {
return;
}
dataSourceListener.notifyLocationProviderAvailable(status == LocationProvider.AVAILABLE);
}
}
/**
* Listener for heading changes.
*
* @author Jimmy Shih
*/
private class HeadingListener implements SensorEventListener {
@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {
// Do nothing
}
@Override
public void onSensorChanged(SensorEvent event) {
dataSourceListener.notifyHeadingChanged(event.values[0]);
}
}
/**
* Listener for preference changes.
*
* @author Jimmy Shih
*/
private class PreferenceListener implements OnSharedPreferenceChangeListener {
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
dataSourceListener.notifyPreferenceChanged(key);
}
}
private final DataSource dataSource;
private final DataSourceListener dataSourceListener;
// Registered listeners
private final Set<TrackDataType> registeredListeners = EnumSet.noneOf(TrackDataType.class);
private final Handler handler;
private final TracksTableObserver tracksTableObserver;
private final WaypointsTableObserver waypointsTableObserver;
private final TrackPointsTableObserver trackPointsTableObserver;
private final CurrentLocationListener currentLocationListener;
private final HeadingListener headingListener;
private final PreferenceListener preferenceListener;
public DataSourceManager(DataSource dataSource, DataSourceListener dataSourceListener) {
this.dataSource = dataSource;
this.dataSourceListener = dataSourceListener;
handler = new Handler();
tracksTableObserver = new TracksTableObserver();
waypointsTableObserver = new WaypointsTableObserver();
trackPointsTableObserver = new TrackPointsTableObserver();
currentLocationListener = new CurrentLocationListener();
headingListener = new HeadingListener();
preferenceListener = new PreferenceListener();
}
/**
* Updates listeners with data source.
*
* @param listeners the listeners
*/
public void updateListeners(EnumSet<TrackDataType> listeners) {
EnumSet<TrackDataType> neededListeners = EnumSet.copyOf(listeners);
/*
* Map SAMPLED_OUT_POINT_UPDATES to POINT_UPDATES since they correspond to
* the same internal listener
*/
if (neededListeners.contains(TrackDataType.SAMPLED_OUT_TRACK_POINTS_TABLE)) {
neededListeners.remove(TrackDataType.SAMPLED_OUT_TRACK_POINTS_TABLE);
neededListeners.add(TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE);
}
Log.d(TAG, "Updating listeners " + neededListeners);
// Unnecessary = registered - needed
Set<TrackDataType> unnecessaryListeners = EnumSet.copyOf(registeredListeners);
unnecessaryListeners.removeAll(neededListeners);
// Missing = needed - registered
Set<TrackDataType> missingListeners = EnumSet.copyOf(neededListeners);
missingListeners.removeAll(registeredListeners);
// Remove unnecessary listeners
for (TrackDataType trackDataType : unnecessaryListeners) {
unregisterListener(trackDataType);
}
// Add missing listeners
for (TrackDataType trackDataType : missingListeners) {
registerListener(trackDataType);
}
// Update registered listeners
registeredListeners.clear();
registeredListeners.addAll(neededListeners);
}
/**
* Registers a listener with data source.
*
* @param trackDataType the listener data type
*/
private void registerListener(TrackDataType trackDataType) {
switch (trackDataType) {
case SELECTED_TRACK:
// Do nothing
break;
case TRACKS_TABLE:
dataSource.registerContentObserver(TracksColumns.CONTENT_URI, tracksTableObserver);
break;
case WAYPOINTS_TABLE:
dataSource.registerContentObserver(WaypointsColumns.CONTENT_URI, waypointsTableObserver);
break;
case SAMPLED_IN_TRACK_POINTS_TABLE:
dataSource.registerContentObserver(
TrackPointsColumns.CONTENT_URI, trackPointsTableObserver);
break;
case SAMPLED_OUT_TRACK_POINTS_TABLE:
// Do nothing. SAMPLED_OUT_POINT_UPDATES is mapped to POINT_UPDATES.
break;
case LOCATION:
dataSource.registerLocationListener(currentLocationListener);
break;
case HEADING:
dataSource.registerHeadingListener(headingListener);
break;
case PREFERENCE:
dataSource.registerOnSharedPreferenceChangeListener(preferenceListener);
break;
default:
break;
}
}
/**
* Unregisters a listener with data source.
*
* @param trackDataType listener data type
*/
private void unregisterListener(TrackDataType trackDataType) {
switch (trackDataType) {
case SELECTED_TRACK:
// Do nothing
break;
case TRACKS_TABLE:
dataSource.unregisterContentObserver(tracksTableObserver);
break;
case WAYPOINTS_TABLE:
dataSource.unregisterContentObserver(waypointsTableObserver);
break;
case SAMPLED_IN_TRACK_POINTS_TABLE:
dataSource.unregisterContentObserver(trackPointsTableObserver);
break;
case SAMPLED_OUT_TRACK_POINTS_TABLE:
// Do nothing. SAMPLED_OUT_POINT_UPDATES is mapped to POINT_UPDATES.
break;
case LOCATION:
dataSource.unregisterLocationListener(currentLocationListener);
break;
case HEADING:
dataSource.unregisterHeadingListener(headingListener);
break;
case PREFERENCE:
dataSource.unregisterOnSharedPreferenceChangeListener(preferenceListener);
break;
default:
break;
}
}
/**
* Unregisters all listeners with data source.
*/
public void unregisterAllListeners() {
for (TrackDataType trackDataType : TrackDataType.values()) {
unregisterListener(trackDataType);
}
}
}
@@ -0,0 +1,275 @@
/*
* 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 HTML_PARAGRAPH_SEPARATOR = "<p>";
private static final String TEXT_LINE_BREAK = "\n";
private static final String TEXT_PARAGRAPH_SEPARATOR = "\n\n";
private Context context;
public DescriptionGeneratorImpl(Context context) {
this.context = context;
}
@Override
public String generateTrackDescription(
Track track, Vector<Double> distances, Vector<Double> elevations, boolean html) {
String paragraphSeparator = html ? HTML_PARAGRAPH_SEPARATOR : TEXT_PARAGRAPH_SEPARATOR;
String lineBreak = html ? HTML_LINE_BREAK : TEXT_LINE_BREAK;
StringBuilder builder = new StringBuilder();
// Created by
String beginAnchor = html
? "<a href='http://" + context.getString(R.string.my_tracks_web_url) + "'>"
: "";
String endAnchor = html ? "</a>" : "";
builder.append(context.getString(R.string.send_google_by_my_tracks, beginAnchor, endAnchor));
builder.append(paragraphSeparator);
writeString(track.getName(), builder, R.string.generic_name_line, lineBreak);
writeString(track.getCategory(), builder, R.string.description_activity_type, lineBreak);
writeString(track.getDescription(), builder, R.string.generic_description_line, lineBreak);
builder.append(generateTripStatisticsDescription(track.getTripStatistics(), html));
// Elevation chart
if (html && 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.getTripStatistics(), false);
}
/**
* Writes a string to a string builder.
*
* @param text the string
* @param builder the string builder
* @param resId the resource id containing one string placeholder
* @param lineBreak the line break
*/
private void writeString(String text, StringBuilder builder, int resId, String lineBreak) {
if (text == null || text.length() == 0) {
text = context.getString(R.string.value_unknown);
}
builder.append(context.getString(resId, text));
builder.append(lineBreak);
}
/**
* 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);
// Fastest pace
writePace(maxSpeed, builder, R.string.description_fastest_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
}
}
@@ -0,0 +1,470 @@
/*
* 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.content;
import com.google.android.apps.mytracks.util.PreferencesUtils;
import com.google.android.maps.mytracks.R;
import com.google.common.annotations.VisibleForTesting;
import android.content.ContentProvider;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.database.sqlite.SQLiteOpenHelper;
import android.database.sqlite.SQLiteQueryBuilder;
import android.net.Uri;
import android.os.Binder;
import android.os.Process;
import android.text.TextUtils;
import android.util.Log;
/**
* A {@link ContentProvider} that handles access to track points, tracks, and
* waypoints tables.
*
* @author Leif Hendrik Wilden
*/
public class MyTracksProvider extends ContentProvider {
private static final String TAG = MyTracksProvider.class.getSimpleName();
@VisibleForTesting
static final String DATABASE_NAME = "mytracks.db";
private static final int DATABASE_VERSION = 20;
/**
* Database helper for creating and upgrading the database.
*/
@VisibleForTesting
static class DatabaseHelper extends SQLiteOpenHelper {
public DatabaseHelper(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(TrackPointsColumns.CREATE_TABLE);
db.execSQL(TracksColumns.CREATE_TABLE);
db.execSQL(WaypointsColumns.CREATE_TABLE);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
Log.w(TAG, "Upgrading database from version " + oldVersion + " to " + newVersion);
if (oldVersion < 17) {
Log.w(TAG, "Deleting all old data.");
db.execSQL("DROP TABLE IF EXISTS " + TrackPointsColumns.TABLE_NAME);
db.execSQL("DROP TABLE IF EXISTS " + TracksColumns.TABLE_NAME);
db.execSQL("DROP TABLE IF EXISTS " + WaypointsColumns.TABLE_NAME);
onCreate(db);
} else {
// Incremental upgrades. One if statement per DB version.
// Add track points SENSOR column
if (oldVersion <= 17) {
Log.w(TAG, "Upgrade DB: Adding sensor column.");
db.execSQL("ALTER TABLE " + TrackPointsColumns.TABLE_NAME + " ADD "
+ TrackPointsColumns.SENSOR + " BLOB");
}
// Add tracks TABLEID column
if (oldVersion <= 18) {
Log.w(TAG, "Upgrade DB: Adding tableid column.");
db.execSQL("ALTER TABLE " + TracksColumns.TABLE_NAME + " ADD " + TracksColumns.TABLEID
+ " STRING");
}
// Add tracks ICON column
if (oldVersion <= 19) {
Log.w(TAG, "Upgrade DB: Adding icon column.");
db.execSQL(
"ALTER TABLE " + TracksColumns.TABLE_NAME + " ADD " + TracksColumns.ICON + " STRING");
}
}
}
}
/**
* Types of url.
*
* @author Jimmy Shih
*/
private enum UrlType {
TRACKPOINTS, TRACKPOINTS_ID, TRACKS, TRACKS_ID, WAYPOINTS, WAYPOINTS_ID
}
private final UriMatcher uriMatcher;
private SQLiteDatabase db;
public MyTracksProvider() {
uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
uriMatcher.addURI(MyTracksProviderUtils.AUTHORITY, TrackPointsColumns.TABLE_NAME,
UrlType.TRACKPOINTS.ordinal());
uriMatcher.addURI(MyTracksProviderUtils.AUTHORITY, TrackPointsColumns.TABLE_NAME + "/#",
UrlType.TRACKPOINTS_ID.ordinal());
uriMatcher.addURI(
MyTracksProviderUtils.AUTHORITY, TracksColumns.TABLE_NAME, UrlType.TRACKS.ordinal());
uriMatcher.addURI(MyTracksProviderUtils.AUTHORITY, TracksColumns.TABLE_NAME + "/#",
UrlType.TRACKS_ID.ordinal());
uriMatcher.addURI(
MyTracksProviderUtils.AUTHORITY, WaypointsColumns.TABLE_NAME, UrlType.WAYPOINTS.ordinal());
uriMatcher.addURI(MyTracksProviderUtils.AUTHORITY, WaypointsColumns.TABLE_NAME + "/#",
UrlType.WAYPOINTS_ID.ordinal());
}
@Override
public boolean onCreate() {
return onCreate(getContext());
}
/**
* Helper method to make onCreate is testable.
* @param context context to creates database
* @return true means run successfully
*/
public boolean onCreate(Context context) {
if (!canAccess()) {
return false;
}
DatabaseHelper databaseHelper = new DatabaseHelper(context);
try {
db = databaseHelper.getWritableDatabase();
} catch (SQLiteException e) {
Log.e(TAG, "Unable to open database for writing.", e);
}
return db != null;
}
@Override
public int delete(Uri url, String where, String[] selectionArgs) {
if (!canAccess()) {
return 0;
}
String table;
boolean shouldVacuum = false;
switch (getUrlType(url)) {
case TRACKPOINTS:
table = TrackPointsColumns.TABLE_NAME;
break;
case TRACKS:
table = TracksColumns.TABLE_NAME;
shouldVacuum = true;
break;
case WAYPOINTS:
table = WaypointsColumns.TABLE_NAME;
break;
default:
throw new IllegalArgumentException("Unknown URL " + url);
}
Log.w(MyTracksProvider.TAG, "Deleting table " + table);
int count;
try {
db.beginTransaction();
count = db.delete(table, where, selectionArgs);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
getContext().getContentResolver().notifyChange(url, null, true);
if (shouldVacuum) {
// If a potentially large amount of data was deleted, reclaim its space.
Log.i(TAG, "Vacuuming the database.");
db.execSQL("VACUUM");
}
return count;
}
@Override
public String getType(Uri url) {
if (!canAccess()) {
return null;
}
switch (getUrlType(url)) {
case TRACKPOINTS:
return TrackPointsColumns.CONTENT_TYPE;
case TRACKPOINTS_ID:
return TrackPointsColumns.CONTENT_ITEMTYPE;
case TRACKS:
return TracksColumns.CONTENT_TYPE;
case TRACKS_ID:
return TracksColumns.CONTENT_ITEMTYPE;
case WAYPOINTS:
return WaypointsColumns.CONTENT_TYPE;
case WAYPOINTS_ID:
return WaypointsColumns.CONTENT_ITEMTYPE;
default:
throw new IllegalArgumentException("Unknown URL " + url);
}
}
@Override
public Uri insert(Uri url, ContentValues initialValues) {
if (!canAccess()) {
return null;
}
if (initialValues == null) {
initialValues = new ContentValues();
}
Uri result = null;
try {
db.beginTransaction();
result = insertContentValues(url, getUrlType(url), initialValues);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
return result;
}
@Override
public int bulkInsert(Uri url, ContentValues[] valuesBulk) {
if (!canAccess()) {
return 0;
}
int numInserted = 0;
try {
// Use a transaction in order to make the insertions run as a single batch
db.beginTransaction();
UrlType urlType = getUrlType(url);
for (numInserted = 0; numInserted < valuesBulk.length; numInserted++) {
ContentValues contentValues = valuesBulk[numInserted];
if (contentValues == null) {
contentValues = new ContentValues();
}
insertContentValues(url, urlType, contentValues);
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
return numInserted;
}
@Override
public Cursor query(
Uri url, String[] projection, String selection, String[] selectionArgs, String sort) {
if (!canAccess()) {
return null;
}
SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
String sortOrder = null;
switch (getUrlType(url)) {
case TRACKPOINTS:
queryBuilder.setTables(TrackPointsColumns.TABLE_NAME);
sortOrder = sort != null ? sort : TrackPointsColumns.DEFAULT_SORT_ORDER;
break;
case TRACKPOINTS_ID:
queryBuilder.setTables(TrackPointsColumns.TABLE_NAME);
queryBuilder.appendWhere("_id=" + url.getPathSegments().get(1));
break;
case TRACKS:
queryBuilder.setTables(TracksColumns.TABLE_NAME);
sortOrder = sort != null ? sort : TracksColumns.DEFAULT_SORT_ORDER;
break;
case TRACKS_ID:
queryBuilder.setTables(TracksColumns.TABLE_NAME);
queryBuilder.appendWhere("_id=" + url.getPathSegments().get(1));
break;
case WAYPOINTS:
queryBuilder.setTables(WaypointsColumns.TABLE_NAME);
sortOrder = sort != null ? sort : WaypointsColumns.DEFAULT_SORT_ORDER;
break;
case WAYPOINTS_ID:
queryBuilder.setTables(WaypointsColumns.TABLE_NAME);
queryBuilder.appendWhere("_id=" + url.getPathSegments().get(1));
break;
default:
throw new IllegalArgumentException("Unknown url " + url);
}
Cursor cursor = queryBuilder.query(
db, projection, selection, selectionArgs, null, null, sortOrder);
cursor.setNotificationUri(getContext().getContentResolver(), url);
return cursor;
}
@Override
public int update(Uri url, ContentValues values, String where, String[] selectionArgs) {
if (!canAccess()) {
return 0;
}
String table;
String whereClause;
switch (getUrlType(url)) {
case TRACKPOINTS:
table = TrackPointsColumns.TABLE_NAME;
whereClause = where;
break;
case TRACKPOINTS_ID:
table = TrackPointsColumns.TABLE_NAME;
whereClause = TrackPointsColumns._ID + "=" + url.getPathSegments().get(1);
if (!TextUtils.isEmpty(where)) {
whereClause += " AND (" + where + ")";
}
break;
case TRACKS:
table = TracksColumns.TABLE_NAME;
whereClause = where;
break;
case TRACKS_ID:
table = TracksColumns.TABLE_NAME;
whereClause = TracksColumns._ID + "=" + url.getPathSegments().get(1);
if (!TextUtils.isEmpty(where)) {
whereClause += " AND (" + where + ")";
}
break;
case WAYPOINTS:
table = WaypointsColumns.TABLE_NAME;
whereClause = where;
break;
case WAYPOINTS_ID:
table = WaypointsColumns.TABLE_NAME;
whereClause = WaypointsColumns._ID + "=" + url.getPathSegments().get(1);
if (!TextUtils.isEmpty(where)) {
whereClause += " AND (" + where + ")";
}
break;
default:
throw new IllegalArgumentException("Unknown url " + url);
}
int count;
try {
db.beginTransaction();
count = db.update(table, values, whereClause, selectionArgs);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
getContext().getContentResolver().notifyChange(url, null, true);
return count;
}
/**
* Returns true if the caller can access the content provider.
*/
private boolean canAccess() {
if (Binder.getCallingPid() == Process.myPid()) {
return true;
} else {
return PreferencesUtils.getBoolean(
getContext(), R.string.allow_access_key, PreferencesUtils.ALLOW_ACCESS_DEFAULT);
}
}
/**
* Gets the {@link UrlType} for a url.
*
* @param url the url
*/
private UrlType getUrlType(Uri url) {
return UrlType.values()[uriMatcher.match(url)];
}
/**
* Inserts a content based on the url type.
*
* @param url the content url
* @param urlType the url type
* @param contentValues the content values
*/
private Uri insertContentValues(Uri url, UrlType urlType, ContentValues contentValues) {
switch (urlType) {
case TRACKPOINTS:
return insertTrackPoint(url, contentValues);
case TRACKS:
return insertTrack(url, contentValues);
case WAYPOINTS:
return insertWaypoint(url, contentValues);
default:
throw new IllegalArgumentException("Unknown url " + url);
}
}
/**
* Inserts a track point.
*
* @param url the content url
* @param values the content values
*/
private Uri insertTrackPoint(Uri url, ContentValues values) {
boolean hasLatitude = values.containsKey(TrackPointsColumns.LATITUDE);
boolean hasLongitude = values.containsKey(TrackPointsColumns.LONGITUDE);
boolean hasTime = values.containsKey(TrackPointsColumns.TIME);
if (!hasLatitude || !hasLongitude || !hasTime) {
throw new IllegalArgumentException("Latitude, longitude, and time values are required.");
}
long rowId = db.insert(TrackPointsColumns.TABLE_NAME, TrackPointsColumns._ID, values);
if (rowId >= 0) {
Uri uri = ContentUris.appendId(TrackPointsColumns.CONTENT_URI.buildUpon(), rowId).build();
getContext().getContentResolver().notifyChange(url, null, true);
return uri;
}
throw new SQLiteException("Failed to insert a track point " + url);
}
/**
* Inserts a track.
*
* @param url the content url
* @param contentValues the content values
*/
private Uri insertTrack(Uri url, ContentValues contentValues) {
boolean hasStartTime = contentValues.containsKey(TracksColumns.STARTTIME);
boolean hasStartId = contentValues.containsKey(TracksColumns.STARTID);
if (!hasStartTime || !hasStartId) {
throw new IllegalArgumentException("Both start time and start id values are required.");
}
long rowId = db.insert(TracksColumns.TABLE_NAME, TracksColumns._ID, contentValues);
if (rowId >= 0) {
Uri uri = ContentUris.appendId(TracksColumns.CONTENT_URI.buildUpon(), rowId).build();
getContext().getContentResolver().notifyChange(url, null, true);
return uri;
}
throw new SQLException("Failed to insert a track " + url);
}
/**
* Inserts a waypoint.
*
* @param url the content url
* @param contentValues the content values
*/
private Uri insertWaypoint(Uri url, ContentValues contentValues) {
long rowId = db.insert(WaypointsColumns.TABLE_NAME, WaypointsColumns._ID, contentValues);
if (rowId >= 0) {
Uri uri = ContentUris.appendId(WaypointsColumns.CONTENT_URI.buildUpon(), rowId).build();
getContext().getContentResolver().notifyChange(url, null, true);
return uri;
}
throw new SQLException("Failed to insert a waypoint " + url);
}
/**
* Gets the db.
*
* @return the SQLiteDatabase object
*/
@VisibleForTesting
SQLiteDatabase getDb() {
return db;
}
}
@@ -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.getTripStatistics();
// 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);
}
}
@@ -0,0 +1,777 @@
/*
* 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.content;
import static com.google.android.apps.mytracks.Constants.MAX_DISPLAYED_WAYPOINTS_POINTS;
import static com.google.android.apps.mytracks.Constants.MAX_LOCATION_AGE_MS;
import static com.google.android.apps.mytracks.Constants.MAX_NETWORK_AGE_MS;
import static com.google.android.apps.mytracks.Constants.TARGET_DISPLAYED_TRACK_POINTS;
import com.google.android.apps.mytracks.Constants;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils.DoubleBufferedLocationFactory;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils.LocationIterator;
import com.google.android.apps.mytracks.content.TrackDataListener.LocationState;
import com.google.android.apps.mytracks.util.LocationUtils;
import com.google.android.apps.mytracks.util.PreferencesUtils;
import com.google.android.maps.mytracks.R;
import com.google.common.annotations.VisibleForTesting;
import android.content.Context;
import android.database.Cursor;
import android.hardware.GeomagneticField;
import android.location.Location;
import android.location.LocationManager;
import android.os.Handler;
import android.os.HandlerThread;
import android.util.Log;
import java.util.Collections;
import java.util.EnumSet;
import java.util.Set;
/**
* Track data hub. Receives data from {@link DataSource} and distributes it to
* {@link TrackDataListener} after some processing.
*
* @author Rodrigo Damazio
*/
public class TrackDataHub implements DataSourceListener {
private static final String TAG = TrackDataHub.class.getSimpleName();
// One hour in milliseconds
private static final int ONE_HOUR = 60 * 60 * 1000;
private final Context context;
private final TrackDataManager trackDataManager;
private final MyTracksProviderUtils myTracksProviderUtils;
private final int targetNumPoints;
private final DoubleBufferedLocationFactory locationFactory;
private boolean started;
private HandlerThread handlerThread;
private Handler handler;
private DataSource dataSource;
private DataSourceManager dataSourceManager;
// Preference values
private long selectedTrackId;
private boolean metricUnits;
private boolean reportSpeed;
private int minRequiredAccuracy;
// Heading values
private float lastHeading = 0;
private float lastDeclination = 0;
private long lastDeclinationUpdate = 0;
// Location values
private Location lastSeenLocation = null;
private boolean hasProviderEnabled = true;
private boolean hasFix = false;
private boolean hasGoodFix = false;
// Track points sampling state
private int numLoadedPoints;
private long firstSeenLocationId;
private long lastSeenLocationId;
/**
* Creates a new instance.
*/
public synchronized static TrackDataHub newInstance(Context context) {
return new TrackDataHub(context, new TrackDataManager(), MyTracksProviderUtils.Factory.get(
context), TARGET_DISPLAYED_TRACK_POINTS);
}
/**
* Constructor.
*
* @param context the context
* @param trackDataManager the track data manager
* @param myTracksProviderUtils the my tracks provider utils
* @param targetNumPoints the target number of points
*/
@VisibleForTesting
TrackDataHub(Context context, TrackDataManager trackDataManager,
MyTracksProviderUtils myTracksProviderUtils, int targetNumPoints) {
this.context = context;
this.trackDataManager = trackDataManager;
this.myTracksProviderUtils = myTracksProviderUtils;
this.targetNumPoints = targetNumPoints;
this.locationFactory = new DoubleBufferedLocationFactory();
resetSamplingState();
}
/**
* Starts.
*/
public void start() {
if (started) {
Log.i(TAG, "TrackDataHub already started, ignoring start.");
return;
}
started = true;
handlerThread = new HandlerThread("TrackDataHubHandlerThread");
handlerThread.start();
handler = new Handler(handlerThread.getLooper());
dataSource = newDataSource();
dataSourceManager = new DataSourceManager(dataSource, this);
runInHanderThread(new Runnable() {
@Override
public void run() {
dataSourceManager.updateListeners(trackDataManager.getRegisteredTrackDataTypes());
selectedTrackId = PreferencesUtils.getLong(context, R.string.selected_track_id_key);
metricUnits = PreferencesUtils.getBoolean(
context, R.string.metric_units_key, PreferencesUtils.METRIC_UNITS_DEFAULT);
reportSpeed = PreferencesUtils.getBoolean(
context, R.string.report_speed_key, PreferencesUtils.REPORT_SPEED_DEFAULT);
minRequiredAccuracy = PreferencesUtils.getInt(context, R.string.min_required_accuracy_key,
PreferencesUtils.MIN_REQUIRED_ACCURACY_DEFAULT);
loadDataForAll();
}
});
}
/**
* Stops.
*/
public void stop() {
if (!started) {
Log.i(TAG, "TrackDataHub not started, ignoring stop.");
return;
}
started = false;
dataSourceManager.unregisterAllListeners();
handlerThread.getLooper().quit();
handlerThread = null;
handler = null;
dataSource = null;
dataSourceManager = null;
}
/**
* Loads a track.
*
* @param trackId the track id
*/
public void loadTrack(final long trackId) {
runInHanderThread(new Runnable() {
@Override
public void run() {
if (trackId == selectedTrackId) {
Log.i(TAG, "Not reloading track " + trackId);
return;
}
selectedTrackId = trackId;
PreferencesUtils.setLong(context, R.string.selected_track_id_key, selectedTrackId);
loadDataForAll();
}
});
}
/**
* Registers a {@link TrackDataListener}.
*
* @param trackDataListener the track data listener
* @param trackDataTypes the track data types
*/
public void registerTrackDataListener(
final TrackDataListener trackDataListener, final EnumSet<TrackDataType> trackDataTypes) {
runInHanderThread(new Runnable() {
@Override
public void run() {
trackDataManager.registerListener(trackDataListener, trackDataTypes);
dataSourceManager.updateListeners(trackDataManager.getRegisteredTrackDataTypes());
loadDataForListener(trackDataListener);
}
});
}
/**
* Unregisters a {@link TrackDataListener}.
*
* @param trackDataListener the track data listener
*/
public void unregisterTrackDataListener(final TrackDataListener trackDataListener) {
runInHanderThread(new Runnable() {
@Override
public void run() {
trackDataManager.unregisterListener(trackDataListener);
dataSourceManager.updateListeners(trackDataManager.getRegisteredTrackDataTypes());
}
});
}
/**
* Reloads data for a {@link TrackDataListener}.
*/
public void reloadDataForListener(final TrackDataListener trackDataListener) {
runInHanderThread(new Runnable() {
@Override
public void run() {
loadDataForListener(trackDataListener);
}
});
}
/**
* Forces update location and reports to all listeners.
*/
public void forceUpdateLocation() {
final Location location = dataSource.getLastKnownLocation();
runInHanderThread(new Runnable() {
@Override
public void run() {
notifyLocationChanged(
location, false, trackDataManager.getListeners(TrackDataType.LOCATION));
}
});
}
/**
* Returns true if the selected track is recording.
*/
public boolean isSelectedTrackRecording() {
long recordingTrackId = PreferencesUtils.getLong(context, R.string.recording_track_id_key);
return recordingTrackId != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT
&& recordingTrackId == selectedTrackId;
}
@Override
public void notifyTracksTableUpdated() {
runInHanderThread(new Runnable() {
@Override
public void run() {
notifyTracksTableUpdate(trackDataManager.getListeners(TrackDataType.TRACKS_TABLE));
}
});
}
@Override
public void notifyWaypointsTableUpdated() {
runInHanderThread(new Runnable() {
@Override
public void run() {
notifyWaypointsTableUpdate(trackDataManager.getListeners(TrackDataType.WAYPOINTS_TABLE));
}
});
}
@Override
public void notifyTrackPointsTableUpdated() {
runInHanderThread(new Runnable() {
@Override
public void run() {
notifyTrackPointsTableUpdate(
true, trackDataManager.getListeners(TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE),
trackDataManager.getListeners(TrackDataType.SAMPLED_OUT_TRACK_POINTS_TABLE));
}
});
}
@Override
public void notifyPreferenceChanged(final String key) {
runInHanderThread(new Runnable() {
@Override
public void run() {
if (PreferencesUtils.getKey(context, R.string.min_required_accuracy_key).equals(key)) {
minRequiredAccuracy = PreferencesUtils.getInt(context, R.string.min_required_accuracy_key,
PreferencesUtils.MIN_REQUIRED_ACCURACY_DEFAULT);
} else if (PreferencesUtils.getKey(context, R.string.metric_units_key).equals(key)) {
metricUnits = PreferencesUtils.getBoolean(
context, R.string.metric_units_key, PreferencesUtils.METRIC_UNITS_DEFAULT);
Set<TrackDataListener> trackDataListeners = trackDataManager.getListeners(
TrackDataType.PREFERENCE);
for (TrackDataListener trackDataListener : trackDataListeners) {
if (trackDataListener.onMetricUnitsChanged(metricUnits)) {
loadDataForListener(trackDataListener);
}
}
} else if (PreferencesUtils.getKey(context, R.string.report_speed_key).equals(key)) {
reportSpeed = PreferencesUtils.getBoolean(
context, R.string.report_speed_key, PreferencesUtils.REPORT_SPEED_DEFAULT);
Set<TrackDataListener> trackDataListeners = trackDataManager.getListeners(
TrackDataType.PREFERENCE);
for (TrackDataListener trackDataListener : trackDataListeners) {
if (trackDataListener.onReportSpeedChanged(reportSpeed)) {
loadDataForListener(trackDataListener);
}
}
} else if (PreferencesUtils.getKey(context, R.string.selected_track_id_key).equals(key)) {
long trackId = PreferencesUtils.getLong(context, R.string.selected_track_id_key);
if (trackId == selectedTrackId) {
Log.i(TAG, "Not reloading track " + trackId);
return;
}
selectedTrackId = trackId;
loadDataForAll();
}
}
});
}
@Override
public void notifyLocationProviderEnabled(final boolean enabled) {
runInHanderThread(new Runnable() {
@Override
public void run() {
hasProviderEnabled = enabled;
notifyLocationStateChanged(trackDataManager.getListeners(TrackDataType.LOCATION));
}
});
}
@Override
public void notifyLocationProviderAvailable(final boolean available) {
runInHanderThread(new Runnable() {
@Override
public void run() {
hasFix = available;
notifyLocationStateChanged(trackDataManager.getListeners(TrackDataType.LOCATION));
}
});
}
@Override
public void notifyLocationChanged(final Location location) {
runInHanderThread(new Runnable() {
@Override
public void run() {
notifyLocationChanged(
location, false, trackDataManager.getListeners(TrackDataType.LOCATION));
}
});
}
@Override
public void notifyHeadingChanged(final float heading) {
runInHanderThread(new Runnable() {
@Override
public void run() {
lastHeading = heading;
if (lastSeenLocation != null) {
// Update the declination at most once an hour
long now = System.currentTimeMillis();
if (now - lastDeclinationUpdate > ONE_HOUR) {
lastDeclinationUpdate = now;
long timestamp = lastSeenLocation.getTime();
if (timestamp == 0) {
// Hack for Samsung phones which don't populate the time field
timestamp = now;
}
lastDeclination = getDeclination(lastSeenLocation, timestamp);
}
}
notifyHeadingChange(trackDataManager.getListeners(TrackDataType.HEADING));
}
});
}
/**
* Loads data for all listeners. To be run in the {@link #handler} thread.
*/
private void loadDataForAll() {
resetSamplingState();
if (trackDataManager.getNumberOfListeners() == 0) {
return;
}
for (TrackDataListener trackDataListener :
trackDataManager.getListeners(TrackDataType.PREFERENCE)) {
trackDataListener.onMetricUnitsChanged(metricUnits);
trackDataListener.onReportSpeedChanged(reportSpeed);
}
notifySelectedTrackChanged(trackDataManager.getListeners(TrackDataType.SELECTED_TRACK));
notifyTracksTableUpdate(trackDataManager.getListeners(TrackDataType.TRACKS_TABLE));
for (TrackDataListener listener :
trackDataManager.getListeners(TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE)) {
listener.clearTrackPoints();
}
notifyTrackPointsTableUpdate(true,
trackDataManager.getListeners(TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE),
trackDataManager.getListeners(TrackDataType.SAMPLED_OUT_TRACK_POINTS_TABLE));
notifyWaypointsTableUpdate(trackDataManager.getListeners(TrackDataType.WAYPOINTS_TABLE));
if (lastSeenLocation != null) {
notifyLocationChanged(
lastSeenLocation, true, trackDataManager.getListeners(TrackDataType.LOCATION));
} else {
notifyLocationStateChanged(trackDataManager.getListeners(TrackDataType.LOCATION));
}
notifyHeadingChange(trackDataManager.getListeners(TrackDataType.HEADING));
}
/**
* Loads data for a listener. To be run in the {@link #handler} thread.
*
* @param trackDataListener the track data listener.
*/
private void loadDataForListener(TrackDataListener trackDataListener) {
Set<TrackDataListener> trackDataListeners = Collections.singleton(trackDataListener);
EnumSet<TrackDataType> trackDataTypes = trackDataManager.getTrackDataTypes(trackDataListener);
if (trackDataTypes.contains(TrackDataType.PREFERENCE)) {
trackDataListener.onMetricUnitsChanged(metricUnits);
trackDataListener.onReportSpeedChanged(reportSpeed);
}
if (trackDataTypes.contains(TrackDataType.SELECTED_TRACK)) {
notifySelectedTrackChanged(trackDataListeners);
}
if (trackDataTypes.contains(TrackDataType.TRACKS_TABLE)) {
notifyTracksTableUpdate(trackDataListeners);
}
boolean hasSampledIn = trackDataTypes.contains(TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE);
boolean hasSampledOut = trackDataTypes.contains(TrackDataType.SAMPLED_OUT_TRACK_POINTS_TABLE);
if (hasSampledIn || hasSampledOut) {
trackDataListener.clearTrackPoints();
boolean isOnlyListener = trackDataManager.getNumberOfListeners() == 1;
if (isOnlyListener) {
resetSamplingState();
}
Set<TrackDataListener> sampledInListeners = trackDataListeners;
Set<TrackDataListener> sampledOutListeners = hasSampledOut ? trackDataListeners
: Collections.<TrackDataListener> emptySet();
notifyTrackPointsTableUpdate(isOnlyListener, sampledInListeners, sampledOutListeners);
}
if (trackDataTypes.contains(TrackDataType.WAYPOINTS_TABLE)) {
notifyWaypointsTableUpdate(trackDataListeners);
}
if (trackDataTypes.contains(TrackDataType.LOCATION)) {
if (lastSeenLocation != null) {
notifyLocationChanged(lastSeenLocation, true, trackDataListeners);
} else {
notifyLocationStateChanged(trackDataListeners);
}
}
if (trackDataTypes.contains(TrackDataType.HEADING)) {
notifyHeadingChange(trackDataListeners);
}
}
/**
* Notifies selected track changed. To be run in the {@link #handler} thread.
*
* @param trackDataListeners the track data listeners to notify
*/
private void notifySelectedTrackChanged(Set<TrackDataListener> trackDataListeners) {
if (trackDataListeners.isEmpty()) {
return;
}
Track track = myTracksProviderUtils.getTrack(selectedTrackId);
for (TrackDataListener trackDataListener : trackDataListeners) {
trackDataListener.onSelectedTrackChanged(track, isSelectedTrackRecording());
}
}
/**
* Notifies track table update. To be run in the {@link #handler} thread.
*
* @param trackDataListeners the track data listeners to notify
*/
private void notifyTracksTableUpdate(Set<TrackDataListener> trackDataListeners) {
if (trackDataListeners.isEmpty()) {
return;
}
Track track = myTracksProviderUtils.getTrack(selectedTrackId);
for (TrackDataListener trackDataListener : trackDataListeners) {
trackDataListener.onTrackUpdated(track);
}
}
/**
* Notifies waypoint table update. Currently, reloads all the waypoints up to
* {@link Constants#MAX_DISPLAYED_WAYPOINTS_POINTS}. To be run in the
* {@link #handler} thread.
*
* @param trackDataListeners the track data listeners to notify
*/
private void notifyWaypointsTableUpdate(Set<TrackDataListener> trackDataListeners) {
if (trackDataListeners.isEmpty()) {
return;
}
for (TrackDataListener trackDataListener : trackDataListeners) {
trackDataListener.clearWaypoints();
}
Cursor cursor = null;
try {
cursor = myTracksProviderUtils.getWaypointsCursor(
selectedTrackId, 0L, MAX_DISPLAYED_WAYPOINTS_POINTS);
if (cursor != null && cursor.moveToFirst()) {
do {
Waypoint waypoint = myTracksProviderUtils.createWaypoint(cursor);
if (!LocationUtils.isValidLocation(waypoint.getLocation())) {
continue;
}
for (TrackDataListener trackDataListener : trackDataListeners) {
trackDataListener.onNewWaypoint(waypoint);
}
} while (cursor.moveToNext());
}
} finally {
if (cursor != null) {
cursor.close();
}
}
for (TrackDataListener trackDataListener : trackDataListeners) {
trackDataListener.onNewWaypointsDone();
}
}
/**
* Notifies track points table update. To be run in the {@link #handler}
* thread.
*
* @param updateSamplingState true to update the sampling state
* @param sampledInListeners the sampled-in listeners
* @param sampledOutListeners the sampled-out listeners
*/
private void notifyTrackPointsTableUpdate(boolean updateSamplingState,
Set<TrackDataListener> sampledInListeners, Set<TrackDataListener> sampledOutListeners) {
if (sampledInListeners.isEmpty() && sampledOutListeners.isEmpty()) {
return;
}
if (updateSamplingState && numLoadedPoints >= targetNumPoints) {
// Reload and resample the track at a lower frequency.
Log.i(TAG, "Resampling track after " + numLoadedPoints + " points.");
resetSamplingState();
for (TrackDataListener listener : sampledInListeners) {
listener.clearTrackPoints();
}
}
int localNumLoadedPoints = updateSamplingState ? numLoadedPoints : 0;
long localFirstSeenLocationId = updateSamplingState ? firstSeenLocationId : -1L;
long localLastSeenLocationId = updateSamplingState ? lastSeenLocationId : -1L;
long maxPointId = updateSamplingState ? -1L : lastSeenLocationId;
long lastLocationId = myTracksProviderUtils.getLastLocationId(selectedTrackId);
int samplingFrequency = -1;
LocationIterator iterator = myTracksProviderUtils.getLocationIterator(
selectedTrackId, localLastSeenLocationId + 1, false, locationFactory);
boolean includeNextPoint = false;
while (iterator.hasNext()) {
Location location = iterator.next();
long locationId = iterator.getLocationId();
// Stop if past the last wanted point
if (maxPointId != -1L && locationId > maxPointId) {
break;
}
if (localFirstSeenLocationId == -1) {
localFirstSeenLocationId = locationId;
}
if (samplingFrequency == -1) {
long numTotalPoints = Math.max(0L, lastLocationId - localFirstSeenLocationId);
samplingFrequency = 1 + (int) (numTotalPoints / targetNumPoints);
}
if (!LocationUtils.isValidLocation(location)) {
// TODO: also include the last valid point before a split
for (TrackDataListener trackDataListener : sampledInListeners) {
trackDataListener.onSegmentSplit();
includeNextPoint = true;
}
} else {
// Also include the last point if the selected track is not recording.
if (includeNextPoint || (localNumLoadedPoints % samplingFrequency == 0)
|| (locationId == lastLocationId && !isSelectedTrackRecording())) {
includeNextPoint = false;
for (TrackDataListener trackDataListener : sampledInListeners) {
trackDataListener.onSampledInTrackPoint(location);
}
} else {
for (TrackDataListener trackDataListener : sampledOutListeners) {
trackDataListener.onSampledOutTrackPoint(location);
}
}
}
localNumLoadedPoints++;
localLastSeenLocationId = locationId;
}
iterator.close();
if (updateSamplingState) {
numLoadedPoints = localNumLoadedPoints;
firstSeenLocationId = localFirstSeenLocationId;
lastSeenLocationId = localLastSeenLocationId;
}
for (TrackDataListener listener : sampledInListeners) {
listener.onNewTrackPointsDone();
}
}
/**
* Notifies location state changed. To be run in the {@link #handler} thread.
*
* @param trackDataListeners the track data listeners to notify
*/
private void notifyLocationStateChanged(Set<TrackDataListener> trackDataListeners) {
if (trackDataListeners.isEmpty()) {
return;
}
TrackDataListener.LocationState locationState;
if (!hasProviderEnabled) {
locationState = LocationState.DISABLED;
} else if (!hasFix) {
locationState = LocationState.NO_FIX;
} else if (!hasGoodFix) {
locationState = LocationState.BAD_FIX;
} else {
locationState = LocationState.GOOD_FIX;
}
for (TrackDataListener trackDataListener : trackDataListeners) {
trackDataListener.onLocationStateChanged(locationState);
}
}
/**
* Notifies location changed. To be run in the {@link #handler} thread.
*
* @param location the location
* @param notifyLocationStateChange true to always notify location state
* change
* @param trackDataListeners the track data listeners to notify
*/
private void notifyLocationChanged(Location location, boolean notifyLocationStateChange,
Set<TrackDataListener> trackDataListeners) {
if (location == null) {
return;
}
boolean oldHasFix = hasFix;
boolean oldHasGoodFix = hasGoodFix;
long now = System.currentTimeMillis();
if (location.getProvider().equals(LocationManager.GPS_PROVIDER)) {
hasFix = !isLocationOld(location, now, MAX_LOCATION_AGE_MS);
hasGoodFix = location.getAccuracy() <= minRequiredAccuracy;
lastSeenLocation = location;
} else {
// A network location.
// If has a recent GPS location, ignore the network location.
if (!isLocationOld(lastSeenLocation, now, MAX_LOCATION_AGE_MS)) {
return;
}
hasFix = false;
hasGoodFix = false;
lastSeenLocation = isLocationOld(location, now, MAX_NETWORK_AGE_MS) ? null : location;
}
if (trackDataListeners.isEmpty()) {
return;
}
if (notifyLocationStateChange || hasFix != oldHasFix || hasGoodFix != oldHasGoodFix) {
notifyLocationStateChanged(trackDataListeners);
}
for (TrackDataListener trackDataListener : trackDataListeners) {
trackDataListener.onLocationChanged(lastSeenLocation);
}
}
/**
* Notifies heading change. To be run in the {@link #handler} thread.
*
* @param trackDataListeners the track data listeners to notify
*/
private void notifyHeadingChange(Set<TrackDataListener> trackDataListeners) {
if (trackDataListeners.isEmpty()) {
return;
}
float value = lastHeading + lastDeclination;
for (TrackDataListener trackDataListener : trackDataListeners) {
trackDataListener.onHeadingChanged(value);
}
}
/**
* Resets the track points sampling states.
*/
private void resetSamplingState() {
numLoadedPoints = 0;
firstSeenLocationId = -1L;
lastSeenLocationId = -1L;
}
/**
* Returns true if a location is invalid or too old.
*
* @param location the location
* @param now the current time
* @param maxAge the maximum age
*/
private boolean isLocationOld(Location location, long now, long maxAge) {
return !LocationUtils.isValidLocation(location) || (now - location.getTime() > maxAge);
}
/**
* Creates a {@link DataSource}.
*/
@VisibleForTesting
protected DataSource newDataSource() {
return new DataSource(context);
}
/**
* Gets a declination.
*
* @param location the location
* @param time the time
*/
@VisibleForTesting
protected float getDeclination(Location location, long time) {
GeomagneticField field = new GeomagneticField((float) location.getLatitude(), (float) location
.getLongitude(), (float) location.getAltitude(), time);
return field.getDeclination();
}
/**
* Run in the handler thread.
*
* @param runnable the runnable
*/
@VisibleForTesting
protected void runInHanderThread(Runnable runnable) {
if (handler == null) {
// Use a Throwable to ensure the stack trace is logged.
Log.e(TAG, "handler is null.", new Throwable());
return;
}
handler.post(runnable);
}
}
@@ -0,0 +1,143 @@
/*
* 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.content;
import android.location.Location;
/**
* Listener for track data changes.
*
* @author Rodrigo Damazio
*/
public interface TrackDataListener {
/**
* Location state.
*
* @author Jimmy Shih
*/
public enum LocationState {
DISABLED, NO_FIX, BAD_FIX, GOOD_FIX;
}
/**
* Called when the location state changes.
*/
public void onLocationStateChanged(LocationState locationState);
/**
* Called when the location changes. This is meant for location display only,
* track point data is reported with other methods like
* {@link #onSampledInTrackPoint(Location)} and
* {@link #onSampledOutTrackPoint(Location)}.
*
* @param location the location
*/
public void onLocationChanged(Location location);
/**
* Called when the heading changes.
*
* @param heading the heading
*/
public void onHeadingChanged(double heading);
/**
* Called when the selected track changes. This will be followed by calls to
* data methods such as {@link #onTrackUpdated(Track)},
* {@link #clearTrackPoints()}, {@link #onSampledInTrackPoint(Location)},
* etc., even if no track is currently selected (in which case you'll only get
* calls to clear the current data).
*
* @param track the selected track or null if no track is selected
* @param isRecording true if the selected track is recording
*/
public void onSelectedTrackChanged(Track track, boolean isRecording);
/**
* Called when the track or its statistics has been updated.
*
* @param track the track
*/
public void onTrackUpdated(Track track);
/**
* Called to clear previously-sent track points.
*/
public void clearTrackPoints();
/**
* Called when a sampled in track point is read.
*
* @param location the location
*/
public void onSampledInTrackPoint(Location location);
/**
* Called when a sampled out track point is read.
*
* @param location the location
*/
public void onSampledOutTrackPoint(Location location);
/**
* Called when an invalid track point representing a segment split is read.
*/
public void onSegmentSplit();
/**
* Called when finish sending new track points. This gets called after every
* batch of calls to {@link #onSampledInTrackPoint(Location)},
* {@link #onSampledOutTrackPoint(Location)} and {@link #onSegmentSplit()}.
*/
public void onNewTrackPointsDone();
/**
* Called to clear previously sent waypoints.
*/
public void clearWaypoints();
/**
* Called when a new waypoint is read.
*
* @param waypoint the waypoint
*/
public void onNewWaypoint(Waypoint waypoint);
/**
* Called when finish sending new waypoints. This gets called after every
* batch of calls to {@link #clearWaypoints()} and
* {@link #onNewWaypoint(Waypoint)}.
*/
public void onNewWaypointsDone();
/**
* Called when the metric units preference value is change.
*
* @param metricUnits true to use metric units, false to use imperial units
* @return true to reload all the data, false otherwise.
*/
public boolean onMetricUnitsChanged(boolean metricUnits);
/**
* Called when the report speed preference value is changed.
*
* @param reportSpeed true to report speed, false to report pace
* @return true to reload all the data, false otherwise.
*/
public boolean onReportSpeedChanged(boolean reportSpeed);
}
@@ -0,0 +1,124 @@
/*
* 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.content;
import static com.google.android.apps.mytracks.Constants.TAG;
import android.util.Log;
import java.util.EnumMap;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
/**
* Manages register/unregister {@link TrackDataListener} and keeping the state
* for each registered listener.
*
* @author Rodrigo Damazio
*/
public class TrackDataManager {
// Map of listener to its track data types
private final Map<TrackDataListener, EnumSet<TrackDataType>>
listenerToTypesMap = new HashMap<TrackDataListener, EnumSet<TrackDataType>>();
// Map of track data type to listeners
private final Map<TrackDataType, Set<TrackDataListener>>
typeToListenersMap = new EnumMap<TrackDataType, Set<TrackDataListener>>(TrackDataType.class);
public TrackDataManager() {
for (TrackDataType trackDataType : TrackDataType.values()) {
typeToListenersMap.put(trackDataType, new LinkedHashSet<TrackDataListener>());
}
}
/**
* Registers a listener.
*
* @param listener the listener
* @param trackDataTypes the track data types the listener is interested
*/
public void registerListener(
TrackDataListener listener, EnumSet<TrackDataType> trackDataTypes) {
if (listenerToTypesMap.containsKey(listener)) {
throw new IllegalStateException("Listener is already registered");
}
listenerToTypesMap.put(listener, trackDataTypes);
for (TrackDataType trackDataType : trackDataTypes) {
typeToListenersMap.get(trackDataType).add(listener);
}
}
/**
* Unregisters a listener.
*
* @param listener the listener
*/
public void unregisterListener(TrackDataListener listener) {
EnumSet<TrackDataType> removedTypes = listenerToTypesMap.remove(listener);
if (removedTypes == null) {
Log.w(TAG, "Tried to unregister a listener that is not registered.");
return;
}
// Remove the listener from the typeToListenersMap
for (TrackDataType trackDataType : removedTypes) {
typeToListenersMap.get(trackDataType).remove(listener);
}
}
/**
* Gets the number of {@link TrackDataListener}.
*/
public int getNumberOfListeners() {
return listenerToTypesMap.size();
}
/**
* Gets the track data types for a listener.
*
* @param listener the listener
*/
public EnumSet<TrackDataType> getTrackDataTypes(TrackDataListener listener) {
return listenerToTypesMap.get(listener);
}
/**
* Gets the listeners for a {@link TrackDataType}.
*
* @param type the type
*/
public Set<TrackDataListener> getListeners(TrackDataType type) {
return typeToListenersMap.get(type);
}
/**
* Gets all the registered {@link TrackDataType}.
*/
public EnumSet<TrackDataType> getRegisteredTrackDataTypes() {
EnumSet<TrackDataType> types = EnumSet.noneOf(TrackDataType.class);
for (EnumSet<TrackDataType> value : listenerToTypesMap.values()) {
types.addAll(value);
}
// Always include preference
types.add(TrackDataType.PREFERENCE);
return types;
}
}
@@ -0,0 +1,33 @@
/*
* 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.content;
/**
* Types of track data.
*
* @author Jimmy Shih
*/
public enum TrackDataType {
SELECTED_TRACK, // selected track changes
TRACKS_TABLE, // tracks table changes
WAYPOINTS_TABLE, // waypoints table changes
SAMPLED_IN_TRACK_POINTS_TABLE, // sampled-in track points table changes
SAMPLED_OUT_TRACK_POINTS_TABLE, // sampled-out track points table changes
LOCATION, // location changes
HEADING, // heading changes
PREFERENCE // preference changes
}
@@ -0,0 +1,57 @@
/*
* 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.fragments;
import com.google.android.apps.mytracks.util.SystemUtils;
import com.google.android.maps.mytracks.R;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.DialogInterface;
import android.os.Bundle;
import android.support.v4.app.DialogFragment;
import android.view.View;
import android.widget.TextView;
/**
* A DialogFragment to show information about My Tracks.
*
* @author Jimmy Shih
*/
public class AboutDialogFragment extends DialogFragment {
public static final String ABOUT_DIALOG_TAG = "aboutDialog";
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
View view = getActivity().getLayoutInflater().inflate(R.layout.about, null);
TextView aboutVersion = (TextView) view.findViewById(R.id.about_version);
aboutVersion.setText(SystemUtils.getMyTracksVersion(getActivity()));
return new AlertDialog.Builder(getActivity())
.setNegativeButton(R.string.about_license, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
EulaDialogFragment.newInstance(true).show(
getActivity().getSupportFragmentManager(), EulaDialogFragment.EULA_DIALOG_TAG);
}
})
.setPositiveButton(R.string.generic_ok, null)
.setTitle(R.string.help_about)
.setView(view)
.create();
}
}
@@ -0,0 +1,536 @@
/*
* 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.fragments;
import com.google.android.apps.mytracks.ChartView;
import com.google.android.apps.mytracks.Constants;
import com.google.android.apps.mytracks.TrackDetailActivity;
import com.google.android.apps.mytracks.content.MyTracksLocation;
import com.google.android.apps.mytracks.content.Sensor;
import com.google.android.apps.mytracks.content.Sensor.SensorDataSet;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.content.TrackDataHub;
import com.google.android.apps.mytracks.content.TrackDataListener;
import com.google.android.apps.mytracks.content.TrackDataType;
import com.google.android.apps.mytracks.content.Waypoint;
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.PreferencesUtils;
import com.google.android.apps.mytracks.util.UnitConversions;
import com.google.android.maps.mytracks.R;
import com.google.common.annotations.VisibleForTesting;
import android.location.Location;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import android.widget.ZoomControls;
import java.util.ArrayList;
import java.util.EnumSet;
/**
* A fragment to display track chart to the user.
*
* @author Sandor Dornbush
* @author Rodrigo Damazio
*/
public class ChartFragment extends Fragment implements TrackDataListener {
public static final String CHART_FRAGMENT_TAG = "chartFragment";
// Android reports 128 when the speed is invalid
private static final int INVALID_SPEED = 128;
private final DoubleBuffer elevationBuffer = new DoubleBuffer(
Constants.ELEVATION_SMOOTHING_FACTOR);
private final DoubleBuffer speedBuffer = new DoubleBuffer(Constants.SPEED_SMOOTHING_FACTOR);
private final ArrayList<double[]> pendingPoints = new ArrayList<double[]>();
private TrackDataHub trackDataHub;
// Stats gathered from the received data
private double totalDistance = 0.0;
private long startTime = -1L;
private Location lastLocation = null;
private double trackMaxSpeed = 0.0;
// Modes of operation
private boolean metricUnits = true;
private boolean reportSpeed = true;
private boolean chartByDistance = true;
private boolean[] chartShow = new boolean[] {true, true, true, true, true, true};
// UI elements
private ChartView chartView;
private ZoomControls zoomControls;
/**
* A runnable that will enable/disable zoom controls and orange pointer as
* appropriate and redraw.
*/
private final Runnable updateChart = new Runnable() {
@Override
public void run() {
if (trackDataHub == null) {
return;
}
zoomControls.setIsZoomInEnabled(chartView.canZoomIn());
zoomControls.setIsZoomOutEnabled(chartView.canZoomOut());
chartView.setShowPointer(isSelectedTrackRecording());
chartView.invalidate();
}
};
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
/*
* Create a chartView here to store data thus won't need to reload all the
* data on every onStart or onResume.
*/
chartView = new ChartView(getActivity());
};
@Override
public View onCreateView(
LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.chart, container, false);
zoomControls = (ZoomControls) view.findViewById(R.id.chart_zoom_controls);
zoomControls.setOnZoomInClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
zoomIn();
}
});
zoomControls.setOnZoomOutClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
zoomOut();
}
});
return view;
}
@SuppressWarnings("deprecation")
@Override
public void onStart() {
super.onStart();
ViewGroup layout = (ViewGroup) getActivity().findViewById(R.id.chart_view_layout);
LayoutParams layoutParams = new LayoutParams(
LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT);
layout.addView(chartView, layoutParams);
}
@Override
public void onResume() {
super.onResume();
resumeTrackDataHub();
checkChartSettings();
getActivity().runOnUiThread(updateChart);
}
@Override
public void onPause() {
super.onPause();
pauseTrackDataHub();
}
@Override
public void onStop() {
super.onStop();
ViewGroup layout = (ViewGroup) getActivity().findViewById(R.id.chart_view_layout);
layout.removeView(chartView);
}
@Override
public void onLocationStateChanged(LocationState state) {
// We don't care.
}
@Override
public void onLocationChanged(Location loc) {
// We don't care.
}
@Override
public void onHeadingChanged(double heading) {
// We don't care.
}
@Override
public void onSelectedTrackChanged(Track track, boolean isRecording) {
// We don't care.
}
@Override
public void onTrackUpdated(Track track) {
if (track == null || track.getTripStatistics() == null) {
trackMaxSpeed = 0.0;
return;
}
trackMaxSpeed = track.getTripStatistics().getMaxSpeed();
}
@Override
public void clearTrackPoints() {
totalDistance = 0.0;
startTime = -1L;
lastLocation = null;
elevationBuffer.reset();
speedBuffer.reset();
pendingPoints.clear();
chartView.reset();
getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
chartView.resetScroll();
}
});
}
@Override
public void onSampledInTrackPoint(Location location) {
if (LocationUtils.isValidLocation(location)) {
double[] data = new double[ChartView.NUM_SERIES + 1];
fillDataPoint(location, data);
pendingPoints.add(data);
}
}
@Override
public void onSampledOutTrackPoint(Location location) {
if (LocationUtils.isValidLocation(location)) {
// Still account for the point in the smoothing buffers.
fillDataPoint(location, null);
}
}
@Override
public void onSegmentSplit() {
// Do nothing.
}
@Override
public void onNewTrackPointsDone() {
chartView.addDataPoints(pendingPoints);
pendingPoints.clear();
getActivity().runOnUiThread(updateChart);
}
@Override
public void clearWaypoints() {
chartView.clearWaypoints();
}
@Override
public void onNewWaypoint(Waypoint waypoint) {
if (waypoint != null && LocationUtils.isValidLocation(waypoint.getLocation())) {
chartView.addWaypoint(waypoint);
}
}
@Override
public void onNewWaypointsDone() {
getActivity().runOnUiThread(updateChart);
}
@Override
public boolean onMetricUnitsChanged(boolean metric) {
if (metricUnits == metric) {
return false;
}
metricUnits = metric;
chartView.setMetricUnits(metricUnits);
getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
chartView.requestLayout();
}
});
return true;
}
@Override
public boolean onReportSpeedChanged(boolean speed) {
if (reportSpeed == speed) {
return false;
}
reportSpeed = speed;
chartView.setReportSpeed(reportSpeed);
boolean chartShowSpeed = PreferencesUtils.getBoolean(
getActivity(), R.string.chart_show_speed_key, PreferencesUtils.CHART_SHOW_SPEED_DEFAULT);
setSeriesEnabled(ChartView.SPEED_SERIES, chartShowSpeed && reportSpeed);
setSeriesEnabled(ChartView.PACE_SERIES, chartShowSpeed && !reportSpeed);
getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
chartView.requestLayout();
}
});
return true;
}
/**
* Checks the chart settings.
*/
private void checkChartSettings() {
boolean needUpdate = false;
if (chartByDistance != PreferencesUtils.getBoolean(getActivity(),
R.string.chart_by_distance_key, PreferencesUtils.CHART_BY_DISTANCE_DEFAULT)) {
chartByDistance = !chartByDistance;
chartView.setChartByDistance(chartByDistance);
reloadTrackDataHub();
needUpdate = true;
}
if (setSeriesEnabled(ChartView.ELEVATION_SERIES, PreferencesUtils.getBoolean(getActivity(),
R.string.chart_show_elevation_key, PreferencesUtils.CHART_SHOW_ELEVATION_DEFAULT))) {
needUpdate = true;
}
boolean chartShowSpeed = PreferencesUtils.getBoolean(
getActivity(), R.string.chart_show_speed_key, PreferencesUtils.CHART_SHOW_SPEED_DEFAULT);
if (setSeriesEnabled(ChartView.SPEED_SERIES, chartShowSpeed && reportSpeed)) {
needUpdate = true;
}
if (setSeriesEnabled(ChartView.PACE_SERIES, chartShowSpeed && !reportSpeed)) {
needUpdate = true;
}
if (setSeriesEnabled(ChartView.POWER_SERIES, PreferencesUtils.getBoolean(
getActivity(), R.string.chart_show_power_key, PreferencesUtils.CHART_SHOW_POWER_DEFAULT))) {
needUpdate = true;
}
if (setSeriesEnabled(ChartView.CADENCE_SERIES, PreferencesUtils.getBoolean(getActivity(),
R.string.chart_show_cadence_key, PreferencesUtils.CHART_SHOW_CADENCE_DEFAULT))) {
needUpdate = true;
}
if (setSeriesEnabled(ChartView.HEART_RATE_SERIES, PreferencesUtils.getBoolean(getActivity(),
R.string.chart_show_heart_rate_key, PreferencesUtils.CHART_SHOW_HEART_RATE_DEFAULT))) {
needUpdate = true;
}
if (needUpdate) {
chartView.postInvalidate();
}
}
/**
* Sets the series enabled value.
*
* @param index the series index
* @param value the value
* @return true if changed
*/
private boolean setSeriesEnabled(int index, boolean value) {
if (chartShow[index] != value) {
chartShow[index] = value;
chartView.setChartValueSeriesEnabled(index, value);
return true;
} else {
return false;
}
}
/**
* Resumes the trackDataHub. Needs to be synchronized because trackDataHub can be
* accessed by multiple threads.
*/
private synchronized void resumeTrackDataHub() {
trackDataHub = ((TrackDetailActivity) getActivity()).getTrackDataHub();
trackDataHub.registerTrackDataListener(this, EnumSet.of(
TrackDataType.SELECTED_TRACK,
TrackDataType.TRACKS_TABLE,
TrackDataType.WAYPOINTS_TABLE,
TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE,
TrackDataType.SAMPLED_OUT_TRACK_POINTS_TABLE,
TrackDataType.PREFERENCE));
}
/**
* Pauses the trackDataHub. Needs to be synchronized because trackDataHub can
* be accessed by multiple threads.
*/
private synchronized void pauseTrackDataHub() {
trackDataHub.unregisterTrackDataListener(this);
trackDataHub = null;
}
/**
* Returns true if the selected track is recording. Needs to be synchronized
* because trackDataHub can be accessed by multiple threads.
*/
private synchronized boolean isSelectedTrackRecording() {
return trackDataHub != null && trackDataHub.isSelectedTrackRecording();
}
/**
* Reloads the trackDataHub. Needs to be synchronized because trackDataHub can be
* accessed by multiple threads.
*/
private synchronized void reloadTrackDataHub() {
if (trackDataHub != null) {
trackDataHub.reloadDataForListener(this);
}
}
/**
* To zoom in.
*/
private void zoomIn() {
chartView.zoomIn();
zoomControls.setIsZoomInEnabled(chartView.canZoomIn());
zoomControls.setIsZoomOutEnabled(chartView.canZoomOut());
}
/**
* To zoom out.
*/
private void zoomOut() {
chartView.zoomOut();
zoomControls.setIsZoomInEnabled(chartView.canZoomIn());
zoomControls.setIsZoomOutEnabled(chartView.canZoomOut());
}
/**
* Given a location, fill in a data point, an array of double[]. <br>
* data[0] = time/distance <br>
* data[1] = elevation <br>
* data[2] = speed <br>
* data[3] = pace <br>
* data[4] = heart rate <br>
* data[5] = cadence <br>
* data[6] = power <br>
*
* @param location the location
* @param data the data point to fill in, can be null
*/
@VisibleForTesting
void fillDataPoint(Location location, double data[]) {
double timeOrDistance = Double.NaN;
double elevation = Double.NaN;
double speed = Double.NaN;
double pace = Double.NaN;
double heartRate = Double.NaN;
double cadence = Double.NaN;
double power = Double.NaN;
// TODO: Use TripStatisticsBuilder
if (chartByDistance) {
if (lastLocation != null) {
double distance = lastLocation.distanceTo(location) * UnitConversions.M_TO_KM;
if (metricUnits) {
totalDistance += distance;
} else {
totalDistance += distance * UnitConversions.KM_TO_MI;
}
}
timeOrDistance = totalDistance;
} else {
if (startTime == -1L) {
startTime = location.getTime();
}
timeOrDistance = location.getTime() - startTime;
}
elevationBuffer.setNext(metricUnits ? location.getAltitude() : location.getAltitude()
* UnitConversions.M_TO_FT);
elevation = elevationBuffer.getAverage();
if (lastLocation == null) {
if (Math.abs(location.getSpeed() - INVALID_SPEED) > 1) {
speedBuffer.setNext(location.getSpeed());
}
} else if (TripStatisticsBuilder.isValidSpeed(location.getTime(), location.getSpeed(),
lastLocation.getTime(), lastLocation.getSpeed(), speedBuffer)
&& (location.getSpeed() <= trackMaxSpeed)) {
speedBuffer.setNext(location.getSpeed());
}
speed = speedBuffer.getAverage() * UnitConversions.MS_TO_KMH;
if (!metricUnits) {
speed *= UnitConversions.KM_TO_MI;
}
pace = speed == 0 ? 0.0 : 60.0 / speed;
if (location instanceof MyTracksLocation
&& ((MyTracksLocation) location).getSensorDataSet() != null) {
SensorDataSet sensorDataSet = ((MyTracksLocation) location).getSensorDataSet();
if (sensorDataSet.hasHeartRate()
&& sensorDataSet.getHeartRate().getState() == Sensor.SensorState.SENDING
&& sensorDataSet.getHeartRate().hasValue()) {
heartRate = sensorDataSet.getHeartRate().getValue();
}
if (sensorDataSet.hasCadence()
&& sensorDataSet.getCadence().getState() == Sensor.SensorState.SENDING
&& sensorDataSet.getCadence().hasValue()) {
cadence = sensorDataSet.getCadence().getValue();
}
if (sensorDataSet.hasPower()
&& sensorDataSet.getPower().getState() == Sensor.SensorState.SENDING
&& sensorDataSet.getPower().hasValue()) {
power = sensorDataSet.getPower().getValue();
}
}
if (data != null) {
data[0] = timeOrDistance;
data[1] = elevation;
data[2] = speed;
data[3] = pace;
data[4] = heartRate;
data[5] = cadence;
data[6] = power;
}
lastLocation = location;
}
@VisibleForTesting
ChartView getChartView() {
return chartView;
}
@VisibleForTesting
void setChartView(ChartView view) {
chartView = view;
}
@VisibleForTesting
void setTrackMaxSpeed(double value) {
trackMaxSpeed = value;
}
@VisibleForTesting
void setMetricUnits(boolean value) {
metricUnits = value;
}
@VisibleForTesting
void setReportSpeed(boolean value) {
reportSpeed = value;
}
@VisibleForTesting
void setChartByDistance(boolean value) {
chartByDistance = value;
}
}
@@ -0,0 +1,77 @@
/*
* 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.fragments;
import com.google.android.apps.mytracks.TrackListActivity;
import com.google.android.apps.mytracks.util.EulaUtils;
import com.google.android.apps.mytracks.util.PreferencesUtils;
import com.google.android.maps.mytracks.R;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.DialogInterface;
import android.os.Bundle;
import android.support.v4.app.DialogFragment;
import java.util.Locale;
/**
* A DialogFragment to check preferred units.
*
* @author Jimmy Shih
*/
public class CheckUnitsDialogFragment extends DialogFragment {
public static final String CHECK_UNITS_DIALOG_TAG = "checkUnitsDialog";
@Override
public void onCancel(DialogInterface arg0) {
onDone();
}
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
Locale defaultLocale = Locale.getDefault();
boolean defaultMetric = !defaultLocale.equals(Locale.US) && !defaultLocale.equals(Locale.UK);
PreferencesUtils.setBoolean(getActivity(), R.string.metric_units_key, defaultMetric);
final String metric = getString(R.string.settings_stats_units_metric);
final String imperial = getString(R.string.settings_stats_units_imperial);
final CharSequence[] items = defaultMetric ? new CharSequence[] { metric, imperial }
: new CharSequence[] { imperial, metric };
return new AlertDialog.Builder(getActivity())
.setPositiveButton(R.string.generic_ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
int position = ((AlertDialog) dialog).getListView().getCheckedItemPosition();
PreferencesUtils.setBoolean(
getActivity(), R.string.metric_units_key, items[position].equals(metric));
onDone();
}
})
.setSingleChoiceItems(items, 0, null)
.setTitle(R.string.settings_stats_units_title).create();
}
/**
* Tasks to perform when done.
*/
private void onDone() {
EulaUtils.setShowCheckUnits(getActivity());
TrackListActivity trackListActivity = (TrackListActivity) getActivity();
trackListActivity.showStartupDialogs();
}
}
@@ -0,0 +1,260 @@
/*
* 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.fragments;
import com.google.android.apps.mytracks.io.sendtogoogle.SendRequest;
import com.google.android.apps.mytracks.io.sendtogoogle.ConfirmSharingActivity;
import com.google.android.apps.mytracks.util.IntentUtils;
import com.google.android.maps.mytracks.R;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.support.v4.app.DialogFragment;
import android.support.v4.app.ShareCompat;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.TextView;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
/**
* A DialogFragment to choose an activity.
*
* @author Jimmy Shih
*/
public class ChooseActivityDialogFragment extends DialogFragment {
public static final String CHOOSE_ACTIVITY_DIALOG_TAG = "chooseActivityDialog";
private static final String KEY_TRACK_ID = "trackId";
private static final String KEY_TRACK_URL = "trackUrl";
private PackageManager packageManager;
public static ChooseActivityDialogFragment newInstance(long trackId, String trackUrl) {
Bundle bundle = new Bundle();
bundle.putLong(KEY_TRACK_ID, trackId);
bundle.putString(KEY_TRACK_URL, trackUrl);
ChooseActivityDialogFragment chooseActivityDialogFragment = new ChooseActivityDialogFragment();
chooseActivityDialogFragment.setArguments(bundle);
return chooseActivityDialogFragment;
}
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
packageManager = getActivity().getPackageManager();
List<DisplayInfo> displayInfos = getDisplayInfos();
ArrayAdapter<DisplayInfo> arrayAdapter = new ArrayAdapter<DisplayInfo>(getActivity(),
R.layout.choose_activity_list_item, R.id.choose_activity_list_item_text1, displayInfos) {
@Override
public View getView(int position, View convertView, ViewGroup parent) {
View view;
if (convertView == null) {
view = getActivity()
.getLayoutInflater().inflate(R.layout.choose_activity_list_item, parent, false);
} else {
view = convertView;
}
DisplayInfo displayInfo = getItem(position);
TextView text1 = (TextView) view.findViewById(R.id.choose_activity_list_item_text1);
TextView text2 = (TextView) view.findViewById(R.id.choose_activity_list_item_text2);
ImageView icon = (ImageView) view.findViewById(R.id.choose_activity_list_item_icon);
text1.setText(displayInfo.primaryLabel);
if (displayInfo.secondaryLabel != null) {
text2.setVisibility(View.VISIBLE);
text2.setText(displayInfo.secondaryLabel);
} else {
text2.setVisibility(View.GONE);
}
icon.setImageDrawable(displayInfo.icon);
return view;
}
};
return new AlertDialog.Builder(getActivity())
.setSingleChoiceItems(arrayAdapter, 0, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
AlertDialog alertDialog = (AlertDialog) dialog;
DisplayInfo displayInfo = (DisplayInfo) alertDialog.getListView()
.getItemAtPosition(which);
ActivityInfo activityInfo = displayInfo.resolveInfo.activityInfo;
String packageName = activityInfo.applicationInfo.packageName;
String className = activityInfo.name;
long trackId = getArguments().getLong(KEY_TRACK_ID);
String trackUrl = getArguments().getString(KEY_TRACK_URL);
if (trackUrl == null) {
SendRequest sendRequest = new SendRequest(trackId);
sendRequest.setSendMaps(true);
sendRequest.setNewMap(true);
sendRequest.setSharingAppPackageName(packageName);
sendRequest.setSharingAppClassName(className);
Intent intent = IntentUtils.newIntent(getActivity(), ConfirmSharingActivity.class)
.putExtra(SendRequest.SEND_REQUEST_KEY, sendRequest);
startActivity(intent);
dismiss();
} else {
Intent intent = IntentUtils.newShareUrlIntent(
getActivity(), trackId, trackUrl, packageName, className);
startActivity(intent);
getActivity().finish();
}
}
})
.setTitle(R.string.share_track_picker_title)
.create();
}
/**
* Gets the display info.
*/
private List<DisplayInfo> getDisplayInfos() {
List<DisplayInfo> displayInfos = new ArrayList<DisplayInfo>();
Intent intent = ShareCompat.IntentBuilder.from(getActivity())
.setType(IntentUtils.TEXT_PLAIN_TYPE).getIntent();
List<ResolveInfo> resolveInfos = packageManager.queryIntentActivities(
intent, PackageManager.MATCH_DEFAULT_ONLY);
if (resolveInfos != null && resolveInfos.size() > 0) {
int size = resolveInfos.size();
ResolveInfo firstResolveInfo = resolveInfos.get(0);
for (int i = 1; i < size; i++) {
ResolveInfo resolveInfo = resolveInfos.get(i);
if (firstResolveInfo.priority != resolveInfo.priority
|| firstResolveInfo.isDefault != resolveInfo.isDefault) {
while (i < size) {
resolveInfos.remove(i);
size--;
}
}
}
if (size > 1) {
ResolveInfo.DisplayNameComparator displayNameComparator = new ResolveInfo.DisplayNameComparator(
packageManager);
Collections.sort(resolveInfos, displayNameComparator);
}
firstResolveInfo = resolveInfos.get(0);
int start = 0;
CharSequence firstLabel = firstResolveInfo.loadLabel(packageManager);
for (int i = 1; i < size; i++) {
if (firstLabel == null) {
firstLabel = firstResolveInfo.activityInfo.packageName;
}
ResolveInfo resolveInfo = resolveInfos.get(i);
CharSequence label = resolveInfo.loadLabel(packageManager);
if (label == null) {
label = resolveInfo.activityInfo.packageName;
}
if (label.equals(firstLabel)) {
continue;
}
processGroup(resolveInfos, displayInfos, start, i - 1);
firstResolveInfo = resolveInfo;
firstLabel = label;
start = i;
}
// Process last group
processGroup(resolveInfos, displayInfos, start, size - 1);
}
return displayInfos;
}
/**
* Contains display info.
*
* @author Jimmy Shih
*/
private final class DisplayInfo {
private ResolveInfo resolveInfo;
private CharSequence primaryLabel;
private CharSequence secondaryLabel;
private Drawable icon;
public DisplayInfo(ResolveInfo resolveInfo, CharSequence primaryLabel,
CharSequence secondaryLabel, Drawable icon) {
this.resolveInfo = resolveInfo;
this.primaryLabel = primaryLabel;
this.secondaryLabel = secondaryLabel;
this.icon = icon;
}
}
/**
* Processes a group of items with the same label.
*
* @param resolveInfos list of resolve infos
* @param displayInfos list of display infos
* @param start start index
* @param end end index
*/
private void processGroup(
List<ResolveInfo> resolveInfos, List<DisplayInfo> displayInfos, int start, int end) {
ResolveInfo startResolveInfo = resolveInfos.get(start);
CharSequence primaryLabel = startResolveInfo.loadLabel(packageManager);
Drawable icon = startResolveInfo.loadIcon(packageManager);
int num = end - start + 1;
if (num == 1) {
// Only one, set the secondary label to null
displayInfos.add(new DisplayInfo(startResolveInfo, primaryLabel, null, icon));
} else {
// Decide package name or application name for the secondary label
boolean usePackageName = false;
CharSequence appName = startResolveInfo.activityInfo.applicationInfo.loadLabel(
packageManager);
if (appName == null) {
usePackageName = true;
} else {
// Use HashSet to track duplicates
HashSet<CharSequence> duplicates = new HashSet<CharSequence>();
duplicates.add(appName);
for (int i = start + 1; i <= end; i++) {
ResolveInfo resolveInfo = resolveInfos.get(i);
CharSequence name = resolveInfo.activityInfo.applicationInfo.loadLabel(packageManager);
if ((name == null) || (duplicates.contains(name))) {
usePackageName = true;
break;
} else {
duplicates.add(name);
}
}
// Clear HashSet for later use
duplicates.clear();
}
for (int i = start; i <= end; i++) {
ResolveInfo resolveInfo = resolveInfos.get(i);
CharSequence secondaryLabel = usePackageName ? resolveInfo.activityInfo.packageName
: resolveInfo.activityInfo.applicationInfo.loadLabel(packageManager);
displayInfos.add(new DisplayInfo(resolveInfo, primaryLabel, secondaryLabel, icon));
}
}
}
}
@@ -0,0 +1,174 @@
/*
* 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.fragments;
import com.google.android.apps.mytracks.io.sendtogoogle.AccountChooserActivity;
import com.google.android.apps.mytracks.io.sendtogoogle.SendRequest;
import com.google.android.apps.mytracks.util.AnalyticsUtils;
import com.google.android.apps.mytracks.util.IntentUtils;
import com.google.android.apps.mytracks.util.PreferencesUtils;
import com.google.android.maps.mytracks.R;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
import android.support.v4.app.DialogFragment;
import android.view.View;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.CompoundButton.OnCheckedChangeListener;
import android.widget.RadioButton;
import android.widget.TableRow;
import android.widget.Toast;
/**
* A DialogFragment to choose upload service.
*
* @author Jimmy Shih
*/
public class ChooseUploadServiceDialogFragment extends DialogFragment {
public static final String CHOOSE_UPLOAD_SERVICE_DIALOG_TAG = "chooseUploadService";
private static final String KEY_SEND_REQUEST = "sendRequest";
public static ChooseUploadServiceDialogFragment newInstance(SendRequest sendRequest) {
Bundle bundle = new Bundle();
bundle.putParcelable(KEY_SEND_REQUEST, sendRequest);
ChooseUploadServiceDialogFragment chooseUploadServiceDialogFragment =
new ChooseUploadServiceDialogFragment();
chooseUploadServiceDialogFragment.setArguments(bundle);
return chooseUploadServiceDialogFragment;
}
private SendRequest sendRequest;
private CheckBox mapsCheckBox;
private CheckBox fusionTablesCheckBox;
private CheckBox docsCheckBox;
private TableRow mapsOptionTableRow;
private RadioButton existingMapRadioButton;
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
sendRequest = getArguments().getParcelable(KEY_SEND_REQUEST);
View view = getActivity().getLayoutInflater().inflate(R.layout.choose_upload_service, null);
mapsCheckBox = (CheckBox) view.findViewById(R.id.choose_upload_service_maps);
fusionTablesCheckBox = (CheckBox) view.findViewById(R.id.choose_upload_service_fusion_tables);
docsCheckBox = (CheckBox) view.findViewById(R.id.choose_upload_service_docs);
mapsCheckBox.setChecked(PreferencesUtils.getBoolean(
getActivity(), R.string.send_to_maps_key, PreferencesUtils.SEND_TO_MAPS_DEFAULT));
fusionTablesCheckBox.setChecked(PreferencesUtils.getBoolean(
getActivity(), R.string.send_to_fusion_tables_key,
PreferencesUtils.SEND_TO_FUSION_TABLES_DEFAULT));
docsCheckBox.setChecked(PreferencesUtils.getBoolean(
getActivity(), R.string.send_to_docs_key, PreferencesUtils.SEND_TO_DOCS_DEFAULT));
mapsCheckBox.setOnCheckedChangeListener(new OnCheckedChangeListener() {
public void onCheckedChanged(CompoundButton button, boolean checked) {
updateMapsOption();
}
});
mapsOptionTableRow = (TableRow) view.findViewById(R.id.choose_upload_service_maps_options);
RadioButton newMapRadioButton = (RadioButton) view.findViewById(
R.id.choose_upload_service_new_map);
boolean defaultMapPublic = PreferencesUtils.getBoolean(getActivity(),
R.string.default_map_public_key, PreferencesUtils.DEFAULT_MAP_PUBLIC_DEFAULT);
newMapRadioButton.setText(defaultMapPublic ? R.string.send_google_new_public_map
: R.string.send_google_new_unlisted_map);
existingMapRadioButton = (RadioButton) view.findViewById(
R.id.choose_upload_service_existing_map);
updateMapsOption();
if (PreferencesUtils.getBoolean(getActivity(), R.string.pick_existing_map_key,
PreferencesUtils.PICK_EXISTING_MAP_DEFAULT)) {
existingMapRadioButton.setChecked(true);
} else {
newMapRadioButton.setChecked(true);
}
return new AlertDialog.Builder(getActivity()).setNegativeButton(R.string.generic_cancel, null)
.setPositiveButton(R.string.send_google_send_now, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
PreferencesUtils.setBoolean(
getActivity(), R.string.pick_existing_map_key, existingMapRadioButton.isChecked());
PreferencesUtils.setBoolean(
getActivity(), R.string.send_to_maps_key, mapsCheckBox.isChecked());
PreferencesUtils.setBoolean(getActivity(), R.string.send_to_fusion_tables_key,
fusionTablesCheckBox.isChecked());
PreferencesUtils.setBoolean(
getActivity(), R.string.send_to_docs_key, docsCheckBox.isChecked());
if (mapsCheckBox.isChecked() || fusionTablesCheckBox.isChecked()
|| docsCheckBox.isChecked()) {
startNextActivity();
} else {
Toast.makeText(
getActivity(), R.string.send_google_no_service_selected, Toast.LENGTH_LONG)
.show();
}
}
})
.setTitle(R.string.send_google_title)
.setView(view)
.create();
}
/**
* Updates map option.
*/
private void updateMapsOption() {
mapsOptionTableRow.setVisibility(mapsCheckBox.isChecked() ? View.VISIBLE : View.GONE);
}
/**
* Starts the next activity, {@link AccountChooserActivity}.
*/
private void startNextActivity() {
sendRequest.setSendMaps(mapsCheckBox.isChecked());
sendRequest.setSendFusionTables(fusionTablesCheckBox.isChecked());
sendRequest.setSendDocs(docsCheckBox.isChecked());
sendRequest.setNewMap(!existingMapRadioButton.isChecked());
sendStats();
Intent intent = IntentUtils.newIntent(getActivity(), AccountChooserActivity.class)
.putExtra(SendRequest.SEND_REQUEST_KEY, sendRequest);
startActivity(intent);
}
/**
* Sends stats to Google Analytics.
*/
private void sendStats() {
if (sendRequest.isSendMaps()) {
AnalyticsUtils.sendPageViews(getActivity(), "/send/maps");
}
if (sendRequest.isSendFusionTables()) {
AnalyticsUtils.sendPageViews(getActivity(), "/send/fusion_tables");
}
if (sendRequest.isSendDocs()) {
AnalyticsUtils.sendPageViews(getActivity(), "/send/docs");
}
}
}
@@ -0,0 +1,52 @@
/*
* 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.fragments;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.util.DialogUtils;
import com.google.android.maps.mytracks.R;
import android.app.Dialog;
import android.content.DialogInterface;
import android.os.Bundle;
import android.support.v4.app.DialogFragment;
/**
* A DialogFragment to delete all tracks.
*
* @author Jimmy Shih
*/
public class DeleteAllTrackDialogFragment extends DialogFragment {
public static final String DELETE_ALL_TRACK_DIALOG_TAG = "deleteAllTrackDialog";
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
return DialogUtils.createConfirmationDialog(getActivity(),
R.string.track_list_delete_all_confirm_message, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
new Thread(new Runnable() {
@Override
public void run() {
MyTracksProviderUtils.Factory.get(getActivity()).deleteAllTracks();
}
}).start();
}
});
}
}
@@ -0,0 +1,75 @@
/*
* 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.fragments;
import com.google.android.apps.mytracks.MarkerListActivity;
import com.google.android.apps.mytracks.content.DescriptionGeneratorImpl;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.util.DialogUtils;
import com.google.android.apps.mytracks.util.IntentUtils;
import com.google.android.maps.mytracks.R;
import android.app.Dialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
import android.support.v4.app.DialogFragment;
/**
* A DialogFragment to delete one marker.
*
* @author Jimmy Shih
*/
public class DeleteOneMarkerDialogFragment extends DialogFragment {
public static final String DELETE_ONE_MARKER_DIALOG_TAG = "deleteOneMarkerDialog";
private static final String KEY_MARKER_ID = "markerId";
private static final String KEY_TRACK_ID = "trackId";
public static DeleteOneMarkerDialogFragment newInstance(long markerId, long trackId) {
Bundle bundle = new Bundle();
bundle.putLong(KEY_MARKER_ID, markerId);
bundle.putLong(KEY_TRACK_ID, trackId);
DeleteOneMarkerDialogFragment deleteOneMarkerDialogFragment = new DeleteOneMarkerDialogFragment();
deleteOneMarkerDialogFragment.setArguments(bundle);
return deleteOneMarkerDialogFragment;
}
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
return DialogUtils.createConfirmationDialog(getActivity(),
R.string.marker_delete_one_marker_confirm_message, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
new Thread(new Runnable() {
@Override
public void run() {
MyTracksProviderUtils.Factory.get(getActivity()).deleteWaypoint(
getArguments().getLong(KEY_MARKER_ID),
new DescriptionGeneratorImpl(getActivity()));
}
}).start();
Intent intent = IntentUtils.newIntent(getActivity(), MarkerListActivity.class)
.putExtra(MarkerListActivity.EXTRA_TRACK_ID, getArguments().getLong(KEY_TRACK_ID));
startActivity(intent);
// Close the activity since its content can change after delete.
getActivity().finish();
}
});
}
}
@@ -0,0 +1,101 @@
/*
* 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.fragments;
import com.google.android.apps.mytracks.TrackListActivity;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.services.TrackRecordingServiceConnection;
import com.google.android.apps.mytracks.util.DialogUtils;
import com.google.android.apps.mytracks.util.IntentUtils;
import com.google.android.apps.mytracks.util.PreferencesUtils;
import com.google.android.apps.mytracks.util.TrackRecordingServiceConnectionUtils;
import com.google.android.maps.mytracks.R;
import android.app.Activity;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
import android.support.v4.app.DialogFragment;
/**
* A DialogFragment to delete one track.
*
* @author Jimmy Shih
*/
public class DeleteOneTrackDialogFragment extends DialogFragment {
public static final String DELETE_ONE_TRACK_DIALOG_TAG = "deleteOneTrackDialog";
private static final String KEY_TRACK_ID = "trackId";
/**
* Interface for caller of this dialog fragment.
*
* @author Jimmy Shih
*/
public interface DeleteOneTrackCaller {
public TrackRecordingServiceConnection getTrackRecordingServiceConnection();
}
private DeleteOneTrackCaller caller;
public static DeleteOneTrackDialogFragment newInstance(long trackId) {
Bundle bundle = new Bundle();
bundle.putLong(KEY_TRACK_ID, trackId);
DeleteOneTrackDialogFragment deleteOneTrackDialogFragment = new DeleteOneTrackDialogFragment();
deleteOneTrackDialogFragment.setArguments(bundle);
return deleteOneTrackDialogFragment;
}
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
try {
caller = (DeleteOneTrackCaller) activity;
} catch (ClassCastException e) {
throw new ClassCastException(activity.toString() + " must implement DeleteOneTrackCaller");
}
}
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
return DialogUtils.createConfirmationDialog(getActivity(),
R.string.track_detail_delete_confirm_message, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
final long trackId = getArguments().getLong(KEY_TRACK_ID);
final Context context = getActivity();
if (trackId == PreferencesUtils.getLong(context, R.string.recording_track_id_key)) {
TrackRecordingServiceConnectionUtils.stop(
context, caller.getTrackRecordingServiceConnection(), false);
}
new Thread(new Runnable() {
@Override
public void run() {
MyTracksProviderUtils.Factory.get(context).deleteTrack(trackId);
}
}).start();
Intent intent = IntentUtils.newIntent(context, TrackListActivity.class);
startActivity(intent);
// Close the activity since its content can change after delete
getActivity().finish();
}
});
}
}
@@ -0,0 +1,115 @@
/*
* 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.fragments;
import com.google.android.apps.mytracks.TrackListActivity;
import com.google.android.apps.mytracks.util.EulaUtils;
import com.google.android.maps.mytracks.R;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.DialogInterface;
import android.os.Bundle;
import android.support.v4.app.DialogFragment;
/**
* A DialogFragment to show EULA.
*
* @author Jimmy Shih
*/
public class EulaDialogFragment extends DialogFragment {
public static final String EULA_DIALOG_TAG = "eulaDialog";
private static final String KEY_HAS_ACCEPTED = "hasAccepted";
private static final String GOOGLE_URL = "m.google.com";
private static final String KOREAN = "ko";
/**
* Creates a new instance of {@link EulaDialogFragment}.
*
* @param hasAccepted true if the user has accepted the eula.
*/
public static EulaDialogFragment newInstance(boolean hasAccepted) {
Bundle bundle = new Bundle();
bundle.putBoolean(KEY_HAS_ACCEPTED, hasAccepted);
EulaDialogFragment eulaDialogFragment = new EulaDialogFragment();
eulaDialogFragment.setArguments(bundle);
return eulaDialogFragment;
}
@Override
public void onCancel(DialogInterface arg0) {
if (!getArguments().getBoolean(KEY_HAS_ACCEPTED)) {
exitApp();
}
}
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
boolean hasAccepted = getArguments().getBoolean(KEY_HAS_ACCEPTED);
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity())
.setMessage(getEulaText())
.setTitle(R.string.eula_title);
if (hasAccepted) {
builder.setPositiveButton(R.string.generic_ok, null);
} else {
builder.setNegativeButton(R.string.eula_decline, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
exitApp();
}
})
.setPositiveButton(R.string.eula_accept, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
EulaUtils.setAcceptEula(getActivity());
TrackListActivity trackListActivity = (TrackListActivity) getActivity();
trackListActivity.showStartupDialogs();
}
});
}
return builder.create();
}
/**
* Exits the application.
*/
private void exitApp() {
getActivity().finish();
}
/**
* Gets the EULA text.
*
*/
private String getEulaText() {
String tos = getString(R.string.eula_date)
+ "\n\n"
+ getString(R.string.eula_body, GOOGLE_URL)
+ "\n\n"
+ getString(R.string.eula_footer, GOOGLE_URL)
+ "\n\n"
+ getString(R.string.eula_copyright_year);
boolean isKorean = getResources().getConfiguration().locale.getLanguage().equals(KOREAN);
if (isKorean) {
tos += "\n\n" + getString(R.string.eula_korean);
}
return tos;
}
}
@@ -0,0 +1,118 @@
/*
* 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.fragments;
import com.google.android.apps.mytracks.util.PreferencesUtils;
import com.google.android.maps.mytracks.R;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.DialogInterface;
import android.os.Bundle;
import android.support.v4.app.DialogFragment;
/**
* A DialogFragment to configure frequency.
*
* @author Jimmy Shih
*/
public class FrequencyDialogFragment extends DialogFragment {
public static final String FREQUENCY_DIALOG_TAG = "frequencyDialog";
private static final String KEY_PREFERENCE_ID = "preferenceId";
private static final String KEY_DEFAULT_VALUE = "defaultValue";
private static final String KEY_TITLE_ID = "titleId";
public static FrequencyDialogFragment newInstance(
int preferenceId, int defaultValue, int titleId) {
Bundle bundle = new Bundle();
bundle.putInt(KEY_PREFERENCE_ID, preferenceId);
bundle.putInt(KEY_DEFAULT_VALUE, defaultValue);
bundle.putInt(KEY_TITLE_ID, titleId);
FrequencyDialogFragment frequencyDialogFragment = new FrequencyDialogFragment();
frequencyDialogFragment.setArguments(bundle);
return frequencyDialogFragment;
}
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
final int preferenceId = getArguments().getInt(KEY_PREFERENCE_ID);
int defaultValue = getArguments().getInt(KEY_DEFAULT_VALUE);
int titleId = getArguments().getInt(KEY_TITLE_ID);
int frequencyValue = PreferencesUtils.getInt(getActivity(), preferenceId, defaultValue);
return new AlertDialog.Builder(getActivity())
.setPositiveButton(R.string.generic_ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
int listIndex = ((AlertDialog) dialog).getListView().getCheckedItemPosition();
PreferencesUtils.setInt(getActivity(), preferenceId, getFrequencyValue(listIndex));
}
})
.setSingleChoiceItems(getFrequencyDisplayOptions(), getListIndex(frequencyValue), null)
.setTitle(titleId)
.create();
}
/**
* Gets the frequency display options.
*/
private String[] getFrequencyDisplayOptions() {
boolean metricUnits = PreferencesUtils.getBoolean(
getActivity(), R.string.metric_units_key, PreferencesUtils.METRIC_UNITS_DEFAULT);
String[] values = getResources().getStringArray(R.array.frequency_values);
String[] options = new String[values.length];
for (int i = 0; i < values.length; i++) {
int value = Integer.parseInt(values[i]);
if (value == PreferencesUtils.FREQUENCY_OFF) {
options[i] = getString(R.string.value_off);
} else if (value < 0) {
options[i] = getString(metricUnits ? R.string.value_integer_kilometer
: R.string.value_integer_mile, Math.abs(value));
} else {
options[i] = getString(R.string.value_integer_minute, value);
}
}
return options;
}
/**
* Gets the list index for a frequency value. Returns 0 if the value is not on
* the list.
*/
private int getListIndex(int frequencyValue) {
String[] values = getResources().getStringArray(R.array.frequency_values);
for (int i = 0; i < values.length; i++) {
if (frequencyValue == Integer.parseInt(values[i])) {
return i;
}
}
return 0;
}
/**
* Gets the frequency value from a list index.
*
* @param listIndex the list index
*/
private int getFrequencyValue(int listIndex) {
String[] values = getResources().getStringArray(R.array.frequency_values);
return Integer.parseInt(values[listIndex]);
}
}
@@ -0,0 +1,61 @@
/*
* 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.fragments;
import com.google.android.apps.mytracks.io.file.SaveActivity;
import com.google.android.maps.mytracks.R;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.ActivityNotFoundException;
import android.content.DialogInterface;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.support.v4.app.DialogFragment;
import android.widget.Toast;
/**
* A DialogFragment to install Google Earth.
*
* @author Jimmy Shih
*/
public class InstallEarthDialogFragment extends DialogFragment {
public static final String INSTALL_EARTH_DIALOG_TAG = "installEarthDialog";
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
return new AlertDialog.Builder(getActivity())
.setMessage(R.string.track_detail_install_earth_message)
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
Intent intent = new Intent().setData(Uri.parse(SaveActivity.GOOGLE_EARTH_MARKET_URL));
try {
startActivity(intent);
} catch (ActivityNotFoundException e) {
Toast.makeText(
getActivity(), R.string.track_detail_install_earth_error, Toast.LENGTH_LONG)
.show();
}
}
})
.create();
}
}
@@ -0,0 +1,545 @@
/*
* 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.fragments;
import com.google.android.apps.mytracks.MapOverlay;
import com.google.android.apps.mytracks.TrackDetailActivity;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils.Factory;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.content.TrackDataHub;
import com.google.android.apps.mytracks.content.TrackDataListener;
import com.google.android.apps.mytracks.content.TrackDataType;
import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.stats.TripStatistics;
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.maps.GeoPoint;
import com.google.android.maps.MapController;
import com.google.android.maps.MapView;
import com.google.android.maps.Overlay;
import com.google.android.maps.mytracks.R;
import android.content.Intent;
import android.location.Location;
import android.os.Bundle;
import android.provider.Settings;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageButton;
import android.widget.TextView;
import android.widget.Toast;
import java.util.EnumSet;
import java.util.List;
/**
* A fragment to display map to the user.
*
* @author Leif Hendrik Wilden
* @author Rodrigo Damazio
*/
public class MapFragment extends Fragment
implements View.OnTouchListener, View.OnClickListener, TrackDataListener {
public static final String MAP_FRAGMENT_TAG = "mapFragment";
private static final String KEY_CURRENT_LOCATION = "currentLocation";
private static final String KEY_KEEP_MY_LOCATION_VISIBLE = "keepMyLocationVisible";
private TrackDataHub trackDataHub;
// True to keep my location visible.
private boolean keepMyLocationVisible;
// True to zoom to my location. Only apply when keepMyLocationVisible is true.
private boolean zoomToMyLocation;
// The track id of the marker to show.
private long markerTrackId;
// The marker id to show
private long markerId;
// The current selected track id. Set in onSelectedTrackChanged.
private long currentSelectedTrackId;
// The current location. Set in onCurrentLocationChanged.
private Location currentLocation;
// UI elements
private View mapViewContainer;
private MapView mapView;
private MapOverlay mapOverlay;
private ImageButton myLocationImageButton;
private TextView messageTextView;
@Override
public void onCreate(Bundle bundle) {
super.onCreate(bundle);
setHasOptionsMenu(true);
}
@Override
public View onCreateView(
LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
mapViewContainer = ((TrackDetailActivity) getActivity()).getMapViewContainer();
mapView = (MapView) mapViewContainer.findViewById(R.id.map_view);
mapOverlay = new MapOverlay(getActivity());
List<Overlay> overlays = mapView.getOverlays();
overlays.clear();
overlays.add(mapOverlay);
mapView.requestFocus();
mapView.setOnTouchListener(this);
mapView.setBuiltInZoomControls(true);
myLocationImageButton = (ImageButton) mapViewContainer.findViewById(R.id.map_my_location);
myLocationImageButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
showMyLocation();
}
});
messageTextView = (TextView) mapViewContainer.findViewById(R.id.map_message);
ApiAdapterFactory.getApiAdapter().invalidMenu(getActivity());
return mapViewContainer;
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
if (savedInstanceState != null) {
keepMyLocationVisible = savedInstanceState.getBoolean(KEY_KEEP_MY_LOCATION_VISIBLE, false);
currentLocation = (Location) savedInstanceState.getParcelable(KEY_CURRENT_LOCATION);
if (currentLocation != null) {
updateCurrentLocation();
}
}
}
@Override
public void onResume() {
super.onResume();
resumeTrackDataHub();
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putBoolean(KEY_KEEP_MY_LOCATION_VISIBLE, keepMyLocationVisible);
if (currentLocation != null) {
outState.putParcelable(KEY_CURRENT_LOCATION, currentLocation);
}
}
@Override
public void onPause() {
super.onPause();
pauseTrackDataHub();
}
@Override
public void onDestroyView() {
super.onDestroyView();
ViewGroup parentViewGroup = (ViewGroup) mapViewContainer.getParent();
if (parentViewGroup != null) {
parentViewGroup.removeView(mapViewContainer);
}
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater menuInflator) {
menuInflator.inflate(R.menu.map, menu);
}
@Override
public void onPrepareOptionsMenu(Menu menu) {
int titleId = R.string.menu_satellite_mode;
if (mapView != null) {
titleId = mapView.isSatellite() ? R.string.menu_map_mode : R.string.menu_satellite_mode;
}
menu.findItem(R.id.map_satellite_mode).setTitle(titleId);
super.onPrepareOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem menuItem) {
if (mapView != null && menuItem.getItemId() == R.id.map_satellite_mode) {
mapView.setSatellite(!mapView.isSatellite());
return true;
}
return super.onOptionsItemSelected(menuItem);
}
/**
* Shows my location.
*/
private void showMyLocation() {
updateTrackDataHub();
keepMyLocationVisible = true;
zoomToMyLocation = true;
if (currentLocation != null) {
updateCurrentLocation();
}
}
/**
* Shows the marker.
*
* @param id the marker id
*/
private void showMarker(long id) {
MyTracksProviderUtils MyTracksProviderUtils = Factory.get(getActivity());
Waypoint waypoint = MyTracksProviderUtils.getWaypoint(id);
if (waypoint != null && waypoint.getLocation() != null) {
keepMyLocationVisible = false;
GeoPoint center = new GeoPoint((int) (waypoint.getLocation().getLatitude() * 1E6),
(int) (waypoint.getLocation().getLongitude() * 1E6));
mapView.getController().setCenter(center);
mapView.getController().setZoom(mapView.getMaxZoomLevel());
mapView.invalidate();
}
}
/**
* Shows the marker.
*
* @param trackId the track id
* @param id the marker id
*/
public void showMarker(long trackId, long id) {
/*
* Synchronize to prevent race condition in changing markerTrackId and
* markerId variables.
*/
synchronized (this) {
if (trackId == currentSelectedTrackId) {
showMarker(id);
markerTrackId = -1L;
markerId = -1L;
return;
}
markerTrackId = trackId;
markerId = id;
}
}
@Override
public boolean onTouch(View view, MotionEvent event) {
if (keepMyLocationVisible && event.getAction() == MotionEvent.ACTION_MOVE) {
if (!isVisible(currentLocation)) {
/*
* Only set to false when no longer visible. Thus can keep showing the
* current location with the next location update.
*/
keepMyLocationVisible = false;
}
}
return false;
}
@Override
public void onClick(View v) {
if (v == messageTextView) {
Intent intent = new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS);
startActivity(intent);
}
}
@Override
public void onLocationStateChanged(LocationState state) {
final int messageId;
final boolean isGpsDisabled;
switch (state) {
case DISABLED:
messageId = R.string.gps_need_to_enable;
isGpsDisabled = true;
break;
case NO_FIX:
case BAD_FIX:
messageId = R.string.gps_wait_for_signal;
isGpsDisabled = false;
break;
case GOOD_FIX:
messageId = -1;
isGpsDisabled = false;
break;
default:
throw new IllegalArgumentException("Unexpected state: " + state);
}
getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
if (messageId != -1) {
messageTextView.setText(messageId);
messageTextView.setVisibility(View.VISIBLE);
if (isGpsDisabled) {
Toast.makeText(getActivity(), R.string.gps_not_found, Toast.LENGTH_LONG).show();
// Click to show the location source settings
messageTextView.setOnClickListener(MapFragment.this);
} else {
messageTextView.setOnClickListener(null);
}
} else {
messageTextView.setVisibility(View.GONE);
}
}
});
}
@Override
public void onLocationChanged(Location location) {
currentLocation = location;
updateCurrentLocation();
}
@Override
public void onHeadingChanged(double heading) {
if (mapOverlay.setHeading((float) heading)) {
mapView.postInvalidate();
}
}
@Override
public void onSelectedTrackChanged(final Track track, final boolean isRecording) {
getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
boolean hasTrack = track != null;
mapOverlay.setTrackDrawingEnabled(hasTrack);
if (hasTrack) {
synchronized (this) {
/*
* Synchronize to prevent race condition in changing markerTrackId
* and markerId variables.
*/
currentSelectedTrackId = track.getId();
updateMap(track);
}
mapOverlay.setShowEndMarker(!isRecording);
}
mapView.invalidate();
}
});
}
@Override
public void onTrackUpdated(Track track) {
// We don't care.
}
@Override
public void clearTrackPoints() {
mapOverlay.clearPoints();
}
@Override
public void onSampledInTrackPoint(Location location) {
if (LocationUtils.isValidLocation(location)) {
mapOverlay.addLocation(location);
}
}
@Override
public void onSampledOutTrackPoint(Location loc) {
// We don't care.
}
@Override
public void onSegmentSplit() {
mapOverlay.addSegmentSplit();
}
@Override
public void onNewTrackPointsDone() {
mapView.postInvalidate();
}
@Override
public void clearWaypoints() {
mapOverlay.clearWaypoints();
}
@Override
public void onNewWaypoint(Waypoint waypoint) {
if (waypoint != null && LocationUtils.isValidLocation(waypoint.getLocation())) {
// TODO: Optimize locking inside addWaypoint
mapOverlay.addWaypoint(waypoint);
}
}
@Override
public void onNewWaypointsDone() {
mapView.postInvalidate();
}
@Override
public boolean onMetricUnitsChanged(boolean metric) {
// We don't care.
return false;
}
@Override
public boolean onReportSpeedChanged(boolean reportSpeed) {
// We don't care.
return false;
}
/**
* Resumes the trackDataHub. Needs to be synchronized because trackDataHub can be
* accessed by multiple threads.
*/
private synchronized void resumeTrackDataHub() {
trackDataHub = ((TrackDetailActivity) getActivity()).getTrackDataHub();
trackDataHub.registerTrackDataListener(this, EnumSet.of(
TrackDataType.SELECTED_TRACK,
TrackDataType.WAYPOINTS_TABLE,
TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE,
TrackDataType.LOCATION,
TrackDataType.HEADING));
}
/**
* Pauses the trackDataHub. Needs to be synchronized because trackDataHub can be
* accessed by multiple threads.
*/
private synchronized void pauseTrackDataHub() {
trackDataHub.unregisterTrackDataListener(this);
trackDataHub = null;
}
/**
* Updates the trackDataHub. Needs to be synchronized because trackDataHub can be
* accessed by multiple threads.
*/
private synchronized void updateTrackDataHub() {
if (trackDataHub != null) {
trackDataHub.forceUpdateLocation();
}
}
/**
* Updates the map by either zooming to the requested marker or showing the track.
*
* @param track the track
*/
private void updateMap(Track track) {
if (track.getId() == markerTrackId) {
// Show the marker
showMarker(markerId);
markerTrackId = -1L;
markerId = -1L;
} else {
// Show the track
showTrack(track);
}
}
/**
* Returns true if the location is visible.
*
* @param location the location
*/
private boolean isVisible(Location location) {
if (location == null || mapView == null) {
return false;
}
GeoPoint mapCenter = mapView.getMapCenter();
int latitudeSpan = mapView.getLatitudeSpan();
int longitudeSpan = mapView.getLongitudeSpan();
/*
* The bottom of the mapView is obscured by the zoom controls, subtract its
* height from the visible area.
*/
GeoPoint zoomControlBottom = mapView.getProjection().fromPixels(0, mapView.getHeight());
GeoPoint zoomControlTop = mapView.getProjection().fromPixels(
0, mapView.getHeight() - mapView.getZoomButtonsController().getZoomControls().getHeight());
int zoomControlMargin = Math.abs(zoomControlTop.getLatitudeE6()
- zoomControlBottom.getLatitudeE6());
GeoRect geoRect = new GeoRect(mapCenter, latitudeSpan, longitudeSpan);
geoRect.top += zoomControlMargin;
GeoPoint geoPoint = LocationUtils.getGeoPoint(location);
return geoRect.contains(geoPoint);
}
/**
* Updates the current location and centers it if necessary.
*/
private void updateCurrentLocation() {
if (mapOverlay == null || mapView == null) {
return;
}
mapOverlay.setMyLocation(currentLocation);
mapView.postInvalidate();
if (currentLocation != null && keepMyLocationVisible && !isVisible(currentLocation)) {
GeoPoint geoPoint = LocationUtils.getGeoPoint(currentLocation);
MapController mapController = mapView.getController();
mapController.animateTo(geoPoint);
if (zoomToMyLocation) {
// Only zoom in the first time we show the location.
zoomToMyLocation = false;
if (mapView.getZoomLevel() < mapView.getMaxZoomLevel()) {
mapController.setZoom(mapView.getMaxZoomLevel());
}
}
}
}
/**
* Shows the track.
*
* @param track the track
*/
private void showTrack(Track track) {
if (mapView == null || track == null || track.getNumberOfPoints() < 2) {
return;
}
TripStatistics tripStatistics = track.getTripStatistics();
int bottom = tripStatistics.getBottom();
int left = tripStatistics.getLeft();
int latitudeSpanE6 = tripStatistics.getTop() - bottom;
int longitudeSpanE6 = tripStatistics.getRight() - left;
if (latitudeSpanE6 > 0 && latitudeSpanE6 < 180E6 && longitudeSpanE6 > 0
&& longitudeSpanE6 < 360E6) {
keepMyLocationVisible = false;
GeoPoint center = new GeoPoint(bottom + latitudeSpanE6 / 2, left + longitudeSpanE6 / 2);
if (LocationUtils.isValidGeoPoint(center)) {
mapView.getController().setCenter(center);
mapView.getController().zoomToSpan(latitudeSpanE6, longitudeSpanE6);
}
}
}
}
@@ -0,0 +1,290 @@
/*
* 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.fragments;
import com.google.android.apps.mytracks.TrackDetailActivity;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.content.TrackDataHub;
import com.google.android.apps.mytracks.content.TrackDataListener;
import com.google.android.apps.mytracks.content.TrackDataType;
import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.stats.TripStatistics;
import com.google.android.apps.mytracks.util.PreferencesUtils;
import com.google.android.apps.mytracks.util.StatsUtils;
import com.google.android.maps.mytracks.R;
import android.location.Location;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import java.util.EnumSet;
/**
* A fragment to display track statistics to the user.
*
* @author Sandor Dornbush
* @author Rodrigo Damazio
*/
public class StatsFragment extends Fragment implements TrackDataListener {
public static final String STATS_FRAGMENT_TAG = "statsFragment";
private static final String TAG = StatsFragment.class.getSimpleName();
private TrackDataHub trackDataHub;
private UiUpdateThread uiUpdateThread;
// The start time of the current track.
private long startTime = -1L;
private Location lastLocation = null;
private TripStatistics lastTripStatistics = null;
// A runnable to update the total time field.
private final Runnable updateTotalTime = new Runnable() {
public void run() {
if (isSelectedTrackRecording()) {
StatsUtils.setTotalTimeValue(getActivity(), System.currentTimeMillis() - startTime);
}
}
};
/**
* A thread that updates the total time field every second.
*/
private class UiUpdateThread extends Thread {
@Override
public void run() {
Log.d(TAG, "UI update thread started");
while (PreferencesUtils.getLong(getActivity(), R.string.recording_track_id_key)
!= PreferencesUtils.RECORDING_TRACK_ID_DEFAULT) {
getActivity().runOnUiThread(updateTotalTime);
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
Log.d(TAG, "UI update thread caught exception", e);
break;
}
}
Log.d(TAG, "UI update thread finished");
}
}
@Override
public View onCreateView(
LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.stats, container, false);
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
updateUi();
}
@Override
public void onResume() {
super.onResume();
resumeTrackDataHub();
}
@Override
public void onPause() {
super.onPause();
pauseTrackDataHub();
if (uiUpdateThread != null) {
uiUpdateThread.interrupt();
uiUpdateThread = null;
}
}
@Override
public void onLocationStateChanged(LocationState state) {
if (isResumed() && (state == LocationState.DISABLED || state == LocationState.NO_FIX)) {
getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
lastLocation = null;
StatsUtils.setLocationValues(getActivity(), lastLocation, true);
}
});
}
}
@Override
public void onLocationChanged(final Location location) {
if (isResumed() && isSelectedTrackRecording()) {
getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
lastLocation = location;
StatsUtils.setLocationValues(getActivity(), lastLocation, true);
}
});
}
}
@Override
public void onHeadingChanged(double heading) {
// We don't care.
}
@Override
public void onSelectedTrackChanged(Track track, boolean isRecording) {
if (isResumed()) {
if (uiUpdateThread == null && isRecording) {
uiUpdateThread = new UiUpdateThread();
uiUpdateThread.start();
} else if (uiUpdateThread != null && !isRecording) {
uiUpdateThread.interrupt();
uiUpdateThread = null;
}
}
}
@Override
public void onTrackUpdated(final Track track) {
if (isResumed()) {
getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
if (track == null || track.getTripStatistics() == null) {
lastLocation = null;
lastTripStatistics = null;
updateUi();
return;
}
lastTripStatistics = track.getTripStatistics();
startTime = track.getTripStatistics().getStartTime();
if (!isSelectedTrackRecording()) {
lastLocation = null;
}
updateUi();
}
});
}
}
@Override
public void clearTrackPoints() {
// We don't care.
}
@Override
public void onSampledInTrackPoint(Location loc) {
// We don't care.
}
@Override
public void onSampledOutTrackPoint(Location loc) {
// We don't care.
}
@Override
public void onSegmentSplit() {
// We don't care.
}
@Override
public void onNewTrackPointsDone() {
// We don't care.
}
@Override
public void clearWaypoints() {
// We don't care.
}
@Override
public void onNewWaypoint(Waypoint wpt) {
// We don't care.
}
@Override
public void onNewWaypointsDone() {
// We don't care.
}
@Override
public boolean onMetricUnitsChanged(final boolean metric) {
if (isResumed()) {
getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
updateUi();
}
});
}
return true;
}
@Override
public boolean onReportSpeedChanged(final boolean speed) {
if (isResumed()) {
getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
updateUi();
}
});
}
return true;
}
/**
* Resumes the trackDataHub. Needs to be synchronized because trackDataHub can
* be accessed by multiple threads.
*/
private synchronized void resumeTrackDataHub() {
trackDataHub = ((TrackDetailActivity) getActivity()).getTrackDataHub();
trackDataHub.registerTrackDataListener(this, EnumSet.of(
TrackDataType.SELECTED_TRACK,
TrackDataType.TRACKS_TABLE,
TrackDataType.LOCATION,
TrackDataType.PREFERENCE));
}
/**
* Pauses the trackDataHub. Needs to be synchronized because trackDataHub can
* be accessed by multiple threads.
*/
private synchronized void pauseTrackDataHub() {
trackDataHub.unregisterTrackDataListener(this);
trackDataHub = null;
}
/**
* Returns true if the selected track is recording. Needs to be synchronized
* because trackDataHub can be accessed by multiple threads.
*/
private synchronized boolean isSelectedTrackRecording() {
return trackDataHub != null && trackDataHub.isSelectedTrackRecording();
}
/**
* Updates the UI.
*/
private void updateUi() {
StatsUtils.setTripStatisticsValues(getActivity(), lastTripStatistics);
StatsUtils.setLocationValues(getActivity(), lastLocation, true);
}
}
@@ -0,0 +1,62 @@
/*
* Copyright 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.fragments;
import com.google.android.apps.mytracks.TrackListActivity;
import com.google.android.apps.mytracks.util.EulaUtils;
import com.google.android.maps.mytracks.R;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.DialogInterface;
import android.os.Bundle;
import android.support.v4.app.DialogFragment;
/**
* A DialogFrament to show the welcome info.
*
* @author Jimmy Shih
*/
public class WelcomeDialogFragment extends DialogFragment {
public static final String WELCOME_DIALOG_TAG = "welcomeDialog";
@Override
public void onCancel(DialogInterface arg0) {
onDone();
}
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
return new AlertDialog.Builder(getActivity())
.setPositiveButton(R.string.generic_ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
onDone();
}
})
.setTitle(R.string.welcome_title)
.setMessage(R.string.welcome_message)
.create();
}
private void onDone() {
EulaUtils.setShowWelcome(getActivity());
TrackListActivity trackListActivity = (TrackListActivity) getActivity();
trackListActivity.showStartupDialogs();
}
}
@@ -0,0 +1,43 @@
/*
* Copyright 2011 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.io.backup;
import android.annotation.TargetApi;
import android.app.backup.BackupManager;
import android.content.Context;
import android.content.SharedPreferences;
/**
* Implementation of {@link BackupPreferencesListener} that calls the
* {@link BackupManager}. <br>
* For API Level 8 or higher.
*
* @author Jimmy Shih
*/
@TargetApi(8)
public class Api8BackupPreferencesListener implements BackupPreferencesListener {
private final BackupManager backupManager;
public Api8BackupPreferencesListener(Context context) {
this.backupManager = new BackupManager(context);
}
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
backupManager.dataChanged();
}
}
@@ -0,0 +1,91 @@
/*
* Copyright 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.io.backup;
import com.google.android.apps.mytracks.util.DialogUtils;
import com.google.android.maps.mytracks.R;
import android.app.Activity;
import android.app.Dialog;
import android.content.DialogInterface;
import android.os.Bundle;
import android.widget.Toast;
/**
* Activity to backup data to the SD card.
*
* @author Jimmy Shih
*/
public class BackupActivity extends Activity {
private static final int DIALOG_PROGRESS_ID = 0;
private BackupAsyncTask backupAsyncTask;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Object retained = getLastNonConfigurationInstance();
if (retained instanceof BackupAsyncTask) {
backupAsyncTask = (BackupAsyncTask) retained;
backupAsyncTask.setActivity(this);
} else {
backupAsyncTask = new BackupAsyncTask(this);
backupAsyncTask.execute();
}
}
@Override
public Object onRetainNonConfigurationInstance() {
backupAsyncTask.setActivity(null);
return backupAsyncTask;
}
@Override
protected Dialog onCreateDialog(int id) {
if (id != DIALOG_PROGRESS_ID) {
return null;
}
return DialogUtils.createSpinnerProgressDialog(
this, R.string.settings_backup_now_progress_message, new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
finish();
}
});
}
/**
* Invokes when the associated AsyncTask completes.
*
* @param success true if the AsyncTask is successful
* @param messageId message id to display to user
*/
public void onAsyncTaskCompleted(boolean success, int messageId) {
removeDialog(DIALOG_PROGRESS_ID);
Toast.makeText(this, messageId, success ? Toast.LENGTH_SHORT : Toast.LENGTH_LONG).show();
finish();
}
/**
* Shows the progress dialog.
*/
public void showProgressDialog() {
showDialog(DIALOG_PROGRESS_ID);
}
}
@@ -0,0 +1,111 @@
/*
* Copyright 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.io.backup;
import com.google.android.apps.mytracks.util.FileUtils;
import com.google.android.maps.mytracks.R;
import android.os.AsyncTask;
import android.util.Log;
import java.io.IOException;
/**
* AsyncTask to backup data to the SD card.
*
* @author Jimmy Shih
*/
public class BackupAsyncTask extends AsyncTask<Void, Integer, Boolean> {
private static final String TAG = BackupAsyncTask.class.getSimpleName();
private BackupActivity backupActivity;
private final ExternalFileBackup externalFileBackup;
// true if the AsyncTask result is success
private boolean success;
// true if the AsyncTask has completed
private boolean completed;
// message id to return to the activity
private int messageId;
/**
* Creates an AsyncTask.
*
* @param backupActivity the activity currently associated with this
* AsyncTask
*/
public BackupAsyncTask(BackupActivity backupActivity) {
this.backupActivity = backupActivity;
this.externalFileBackup = new ExternalFileBackup(backupActivity);
success = false;
completed = false;
messageId = R.string.sd_card_save_error;
}
/**
* Sets the current {@link BackupActivity} associated with this AyncTask.
*
* @param activity the current {@link BackupActivity}, can be null
*/
public void setActivity(BackupActivity activity) {
this.backupActivity = activity;
if (completed && backupActivity != null) {
backupActivity.onAsyncTaskCompleted(success, messageId);
}
}
@Override
protected void onPreExecute() {
if (backupActivity != null) {
backupActivity.showProgressDialog();
}
}
@Override
protected Boolean doInBackground(Void... params) {
if (!FileUtils.isSdCardAvailable()) {
messageId = R.string.sd_card_error_no_storage;
return false;
}
if (!externalFileBackup.isBackupsDirectoryAvailable(true)) {
messageId = R.string.sd_card_save_error_create_dir;
return false;
}
try {
externalFileBackup.writeToDefaultFile();
messageId = R.string.sd_card_save_success;
return true;
} catch (IOException e) {
Log.d(TAG, "IO exception", e);
return false;
}
}
@Override
protected void onPostExecute(Boolean result) {
success = result;
completed = true;
if (backupActivity != null) {
backupActivity.onAsyncTaskCompleted(success, messageId);
}
}
}
@@ -0,0 +1,27 @@
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.io.backup;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
/**
* Shared preferences listener which notifies the backup system about new data
* being available for backup.
*
* @author Rodrigo Damazio
*/
public interface BackupPreferencesListener extends OnSharedPreferenceChangeListener {
}
@@ -0,0 +1,248 @@
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.io.backup;
import com.google.android.apps.mytracks.content.ContentTypeIds;
import android.database.Cursor;
import android.database.MergeCursor;
import java.io.DataOutputStream;
import java.io.IOException;
/**
* Database dumper which is able to write only part of the database
* according to some query.
*
* This dumper is symmetrical to {@link DatabaseImporter}.
*
* @author Rodrigo Damazio
*/
class DatabaseDumper {
/** The names of the columns being dumped. */
private final String[] columnNames;
/** The types of the columns being dumped. */
private final byte[] columnTypes;
/** Whether to output null fields. */
private final boolean outputNullFields;
// Temporary state
private int[] columnIndices;
private boolean[] hasFields;
public DatabaseDumper(String[] columnNames, byte[] columnTypes,
boolean outputNullFields) {
if (columnNames.length != columnTypes.length) {
throw new IllegalArgumentException("Names don't match types");
}
this.columnNames = columnNames;
this.columnTypes = columnTypes;
this.outputNullFields = outputNullFields;
}
/**
* Writes the header plus all rows that can be read from the given cursor.
* This assumes the cursor will have the same column and column indices on
* every row (and thus may not work with a {@link MergeCursor}).
*/
public void writeAllRows(Cursor cursor, DataOutputStream writer)
throws IOException {
writeHeaders(cursor, cursor.getCount(), writer);
if (!cursor.moveToFirst()) {
return;
}
do {
writeOneRow(cursor, writer);
} while (cursor.moveToNext());
}
/**
* Writes just the headers for the data that will come from the given cursor.
* The headers include column information and the number of rows that will be
* written.
*
* @param cursor the cursor to get columns from
* @param numRows the number of rows that will be later written
* @param writer the output to write to
* @throws IOException if there are errors while writing
*/
public void writeHeaders(Cursor cursor, int numRows, DataOutputStream writer)
throws IOException {
initializeCachedValues(cursor);
writeQueryMetadata(numRows, writer);
}
/**
* Writes the current row from the cursor. The cursor is not advanced.
* This must be called after {@link #writeHeaders}.
*
* @param cursor the cursor to write data from
* @param writer the output to write to
* @throws IOException if there are any errors while writing
*/
public void writeOneRow(Cursor cursor, DataOutputStream writer)
throws IOException {
if (columnIndices == null) {
throw new IllegalStateException(
"Cannot write rows before writing the header");
}
if (columnIndices.length > Long.SIZE) {
throw new IllegalArgumentException("Too many fields");
}
// Build a bitmap of which fields are present
long fields = 0;
for (int i = 0; i < columnIndices.length; i++) {
hasFields[i] = !cursor.isNull(columnIndices[i]);
fields |= (hasFields[i] ? 1 : 0) << i;
}
writer.writeLong(fields);
// Actually write the present fields
for (int i = 0; i < columnIndices.length; i++) {
if (hasFields[i]) {
writeCell(columnIndices[i], columnTypes[i], cursor, writer);
} else if (outputNullFields) {
writeDummyCell(columnTypes[i], writer);
}
}
}
/**
* Initializes the column indices and other temporary state for reading from
* the given cursor.
*/
private void initializeCachedValues(Cursor cursor) {
// These indices are constant for every row (unless we're fed a MergeCursor)
if (cursor instanceof MergeCursor) {
throw new IllegalArgumentException("Cannot use a MergeCursor");
}
columnIndices = new int[columnNames.length];
for (int i = 0; i < columnNames.length; i++) {
String columnName = columnNames[i];
columnIndices[i] = cursor.getColumnIndexOrThrow(columnName);
}
hasFields = new boolean[columnIndices.length];
}
/**
* Writes metadata about the query to be dumped.
*
* @param numRows the number of rows that will be dumped
* @param writer the output to write to
* @throws IOException if there are any errors while writing
*/
private void writeQueryMetadata(
int numRows, DataOutputStream writer) throws IOException {
// Write column data
writer.writeInt(columnNames.length);
for (int i = 0; i < columnNames.length; i++) {
String columnName = columnNames[i];
byte columnType = columnTypes[i];
writer.writeUTF(columnName);
writer.writeByte(columnType);
}
// Write the number of rows
writer.writeInt(numRows);
}
/**
* Writes a single cell of the database to the output.
*
* @param columnIdx the column index to read from
* @param columnTypeId the type of the column to be read
* @param cursor the cursor to read from
* @param writer the output to write to
* @throws IOException if there are any errors while writing
*/
private void writeCell(
int columnIdx, byte columnTypeId, Cursor cursor, DataOutputStream writer)
throws IOException {
switch (columnTypeId) {
case ContentTypeIds.LONG_TYPE_ID:
writer.writeLong(cursor.getLong(columnIdx));
return;
case ContentTypeIds.DOUBLE_TYPE_ID:
writer.writeDouble(cursor.getDouble(columnIdx));
return;
case ContentTypeIds.FLOAT_TYPE_ID:
writer.writeFloat(cursor.getFloat(columnIdx));
return;
case ContentTypeIds.BOOLEAN_TYPE_ID:
writer.writeBoolean(cursor.getInt(columnIdx) != 0);
return;
case ContentTypeIds.INT_TYPE_ID:
writer.writeInt(cursor.getInt(columnIdx));
return;
case ContentTypeIds.STRING_TYPE_ID:
writer.writeUTF(cursor.getString(columnIdx));
return;
case ContentTypeIds.BLOB_TYPE_ID: {
byte[] blob = cursor.getBlob(columnIdx);
writer.writeInt(blob.length);
writer.write(blob);
return;
}
default:
throw new IllegalArgumentException(
"Type " + columnTypeId + " not supported");
}
}
/**
* Writes a dummy cell value to the output.
*
* @param columnTypeId the type of the value to write
* @throws IOException if there are any errors while writing
*/
private void writeDummyCell(byte columnTypeId, DataOutputStream writer)
throws IOException {
switch (columnTypeId) {
case ContentTypeIds.LONG_TYPE_ID:
writer.writeLong(0L);
return;
case ContentTypeIds.DOUBLE_TYPE_ID:
writer.writeDouble(0.0);
return;
case ContentTypeIds.FLOAT_TYPE_ID:
writer.writeFloat(0.0f);
return;
case ContentTypeIds.BOOLEAN_TYPE_ID:
writer.writeBoolean(false);
return;
case ContentTypeIds.INT_TYPE_ID:
writer.writeInt(0);
return;
case ContentTypeIds.STRING_TYPE_ID:
writer.writeUTF("");
return;
case ContentTypeIds.BLOB_TYPE_ID:
writer.writeInt(0);
return;
default:
throw new IllegalArgumentException(
"Type " + columnTypeId + " not supported");
}
}
}
@@ -0,0 +1,198 @@
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.io.backup;
import com.google.android.apps.mytracks.content.ContentTypeIds;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.net.Uri;
import java.io.DataInputStream;
import java.io.IOException;
import java.util.Locale;
/**
* Database importer which reads values written by {@link DatabaseDumper}.
*
* @author Rodrigo Damazio
*/
public class DatabaseImporter {
/** Maximum number of entries in a bulk insertion */
private static final int DEFAULT_BULK_SIZE = 1024;
private final Uri destinationUri;
private final ContentResolver resolver;
private final boolean readNullFields;
private final int bulkSize;
// Metadata read from the reader
private String[] columnNames;
private byte[] columnTypes;
public DatabaseImporter(Uri destinationUri, ContentResolver resolver,
boolean readNullFields) {
this(destinationUri, resolver, readNullFields, DEFAULT_BULK_SIZE);
}
protected DatabaseImporter(Uri destinationUri, ContentResolver resolver,
boolean readNullFields, int bulkSize) {
this.destinationUri = destinationUri;
this.resolver = resolver;
this.readNullFields = readNullFields;
this.bulkSize = bulkSize;
}
/**
* Reads the header which includes metadata about the table being imported.
*
* @throws IOException if there are any problems while reading
*/
private void readHeaders(DataInputStream reader) throws IOException {
int numColumns = reader.readInt();
columnNames = new String[numColumns];
columnTypes = new byte[numColumns];
for (int i = 0; i < numColumns; i++) {
columnNames[i] = reader.readUTF();
columnTypes[i] = reader.readByte();
}
}
/**
* Imports all rows from the reader into the database.
* Insertion is done in bulks for efficiency.
*
* @throws IOException if there are any errors while reading
*/
public void importAllRows(DataInputStream reader) throws IOException {
readHeaders(reader);
ContentValues[] valueBulk = new ContentValues[bulkSize];
int numValues = 0;
int numRows = reader.readInt();
int numColumns = columnNames.length;
// For each row
for (int r = 0; r < numRows; r++) {
if (valueBulk[numValues] == null) {
valueBulk[numValues] = new ContentValues(numColumns);
} else {
// Reuse values objects
valueBulk[numValues].clear();
}
// Read the fields bitmap
long fields = reader.readLong();
for (int c = 0; c < numColumns; c++) {
if ((fields & 1) == 1) {
// Field is present, read into values
readOneCell(columnNames[c], columnTypes[c], valueBulk[numValues],
reader);
} else if (readNullFields) {
// Field not present but still written, read and discard
readOneCell(columnNames[c], columnTypes[c], null, reader);
}
fields >>= 1;
}
numValues++;
// If we have enough values, flush them as a bulk insertion
if (numValues >= bulkSize) {
doBulkInsert(valueBulk);
numValues = 0;
}
}
// Do a final bulk insert with the leftovers
if (numValues > 0) {
ContentValues[] leftovers = new ContentValues[numValues];
System.arraycopy(valueBulk, 0, leftovers, 0, numValues);
doBulkInsert(leftovers);
}
}
protected void doBulkInsert(ContentValues[] values) {
resolver.bulkInsert(destinationUri, values);
}
/**
* Reads a single cell from the reader.
*
* @param name the name of the column to be read
* @param typeId the type ID of the column to be read
* @param values the {@link ContentValues} object to put the read cell value
* in - if null, the value is just discarded
* @throws IOException if there are any problems while reading
*/
private void readOneCell(String name, byte typeId, ContentValues values,
DataInputStream reader) throws IOException {
switch (typeId) {
case ContentTypeIds.BOOLEAN_TYPE_ID: {
boolean value = reader.readBoolean();
if (values != null) { values.put(name, value); }
return;
}
case ContentTypeIds.LONG_TYPE_ID: {
long value = reader.readLong();
if (values != null) { values.put(name, value); }
return;
}
case ContentTypeIds.DOUBLE_TYPE_ID: {
double value = reader.readDouble();
if (values != null) { values.put(name, value); }
return;
}
case ContentTypeIds.FLOAT_TYPE_ID: {
Float value = reader.readFloat();
if (values != null) { values.put(name, value); }
return;
}
case ContentTypeIds.INT_TYPE_ID: {
int value = reader.readInt();
if (values != null) { values.put(name, value); }
return;
}
case ContentTypeIds.STRING_TYPE_ID: {
String value = reader.readUTF();
if (values != null) { values.put(name, value); }
return;
}
case ContentTypeIds.BLOB_TYPE_ID: {
int blobLength = reader.readInt();
if (blobLength != 0) {
byte[] blob = new byte[blobLength];
int readBytes = reader.read(blob, 0, blobLength);
if (readBytes != blobLength) {
throw new IOException(String.format(Locale.US,
"Short read on column %s; expected %d bytes, read %d",
name, blobLength, readBytes));
}
if (values != null) {
values.put(name, blob);
}
}
return;
}
default:
throw new IOException("Read unknown type " + typeId);
}
}
}
@@ -0,0 +1,264 @@
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.io.backup;
import static com.google.android.apps.mytracks.Constants.TAG;
import com.google.android.apps.mytracks.Constants;
import com.google.android.apps.mytracks.content.TrackPointsColumns;
import com.google.android.apps.mytracks.content.TracksColumns;
import com.google.android.apps.mytracks.content.WaypointsColumns;
import com.google.android.apps.mytracks.util.FileUtils;
import android.content.ContentResolver;
import android.content.Context;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.util.Log;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.TimeZone;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipOutputStream;
/**
* Handler for writing or reading single-file backups.
*
* @author Rodrigo Damazio
*/
class ExternalFileBackup {
// Filename format - in UTC
private static final SimpleDateFormat BACKUP_FILENAME_FORMAT =
new SimpleDateFormat("'backup-'yyyy-MM-dd_HH-mm-ss'.zip'");
static {
BACKUP_FILENAME_FORMAT.setTimeZone(TimeZone.getTimeZone("UTC"));
}
private static final String BACKUPS_SUBDIR = "backups";
private static final int BACKUP_FORMAT_VERSION = 1;
private static final String ZIP_ENTRY_NAME =
"backup.mytracks.v" + BACKUP_FORMAT_VERSION;
private static final int COMPRESSION_LEVEL = 8;
private final Context context;
public ExternalFileBackup(Context context) {
this.context = context;
}
/**
* Returns whether the backups directory is (or can be made) available.
*
* @param create whether to try creating the directory if it doesn't exist
*/
public boolean isBackupsDirectoryAvailable(boolean create) {
return getBackupsDirectory(create) != null;
}
/**
* Returns the backup directory, or null if not available.
*
* @param create whether to try creating the directory if it doesn't exist
*/
private File getBackupsDirectory(boolean create) {
String dirName = FileUtils.buildExternalDirectoryPath(BACKUPS_SUBDIR);
final File dir = new File(dirName);
Log.d(Constants.TAG, "Dir: " + dir.getAbsolutePath());
if (create) {
// Try to create - if that fails, return null
return FileUtils.ensureDirectoryExists(dir) ? dir : null;
} else {
// Return it if it already exists, otherwise return null
return dir.isDirectory() ? dir : null;
}
}
/**
* Returns a list of available backups to be restored.
*/
public Date[] getAvailableBackups() {
File dir = getBackupsDirectory(false);
if (dir == null) { return null; }
String[] fileNames = dir.list();
List<Date> backupDates = new ArrayList<Date>(fileNames.length);
for (int i = 0; i < fileNames.length; i++) {
String fileName = fileNames[i];
try {
backupDates.add(BACKUP_FILENAME_FORMAT.parse(fileName));
} catch (ParseException e) {
// Not a backup file, ignore
}
}
return backupDates.toArray(new Date[backupDates.size()]);
}
/**
* Writes the backup to the default file.
*/
public void writeToDefaultFile() throws IOException {
writeToFile(getFileForDate(new Date()));
}
/**
* Restores the backup from the given date.
*/
public void restoreFromDate(Date when) throws IOException {
restoreFromFile(getFileForDate(when));
}
/**
* Produces the proper file descriptor for the given backup date.
*/
private File getFileForDate(Date when) {
File dir = getBackupsDirectory(false);
String fileName = BACKUP_FILENAME_FORMAT.format(when);
File file = new File(dir, fileName);
return file;
}
/**
* Synchronously writes a backup to the given file.
*/
private void writeToFile(File outputFile) throws IOException {
Log.d(Constants.TAG,
"Writing backup to file " + outputFile.getAbsolutePath());
// Create all the auxiliary classes that will do the writing
PreferenceBackupHelper preferencesHelper = new PreferenceBackupHelper();
DatabaseDumper trackDumper = new DatabaseDumper(
TracksColumns.COLUMNS,
TracksColumns.COLUMN_TYPES,
false);
DatabaseDumper waypointDumper = new DatabaseDumper(
WaypointsColumns.COLUMNS,
WaypointsColumns.COLUMN_TYPES,
false);
DatabaseDumper pointDumper = new DatabaseDumper(
TrackPointsColumns.COLUMNS,
TrackPointsColumns.COLUMN_TYPES,
false);
// Open the target for writing
FileOutputStream outputStream = new FileOutputStream(outputFile);
ZipOutputStream compressedStream = new ZipOutputStream(outputStream);
compressedStream.setLevel(COMPRESSION_LEVEL);
compressedStream.putNextEntry(new ZipEntry(ZIP_ENTRY_NAME));
DataOutputStream outWriter = new DataOutputStream(compressedStream);
try {
// Dump the entire contents of each table
ContentResolver contentResolver = context.getContentResolver();
Cursor tracksCursor = contentResolver.query(
TracksColumns.CONTENT_URI, null, null, null, null);
try {
trackDumper.writeAllRows(tracksCursor, outWriter);
} finally {
tracksCursor.close();
}
Cursor waypointsCursor = contentResolver.query(
WaypointsColumns.CONTENT_URI, null, null, null, null);
try {
waypointDumper.writeAllRows(waypointsCursor, outWriter);
} finally {
waypointsCursor.close();
}
Cursor pointsCursor = contentResolver.query(
TrackPointsColumns.CONTENT_URI, null, null, null, null);
try {
pointDumper.writeAllRows(pointsCursor, outWriter);
} finally {
pointsCursor.close();
}
// Dump preferences
SharedPreferences preferences = context.getSharedPreferences(
Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
preferencesHelper.exportPreferences(preferences, outWriter);
} catch (IOException e) {
// We tried to delete the partially created file, but do nothing
// if that also fails.
if (!outputFile.delete()) {
Log.w(TAG, "Failed to delete file " + outputFile.getAbsolutePath());
}
throw e;
} finally {
compressedStream.closeEntry();
compressedStream.close();
}
}
/**
* Synchronously restores the backup from the given file.
*/
private void restoreFromFile(File inputFile) throws IOException {
Log.d(Constants.TAG,
"Restoring from file " + inputFile.getAbsolutePath());
PreferenceBackupHelper preferencesHelper = new PreferenceBackupHelper();
ContentResolver resolver = context.getContentResolver();
DatabaseImporter trackImporter =
new DatabaseImporter(TracksColumns.CONTENT_URI, resolver, false);
DatabaseImporter waypointImporter =
new DatabaseImporter(WaypointsColumns.CONTENT_URI, resolver, false);
DatabaseImporter pointImporter =
new DatabaseImporter(TrackPointsColumns.CONTENT_URI, resolver, false);
ZipFile zipFile = new ZipFile(inputFile, ZipFile.OPEN_READ);
ZipEntry zipEntry = zipFile.getEntry(ZIP_ENTRY_NAME);
if (zipEntry == null) {
throw new IOException("Invalid backup ZIP file");
}
InputStream compressedStream = zipFile.getInputStream(zipEntry);
DataInputStream reader = new DataInputStream(compressedStream);
try {
// Delete all previous contents of the tables and preferences.
resolver.delete(TracksColumns.CONTENT_URI, null, null);
resolver.delete(TrackPointsColumns.CONTENT_URI, null, null);
resolver.delete(WaypointsColumns.CONTENT_URI, null, null);
// Import the new contents of each table
trackImporter.importAllRows(reader);
waypointImporter.importAllRows(reader);
pointImporter.importAllRows(reader);
// Restore preferences
SharedPreferences preferences = context.getSharedPreferences(
Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
preferencesHelper.importPreferences(reader, preferences);
} finally {
compressedStream.close();
zipFile.close();
}
}
}
@@ -0,0 +1,101 @@
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.io.backup;
import com.google.android.apps.mytracks.Constants;
import android.annotation.TargetApi;
import android.app.backup.BackupAgent;
import android.app.backup.BackupDataInput;
import android.app.backup.BackupDataOutput;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.ParcelFileDescriptor;
import android.util.Log;
import java.io.IOException;
/**
* Backup agent used to backup and restore all preferences.
* We use a regular {@link BackupAgent} instead of the convenient helpers in
* order to be future-proof (assuming we'll want to back up tracks later).
*
* @author Rodrigo Damazio
*/
@TargetApi(8)
public class MyTracksBackupAgent extends BackupAgent {
private static final String PREFERENCES_ENTITY = "prefs";
@Override
public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data,
ParcelFileDescriptor newState) throws IOException {
Log.i(Constants.TAG, "Performing backup");
SharedPreferences preferences = this.getSharedPreferences(
Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
backupPreferences(data, preferences);
Log.i(Constants.TAG, "Backup complete");
}
private void backupPreferences(BackupDataOutput data,
SharedPreferences preferences) throws IOException {
PreferenceBackupHelper preferenceDumper = createPreferenceBackupHelper();
byte[] dumpedContents = preferenceDumper.exportPreferences(preferences);
data.writeEntityHeader(PREFERENCES_ENTITY, dumpedContents.length);
data.writeEntityData(dumpedContents, dumpedContents.length);
}
protected PreferenceBackupHelper createPreferenceBackupHelper() {
return new PreferenceBackupHelper();
}
@Override
public void onRestore(BackupDataInput data, int appVersionCode,
ParcelFileDescriptor newState) throws IOException {
Log.i(Constants.TAG, "Restoring from backup");
while (data.readNextHeader()) {
String key = data.getKey();
Log.d(Constants.TAG, "Restoring entity " + key);
if (key.equals(PREFERENCES_ENTITY)) {
restorePreferences(data);
} else {
Log.e(Constants.TAG, "Found unknown backup entity: " + key);
data.skipEntityData();
}
}
Log.i(Constants.TAG, "Done restoring from backup");
}
/**
* Restores all preferences from the backup.
*
* @param data the backup data to read from
* @throws IOException if there are any errors while reading
*/
private void restorePreferences(BackupDataInput data) throws IOException {
int dataSize = data.getDataSize();
byte[] dataBuffer = new byte[dataSize];
int read = data.readEntityData(dataBuffer, 0, dataSize);
if (read != dataSize) {
throw new IOException("Failed to read all the preferences data");
}
SharedPreferences preferences = this.getSharedPreferences(
Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
PreferenceBackupHelper importer = createPreferenceBackupHelper();
importer.importPreferences(dataBuffer, preferences);
}
}
@@ -0,0 +1,174 @@
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.io.backup;
import com.google.android.apps.mytracks.content.ContentTypeIds;
import com.google.android.apps.mytracks.util.ApiAdapterFactory;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.util.Map;
/**
* Helper for backing up and restoring shared preferences.
*
* @author Rodrigo Damazio
*/
class PreferenceBackupHelper {
private static final int BUFFER_SIZE = 2048;
/**
* Exports all shared preferences from the given object as a byte array.
*
* @param preferences the preferences to export
* @return the corresponding byte array
* @throws IOException if there are any errors while writing to the byte array
*/
public byte[] exportPreferences(SharedPreferences preferences)
throws IOException {
ByteArrayOutputStream bufStream = new ByteArrayOutputStream(BUFFER_SIZE);
DataOutputStream outWriter = new DataOutputStream(bufStream);
exportPreferences(preferences, outWriter);
return bufStream.toByteArray();
}
/**
* Exports all shared preferences from the given object into the given output
* stream.
*
* @param preferences the preferences to export
* @param outWriter the stream to write them to
* @throws IOException if there are any errors while writing the output
*/
public void exportPreferences(
SharedPreferences preferences,
DataOutputStream outWriter) throws IOException {
Map<String, ?> values = preferences.getAll();
outWriter.writeInt(values.size());
for (Map.Entry<String, ?> entry : values.entrySet()) {
writePreference(entry.getKey(), entry.getValue(), outWriter);
}
outWriter.flush();
}
/**
* Imports all preferences from the given byte array.
*
* @param data the byte array to read preferences from
* @param preferences the shared preferences to edit
* @throws IOException if there are any errors while reading
*/
public void importPreferences(byte[] data, SharedPreferences preferences)
throws IOException {
ByteArrayInputStream bufStream = new ByteArrayInputStream(data);
DataInputStream reader = new DataInputStream(bufStream);
importPreferences(reader, preferences);
}
/**
* Imports all preferences from the given stream.
*
* @param reader the stream to read from
* @param preferences the shared preferences to edit
* @throws IOException if there are any errors while reading
*/
public void importPreferences(DataInputStream reader,
SharedPreferences preferences) throws IOException {
Editor editor = preferences.edit();
editor.clear();
int numPreferences = reader.readInt();
for (int i = 0; i < numPreferences; i++) {
String name = reader.readUTF();
byte typeId = reader.readByte();
readAndSetPreference(name, typeId, reader, editor);
}
ApiAdapterFactory.getApiAdapter().applyPreferenceChanges(editor);
}
/**
* Reads a single preference and sets it into the given editor.
*
* @param name the name of the preference to read
* @param typeId the type ID of the preference to read
* @param reader the reader to read from
* @param editor the editor to set the preference in
* @throws IOException if there are errors while reading
*/
private void readAndSetPreference(String name, byte typeId,
DataInputStream reader, Editor editor) throws IOException {
switch (typeId) {
case ContentTypeIds.BOOLEAN_TYPE_ID:
editor.putBoolean(name, reader.readBoolean());
return;
case ContentTypeIds.LONG_TYPE_ID:
editor.putLong(name, reader.readLong());
return;
case ContentTypeIds.FLOAT_TYPE_ID:
editor.putFloat(name, reader.readFloat());
return;
case ContentTypeIds.INT_TYPE_ID:
editor.putInt(name, reader.readInt());
return;
case ContentTypeIds.STRING_TYPE_ID:
editor.putString(name, reader.readUTF());
return;
}
}
/**
* Writes a single preference.
*
* @param name the name of the preference to write
* @param value the correctly-typed value of the preference
* @param writer the writer to write to
* @throws IOException if there are errors while writing
*/
private void writePreference(String name, Object value, DataOutputStream writer)
throws IOException {
writer.writeUTF(name);
if (value instanceof Boolean) {
writer.writeByte(ContentTypeIds.BOOLEAN_TYPE_ID);
writer.writeBoolean((Boolean) value);
} else if (value instanceof Integer) {
writer.writeByte(ContentTypeIds.INT_TYPE_ID);
writer.writeInt((Integer) value);
} else if (value instanceof Long) {
writer.writeByte(ContentTypeIds.LONG_TYPE_ID);
writer.writeLong((Long) value);
} else if (value instanceof Float) {
writer.writeByte(ContentTypeIds.FLOAT_TYPE_ID);
writer.writeFloat((Float) value);
} else if (value instanceof String) {
writer.writeByte(ContentTypeIds.STRING_TYPE_ID);
writer.writeUTF((String) value);
} else {
throw new IllegalArgumentException(
"Type " + value.getClass().getName() + " not supported");
}
}
}
@@ -0,0 +1,107 @@
/*
* Copyright 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.io.backup;
import com.google.android.apps.mytracks.TrackListActivity;
import com.google.android.apps.mytracks.util.DialogUtils;
import com.google.android.apps.mytracks.util.IntentUtils;
import com.google.android.maps.mytracks.R;
import android.app.Activity;
import android.app.Dialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.widget.Toast;
import java.util.Date;
/**
* An activity to restore data from the SD card.
*
* @author Jimmy Shih
*/
public class RestoreActivity extends Activity {
public static final String EXTRA_DATE = "date";
private static final String TAG = RestoreActivity.class.getSimpleName();
private static final int DIALOG_PROGRESS_ID = 0;
private RestoreAsyncTask restoreAsyncTask;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Object retained = getLastNonConfigurationInstance();
if (retained instanceof RestoreAsyncTask) {
restoreAsyncTask = (RestoreAsyncTask) retained;
restoreAsyncTask.setActivity(this);
} else {
long date = getIntent().getLongExtra(EXTRA_DATE, -1L);
if (date == -1L) {
Log.d(TAG, "Invalid date");
finish();
return;
}
restoreAsyncTask = new RestoreAsyncTask(this, new Date(date));
restoreAsyncTask.execute();
}
}
@Override
public Object onRetainNonConfigurationInstance() {
restoreAsyncTask.setActivity(null);
return restoreAsyncTask;
}
@Override
protected Dialog onCreateDialog(int id) {
if (id != DIALOG_PROGRESS_ID) {
return null;
}
return DialogUtils.createSpinnerProgressDialog(this,
R.string.settings_backup_restore_progress_message, new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
finish();
}
});
}
/**
* Invokes when the associated AsyncTask completes.
*
* @param success true if the AsyncTask is successful
* @param messageId message id to display to user
*/
public void onAsyncTaskCompleted(boolean success, int messageId) {
removeDialog(DIALOG_PROGRESS_ID);
Toast.makeText(this, messageId, success ? Toast.LENGTH_SHORT : Toast.LENGTH_LONG).show();
Intent intent = IntentUtils.newIntent(this, TrackListActivity.class);
startActivity(intent);
}
/**
* Shows the progress dialog.
*/
public void showProgressDialog() {
showDialog(DIALOG_PROGRESS_ID);
}
}
@@ -0,0 +1,104 @@
/*
* Copyright 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.io.backup;
import com.google.android.maps.mytracks.R;
import android.os.AsyncTask;
import android.util.Log;
import java.io.IOException;
import java.util.Date;
/**
* AsyncTask to restore data from the SD card.
*
* @author Jimmy Shih
*/
public class RestoreAsyncTask extends AsyncTask<Void, Integer, Boolean> {
private static final String TAG = RestoreAsyncTask.class.getSimpleName();
private RestoreActivity restoreActivity;
private final Date date;
private final ExternalFileBackup externalFileBackup;
// true if the AsyncTask result is success
private boolean success;
// true if the AsyncTask has completed
private boolean completed;
// message id to return to the activity
private int messageId;
/**
* Creates an AsyncTask.
*
* @param restoreActivity the activity currently associated with this
* AsyncTask
* @param date the date to retore from
*/
public RestoreAsyncTask(RestoreActivity restoreActivity, Date date) {
this.restoreActivity = restoreActivity;
this.date = date;
this.externalFileBackup = new ExternalFileBackup(restoreActivity);
success = false;
completed = false;
messageId = R.string.sd_card_import_error;
}
/**
* Sets the current {@link RestoreActivity} associated with this AyncTask.
*
* @param activity the current {@link RestoreActivity}, can be null
*/
public void setActivity(RestoreActivity activity) {
this.restoreActivity = activity;
if (completed && restoreActivity != null) {
restoreActivity.onAsyncTaskCompleted(success, messageId);
}
}
@Override
protected void onPreExecute() {
if (restoreActivity != null) {
restoreActivity.showProgressDialog();
}
}
@Override
protected Boolean doInBackground(Void... params) {
try {
externalFileBackup.restoreFromDate(date);
messageId = R.string.sd_card_import_success;
return true;
} catch (IOException e) {
Log.d(TAG, "IO exception", e);
return false;
}
}
@Override
protected void onPostExecute(Boolean result) {
success = result;
completed = true;
if (restoreActivity != null) {
restoreActivity.onAsyncTaskCompleted(success, messageId);
}
}
}
@@ -0,0 +1,121 @@
/*
* Copyright 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.io.backup;
import com.google.android.apps.mytracks.util.FileUtils;
import com.google.android.apps.mytracks.util.IntentUtils;
import com.google.android.apps.mytracks.util.StringUtils;
import com.google.android.maps.mytracks.R;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
import android.widget.Toast;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Date;
/**
* An activity to choose a date to restore from.
*
* @author Jimmy Shih
*/
public class RestoreChooserActivity extends Activity {
private static final Comparator<Date> REVERSE_DATE_ORDER = new Comparator<Date>() {
@Override
public int compare(Date s1, Date s2) {
return s2.compareTo(s1);
}
};
private static final int DIALOG_CHOOSER_ID = 0;
private Date[] backupDates;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ExternalFileBackup externalFileBackup = new ExternalFileBackup(this);
// Get the list of existing backups
if (!FileUtils.isSdCardAvailable()) {
Toast.makeText(this, R.string.sd_card_error_no_storage, Toast.LENGTH_LONG).show();
finish();
return;
}
if (!externalFileBackup.isBackupsDirectoryAvailable(false)) {
Toast.makeText(this, R.string.settings_backup_restore_no_backup, Toast.LENGTH_LONG).show();
finish();
return;
}
backupDates = externalFileBackup.getAvailableBackups();
if (backupDates == null || backupDates.length == 0) {
Toast.makeText(this, R.string.settings_backup_restore_no_backup, Toast.LENGTH_LONG).show();
finish();
return;
}
if (backupDates.length == 1) {
Intent intent = IntentUtils.newIntent(this, RestoreActivity.class)
.putExtra(RestoreActivity.EXTRA_DATE, backupDates[0].getTime());
startActivity(intent);
finish();
return;
}
Arrays.sort(backupDates, REVERSE_DATE_ORDER);
showDialog(DIALOG_CHOOSER_ID);
}
@Override
protected Dialog onCreateDialog(int id) {
if (id != DIALOG_CHOOSER_ID) {
return null;
}
String items[] = new String[backupDates.length];
for (int i = 0; i < backupDates.length; i++) {
items[i] = StringUtils.formatDateTime(this, backupDates[i].getTime());
}
return new AlertDialog.Builder(this)
.setCancelable(true)
.setItems(items, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
Intent intent = IntentUtils.newIntent(
RestoreChooserActivity.this, RestoreActivity.class)
.putExtra(RestoreActivity.EXTRA_DATE, backupDates[which].getTime());
startActivity(intent);
finish();
}
})
.setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
finish();
}
})
.setTitle(R.string.settings_backup_restore_select_title)
.create();
}
}
@@ -0,0 +1,52 @@
/*
* 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.apps.mytracks.util.IntentUtils;
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 = IntentUtils.newIntent(this, UploadResultActivity.class)
.putExtra(SendRequest.SEND_REQUEST_KEY, sendRequest);
startActivity(intent);
finish();
}
}
@@ -0,0 +1,324 @@
/*
* 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;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
/**
* 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, "No track for " + trackId);
return false;
}
String title = context.getString(R.string.my_tracks_app_name);
if (track.getCategory() != null && !track.getCategory().equals("")) {
title += "-" + track.getCategory();
}
try {
title = URLEncoder.encode(title, "UTF-8");
title = title.replace('+', ' ');
} catch (UnsupportedEncodingException e) {
Log.d(TAG, "No UTF-8 encoding", e);
return false;
}
// 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,421 @@
/*
* Copyright 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.io.docs;
import com.google.android.apps.mytracks.content.Track;
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.docs.SpreadsheetsClient.WorksheetEntry;
import com.google.android.apps.mytracks.io.maps.SendMapsUtils;
import com.google.android.apps.mytracks.stats.TripStatistics;
import com.google.android.apps.mytracks.util.PreferencesUtils;
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.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 {
@VisibleForTesting
public 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";
@VisibleForTesting
public static final String GET_WORKSHEET_URI =
"https://spreadsheets.google.com/feeds/list/%s/%s/private/full";
@VisibleForTesting
public static final String SPREADSHEET_ID_PREFIX =
"https://docs.google.com/feeds/documents/private/full/spreadsheet%3A";
@VisibleForTesting
public static final String CONTENT_TYPE = "Content-Type";
@VisibleForTesting
public static final String ATOM_FEED_MIME_TYPE = "application/atom+xml";
private static final String OPENDOCUMENT_SPREADSHEET_MIME_TYPE =
"application/x-vnd.oasis.opendocument.spreadsheet";
@VisibleForTesting
public static final String AUTHORIZATION = "Authorization";
@VisibleForTesting
public 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(Locale.US, 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(Locale.US, 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(Locale.US, GET_WORKSHEET_URI, spreadsheetId, worksheetId);
boolean metricUnits = PreferencesUtils.getBoolean(
context, R.string.metric_units_key, PreferencesUtils.METRIC_UNITS_DEFAULT);
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.getTripStatistics();
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);
String map = SendMapsUtils.getMapUrl(track);
if (map == null) {
map = context.getString(R.string.value_unknown);
}
appendTag(builder, "map", map);
String fusionTable = SendFusionTablesUtils.getMapUrl(track);
if (fusionTable == null) {
fusionTable = context.getString(R.string.value_unknown);
}
appendTag(builder, "fusiontable", fusionTable);
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();
}
}
@@ -0,0 +1,243 @@
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.io.file;
import com.google.android.apps.mytracks.content.MyTracksLocation;
import com.google.android.apps.mytracks.content.Sensor;
import com.google.android.apps.mytracks.content.Sensor.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 android.content.Context;
import android.location.Location;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.text.NumberFormat;
/**
* Write track as CSV to a file. See RFC 4180 for info on CSV. Output three
* tables.<br>
* The first table contains the track info. Its columns are:<br>
* "Track name","Activity type","Track description" <br>
* <br>
* The second table contains the markers. Its columns are:<br>
* "Marker name","Marker type","Marker description","Latitude (deg)","Longitude
* (deg)","Altitude (m)","Bearing (deg)","Accuracy (m)","Speed (m/s)","Time"<br>
* <br>
* The thrid table contains the points. Its columns are:<br>
* "Segment","Point","Latitude (deg)","Longitude (deg)","Altitude (m)","Bearing
* (deg)","Accuracy (m)","Speed (m/s)","Time","Power (W)","Cadence (rpm)","Heart
* rate (bpm)","Battery level (%)"<br>
*
* @author Rodrigo Damazio
*/
public class CsvTrackWriter implements TrackFormatWriter {
private static final NumberFormat SHORT_FORMAT = NumberFormat.getInstance();
static {
SHORT_FORMAT.setMaximumFractionDigits(4);
}
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();
}
@Override
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(context.getString(R.string.generic_name),
context.getString(R.string.track_edit_activity_type_hint),
context.getString(R.string.generic_description));
writeCommaSeparatedLine(track.getName(), track.getCategory(), track.getDescription());
writeCommaSeparatedLine();
}
@Override
public void writeFooter() {
// Do nothing
}
@Override
public void writeBeginWaypoints() {
writeCommaSeparatedLine(context.getString(R.string.generic_name),
context.getString(R.string.marker_edit_marker_type_hint),
context.getString(R.string.generic_description),
context.getString(R.string.description_location_latitude),
context.getString(R.string.description_location_longitude),
context.getString(R.string.description_location_altitude),
context.getString(R.string.description_location_bearing),
context.getString(R.string.description_location_accuracy),
context.getString(R.string.description_location_speed),
context.getString(R.string.description_time));
}
@Override
public void writeEndWaypoints() {
writeCommaSeparatedLine();
}
@Override
public void writeWaypoint(Waypoint waypoint) {
Location location = waypoint.getLocation();
writeCommaSeparatedLine(waypoint.getName(),
waypoint.getCategory(),
waypoint.getDescription(),
Double.toString(location.getLatitude()),
Double.toString(location.getLongitude()),
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_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());
}
}
@@ -0,0 +1,542 @@
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.io.file;
import com.google.android.apps.mytracks.Constants;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.stats.TripStatisticsBuilder;
import com.google.android.apps.mytracks.util.LocationUtils;
import com.google.android.apps.mytracks.util.StringUtils;
import android.location.Location;
import android.location.LocationManager;
import android.net.Uri;
import android.util.Log;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import org.xml.sax.Attributes;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
/**
* Imports GPX file to My Tracks.
*
* @author Leif Hendrik Wilden
* @author Steffen Horlacher
* @author Rodrigo Damazio
*/
public class GpxImporter extends DefaultHandler {
// GPX tag names and attributes.
private static final String TAG_TRACK = "trk";
private static final String TAG_TRACK_POINT = "trkpt";
private static final Object TAG_TRACK_SEGMENT = "trkseg";
private static final String TAG_NAME = "name";
private static final String TAG_DESCRIPTION = "desc";
private static final String TAG_ALTITUDE = "ele";
private static final String TAG_TIME = "time";
private static final String ATT_LAT = "lat";
private static final String ATT_LON = "lon";
/**
* The maximum number of locations to buffer for bulk-insertion into the
* database.
*/
private static final int MAX_BUFFERED_LOCATIONS = 512;
/**
* Utilities for accessing the content provider.
*/
private final MyTracksProviderUtils myTracksProviderUtils;
/**
* List of track ids written in the database. Only contains successfully
* written tracks.
*/
private final List<Long> tracksIds;
/**
* Contains the current elements content.
*/
private String content;
/**
* Currently location.
*/
private Location location;
/**
* Last location.
*/
private Location lastLocation;
/**
* Last segment location.
*/
private Location lastSegmentLocation;
/**
* Currently reading track.
*/
private Track track;
/**
* Statistics builder for the current track.
*/
private TripStatisticsBuilder tripStatisticsBuilder;
/**
* Buffer of locations to be bulk-inserted into the database.
*/
private Location[] bufferedPointInserts = new Location[MAX_BUFFERED_LOCATIONS];
/**
* Number of locations buffered to be inserted into the database.
*/
private int numBufferedPointInserts = 0;
/**
* Number of locations already processed.
*/
private int numberOfLocations = 0;
/**
* Number of segments already processed.
*/
private int numberOfSegments = 0;
/**
* Used to identify if a track was written to the database but not yet
* finished successfully.
*/
private boolean isCurrentTrackRollbackable = false;
/**
* Flag to indicate if we're inside a track's xml element. Some sub elements
* like name may be used in other parts of the GPX file, and we use this to
* ignore them.
*/
private boolean isInTrackElement = false;
/**
* Counter to find out which child level of the track we are processing.
*/
private int trackChildDepth = 0;
/**
* SAX locator to get the current line information.
*/
private Locator locator;
/**
* Reads GPS tracks from a GPX file and writes tracks and their coordinates to
* the database.
*
* @param inputStream an input stream for the GPX file
* @param myTracksProviderUtils my tracks provider utils
* @return long[] array of track ids written to the database.
*/
public static long[] importGPXFile(
final InputStream inputStream, final MyTracksProviderUtils myTracksProviderUtils)
throws ParserConfigurationException, SAXException, IOException {
SAXParserFactory saxParserFactory = SAXParserFactory.newInstance();
SAXParser saxParser = saxParserFactory.newSAXParser();
GpxImporter gpxImporter = new GpxImporter(myTracksProviderUtils);
long[] trackIds = new long[0];
try {
long start = System.currentTimeMillis();
saxParser.parse(inputStream, gpxImporter);
long end = System.currentTimeMillis();
Log.d(Constants.TAG, "Total import time: " + (end - start) + "ms");
trackIds = gpxImporter.getImportedTrackIds();
if (trackIds.length == 0) {
throw new IOException("No track imported.");
}
} finally {
// Delete track if not finished
gpxImporter.rollbackUnfinishedTracks();
}
return trackIds;
}
public GpxImporter(MyTracksProviderUtils myTracksProviderUtils) {
this.myTracksProviderUtils = myTracksProviderUtils;
tracksIds = new ArrayList<Long>();
}
@Override
public void characters(char[] ch, int start, int length) throws SAXException {
String newContent = new String(ch, start, length);
if (content == null) {
content = newContent;
} else {
/*
* In 99% of the cases, a single call to this method will be made for each
* sequence of characters we're interested in, so we'll rarely be
* concatenating strings, thus not justifying the use of a StringBuilder.
*/
content += newContent;
}
}
@Override
public void startElement(String uri, String localName, String name, Attributes attributes)
throws SAXException {
if (isInTrackElement) {
trackChildDepth++;
if (localName.equals(TAG_TRACK_POINT)) {
onTrackPointElementStart(attributes);
} else if (localName.equals(TAG_TRACK_SEGMENT)) {
onTrackSegmentElementStart();
} else if (localName.equals(TAG_TRACK)) {
throw new SAXException(createErrorMessage("Invalid GPX detected."));
}
} else if (localName.equals(TAG_TRACK)) {
isInTrackElement = true;
trackChildDepth = 0;
onTrackElementStart();
}
}
@Override
public void endElement(String uri, String localName, String name) throws SAXException {
if (!isInTrackElement) {
content = null;
return;
}
// process these elements only as sub-elements of track
if (localName.equals(TAG_TRACK_POINT)) {
onTrackPointElementEnd();
} else if (localName.equals(TAG_ALTITUDE)) {
onAltitudeElementEnd();
} else if (localName.equals(TAG_TIME)) {
onTimeElementEnd();
} else if (localName.equals(TAG_NAME)) {
// we are only interested in the first level name element
if (trackChildDepth == 1) {
onNameElementEnd();
}
} else if (localName.equals(TAG_DESCRIPTION)) {
// we are only interested in the first level description element
if (trackChildDepth == 1) {
onDescriptionElementEnd();
}
} else if (localName.equals(TAG_TRACK_SEGMENT)) {
onTrackSegmentElementEnd();
} else if (localName.equals(TAG_TRACK)) {
onTrackElementEnd();
isInTrackElement = false;
trackChildDepth = 0;
}
trackChildDepth--;
// reset element content
content = null;
}
@Override
public void setDocumentLocator(Locator locator) {
this.locator = locator;
}
/**
* Rolls back last track if possible.
*/
public void rollbackUnfinishedTracks() {
if (isCurrentTrackRollbackable) {
myTracksProviderUtils.deleteTrack(track.getId());
isCurrentTrackRollbackable = false;
}
}
/**
* On track element start.
*/
private void onTrackElementStart() {
track = new Track();
numberOfLocations = 0;
Uri uri = myTracksProviderUtils.insertTrack(track);
long trackId = Long.parseLong(uri.getLastPathSegment());
track.setId(trackId);
isCurrentTrackRollbackable = true;
}
/**
* On track element end.
*/
private void onTrackElementEnd() {
if (lastLocation != null) {
flushPoints();
// Calculate statistics for the imported track
tripStatisticsBuilder.pauseAt(lastLocation.getTime());
track.setStopId(getLastPointId());
} else {
tripStatisticsBuilder = new TripStatisticsBuilder(0);
tripStatisticsBuilder.pauseAt(0);
}
track.setTripStatistics(tripStatisticsBuilder.getStatistics());
track.setNumberOfPoints(numberOfLocations);
myTracksProviderUtils.updateTrack(track);
tracksIds.add(track.getId());
isCurrentTrackRollbackable = false;
lastSegmentLocation = null;
lastLocation = null;
tripStatisticsBuilder = null;
}
/**
* On track segment start.
*/
private void onTrackSegmentElementStart() {
if (numberOfSegments > 0) {
// Add a segment separator
location = new Location(LocationManager.GPS_PROVIDER);
location.setLatitude(100.0);
location.setLongitude(100.0);
location.setAltitude(0);
if (lastLocation != null) {
location.setTime(lastLocation.getTime());
}
insertPoint(location);
lastLocation = location;
lastSegmentLocation = null;
location = null;
}
numberOfSegments++;
}
/**
* On track segment element end.
*/
private void onTrackSegmentElementEnd() {
// Nothing to be done
}
/**
* On track point element start.
*
* @param attributes the attributes
*/
private void onTrackPointElementStart(Attributes attributes) throws SAXException {
if (location != null) {
throw new SAXException(createErrorMessage("Found a track point inside another one."));
}
String latitude = attributes.getValue(ATT_LAT);
String longitude = attributes.getValue(ATT_LON);
if (latitude == null || longitude == null) {
throw new SAXException(createErrorMessage("Point with no longitude or latitude."));
}
Location newLocation = new Location(LocationManager.GPS_PROVIDER);
try {
newLocation.setLatitude(Double.parseDouble(latitude));
newLocation.setLongitude(Double.parseDouble(longitude));
} catch (NumberFormatException e) {
throw new SAXException(createErrorMessage(
"Unable to parse latitude/longitude: " + latitude + "/" + longitude), e);
}
location = newLocation;
}
/**
* On track point element end.
*/
private void onTrackPointElementEnd() throws SAXException {
if (LocationUtils.isValidLocation(location)) {
if (tripStatisticsBuilder == null) {
// first point did not have a time, start stats builder without it
tripStatisticsBuilder = new TripStatisticsBuilder(0);
}
tripStatisticsBuilder.addLocation(location, location.getTime());
// insert in db
insertPoint(location);
// first track point?
if (lastLocation == null && numberOfSegments == 1) {
track.setStartId(getLastPointId());
}
lastLocation = location;
lastSegmentLocation = location;
location = null;
} else {
// invalid location - abort import
throw new SAXException(createErrorMessage("Invalid location detected: " + location));
}
}
/**
* On time element end. Sets location time and doing additional calculations
* as this is the last value required for the location. Also sets the start time
* for the trip statistics builder as there is no start time in the track root
* element.
*/
private void onTimeElementEnd() throws SAXException {
if (location == null) {
return;
}
if (content == null) {
return;
}
// Parse the time
long time;
try {
time = StringUtils.getTime(content.trim());
} catch (IllegalArgumentException e) {
throw new SAXException(createErrorMessage("Unable to parse time: " + content), e);
}
// Calculate derived attributes from previous point
if (lastSegmentLocation != null) {
long timeDifference = time - lastSegmentLocation.getTime();
// check for negative time change
if (timeDifference < 0) {
Log.w(Constants.TAG, "Found negative time change.");
} else {
/*
* We don't have a speed and bearing in GPX, make something up from the
* last two points. GPS points tend to have some inherent imprecision,
* speed and bearing will likely be off, so the statistics for things
* like max speed will also be off.
*/
float speed = location.distanceTo(lastLocation) * 1000.0f / timeDifference;
location.setSpeed(speed);
location.setBearing(lastSegmentLocation.bearingTo(location));
}
}
// Fill in the time
location.setTime(time);
// initialize start time with time of first track point
if (tripStatisticsBuilder == null) {
tripStatisticsBuilder = new TripStatisticsBuilder(time);
}
}
/**
* On altitude element end.
*/
private void onAltitudeElementEnd() throws SAXException {
if (location != null && content != null) {
try {
location.setAltitude(Double.parseDouble(content));
} catch (NumberFormatException e) {
throw new SAXException(createErrorMessage("Unable to parse altitude: " + content), e);
}
}
}
/**
* On name element end.
*/
private void onNameElementEnd() {
if (content != null) {
track.setName(content.toString().trim());
}
}
/**
* On description element end.
*/
private void onDescriptionElementEnd() {
if (content != null) {
track.setDescription(content.toString().trim());
}
}
/**
* Inserts a point.
*
* @param newLocation the location
*/
private void insertPoint(Location newLocation) {
bufferedPointInserts[numBufferedPointInserts] = newLocation;
numBufferedPointInserts++;
numberOfLocations++;
if (numBufferedPointInserts >= MAX_BUFFERED_LOCATIONS) {
flushPoints();
}
}
/**
* Flushes the points to the database.
*/
private void flushPoints() {
if (numBufferedPointInserts <= 0) {
return;
}
myTracksProviderUtils.bulkInsertTrackPoints(
bufferedPointInserts, numBufferedPointInserts, track.getId());
numBufferedPointInserts = 0;
}
/**
* Gets the imported track ids.
*
*/
private long[] getImportedTrackIds() {
long[] result = new long[tracksIds.size()];
for (int i = 0; i < result.length; i++) {
result[i] = tracksIds.get(i);
}
return result;
}
/**
* Gets the last point id inserted into the database.
*/
private long getLastPointId() {
flushPoints();
return myTracksProviderUtils.getLastLocationId(track.getId());
}
/**
* Creates an error message.
*
* @param message the message
*/
private String createErrorMessage(String message) {
return String.format(Locale.US, "Parsing error at line: %d column: %d. %s",
locator.getLineNumber(), locator.getColumnNumber(), message);
}
}
@@ -0,0 +1,181 @@
/*
* Copyright 2008 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.io.file;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.io.file.TrackWriterFactory.TrackFileFormat;
import com.google.android.apps.mytracks.util.StringUtils;
import com.google.android.maps.mytracks.R;
import android.content.Context;
import android.location.Location;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.text.NumberFormat;
import java.util.Locale;
/**
* Write track as GPX to a file.
*
* @author Sandor Dornbush
*/
public class GpxTrackWriter implements TrackFormatWriter {
private static final NumberFormat ELEVATION_FORMAT = NumberFormat.getInstance(Locale.US);
private static final NumberFormat COORDINATE_FORMAT = NumberFormat.getInstance(Locale.US);
static {
// GPX readers expect to see fractional numbers with US-style punctuation.
// That is, they want periods for decimal points, rather than commas.
ELEVATION_FORMAT.setMaximumFractionDigits(1);
ELEVATION_FORMAT.setGroupingUsed(false);
COORDINATE_FORMAT.setMaximumFractionDigits(5);
COORDINATE_FORMAT.setMaximumIntegerDigits(3);
COORDINATE_FORMAT.setGroupingUsed(false);
}
private final Context context;
private Track track;
private PrintWriter printWriter;
public GpxTrackWriter(Context context) {
this.context = context;
}
@Override
public String getExtension() {
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 (printWriter != null) {
printWriter.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
printWriter.println("<gpx");
printWriter.println("version=\"1.1\"");
printWriter.println(
"creator=\"" + context.getString(R.string.send_google_by_my_tracks, "", "") + "\"");
printWriter.println("xmlns=\"http://www.topografix.com/GPX/1/1\"");
printWriter.println(
"xmlns:topografix=\"http://www.topografix.com/GPX/Private/TopoGrafix/0/1\"");
printWriter.println("xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"");
printWriter.println("xsi:schemaLocation=\"http://www.topografix.com/GPX/1/1"
+ " http://www.topografix.com/GPX/1/1/gpx.xsd"
+ " http://www.topografix.com/GPX/Private/TopoGrafix/0/1"
+ " http://www.topografix.com/GPX/Private/TopoGrafix/0/1/topografix.xsd\">");
printWriter.println("<metadata>");
printWriter.println("<name>" + StringUtils.formatCData(track.getName()) + "</name>");
printWriter.println("<desc>" + StringUtils.formatCData(track.getDescription()) + "</desc>");
printWriter.println("</metadata>");
}
}
@Override
public void writeFooter() {
if (printWriter != null) {
printWriter.println("</gpx>");
}
}
@Override
public void writeBeginTrack(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 lastLocation) {
if (printWriter != null) {
printWriter.println("</trk>");
}
}
@Override
public void writeOpenSegment() {
printWriter.println("<trkseg>");
}
@Override
public void writeCloseSegment() {
printWriter.println("</trkseg>");
}
@Override
public void writeLocation(Location location) {
if (printWriter != null) {
printWriter.println("<trkpt " + formatLocation(location) + ">");
printWriter.println("<ele>" + ELEVATION_FORMAT.format(location.getAltitude()) + "</ele>");
printWriter.println("<time>" + StringUtils.formatDateTimeIso8601(location.getTime()) + "</time>");
printWriter.println("</trkpt>");
}
}
@Override
public void writeBeginWaypoints() {
// Do nothing
}
@Override
public void writeEndWaypoints() {
// Do nothing
}
@Override
public void writeWaypoint(Waypoint waypoint) {
if (printWriter != null) {
Location location = waypoint.getLocation();
if (location != null) {
printWriter.println("<wpt " + formatLocation(location) + ">");
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()) + "\"";
}
}
@@ -0,0 +1,376 @@
/*
* Copyright 2008 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.io.file;
import com.google.android.apps.mytracks.content.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 java.io.OutputStream;
import java.io.PrintWriter;
import java.util.ArrayList;
/**
* Write track as KML to a file.
*
* @author Leif Hendrik Wilden
*/
public class KmlTrackWriter implements TrackFormatWriter {
/**
* 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 static final String WAYPOINT_ICON =
"http://maps.google.com/mapfiles/kml/pushpin/blue-pushpin.png";
private static final String STATISTICS_ICON =
"http://maps.google.com/mapfiles/kml/pushpin/ylw-pushpin.png";
private static final String START_ICON =
"http://maps.google.com/mapfiles/kml/paddle/grn-circle.png";
private static final String END_ICON =
"http://maps.google.com/mapfiles/kml/paddle/red-circle.png";
private static final String TRACK_ICON =
"http://earth.google.com/images/kml-icons/track-directional/track-0.png";
private final Context context;
private final 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) {
this(context, new DescriptionGeneratorImpl(context));
}
@VisibleForTesting
KmlTrackWriter(Context context, DescriptionGenerator descriptionGenerator) {
this.context = context;
this.descriptionGenerator = descriptionGenerator;
}
@Override
public String getExtension() {
return TrackFileFormat.KML.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 (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, START_ICON, 32, 1);
writePlacemarkerStyle(END_STYLE, END_ICON, 32, 1);
writePlacemarkerStyle(STATISTICS_STYLE, STATISTICS_ICON, 20, 2);
writePlacemarkerStyle(WAYPOINT_STYLE, WAYPOINT_ICON, 20, 2);
printWriter.println("<Schema id=\"" + SCHEMA_ID + "\">");
writeSensorStyle(POWER, context.getString(R.string.description_sensor_power));
writeSensorStyle(CADENCE, context.getString(R.string.description_sensor_cadence));
writeSensorStyle(HEART_RATE, context.getString(R.string.description_sensor_heart_rate));
writeSensorStyle(BATTER_LEVEL, context.getString(R.string.description_sensor_battery_level));
printWriter.println("</Schema>");
}
}
@Override
public void writeFooter() {
if (printWriter != null) {
printWriter.println("</Document>");
printWriter.println("</kml>");
}
}
@Override
public void writeBeginWaypoints() {
if (printWriter != null) {
printWriter.println(
"<Folder><name>" + StringUtils.formatCData(context.getString(R.string.menu_markers))
+ "</name>");
}
}
@Override
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, false);
writePlacemark(name, description, END_STYLE, lastLocation);
}
}
@Override
public void writeOpenSegment() {
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 (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 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);
}
}
}
/**
* Writes the sensor data.
*
* @param list a list of sensor data
* @param name the name of the sensor data
*/
private void writeSensorData(ArrayList<Integer> list, String name) {
printWriter.println("<gx:SimpleArrayData name=\"" + name + "\">");
for (int i = 0; i < list.size(); i++) {
printWriter.println("<gx:value>" + list.get(i) + "</gx:value>");
}
printWriter.println("</gx:SimpleArrayData>");
}
/**
* Writes a placemark.
*
* @param name the name 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) {
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 + "\">");
printWriter.println("<LineStyle><color>7f0000ff</color><width>4</width></LineStyle>");
printWriter.println("<IconStyle>");
printWriter.println("<scale>1.3</scale>");
printWriter.println("<Icon><href>" + TRACK_ICON + "</href></Icon>");
printWriter.println("</IconStyle>");
printWriter.println("</Style>");
}
/**
* Writes a placemarker style.
*
* @param name the name of the style
* @param url the url of the style icon
* @param x the x position of the hotspot
* @param y the y position of the hotspot
*/
private void writePlacemarkerStyle(String name, String url, int x, int y) {
printWriter.println("<Style id=\"" + name + "\"><IconStyle>");
printWriter.println("<scale>1.3</scale>");
printWriter.println("<Icon><href>" + url + "</href></Icon>");
printWriter.println(
"<hotSpot x=\"" + x + "\" y=\"" + y + "\" xunits=\"pixels\" yunits=\"pixels\"/>");
printWriter.println("</IconStyle></Style>");
}
/**
* Writes a sensor style.
*
* @param name the name of the sesnor
* @param displayName the sensor display name
*/
private void writeSensorStyle(String name, String displayName) {
printWriter.println("<gx:SimpleArrayField name=\"" + name + "\" type=\"int\">");
printWriter.println(
"<displayName>" + StringUtils.formatCData(displayName) + "</displayName>");
printWriter.println("</gx:SimpleArrayField>");
}
}
@@ -0,0 +1,203 @@
/*
* Copyright 2011 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.io.file;
import com.google.android.apps.mytracks.io.file.TrackWriterFactory.TrackFileFormat;
import com.google.android.apps.mytracks.util.DialogUtils;
import com.google.android.apps.mytracks.util.IntentUtils;
import com.google.android.maps.mytracks.R;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.ProgressDialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import java.io.File;
/**
* An activity for saving tracks to the SD card. If saving a specific track,
* option to save it to a temp directory and play the track afterward.
*
* @author Rodrigo Damazio
*/
public class SaveActivity extends Activity {
public static final String EXTRA_TRACK_FILE_FORMAT = "track_file_format";
public static final String EXTRA_TRACK_ID = "track_id";
public static final String EXTRA_PLAY_TRACK = "play_track";
public static final String GOOGLE_EARTH_KML_MIME_TYPE = "application/vnd.google-earth.kml+xml";
public static final String GOOGLE_EARTH_PACKAGE = "com.google.earth";
public static final String GOOGLE_EARTH_MARKET_URL = "market://details?id="
+ GOOGLE_EARTH_PACKAGE;
private static final String
GOOGLE_EARTH_TOUR_FEATURE_ID = "com.google.earth.EXTRA.tour_feature_id";
private static final String GOOGLE_EARTH_CLASS = "com.google.earth.EarthActivity";
private static final int DIALOG_PROGRESS_ID = 0;
private static final int DIALOG_RESULT_ID = 1;
private TrackFileFormat trackFileFormat;
private long trackId;
private boolean playTrack;
private SaveAsyncTask saveAsyncTask;
private ProgressDialog progressDialog;
// result from the AsyncTask
private boolean success;
// message id from the AsyncTask
private int messageId;
// saved file path from the AsyncTask
private String savedPath;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Intent intent = getIntent();
trackFileFormat = intent.getParcelableExtra(EXTRA_TRACK_FILE_FORMAT);
trackId = intent.getLongExtra(EXTRA_TRACK_ID, -1L);
playTrack = intent.getBooleanExtra(EXTRA_PLAY_TRACK, false);
Object retained = getLastNonConfigurationInstance();
if (retained instanceof SaveAsyncTask) {
saveAsyncTask = (SaveAsyncTask) retained;
saveAsyncTask.setActivity(this);
} else {
saveAsyncTask = new SaveAsyncTask(this, trackFileFormat, trackId, playTrack);
saveAsyncTask.execute();
}
}
@Override
public Object onRetainNonConfigurationInstance() {
saveAsyncTask.setActivity(null);
return saveAsyncTask;
}
@Override
protected Dialog onCreateDialog(int id) {
switch (id) {
case DIALOG_PROGRESS_ID:
progressDialog = DialogUtils.createHorizontalProgressDialog(
this, R.string.sd_card_save_progress_message, new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
saveAsyncTask.cancel(true);
finish();
}
});
return progressDialog;
case DIALOG_RESULT_ID:
AlertDialog.Builder builder = new AlertDialog.Builder(this)
.setCancelable(true)
.setIcon(success
? android.R.drawable.ic_dialog_info : android.R.drawable.ic_dialog_alert)
.setMessage(messageId)
.setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
dialog.dismiss();
onPostResultDialog();
}
})
.setPositiveButton(R.string.generic_ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int arg1) {
dialog.dismiss();
onPostResultDialog();
}
})
.setTitle(success ? R.string.generic_success_title : R.string.generic_error_title);
if (success && trackId != -1L && !playTrack) {
builder.setNegativeButton(
R.string.share_track_share_file, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
Intent intent = IntentUtils.newShareFileIntent(
SaveActivity.this, trackId, savedPath, trackFileFormat);
startActivity(
Intent.createChooser(intent, getString(R.string.share_track_picker_title)));
finish();
}
});
}
return builder.create();
default:
return null;
}
}
/**
* Invokes when the associated AsyncTask completes.
*
* @param isSuccess true if the AsyncTask is successful
* @param aMessageId the id of the AsyncTask message
* @param aSavedPath the path of the saved file
*/
public void onAsyncTaskCompleted(boolean isSuccess, int aMessageId, String aSavedPath) {
this.success = isSuccess;
this.messageId = aMessageId;
this.savedPath = aSavedPath;
removeDialog(DIALOG_PROGRESS_ID);
showDialog(DIALOG_RESULT_ID);
}
/**
* Shows the progress dialog.
*/
public void showProgressDialog() {
showDialog(DIALOG_PROGRESS_ID);
}
/**
* Sets the progress dialog value.
*
* @param number the number of points saved
* @param max the maximum number of points
*/
public void setProgressDialogValue(int number, int max) {
if (progressDialog != null) {
progressDialog.setIndeterminate(false);
progressDialog.setMax(max);
progressDialog.setProgress(Math.min(number, max));
}
}
/**
* To be invoked after showing the result dialog.
*/
private void onPostResultDialog() {
if (success && playTrack) {
Intent intent = new Intent()
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK)
.putExtra(GOOGLE_EARTH_TOUR_FEATURE_ID, KmlTrackWriter.TOUR_FEATURE_ID)
.setClassName(GOOGLE_EARTH_PACKAGE, GOOGLE_EARTH_CLASS)
.setDataAndType(Uri.fromFile(new File(savedPath)), GOOGLE_EARTH_KML_MIME_TYPE);
startActivity(intent);
}
finish();
}
}
@@ -0,0 +1,222 @@
/*
* Copyright 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.io.file;
import com.google.android.apps.mytracks.content.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.PreferencesUtils;
import com.google.android.apps.mytracks.util.SystemUtils;
import com.google.android.maps.mytracks.R;
import android.content.Context;
import android.database.Cursor;
import android.os.AsyncTask;
import android.os.PowerManager.WakeLock;
import android.util.Log;
import java.io.File;
/**
* Async Task to save tracks to the SD card.
*
* @author Jimmy Shih
*/
public class SaveAsyncTask extends AsyncTask<Void, Integer, Boolean> {
private static final String TAG = SaveAsyncTask.class.getSimpleName();
private SaveActivity saveActivity;
private final TrackFileFormat trackFileFormat;
private final long trackId;
private final boolean useTempDir;
private final Context context;
private final MyTracksProviderUtils myTracksProviderUtils;
private WakeLock wakeLock;
private TrackWriter trackWriter;
// true if the AsyncTask result is success
private boolean success;
// true if the AsyncTask has completed
private boolean completed;
// message id to return to the activity
private int messageId;
// saved file path to return to the activity
private String savedPath;
/**
* Creates an AsyncTask.
*
* @param saveActivity the activity currently associated with this task.
* @param trackFileFormat the track format
* @param trackId the track id
* @param useTempDir true to use the temp directory
*/
public SaveAsyncTask(SaveActivity saveActivity, TrackFileFormat trackFileFormat, long trackId,
boolean useTempDir) {
this.saveActivity = saveActivity;
this.trackFileFormat = trackFileFormat;
this.trackId = trackId;
this.useTempDir = useTempDir;
context = saveActivity.getApplicationContext();
myTracksProviderUtils = MyTracksProviderUtils.Factory.get(saveActivity);
// Get the wake lock if not recording
if (PreferencesUtils.getLong(saveActivity, R.string.recording_track_id_key)
== PreferencesUtils.RECORDING_TRACK_ID_DEFAULT) {
wakeLock = SystemUtils.acquireWakeLock(saveActivity, wakeLock);
}
trackWriter = null;
success = false;
completed = false;
messageId = R.string.sd_card_save_error;
savedPath = null;
}
/**
* Sets the current activity associated with this AyncTask.
*
* @param saveActivity the current activity, can be null
*/
public void setActivity(SaveActivity saveActivity) {
this.saveActivity = saveActivity;
if (completed && saveActivity != null) {
saveActivity.onAsyncTaskCompleted(success, messageId, savedPath);
}
}
@Override
protected void onPreExecute() {
if (saveActivity != null) {
saveActivity.showProgressDialog();
}
}
@Override
protected Boolean doInBackground(Void... params) {
try {
if (trackId != -1L) {
return saveOneTrack(trackId, true);
} else {
return saveAllTracks();
}
} finally {
// Release the wake lock if obtained
if (wakeLock != null && wakeLock.isHeld()) {
wakeLock.release();
}
}
}
/**
* Saves one track.
*
* @param id the track id
* @param updateSavingProgress true to update the saving progress
*/
private Boolean saveOneTrack(long id, final boolean updateSavingProgress) {
trackWriter = TrackWriterFactory.newWriter(
context, myTracksProviderUtils, id, trackFileFormat);
if (trackWriter == null) {
Log.e(TAG, "Track writer is null");
return false;
}
if (useTempDir) {
String dirName = FileUtils.buildExternalDirectoryPath(trackFileFormat.getExtension(), "tmp");
trackWriter.setDirectory(new File(dirName));
}
trackWriter.setOnWriteListener(new TrackWriter.OnWriteListener() {
@Override
public void onWrite(int number, int max) {
// Update the progress dialog once every 500 points
if (updateSavingProgress && number % 500 == 0) {
publishProgress(number, max);
}
}
});
trackWriter.writeTrack();
messageId = trackWriter.getErrorMessage();
savedPath = trackWriter.getAbsolutePath();
return trackWriter.wasSuccess();
}
/**
* Saves all the tracks.
*/
private Boolean saveAllTracks() {
Cursor cursor = null;
try {
cursor = myTracksProviderUtils.getTracksCursor(null, null, TracksColumns._ID);
if (cursor == null) {
messageId = R.string.sd_card_save_error_no_track;
return false;
}
int count = cursor.getCount();
if (count == 0) {
messageId = R.string.sd_card_save_error_no_track;
return false;
}
int idIndex = cursor.getColumnIndexOrThrow(TracksColumns._ID);
for (int i = 0; i < count; i++) {
if (isCancelled()) {
return false;
}
cursor.moveToPosition(i);
long id = cursor.getLong(idIndex);
if (!saveOneTrack(id, false)) {
return false;
}
publishProgress(i + 1, count);
}
messageId = R.string.sd_card_save_success;
return true;
} finally {
if (cursor != null) {
cursor.close();
}
}
}
@Override
protected void onProgressUpdate(Integer... values) {
if (saveActivity != null) {
saveActivity.setProgressDialogValue(values[0], values[1]);
}
}
@Override
protected void onPostExecute(Boolean result) {
success = result;
completed = true;
if (saveActivity != null) {
saveActivity.onAsyncTaskCompleted(success, messageId, savedPath);
}
}
@Override
protected void onCancelled() {
if (trackWriter != null) {
trackWriter.stopWriteTrack();
}
}
}
@@ -0,0 +1,329 @@
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.io.file;
import com.google.android.apps.mytracks.content.MyTracksLocation;
import com.google.android.apps.mytracks.content.Sensor;
import com.google.android.apps.mytracks.content.Sensor.SensorDataSet;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.io.file.TrackWriterFactory.TrackFileFormat;
import com.google.android.apps.mytracks.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 java.io.OutputStream;
import java.io.PrintWriter;
import java.util.Locale;
/**
* Write track as TCX to a file. See http://developer.garmin.com/schemas/tcx/v2/
* for info on TCX. <br>
* The TCX file output is verified by uploading the file to
* http://connect.garmin.com/.
*
* @author Sandor Dornbush
* @author Dominik Ršttsches
*/
public class TcxTrackWriter implements TrackFormatWriter {
/**
* TCX sport type. See the TCX spec.
*
* @author Jimmy Shih
*/
private enum SportType {
RUNNING("Running"),
BIKING("Biking"),
OTHER("Other");
private final String name;
private SportType(String name) {
this.name = name;
}
/**
* Gets the name of the sport type
*/
public String getName() {
return name;
}
}
// My Tracks categories that are considered as TCX biking sport type.
private static final int TCX_SPORT_BIKING_IDS[] = {
R.string.activity_type_cycling,
R.string.activity_type_dirt_bike,
R.string.activity_type_mountain_biking,
R.string.activity_type_road_biking,
R.string.activity_type_track_cycling };
// My Tracks categories that are considered as TCX running sport type.
private static final int TCX_SPORT_RUNNING_IDS[] = {
R.string.activity_type_running,
R.string.activity_type_speed_walking,
R.string.activity_type_street_running,
R.string.activity_type_track_running,
R.string.activity_type_trail_running,
R.string.activity_type_walking };
private final Context context;
private Track track;
private PrintWriter printWriter;
private SportType sportType;
public TcxTrackWriter(Context context) {
this.context = context;
}
@Override
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 (printWriter != null) {
printWriter.close();
printWriter = null;
}
}
@Override
public String getExtension() {
return TrackFileFormat.TCX.getExtension();
}
@Override
public void writeHeader() {
if (printWriter != null) {
printWriter.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
printWriter.println("<TrainingCenterDatabase"
+ " xmlns=\"http://www.garmin.com/xmlschemas/TrainingCenterDatabase/v2\"");
printWriter.println("xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"");
printWriter.println("xsi:schemaLocation="
+ "\"http://www.garmin.com/xmlschemas/TrainingCenterDatabase/v2"
+ " http://www.garmin.com/xmlschemas/TrainingCenterDatabasev2.xsd\">");
}
}
@Override
public void writeFooter() {
if (printWriter != null) {
printWriter.println("<Author xsi:type=\"Application_t\">");
printWriter.println("<Name>"
+ StringUtils.formatCData(context.getString(R.string.send_google_by_my_tracks, "", ""))
+ "</Name>");
// <Build>, <LangID>, and <PartNumber> are required by type=Application_t.
printWriter.println("<Build>");
writeVersion();
printWriter.println("</Build>");
printWriter.println("<LangID>" + Locale.getDefault().getLanguage() + "</LangID>");
printWriter.println("<PartNumber>000-00000-00</PartNumber>");
printWriter.println("</Author>");
printWriter.println("</TrainingCenterDatabase>");
}
}
@Override
public void writeBeginTrack(Location firstPoint) {
if (printWriter != null) {
String startTime = StringUtils.formatDateTimeIso8601(
track.getTripStatistics().getStartTime());
long totalTimeInSeconds = track.getTripStatistics().getTotalTime() / 1000;
printWriter.println("<Activities>");
printWriter.println("<Activity Sport=\"" + sportType.getName() + "\">");
printWriter.println("<Id>" + startTime + "</Id>");
printWriter.println("<Lap StartTime=\"" + startTime + "\">");
printWriter.println("<TotalTimeSeconds>" + totalTimeInSeconds + "</TotalTimeSeconds>");
printWriter.println("<DistanceMeters>" + track.getTripStatistics().getTotalDistance()
+ "</DistanceMeters>");
// <Calories> is required, just put in 0.
printWriter.println("<Calories>0</Calories>");
printWriter.println("<Intensity>Active</Intensity>");
printWriter.println("<TriggerMethod>Manual</TriggerMethod>");
}
}
@Override
public void writeEndTrack(Location lastPoint) {
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>");
}
}
@Override
public void writeOpenSegment() {
if (printWriter != null) {
printWriter.println("<Track>");
}
}
@Override
public void writeCloseSegment() {
if (printWriter != null) {
printWriter.println("</Track>");
}
}
@Override
public void writeLocation(Location location) {
if (printWriter != null) {
printWriter.println("<Trackpoint>");
printWriter.println("<Time>" + StringUtils.formatDateTimeIso8601(location.getTime()) + "</Time>");
printWriter.println("<Position>");
printWriter.println("<LatitudeDegrees>" + location.getLatitude() + "</LatitudeDegrees>");
printWriter.println("<LongitudeDegrees>" + location.getLongitude() + "</LongitudeDegrees>");
printWriter.println("</Position>");
printWriter.println("<AltitudeMeters>" + location.getAltitude() + "</AltitudeMeters>");
if (location instanceof MyTracksLocation) {
SensorDataSet sensorDataSet = ((MyTracksLocation) location).getSensorDataSet();
if (sensorDataSet != null) {
boolean heartRateAvailable = sensorDataSet.hasHeartRate()
&& sensorDataSet.getHeartRate().hasValue()
&& sensorDataSet.getHeartRate().getState() == Sensor.SensorState.SENDING;
boolean cadenceAvailable = sensorDataSet.hasCadence()
&& sensorDataSet.getCadence().hasValue()
&& sensorDataSet.getCadence().getState() == Sensor.SensorState.SENDING;
boolean powerAvailable = sensorDataSet.hasPower()
&& sensorDataSet.getPower().hasValue()
&& sensorDataSet.getPower().getState() == Sensor.SensorState.SENDING;
if (heartRateAvailable) {
printWriter.println("<HeartRateBpm>");
printWriter.println("<Value>" + sensorDataSet.getHeartRate().getValue() + "</Value>");
printWriter.println("</HeartRateBpm>");
}
// <Cadence> needs to be put before <Extensions>.
// According to the TCX spec, <Cadence> is only for the biking sport
// type. For others, use <RunCadence> in <Extensions>.
if (cadenceAvailable && sportType == SportType.BIKING) {
// The spec requires the max value be 254.
printWriter.println(
"<Cadence>" + Math.min(254, sensorDataSet.getCadence().getValue()) + "</Cadence>");
}
if ((cadenceAvailable && sportType != SportType.BIKING) || powerAvailable) {
printWriter.println("<Extensions>");
printWriter.println(
"<TPX xmlns=\"http://www.garmin.com/xmlschemas/ActivityExtension/v2\">");
// <RunCadence> needs to be put before <Watts>.
if (cadenceAvailable && sportType != SportType.BIKING) {
// The spec requires the max value to be 254.
printWriter.println("<RunCadence>"
+ Math.min(254, sensorDataSet.getCadence().getValue()) + "</RunCadence>");
}
if (powerAvailable) {
printWriter.println("<Watts>" + sensorDataSet.getPower().getValue() + "</Watts>");
}
printWriter.println("</TPX>");
printWriter.println("</Extensions>");
}
}
}
printWriter.println("</Trackpoint>");
}
}
@Override
public void writeBeginWaypoints() {
// Do nothing.
}
@Override
public void writeEndWaypoints() {
// Do nothing.
}
@Override
public void writeWaypoint(Waypoint waypoint) {
// Do nothing.
}
/**
* Writes the TCX Version.
*/
private void writeVersion() {
// Split the My Tracks version code into VersionMajor, VersionMinor, and,
// BuildMajor to fit the integer type requirement for these fields in the
// TCX spec.
String[] versionComponents = SystemUtils.getMyTracksVersion(context).split("\\.");
int versionMajor = versionComponents.length > 0 ? Integer.valueOf(versionComponents[0]) : 0;
int versionMinor = versionComponents.length > 1 ? Integer.valueOf(versionComponents[1]) : 0;
int buildMajor = versionComponents.length > 2 ? Integer.valueOf(versionComponents[2]) : 0;
printWriter.println("<Version>");
printWriter.println("<VersionMajor>" + versionMajor + "</VersionMajor>");
printWriter.println("<VersionMinor>" + versionMinor + "</VersionMinor>");
// According to TCX spec, these are optional. But http://connect.garmin.com
// requires them.
printWriter.println("<BuildMajor>" + buildMajor + "</BuildMajor>");
printWriter.println("<BuildMinor>0</BuildMinor>");
printWriter.println("</Version>");
}
/**
* Gets the sport type from the category.
*
* @param category the category
*/
private SportType getSportType(String category) {
category = category.trim();
// For tracks with localized category.
for (int i : TCX_SPORT_RUNNING_IDS) {
if (category.equalsIgnoreCase(context.getString(i))) {
return SportType.RUNNING;
}
}
for (int i : TCX_SPORT_BIKING_IDS) {
if (category.equalsIgnoreCase(context.getString(i))) {
return SportType.BIKING;
}
}
// For tracks without localized category.
if (category.equalsIgnoreCase(SportType.RUNNING.getName())) {
return SportType.RUNNING;
} else if (category.equalsIgnoreCase(SportType.BIKING.getName())) {
return SportType.BIKING;
} else {
return SportType.OTHER;
}
}
}
@@ -0,0 +1,126 @@
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.io.file;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.content.Waypoint;
import android.location.Location;
import java.io.OutputStream;
/**
* Interface for writing 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>
* <li>{@link #writeOpenSegment}
* <li>For each location in the segment: {@link #writeLocation}
* <li>{@link #writeCloseSegment}
* </ol>
* <li>{@link #writeEndTrack}
* <li>{@link #writeFooter}
* <li>{@link #close}
* </ol>
*
* @author Rodrigo Damazio
*/
public interface TrackFormatWriter {
/**
* Gets the file extension (i.e. gpx, kml, ...)
*/
public String getExtension();
/**
* 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
*/
public void prepare(Track track, OutputStream outputStream);
/**
* Closes the underlying file handler.
*/
public void close();
/**
* Writes the header.
*/
public void writeHeader();
/**
* Writes the footer.
*/
public void writeFooter();
/**
* Writes the beginning of the waypoints.
*/
public void writeBeginWaypoints();
/**
* Writes the end of the waypoints.
*/
public void writeEndWaypoints();
/**
* Writes a waypoint.
*
* @param waypoint the waypoint
*/
public void writeWaypoint(Waypoint waypoint);
/**
* Writes the beginning of the track.
*
* @param 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
*/
public void writeLocation(Location location);
}
@@ -0,0 +1,77 @@
/*
* Copyright 2011 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.io.file;
import java.io.File;
/**
* Implementations of this class export tracks to the SD card. This class is
* intended to be format-neutral - it handles creating the output file and
* reading the track to be exported, but requires an instance of
* {@link TrackFormatWriter} to actually format the data.
*
* @author Sandor Dornbush
* @author Rodrigo Damazio
*/
public interface TrackWriter {
/** This listener is used to signal track writes. */
public interface OnWriteListener {
/**
* This method is invoked whenever a location within a track is written.
* @param number the location number
* @param max the maximum number of locations, for calculation of
* completion percentage
*/
public void onWrite(int number, int max);
}
/**
* Sets a listener to be invoked for each location writer.
*/
void setOnWriteListener(OnWriteListener onWriteListener);
/**
* Sets a custom directory where the file will be written.
*/
void setDirectory(File directory);
/**
* Returns the absolute path to the file which was created.
*/
String getAbsolutePath();
/**
* Writes the given track id to the SD card.
* This is blocking.
*/
void writeTrack();
/**
* Stop any in-progress writes
*/
void stopWriteTrack();
/**
* Returns true if the write completed successfully.
*/
boolean wasSuccess();
/**
* Returns the error message (if any) generated by a writer failure.
*/
int getErrorMessage();
}
@@ -0,0 +1,145 @@
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.io.file;
import static com.google.android.apps.mytracks.Constants.TAG;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.content.Track;
import android.content.Context;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.Log;
/**
* A factory to produce track writers for any format.
*
* @author Rodrigo Damazio
*/
public class TrackWriterFactory {
/**
* Definition of all possible track formats.
*/
public enum TrackFileFormat implements Parcelable {
GPX {
@Override
TrackFormatWriter newFormatWriter(Context context) {
return new GpxTrackWriter(context);
}
},
KML {
@Override
TrackFormatWriter newFormatWriter(Context context) {
return new KmlTrackWriter(context);
}
},
CSV {
@Override
public TrackFormatWriter newFormatWriter(Context context) {
return new CsvTrackWriter(context);
}
},
TCX {
@Override
public TrackFormatWriter newFormatWriter(Context context) {
return new TcxTrackWriter(context);
}
};
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(final Parcel dest, final int flags) {
dest.writeInt(ordinal());
}
public static final Creator<TrackFileFormat> CREATOR = new Creator<TrackFileFormat>() {
@Override
public TrackFileFormat createFromParcel(final Parcel source) {
return TrackFileFormat.values()[source.readInt()];
}
@Override
public TrackFileFormat[] newArray(final int size) {
return new TrackFileFormat[size];
}
};
/**
* Creates and returns a new format writer for each format.
*/
abstract TrackFormatWriter newFormatWriter(Context context);
/**
* Returns the mime type for each format.
*/
public String getMimeType() {
return "application/" + getExtension() + "+xml";
}
/**
* Returns the file extension for each format.
*/
public String getExtension() {
return this.name().toLowerCase();
}
}
/**
* Creates a new track writer to write the track with the given ID.
*
* @param context the context in which the track will be read
* @param providerUtils the data provider utils to read the track with
* @param trackId the ID of the track to be written
* @param format the output format to write in
* @return the new track writer
*/
public static TrackWriter newWriter(Context context,
MyTracksProviderUtils providerUtils,
long trackId, TrackFileFormat format) {
Track track = providerUtils.getTrack(trackId);
if (track == null) {
Log.d(TAG, "No track for " + trackId);
return null;
}
return newWriter(context, providerUtils, track, format);
}
/**
* Creates a new track writer to write the given track.
*
* @param context the context in which the track will be read
* @param providerUtils the data provider utils to read the track with
* @param track the track to be written
* @param format the output format to write in
* @return the new track writer
*/
private static TrackWriter newWriter(Context context,
MyTracksProviderUtils providerUtils,
Track track, TrackFileFormat format) {
TrackFormatWriter writer = format.newFormatWriter(context);
return new TrackWriterImpl(context, providerUtils, track, writer);
}
private TrackWriterFactory() { }
}
@@ -0,0 +1,379 @@
/*
* Copyright 2008 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.io.file;
import static com.google.android.apps.mytracks.Constants.TAG;
import com.google.android.apps.mytracks.Constants;
import com.google.android.apps.mytracks.content.MyTracksLocation;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils.LocationIterator;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.util.FileUtils;
import com.google.android.apps.mytracks.util.LocationUtils;
import com.google.android.maps.mytracks.R;
import android.app.Activity;
import android.content.Context;
import android.database.Cursor;
import android.location.Location;
import android.util.Log;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.OutputStream;
/**
* This class exports tracks to the SD card. It is intended to be format-
* neutral -- it handles creating the output file and reading the track to be
* exported, but requires an instance of {@link TrackFormatWriter} to actually
* format the data.
*
* @author Sandor Dornbush
* @author Rodrigo Damazio
*/
class TrackWriterImpl implements TrackWriter {
private final Context context;
private final MyTracksProviderUtils providerUtils;
private final Track track;
private final TrackFormatWriter writer;
private boolean success = false;
private int errorMessage = R.string.sd_card_save_error;
private File directory = null;
private File file = null;
private OnWriteListener onWriteListener;
private Thread writeThread;
TrackWriterImpl(Context context, MyTracksProviderUtils providerUtils,
Track track, TrackFormatWriter writer) {
this.context = context;
this.providerUtils = providerUtils;
this.track = track;
this.writer = writer;
}
@Override
public void setOnWriteListener(OnWriteListener onWriteListener) {
this.onWriteListener = onWriteListener;
}
@Override
public void setDirectory(File directory) {
this.directory = directory;
}
@Override
public String getAbsolutePath() {
return file.getAbsolutePath();
}
private void writeTrackAsync() {
writeThread = new Thread() {
@Override
public void run() {
doWriteTrack();
}
};
writeThread.start();
}
@Override
public void writeTrack() {
writeTrackAsync();
try {
writeThread.join();
} catch (InterruptedException e) {
Log.e(Constants.TAG, "Interrupted waiting for write to complete", e);
}
}
private void doWriteTrack() {
// Open the input and output
success = false;
errorMessage = R.string.sd_card_save_error;
if (track != null) {
if (openFile()) {
try {
writeDocument();
} catch (InterruptedException e) {
Log.i(Constants.TAG, "The track write was interrupted");
if (file != null) {
if (!file.delete()) {
Log.w(TAG, "Failed to delete file " + file.getAbsolutePath());
}
}
success = false;
errorMessage = R.string.sd_card_canceled;
}
}
}
}
public void stopWriteTrack() {
if (writeThread != null && writeThread.isAlive()) {
Log.i(Constants.TAG, "Attempting to stop track write");
writeThread.interrupt();
try {
writeThread.join();
Log.i(Constants.TAG, "Track write stopped");
} catch (InterruptedException e) {
Log.e(Constants.TAG, "Failed to wait for writer to stop", e);
}
}
}
@Override
public int getErrorMessage() {
return errorMessage;
}
@Override
public boolean wasSuccess() {
return success;
}
/*
* Helper methods:
* ===============
*/
/**
* Runs the given runnable in the UI thread.
*/
protected void runOnUiThread(Runnable runnable) {
if (context instanceof Activity) {
((Activity) context).runOnUiThread(runnable);
}
}
/**
* Opens the file and prepares the format writer for it.
*
* @return true on success, false otherwise (and errorMessage is set)
*/
protected boolean openFile() {
if (!canWriteFile()) {
return false;
}
// Make sure the file doesn't exist yet (possibly by changing the filename)
String fileName = FileUtils.buildUniqueFileName(
directory, track.getName(), writer.getExtension());
if (fileName == null) {
Log.e(Constants.TAG,
"Unable to get a unique filename for " + track.getName());
return false;
}
Log.i(Constants.TAG, "Writing track to: " + fileName);
try {
writer.prepare(track, newOutputStream(fileName));
} catch (FileNotFoundException e) {
Log.e(Constants.TAG, "Failed to open output file.", e);
errorMessage = R.string.sd_card_save_error;
return false;
}
return true;
}
/**
* Checks and returns whether we're ready to create the output file.
*/
protected boolean canWriteFile() {
if (directory == null) {
String dirName =
FileUtils.buildExternalDirectoryPath(writer.getExtension());
directory = newFile(dirName);
}
if (!FileUtils.isSdCardAvailable()) {
Log.i(Constants.TAG, "Could not find SD card.");
errorMessage = R.string.sd_card_error_no_storage;
return false;
}
if (!FileUtils.ensureDirectoryExists(directory)) {
Log.i(Constants.TAG, "Could not create export directory.");
errorMessage = R.string.sd_card_save_error_create_dir;
return false;
}
return true;
}
/**
* Creates a new output stream to write to the given filename.
*
* @throws FileNotFoundException if the file could't be created
*/
protected OutputStream newOutputStream(String fileName)
throws FileNotFoundException {
file = new File(directory, fileName);
return new FileOutputStream(file);
}
/**
* Creates a new file object for the given path.
*/
protected File newFile(String path) {
return new File(path);
}
/**
* Writes the waypoints for the given track.
*
* @param trackId the ID of the track to write waypoints for
*/
private void writeWaypoints(long trackId) {
// TODO: Stream through he waypoints in chunks.
// I am leaving the number of waypoints very high which should not be a
// problem because we don't try to load them into objects all at the
// same time.
Cursor cursor = null;
cursor = providerUtils.getWaypointsCursor(trackId, 0,
Constants.MAX_LOADED_WAYPOINTS_POINTS);
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);
}
}
} finally {
cursor.close();
}
}
if (hasWaypoints) {
writer.writeEndWaypoints();
}
}
/**
* Does the actual work of writing the track to the now open file.
*/
void writeDocument() throws InterruptedException {
Log.d(Constants.TAG, "Started writing track.");
writer.writeHeader();
writeWaypoints(track.getId());
writeLocations();
writer.writeFooter();
writer.close();
success = true;
Log.d(Constants.TAG, "Done writing track.");
errorMessage = R.string.sd_card_save_success;
}
private void writeLocations() throws InterruptedException {
boolean wroteFirst = false;
boolean segmentOpen = false;
boolean isLastValid = false;
class TrackWriterLocationFactory implements MyTracksProviderUtils.LocationFactory {
Location currentLocation;
Location lastLocation;
@Override
public Location createLocation() {
if (currentLocation == null) {
currentLocation = new MyTracksLocation("");
}
return currentLocation;
}
public void swapLocations() {
Location tmpLoc = lastLocation;
lastLocation = currentLocation;
currentLocation = tmpLoc;
if (currentLocation != null) {
currentLocation.reset();
}
}
};
TrackWriterLocationFactory locationFactory = new TrackWriterLocationFactory();
LocationIterator it = providerUtils.getLocationIterator(track.getId(), 0, false,
locationFactory);
try {
if (!it.hasNext()) {
// Writes an empty track
writer.writeBeginTrack(null);
writer.writeEndTrack(null);
return;
}
int pointNumber = 0;
while (it.hasNext()) {
Location loc = it.next();
if (Thread.interrupted()) {
throw new InterruptedException();
}
pointNumber++;
boolean isValid = LocationUtils.isValidLocation(loc);
boolean validSegment = isValid && isLastValid;
if (!wroteFirst && validSegment) {
// Found the first two consecutive points which are valid
writer.writeBeginTrack(locationFactory.lastLocation);
wroteFirst = true;
}
if (validSegment) {
if (!segmentOpen) {
// Start a segment for this point
writer.writeOpenSegment();
segmentOpen = true;
// Write the previous point, which we had previously skipped
writer.writeLocation(locationFactory.lastLocation);
}
// Write the current point
writer.writeLocation(loc);
if (onWriteListener != null) {
onWriteListener.onWrite(pointNumber, track.getNumberOfPoints());
}
} else {
if (segmentOpen) {
writer.writeCloseSegment();
segmentOpen = false;
}
}
locationFactory.swapLocations();
isLastValid = isValid;
}
if (segmentOpen) {
writer.writeCloseSegment();
segmentOpen = false;
}
if (wroteFirst) {
writer.writeEndTrack(locationFactory.lastLocation);
}
} finally {
it.close();
}
}
}
@@ -0,0 +1,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.apps.mytracks.util.IntentUtils;
import com.google.android.maps.mytracks.R;
import com.google.common.annotations.VisibleForTesting;
import android.content.Intent;
/**
* An activity to send a track to Google Fusion Tables.
*
* @author Jimmy Shih
*/
public class SendFusionTablesActivity extends AbstractSendActivity {
@Override
protected AbstractSendAsyncTask createAsyncTask() {
return new SendFusionTablesAsyncTask(this, sendRequest.getTrackId(), sendRequest.getAccount());
}
@Override
protected String getServiceName() {
return getString(R.string.send_google_fusion_tables);
}
@Override
protected void startNextActivity(boolean success, boolean isCancel) {
sendRequest.setFusionTablesSuccess(success);
Class<?> next = getNextClass(sendRequest, isCancel);
Intent intent = IntentUtils.newIntent(this, next)
.putExtra(SendRequest.SEND_REQUEST_KEY, sendRequest);
startActivity(intent);
finish();
}
@VisibleForTesting
Class<?> getNextClass(SendRequest request, boolean isCancel) {
if (isCancel) {
return UploadResultActivity.class;
} else {
if (request.isSendDocs()) {
return SendDocsActivity.class;
} else {
return UploadResultActivity.class;
}
}
}
}
@@ -0,0 +1,477 @@
// 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.PreferencesUtils;
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 com.google.common.annotations.VisibleForTesting;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.accounts.AuthenticatorException;
import android.accounts.OperationCanceledException;
import android.content.Context;
import android.database.Cursor;
import android.location.Location;
import android.util.Log;
import java.io.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 {
@VisibleForTesting
public static final String APP_NAME_PREFIX = "Google-MyTracks-";
private static final String SQL_KEY = "sql=";
@VisibleForTesting
public static final String CONTENT_TYPE = "application/x-www-form-urlencoded";
@VisibleForTesting
public static final String FUSION_TABLES_BASE_URL =
"https://www.google.com/fusiontables/api/query";
private static final int MAX_POINTS_PER_UPLOAD = 2048;
@VisibleForTesting
public 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_blue";
private static final String MARKER_TYPE_STATISTICS = "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) {
Log.d(TAG, "No track for " + trackId);
return;
}
track.setTableId(tableId);
myTracksProviderUtils.updateTrack(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, "No track for " + trackId);
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 {
boolean metricUnits = PreferencesUtils.getBoolean(
context, R.string.metric_units_key, PreferencesUtils.METRIC_UNITS_DEFAULT);
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.getTripStatistics().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(
descriptionGenerator.generateTrackDescription(track, distances, elevations, true));
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);
String type = wpt.getType() == Waypoint.TYPE_STATISTICS ? MARKER_TYPE_STATISTICS
: MARKER_TYPE_WAYPOINT;
if (!createNewPoint(wpt.getName(), wpt.getDescription(), wpt.getLocation(), type)) {
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 = null;
try {
content = response.getContent();
if (setTableId) {
tableId = SendFusionTablesUtils.getTableId(content);
if (tableId == null) {
Log.d(TAG, "tableId is null");
return false;
}
}
} catch (IOException e) {
Log.d(TAG, "Unable to get response", e);
return false;
} finally {
if (content != null) {
try {
content.close();
} catch (IOException e) {
Log.d(TAG, "Unable to close content", e);
}
}
}
} else {
Log.d(TAG,
"sendQuery failed: " + response.getStatusMessage() + ": " + response.getStatusCode());
return false;
}
return true;
}
/**
* Updates the progress based on the number of locations uploaded.
*
* @param uploaded the number of uploaded locations
* @param total the number of total locations
*/
private void updateProgress(int uploaded, int total) {
double totalPercentage = (double) uploaded / total;
double scaledPercentage = totalPercentage
* (PROGRESS_UPLOAD_DATA_MAX - PROGRESS_UPLOAD_DATA_MIN) + PROGRESS_UPLOAD_DATA_MIN;
publishProgress((int) scaledPercentage);
}
}
@@ -0,0 +1,205 @@
/*
* 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.getTripStatistics() == null
|| track.getTableId() == null
|| track.getTableId().length() == 0) {
Log.e(TAG, "Invalid track");
return null;
}
double latE6;
double lonE6;
int z;
if (track.getNumberOfPoints() < 2) {
// Use Google's latitude and longitude
latE6 = 37.423 * 1.E6;
lonE6 = -122.084 * 1.E6;
z = 2;
} else {
TripStatistics stats = track.getTripStatistics();
latE6 = stats.getBottom() + (stats.getTop() - stats.getBottom()) / 2;
lonE6 = stats.getLeft() + (stats.getRight() - stats.getLeft()) / 2;
z = 15;
}
// We explicitly format with Locale.US because we need the latitude and
// longitude to be formatted in a locale-independent manner. Specifically,
// we need the decimal separator to be a period rather than a comma.
return String.format(
Locale.US, MAP_URL, track.getTableId(), latE6 / 1.E6, lonE6 / 1.E6, z);
}
/**
* Formats an array of values as a SQL VALUES like
* ('value1','value2',...,'value_n'). Escapes single quotes with two single
* quotes.
*
* @param values an array of values to format
* @return the formated SQL VALUES.
*/
public static String formatSqlValues(String... values) {
StringBuilder builder = new StringBuilder("(");
for (int i = 0; i < values.length; i++) {
if (i > 0) {
builder.append(',');
}
builder.append('\'');
builder.append(escapeSqlString(values[i]));
builder.append('\'');
}
builder.append(")");
return builder.toString();
}
/**
* Escapes a SQL string. Escapes single quotes with two single quotes.
*
* @param string the string
* @return the escaped string.
*/
public static String escapeSqlString(String string) {
return string.replaceAll("'", "''");
}
/**
* Gets a KML Point value representing a location.
*
* @param location the location
* @return the KML Point value.
*/
public static String getKmlPoint(Location location) {
StringBuilder builder = new StringBuilder("<Point><coordinates>");
if (location != null) {
appendLocation(location, builder);
}
builder.append("</coordinates></Point>");
return builder.toString();
}
/**
* Gets a KML LineString value representing an array of locations.
*
* @param locations the locations.
* @return the KML LineString value.
*/
public static String getKmlLineString(ArrayList<Location> locations) {
StringBuilder builder = new StringBuilder("<LineString><coordinates>");
if (locations != null) {
for (int i = 0; i < locations.size(); i++) {
if (i != 0) {
builder.append(' ');
}
appendLocation(locations.get(i), builder);
}
}
builder.append("</coordinates></LineString>");
return builder.toString();
}
/**
* Appends a location to a string builder using "longitude,latitude[,altitude]" format.
*
* @param location the location
* @param builder the string builder
*/
@VisibleForTesting
static void appendLocation(Location location, StringBuilder builder) {
builder.append(location.getLongitude()).append(",").append(location.getLatitude());
if (location.hasAltitude()) {
builder.append(",");
builder.append(location.getAltitude());
}
}
/**
* Gets the table id from an input streawm.
*
* @param inputStream input stream
* @return table id or null if not available.
*/
public static String getTableId(InputStream inputStream) {
if (inputStream == null) {
Log.d(TAG, "inputStream is null");
return null;
}
byte[] result = new byte[1024];
int read;
try {
read = inputStream.read(result);
} catch (IOException e) {
Log.d(TAG, "Unable to read result", e);
return null;
}
if (read == -1) {
Log.d(TAG, "no data read");
return null;
}
String s;
try {
s = new String(result, 0, read, UTF8);
} catch (UnsupportedEncodingException e) {
Log.d(TAG, "Unable to parse result", e);
return null;
}
String[] lines = s.split(Strings.LINE_SEPARATOR);
if (lines.length > 1 && lines[0].equals(TABLE_ID)) {
// returns the next line
return lines[1];
} else {
Log.d(TAG, "Response is not valid: " + s);
return null;
}
}
}
@@ -0,0 +1,459 @@
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.io.gdata;
import com.google.wireless.gdata.client.GDataClient;
import com.google.wireless.gdata.client.HttpException;
import com.google.wireless.gdata.client.QueryParams;
import com.google.wireless.gdata.data.StringUtils;
import com.google.wireless.gdata.parser.ParseException;
import com.google.wireless.gdata.serializer.GDataSerializer;
import android.text.TextUtils;
import android.util.Log;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLEncoder;
import java.util.zip.GZIPInputStream;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.StatusLine;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.entity.AbstractHttpEntity;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.entity.InputStreamEntity;
import org.apache.http.impl.client.DefaultHttpClient;
/**
* Implementation of a GDataClient using GoogleHttpClient to make HTTP requests.
* Always issues GETs and POSTs, using the X-HTTP-Method-Override header when a
* PUT or DELETE is desired, to avoid issues with firewalls, etc., that do not
* allow methods other than GET or POST.
*/
public class AndroidGDataClient implements GDataClient {
private static final String TAG = "GDataClient";
private static final boolean DEBUG = false;
private static final String X_HTTP_METHOD_OVERRIDE = "X-HTTP-Method-Override";
private static final int MAX_REDIRECTS = 10;
private final HttpClient httpClient;
/**
* Interface for creating HTTP requests. Used by
* {@link AndroidGDataClient#createAndExecuteMethod}, since HttpUriRequest
* does not allow for changing the URI after creation, e.g., when you want to
* follow a redirect.
*/
private interface HttpRequestCreator {
HttpUriRequest createRequest(URI uri);
}
private static class GetRequestCreator implements HttpRequestCreator {
public HttpUriRequest createRequest(URI uri) {
return new HttpGet(uri);
}
}
private static class PostRequestCreator implements HttpRequestCreator {
private final String mMethodOverride;
private final HttpEntity mEntity;
public PostRequestCreator(String methodOverride, HttpEntity entity) {
mMethodOverride = methodOverride;
mEntity = entity;
}
public HttpUriRequest createRequest(URI uri) {
HttpPost post = new HttpPost(uri);
if (mMethodOverride != null) {
post.addHeader(X_HTTP_METHOD_OVERRIDE, mMethodOverride);
}
post.setEntity(mEntity);
return post;
}
}
// MAJOR TODO: make this work across redirects (if we can reset the
// InputStream).
// OR, read the bits into a local buffer (yuck, the media could be large).
private static class MediaPutRequestCreator implements HttpRequestCreator {
private final InputStream mMediaInputStream;
private final String mContentType;
public MediaPutRequestCreator(InputStream mediaInputStream,
String contentType) {
mMediaInputStream = mediaInputStream;
mContentType = contentType;
}
public HttpUriRequest createRequest(URI uri) {
HttpPost post = new HttpPost(uri);
post.addHeader(X_HTTP_METHOD_OVERRIDE, "PUT");
InputStreamEntity entity =
new InputStreamEntity(mMediaInputStream, -1 /* read until EOF */);
entity.setContentType(mContentType);
post.setEntity(entity);
return post;
}
}
/**
* Creates a new AndroidGDataClient.
*/
public AndroidGDataClient() {
httpClient = new DefaultHttpClient();
}
public void close() {
}
/*
* (non-Javadoc)
*
* @see GDataClient#encodeUri(java.lang.String)
*/
public String encodeUri(String uri) {
String encodedUri;
try {
encodedUri = URLEncoder.encode(uri, "UTF-8");
} catch (UnsupportedEncodingException uee) {
// should not happen.
Log.e("JakartaGDataClient", "UTF-8 not supported -- should not happen. "
+ "Using default encoding.", uee);
encodedUri = URLEncoder.encode(uri);
}
return encodedUri;
}
/*
* (non-Javadoc)
*
* @see com.google.wireless.gdata.client.GDataClient#createQueryParams()
*/
public QueryParams createQueryParams() {
return new QueryParamsImpl();
}
// follows redirects
private InputStream createAndExecuteMethod(HttpRequestCreator creator,
String uriString, String authToken) throws HttpException, IOException {
HttpResponse response = null;
int status = 500;
int redirectsLeft = MAX_REDIRECTS;
URI uri;
try {
uri = new URI(uriString);
} catch (URISyntaxException use) {
Log.w(TAG, "Unable to parse " + uriString + " as URI.", use);
throw new IOException("Unable to parse " + uriString + " as URI: "
+ use.getMessage());
}
// we follow redirects ourselves, since we want to follow redirects even on
// POSTs, which
// the HTTP library does not do. following redirects ourselves also allows
// us to log
// the redirects using our own logging.
while (redirectsLeft > 0) {
HttpUriRequest request = creator.createRequest(uri);
request.addHeader("User-Agent", "Android-GData");
request.addHeader("Accept-Encoding", "gzip");
// only add the auth token if not null (to allow for GData feeds that do
// not require
// authentication.)
if (!TextUtils.isEmpty(authToken)) {
request.addHeader("Authorization", "GoogleLogin auth=" + authToken);
}
if (DEBUG) {
for (Header h : request.getAllHeaders()) {
Log.v(TAG, h.getName() + ": " + h.getValue());
}
Log.d(TAG, "Executing " + request.getRequestLine().toString());
}
response = null;
try {
response = httpClient.execute(request);
} catch (IOException ioe) {
Log.w(TAG, "Unable to execute HTTP request." + ioe);
throw ioe;
}
StatusLine statusLine = response.getStatusLine();
if (statusLine == null) {
Log.w(TAG, "StatusLine is null.");
throw new NullPointerException(
"StatusLine is null -- should not happen.");
}
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, response.getStatusLine().toString());
for (Header h : response.getAllHeaders()) {
Log.d(TAG, h.getName() + ": " + h.getValue());
}
}
status = statusLine.getStatusCode();
HttpEntity entity = response.getEntity();
if ((status >= 200) && (status < 300) && entity != null) {
return getUngzippedContent(entity);
}
// TODO: handle 301, 307?
// TODO: let the http client handle the redirects, if we can be sure we'll
// never get a
// redirect on POST.
if (status == 302) {
// consume the content, so the connection can be closed.
entity.consumeContent();
Header location = response.getFirstHeader("Location");
if (location == null) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Redirect requested but no Location " + "specified.");
}
break;
}
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Following redirect to " + location.getValue());
}
try {
uri = new URI(location.getValue());
} catch (URISyntaxException use) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Unable to parse " + location.getValue() + " as URI.",
use);
throw new IOException("Unable to parse " + location.getValue()
+ " as URI.");
}
break;
}
--redirectsLeft;
} else {
break;
}
}
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "Received " + status + " status code.");
}
String errorMessage = null;
HttpEntity entity = response.getEntity();
try {
if (entity != null) {
InputStream in = entity.getContent();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buf = new byte[8192];
int bytesRead = -1;
while ((bytesRead = in.read(buf)) != -1) {
baos.write(buf, 0, bytesRead);
}
// TODO: use appropriate encoding, picked up from Content-Type.
errorMessage = new String(baos.toByteArray());
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, errorMessage);
}
}
} finally {
if (entity != null) {
entity.consumeContent();
}
}
String exceptionMessage = "Received " + status + " status code";
if (errorMessage != null) {
exceptionMessage += (": " + errorMessage);
}
throw new HttpException(exceptionMessage, status, null /* InputStream */);
}
/**
* Gets the input stream from a response entity. If the entity is gzipped
* then this will get a stream over the uncompressed data.
*
* @param entity the entity whose content should be read
* @return the input stream to read from
* @throws IOException
*/
private static InputStream getUngzippedContent(HttpEntity entity)
throws IOException {
InputStream responseStream = entity.getContent();
if (responseStream == null) {
return responseStream;
}
Header header = entity.getContentEncoding();
if (header == null) {
return responseStream;
}
String contentEncoding = header.getValue();
if (contentEncoding == null) {
return responseStream;
}
if (contentEncoding.contains("gzip")){
responseStream = new GZIPInputStream(responseStream);
}
return responseStream;
}
/*
* (non-Javadoc)
*
* @see GDataClient#getFeedAsStream(java.lang.String, java.lang.String)
*/
public InputStream getFeedAsStream(String feedUrl, String authToken)
throws HttpException, IOException {
InputStream in =
createAndExecuteMethod(new GetRequestCreator(), feedUrl, authToken);
if (in != null) {
return in;
}
throw new IOException("Unable to access feed.");
}
public InputStream getMediaEntryAsStream(String mediaEntryUrl,
String authToken) throws HttpException, IOException {
InputStream in =
createAndExecuteMethod(new GetRequestCreator(), mediaEntryUrl,
authToken);
if (in != null) {
return in;
}
throw new IOException("Unable to access media entry.");
}
/*
* (non-Javadoc)
*
* @see GDataClient#createEntry
*/
public InputStream createEntry(String feedUrl, String authToken,
GDataSerializer entry) throws HttpException, IOException {
HttpEntity entity =
createEntityForEntry(entry, GDataSerializer.FORMAT_CREATE);
InputStream in =
createAndExecuteMethod(new PostRequestCreator(null /* override */,
entity), feedUrl, authToken);
if (in != null) {
return in;
}
throw new IOException("Unable to create entry.");
}
/*
* (non-Javadoc)
*
* @see GDataClient#updateEntry
*/
public InputStream updateEntry(String editUri, String authToken,
GDataSerializer entry) throws HttpException, IOException {
HttpEntity entity =
createEntityForEntry(entry, GDataSerializer.FORMAT_UPDATE);
InputStream in =
createAndExecuteMethod(new PostRequestCreator("PUT", entity), editUri,
authToken);
if (in != null) {
return in;
}
throw new IOException("Unable to update entry.");
}
/*
* (non-Javadoc)
*
* @see GDataClient#deleteEntry
*/
public void deleteEntry(String editUri, String authToken)
throws HttpException, IOException {
if (StringUtils.isEmpty(editUri)) {
throw new IllegalArgumentException(
"you must specify an non-empty edit url");
}
InputStream in =
createAndExecuteMethod(
new PostRequestCreator("DELETE", null /* entity */), editUri,
authToken);
if (in == null) {
throw new IOException("Unable to delete entry.");
}
try {
in.close();
} catch (IOException ioe) {
// ignore
}
}
public InputStream updateMediaEntry(String editUri, String authToken,
InputStream mediaEntryInputStream, String contentType)
throws HttpException, IOException {
InputStream in =
createAndExecuteMethod(new MediaPutRequestCreator(
mediaEntryInputStream, contentType), editUri, authToken);
if (in != null) {
return in;
}
throw new IOException("Unable to write media entry.");
}
private HttpEntity createEntityForEntry(GDataSerializer entry, int format)
throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try {
entry.serialize(baos, format);
} catch (IOException ioe) {
Log.e(TAG, "Unable to serialize entry.", ioe);
throw ioe;
} catch (ParseException pe) {
Log.e(TAG, "Unable to serialize entry.", pe);
throw new IOException("Unable to serialize entry: " + pe.getMessage());
}
byte[] entryBytes = baos.toByteArray();
if (entryBytes != null && Log.isLoggable(TAG, Log.DEBUG)) {
try {
Log.d(TAG, "Serialized entry: " + new String(entryBytes, "UTF-8"));
} catch (UnsupportedEncodingException uee) {
// should not happen
throw new IllegalStateException("UTF-8 should be supported!", uee);
}
}
AbstractHttpEntity entity = new ByteArrayEntity(entryBytes);
entity.setContentType(entry.getContentType());
return entity;
}
}
@@ -0,0 +1,51 @@
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.io.gdata;
import com.google.android.apps.mytracks.Constants;
import com.google.wireless.gdata.client.GDataClient;
import android.content.Context;
import android.util.Log;
/**
* This factory will fetch the right class for the platform.
*
* @author Sandor Dornbush
*/
public class GDataClientFactory {
private GDataClientFactory() { }
/**
* Creates a new GData client.
* This factory will fetch the right class for the platform.
* @return A GDataClient appropriate for this platform
*/
public static GDataClient getGDataClient(Context context) {
// TODO This should be moved into ApiAdapter
try {
// Try to use the official unbundled gdata client implementation.
// This should work on Froyo and beyond.
return new com.google.android.common.gdata.AndroidGDataClient(context);
} catch (LinkageError e) {
// On all other platforms use the client implementation packaged in the
// apk.
Log.i(Constants.TAG, "Using mytracks AndroidGDataClient.", e);
return new com.google.android.apps.mytracks.io.gdata.AndroidGDataClient();
}
}
}
@@ -0,0 +1,106 @@
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.io.gdata;
import com.google.wireless.gdata.client.QueryParams;
import android.text.TextUtils;
import android.util.Log;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
/**
* Simple implementation of the QueryParams interface.
*/
// TODO: deal with categories
public class QueryParamsImpl extends QueryParams {
private final Map<String, String> mParams = new HashMap<String, String>();
@Override
public void clear() {
setEntryId(null);
mParams.clear();
}
@Override
public String generateQueryUrl(String feedUrl) {
if (TextUtils.isEmpty(getEntryId()) && mParams.isEmpty()) {
// nothing to do
return feedUrl;
}
// handle entry IDs
if (!TextUtils.isEmpty(getEntryId())) {
if (!mParams.isEmpty()) {
throw new IllegalStateException("Cannot set both an entry ID "
+ "and other query paramters.");
}
return feedUrl + '/' + getEntryId();
}
// otherwise, append the querystring params.
StringBuilder sb = new StringBuilder();
sb.append(feedUrl);
Set<String> params = mParams.keySet();
boolean first = true;
if (feedUrl.contains("?")) {
first = false;
} else {
sb.append('?');
}
for (String param : params) {
if (first) {
first = false;
} else {
sb.append('&');
}
sb.append(param);
sb.append('=');
String value = mParams.get(param);
String encodedValue = null;
try {
encodedValue = URLEncoder.encode(value, "UTF-8");
} catch (UnsupportedEncodingException uee) {
// should not happen.
Log.w("QueryParamsImpl", "UTF-8 not supported -- should not happen. "
+ "Using default encoding.", uee);
encodedValue = URLEncoder.encode(value);
}
sb.append(encodedValue);
}
return sb.toString();
}
@Override
public String getParamValue(String param) {
if (!(mParams.containsKey(param))) {
return null;
}
return mParams.get(param);
}
@Override
public void setParamValue(String param, String value) {
mParams.put(param, value);
}
}
@@ -0,0 +1,46 @@
/*
* Copyright 2009 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.io.gdata.docs;
import com.google.wireless.gdata.client.GDataClient;
import com.google.wireless.gdata.client.GDataParserFactory;
import com.google.wireless.gdata.client.GDataServiceClient;
/**
* GDataServiceClient for accessing Google Documents. This is not a full
* implementation.
*/
public class DocumentsClient extends GDataServiceClient {
/** The name of the service, dictated to be 'wise' by the protocol. */
public static final String SERVICE = "writely";
/**
* Creates a new DocumentsClient.
*
* @param client The GDataClient that should be used to authenticate requests,
* retrieve feeds, etc
* @param parserFactory The GDataParserFactory that should be used to obtain
* GDataParsers used by this client
*/
public DocumentsClient(GDataClient client, GDataParserFactory parserFactory) {
super(client, parserFactory);
}
@Override
public String getServiceName() {
return SERVICE;
}
}
@@ -0,0 +1,269 @@
/*
* Copyright 2007 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.io.gdata.docs;
import com.google.wireless.gdata.client.GDataClient;
import com.google.wireless.gdata.client.GDataParserFactory;
import com.google.wireless.gdata.client.GDataServiceClient;
import com.google.wireless.gdata.client.HttpException;
import com.google.wireless.gdata.data.Entry;
import com.google.wireless.gdata.data.StringUtils;
import com.google.wireless.gdata.parser.GDataParser;
import com.google.wireless.gdata.parser.ParseException;
import com.google.wireless.gdata.serializer.GDataSerializer;
import com.google.wireless.gdata2.client.AuthenticationException;
import java.io.IOException;
import java.io.InputStream;
/**
* GDataServiceClient for accessing Google Spreadsheets. This client can access
* and parse all of the Spreadsheets feed types: Spreadsheets feed, Worksheets
* feed, List feed, and Cells feed. Read operations are supported on all feed
* types, but only the List and Cells feeds support write operations. (This is a
* limitation of the protocol, not this API. Such write access may be added to
* the protocol in the future, requiring changes to this implementation.)
*
* Only 'private' visibility and 'full' projections are currently supported.
*/
public class SpreadsheetsClient extends GDataServiceClient {
/** The name of the service, dictated to be 'wise' by the protocol. */
public static final String SERVICE = "wise";
/** Standard base feed url for spreadsheets. */
public static final String SPREADSHEETS_BASE_FEED_URL =
"http://spreadsheets.google.com/feeds/spreadsheets/private/full";
/**
* Represents an entry in a GData Spreadsheets meta-feed.
*/
public static class SpreadsheetEntry extends Entry { }
/**
* Represents an entry in a GData Worksheets meta-feed.
*/
public static class WorksheetEntry extends Entry { }
/**
* Creates a new SpreadsheetsClient. Uses the standard base URL for
* spreadsheets feeds.
*
* @param client The GDataClient that should be used to authenticate requests,
* retrieve feeds, etc
*/
public SpreadsheetsClient(GDataClient client,
GDataParserFactory spreadsheetFactory) {
super(client, spreadsheetFactory);
}
@Override
public String getServiceName() {
return SERVICE;
}
/**
* Returns a parser for the specified feed type.
*
* @param feedEntryClass the Class of entry type that will be parsed, which
* lets this method figure out which parser to create
* @param feedUri the URI of the feed to be fetched and parsed
* @param authToken the current authToken to use for the request
* @return a parser for the indicated feed
* @throws AuthenticationException if the authToken is not valid
* @throws ParseException if the response from the server could not be parsed
*/
private GDataParser getParserForTypedFeed(
Class<? extends Entry> feedEntryClass, String feedUri, String authToken)
throws AuthenticationException, ParseException, IOException {
GDataClient gDataClient = getGDataClient();
GDataParserFactory gDataParserFactory = getGDataParserFactory();
try {
InputStream is = gDataClient.getFeedAsStream(feedUri, authToken);
return gDataParserFactory.createParser(feedEntryClass, is);
} catch (HttpException e) {
convertHttpExceptionForReads("Could not fetch parser feed.", e);
return null; // never reached
}
}
/**
* Converts an HTTP exception that happened while reading into the equivalent
* local exception.
*/
public void convertHttpExceptionForReads(String message, HttpException cause)
throws AuthenticationException, IOException {
switch (cause.getStatusCode()) {
case HttpException.SC_FORBIDDEN:
case HttpException.SC_UNAUTHORIZED:
throw new AuthenticationException(message, cause);
case HttpException.SC_GONE:
default:
throw new IOException(message + ": " + cause.getMessage());
}
}
@Override
public Entry createEntry(String feedUri, String authToken, Entry entry)
throws ParseException, IOException {
GDataParserFactory factory = getGDataParserFactory();
GDataSerializer serializer = factory.createSerializer(entry);
InputStream is;
try {
is = getGDataClient().createEntry(feedUri, authToken, serializer);
} catch (HttpException e) {
convertHttpExceptionForWrites(entry.getClass(),
"Could not update entry.", e);
return null; // never reached.
}
GDataParser parser = factory.createParser(entry.getClass(), is);
try {
return parser.parseStandaloneEntry();
} finally {
parser.close();
}
}
/**
* Fetches a GDataParser for the indicated feed. The parser can be used to
* access the contents of URI. WARNING: because we cannot reliably infer the
* feed type from the URI alone, this method assumes the default feed type!
* This is probably NOT what you want. Please use the getParserFor[Type]Feed
* methods.
*
* @param feedEntryClass
* @param feedUri the URI of the feed to be fetched and parsed
* @param authToken the current authToken to use for the request
* @return a parser for the indicated feed
* @throws ParseException if the response from the server could not be parsed
*/
@SuppressWarnings("rawtypes")
@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("rawtypes")
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("rawtypes")
private Entry parseEntry(Class entryClass, InputStream is)
throws ParseException, IOException {
GDataParser parser = null;
try {
parser = getGDataParserFactory().createParser(entryClass, is);
return parser.parseStandaloneEntry();
} finally {
if (parser != null) {
parser.close();
}
}
}
}
@@ -0,0 +1,72 @@
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.io.gdata.docs;
import com.google.wireless.gdata.client.GDataParserFactory;
import com.google.wireless.gdata.data.Entry;
import com.google.wireless.gdata.parser.GDataParser;
import com.google.wireless.gdata.parser.ParseException;
import com.google.wireless.gdata.parser.xml.XmlGDataParser;
import com.google.wireless.gdata.parser.xml.XmlParserFactory;
import com.google.wireless.gdata.serializer.GDataSerializer;
import com.google.wireless.gdata.serializer.xml.XmlEntryGDataSerializer;
import java.io.InputStream;
import org.xmlpull.v1.XmlPullParserException;
/**
* Factory of Xml parsers for gdata maps data.
*/
public class XmlDocsGDataParserFactory implements GDataParserFactory {
private XmlParserFactory xmlFactory;
public XmlDocsGDataParserFactory(XmlParserFactory xmlFactory) {
this.xmlFactory = xmlFactory;
}
@Override
public GDataParser createParser(InputStream is) throws ParseException {
try {
return new XmlGDataParser(is, xmlFactory.createParser());
} catch (XmlPullParserException e) {
e.printStackTrace();
return null;
}
}
@SuppressWarnings("rawtypes")
@Override
public GDataParser createParser(Class cls, InputStream is)
throws ParseException {
try {
return createParserForClass(is);
} catch (XmlPullParserException e) {
e.printStackTrace();
return null;
}
}
private GDataParser createParserForClass(InputStream is)
throws ParseException, XmlPullParserException {
return new XmlGDataParser(is, xmlFactory.createParser());
}
@Override
public GDataSerializer createSerializer(Entry en) {
return new XmlEntryGDataSerializer(xmlFactory, en);
}
}
@@ -0,0 +1,36 @@
// Copyright 2010 Google Inc. All Rights Reserved.
package com.google.android.apps.mytracks.io.gdata.maps;
import com.google.wireless.gdata.data.Entry;
import java.util.HashMap;
import java.util.Map;
/**
* GData entry for a map feature.
*/
public class MapFeatureEntry extends Entry {
private String mPrivacy = null;
private Map<String, String> mAttributes = new HashMap<String, String>();
public void setPrivacy(String privacy) {
mPrivacy = privacy;
}
public String getPrivacy() {
return mPrivacy;
}
public void setAttribute(String name, String value) {
mAttributes.put(name, value);
}
public void removeAttribute(String name) {
mAttributes.remove(name);
}
public Map<String, String> getAllAttributes() {
return mAttributes;
}
}
@@ -0,0 +1,102 @@
// Copyright 2010 Google Inc. All Rights Reserved.
package com.google.android.apps.mytracks.io.gdata.maps;
import com.google.wireless.gdata.client.GDataClient;
import com.google.wireless.gdata.client.GDataParserFactory;
import com.google.wireless.gdata.client.GDataServiceClient;
import android.util.Log;
import java.util.Locale;
/**
* Client to talk to Google Maps via GData.
*/
public class MapsClient extends GDataServiceClient {
private static final boolean DEBUG = false;
public static final boolean LOG_COMMUNICATION = false;
private static final String MAPS_BASE_FEED_URL =
"http://maps.google.com/maps/feeds/";
private static final String MAPS_MAP_FEED_PATH = "maps/default/full";
private static final String MAPS_FEATURE_FEED_PATH_BEFORE_MAPID = "features/";
private static final String MAPS_FEATURE_FEED_PATH_AFTER_MAPID = "/full";
private static final String MAPS_VERSION_FEED_PATH_FORMAT =
"%smaps/%s/versions/%s/full/%s";
private static final String MAP_ENTRY_ID_BEFORE_USER_ID = "maps/feeds/maps/";
private static final String MAP_ENTRY_ID_BETWEEN_USER_ID_AND_MAP_ID = "/";
private static final String V2_ONLY_PARAM = "?v=2.0";
public MapsClient(GDataClient dataClient,
GDataParserFactory dataParserFactory) {
super(dataClient, dataParserFactory);
}
@Override
public String getServiceName() {
return MapsConstants.SERVICE_NAME;
}
public static String buildMapUrl(String mapId) {
return MapsConstants.MAPSHOP_BASE_URL + "?msa=0&msid=" + mapId;
}
public static String getMapsFeed() {
if (DEBUG) {
Log.d("Maps Client", "Requesting map feed:");
}
return MAPS_BASE_FEED_URL + MAPS_MAP_FEED_PATH + V2_ONLY_PARAM;
}
public static String getFeaturesFeed(String mapid) {
StringBuilder feed = new StringBuilder();
feed.append(MAPS_BASE_FEED_URL);
feed.append(MAPS_FEATURE_FEED_PATH_BEFORE_MAPID);
feed.append(mapid);
feed.append(MAPS_FEATURE_FEED_PATH_AFTER_MAPID);
feed.append(V2_ONLY_PARAM);
return feed.toString();
}
public static String getMapIdFromMapEntryId(String entryId) {
String userId = null;
String mapId = null;
if (DEBUG) {
Log.d("Maps GData Client", "Getting mapid from entry id: " + entryId);
}
int userIdStart =
entryId.indexOf(MAP_ENTRY_ID_BEFORE_USER_ID)
+ MAP_ENTRY_ID_BEFORE_USER_ID.length();
int userIdEnd =
entryId.indexOf(MAP_ENTRY_ID_BETWEEN_USER_ID_AND_MAP_ID, userIdStart);
if (userIdStart >= 0 && userIdEnd < entryId.length()
&& userIdStart <= userIdEnd) {
userId = entryId.substring(userIdStart, userIdEnd);
}
int mapIdStart =
entryId.indexOf(MAP_ENTRY_ID_BETWEEN_USER_ID_AND_MAP_ID, userIdEnd)
+ MAP_ENTRY_ID_BETWEEN_USER_ID_AND_MAP_ID.length();
if (mapIdStart >= 0 && mapIdStart < entryId.length()) {
mapId = entryId.substring(mapIdStart);
}
if (userId == null) {
userId = "";
}
if (mapId == null) {
mapId = "";
}
if (DEBUG) {
Log.d("Maps GData Client", "Got user id: " + userId);
Log.d("Maps GData Client", "Got map id: " + mapId);
}
return userId + "." + mapId;
}
public static String getVersionFeed(String versionUserId,
String versionClient, String currentVersion) {
return String.format(Locale.US, MAPS_VERSION_FEED_PATH_FORMAT, MAPS_BASE_FEED_URL,
versionUserId, versionClient, currentVersion);
}
}
@@ -0,0 +1,16 @@
// Copyright 2009 Google Inc. All Rights Reserved.
package com.google.android.apps.mytracks.io.gdata.maps;
/**
* Constants for Google Maps.
*/
public class MapsConstants {
static final String MAPSHOP_BASE_URL =
"https://maps.google.com/maps/ms";
public static final String SERVICE_NAME = "local";
/**
* Private constructor to prevent instantiation.
*/
private MapsConstants() { }
}
@@ -0,0 +1,233 @@
// Copyright 2009 Google Inc. All Rights Reserved.
package com.google.android.apps.mytracks.io.gdata.maps;
import com.google.android.maps.GeoPoint;
import java.util.Random;
import java.util.Vector;
/**
* MapsFeature contains all of the data associated with a feature in Google
* Maps, where a feature is a marker, line, or shape. Some of the data is stored
* in a {@link MapsFeatureMetadata} object so that it can be more efficiently
* transmitted to other activities.
*/
public class MapsFeature {
/** A marker feature displays an icon at a single point on the map. */
public static final int MARKER = 0;
/**
* A line feature displays a line connecting a set of points on the map.
*/
public static final int LINE = 1;
/**
* A shape feature displays a border defined by connecting a set of points,
* including connecting the last to the first, and displays the area
* confined by this border.
*/
public static final int SHAPE = 2;
/** The local feature id for this feature, if needed. */
private String androidId;
/**
* The latitudes of the points of this feature in order, specified in
* millionths of a degree north.
*/
private final Vector<Integer> latitudeE6 = new Vector<Integer>();
/**
* The longitudes of the points of this feature in order, specified in
* millionths of a degree east.
*/
private final Vector<Integer> longitudeE6 = new Vector<Integer>();
/** The metadata of this feature in a format efficient for transmission. */
private MapsFeatureMetadata featureInfo = new MapsFeatureMetadata();
private final Random random = new Random();
/**
* Initializes a valid but empty feature. It will default to a
* {@link #MARKER} with a blue placemark with a dot as an icon at the
* location (0, 0).
*/
public MapsFeature() {
}
/**
* Adds a new point to the end of this feature.
*
* @param point The new point to add
*/
public void addPoint(GeoPoint point) {
latitudeE6.add(point.getLatitudeE6());
longitudeE6.add(point.getLongitudeE6());
}
/**
* Generates a new local id for this feature based on the current time and
* a random number.
*/
public void generateAndroidId() {
long time = System.currentTimeMillis();
int rand = random.nextInt(10000);
androidId = time + "." + rand;
}
/**
* Retrieves the current local id for this feature if one is available.
*
* @return The local id for this feature
*/
public String getAndroidId() {
return androidId;
}
/**
* Retrieves the current (html) description of this feature. The description
* is stored in the feature metadata.
*
* @return The description of this feature
*/
public String getDescription() {
return featureInfo.getDescription();
}
/**
* Sets the description of this feature. That description is stored in the
* feature metadata.
*
* @param description The new description of this feature
*/
public void setDescription(String description) {
featureInfo.setDescription(description);
}
/**
* Retrieves the point at the given index for this feature.
*
* @param index The index of the point desired
* @return A {@link GeoPoint} representing the point or null if that point
* doesn't exist
*/
public GeoPoint getPoint(int index) {
if (latitudeE6.size() <= index) {
return null;
}
return new GeoPoint(latitudeE6.get(index), longitudeE6.get(index));
}
/**
* Counts the number of points in this feature and return that count.
*
* @return The number of points in this feature
*/
public int getPointCount() {
return latitudeE6.size();
}
/**
* Retrieves the title of this feature. That title is stored in the feature
* metadata.
*
* @return the current title of this feature
*/
public String getTitle() {
return featureInfo.getTitle();
}
/**
* Retrieves the type of this feature. That type is stored in the feature
* metadata.
*
* @return One of {@link #MARKER}, {@link #LINE}, or {@link #SHAPE}
* identifying the type of this feature
*/
public int getType() {
return featureInfo.getType();
}
/**
* Retrieves the current color of this feature as an ARGB color integer.
* That color is stored in the feature metadata.
*
* @return The ARGB color of this feature
*/
public int getColor() {
return featureInfo.getColor();
}
/**
* Retrieves the current line width of this feature. That line width is
* stored in the feature metadata.
*
* @return The line width of this feature
*/
public int getLineWidth() {
return featureInfo.getLineWidth();
}
/**
* Retrieves the current fill color of this feature as an ARGB color
* integer. That color is stored in the feature metadata.
*
* @return The ARGB fill color of this feature
*/
public int getFillColor() {
return featureInfo.getFillColor();
}
/**
* Retrieves the current icon url of this feature. That icon url is stored
* in the feature metadata.
*
* @return The icon url for this feature
*/
public String getIconUrl() {
return featureInfo.getIconUrl();
}
/**
* Sets the title of this feature. That title is stored in the feature
* metadata.
*
* @param title The new title of this feature
*/
public void setTitle(String title) {
featureInfo.setTitle(title);
}
/**
* Sets the type of this feature. That type is stored in the feature
* metadata.
*
* @param type The new type of the feature. That type must be one of
* {@link #MARKER}, {@link #LINE}, or {@link #SHAPE}
*/
public void setType(int type) {
featureInfo.setType(type);
}
/**
* Sets the ARGB color of this feature. That color is stored in the feature
* metadata.
*
* @param color The new ARGB color of this feature
*/
public void setColor(int color) {
featureInfo.setColor(color);
}
/**
* Sets the icon url of this feature. That icon url is stored in the feature
* metadata.
*
* @param url The new icon url of the feature
*/
public void setIconUrl(String url) {
featureInfo.setIconUrl(url);
}
}
@@ -0,0 +1,88 @@
// Copyright 2009 Google Inc. All Rights Reserved.
package com.google.android.apps.mytracks.io.gdata.maps;
/**
* Metadata about a maps feature.
*/
class MapsFeatureMetadata {
private static final String BLUE_DOT_URL =
"http://maps.google.com/mapfiles/ms/micons/blue-dot.png";
private static final int DEFAULT_COLOR = 0x800000FF;
private static final int DEFAULT_FILL_COLOR = 0xC00000FF;
private String title;
private String description;
private int type;
private int color;
private int lineWidth;
private int fillColor;
private String iconUrl;
public MapsFeatureMetadata() {
title = "";
description = "";
type = MapsFeature.MARKER;
color = DEFAULT_COLOR;
lineWidth = 5;
fillColor = DEFAULT_FILL_COLOR;
iconUrl = BLUE_DOT_URL;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public int getType() {
return type;
}
public void setType(int type) {
this.type = type;
}
public int getColor() {
return color;
}
public void setColor(int color) {
this.color = color;
}
public int getLineWidth() {
return lineWidth;
}
public void setLineWidth(int width) {
lineWidth = width;
}
public int getFillColor() {
return fillColor;
}
public void setFillColor(int color) {
fillColor = color;
}
public String getIconUrl() {
return iconUrl;
}
public void setIconUrl(String url) {
iconUrl = url;
}
}
@@ -0,0 +1,178 @@
// Copyright 2009 Google Inc. All Rights Reserved.
package com.google.android.apps.mytracks.io.gdata.maps;
import com.google.wireless.gdata.data.Entry;
import com.google.wireless.gdata.data.StringUtils;
import android.graphics.Color;
import android.util.Log;
import java.io.IOException;
import java.io.StringWriter;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlPullParserFactory;
import org.xmlpull.v1.XmlSerializer;
/**
* Converter from GData objects to Maps objects.
*/
public class MapsGDataConverter {
private final XmlSerializer xmlSerializer;
public MapsGDataConverter() throws XmlPullParserException {
xmlSerializer = XmlPullParserFactory.newInstance().newSerializer();
}
public static MapsMapMetadata getMapMetadataForEntry(
MapFeatureEntry entry) {
MapsMapMetadata metadata = new MapsMapMetadata();
if ("public".equals(entry.getPrivacy())) {
metadata.setSearchable(true);
} else {
metadata.setSearchable(false);
}
metadata.setTitle(entry.getTitle());
metadata.setDescription(entry.getSummary());
String editUri = entry.getEditUri();
if (editUri != null) {
metadata.setGDataEditUri(editUri);
}
return metadata;
}
public static String getMapidForEntry(Entry entry) {
return MapsClient.getMapIdFromMapEntryId(entry.getId());
}
public static Entry getMapEntryForMetadata(MapsMapMetadata metadata) {
MapFeatureEntry entry = new MapFeatureEntry();
entry.setEditUri(metadata.getGDataEditUri());
entry.setTitle(metadata.getTitle());
entry.setSummary(metadata.getDescription());
entry.setPrivacy(metadata.getSearchable() ? "public" : "unlisted");
entry.setAuthor("android");
entry.setEmail("nobody@google.com");
return entry;
}
public MapFeatureEntry getEntryForFeature(MapsFeature feature) {
MapFeatureEntry entry = new MapFeatureEntry();
entry.setTitle(feature.getTitle());
entry.setAuthor("android");
entry.setEmail("nobody@google.com");
entry.setCategoryScheme("http://schemas.google.com/g/2005#kind");
entry.setCategory("http://schemas.google.com/g/2008#mapfeature");
entry.setEditUri("");
if (!StringUtils.isEmpty(feature.getAndroidId())) {
entry.setAttribute("_androidId", feature.getAndroidId());
}
try {
StringWriter writer = new StringWriter();
xmlSerializer.setOutput(writer);
xmlSerializer.startTag(null, "Placemark");
xmlSerializer.attribute(null, "xmlns", "http://earth.google.com/kml/2.2");
xmlSerializer.startTag(null, "Style");
if (feature.getType() == MapsFeature.MARKER) {
xmlSerializer.startTag(null, "IconStyle");
xmlSerializer.startTag(null, "Icon");
xmlSerializer.startTag(null, "href");
xmlSerializer.text(feature.getIconUrl());
xmlSerializer.endTag(null, "href");
xmlSerializer.endTag(null, "Icon");
xmlSerializer.endTag(null, "IconStyle");
} else {
xmlSerializer.startTag(null, "LineStyle");
xmlSerializer.startTag(null, "color");
int color = feature.getColor();
// Reverse the color because KML is ABGR and Android is ARGB
xmlSerializer.text(Integer.toHexString(
Color.argb(Color.alpha(color), Color.blue(color),
Color.green(color), Color.red(color))));
xmlSerializer.endTag(null, "color");
xmlSerializer.startTag(null, "width");
xmlSerializer.text(Integer.toString(feature.getLineWidth()));
xmlSerializer.endTag(null, "width");
xmlSerializer.endTag(null, "LineStyle");
if (feature.getType() == MapsFeature.SHAPE) {
xmlSerializer.startTag(null, "PolyStyle");
xmlSerializer.startTag(null, "color");
int fcolor = feature.getFillColor();
// Reverse the color because KML is ABGR and Android is ARGB
xmlSerializer.text(Integer.toHexString(Color.argb(Color.alpha(fcolor),
Color.blue(fcolor), Color.green(fcolor), Color.red(fcolor))));
xmlSerializer.endTag(null, "color");
xmlSerializer.startTag(null, "fill");
xmlSerializer.text("1");
xmlSerializer.endTag(null, "fill");
xmlSerializer.startTag(null, "outline");
xmlSerializer.text("1");
xmlSerializer.endTag(null, "outline");
xmlSerializer.endTag(null, "PolyStyle");
}
}
xmlSerializer.endTag(null, "Style");
xmlSerializer.startTag(null, "name");
xmlSerializer.text(feature.getTitle());
xmlSerializer.endTag(null, "name");
xmlSerializer.startTag(null, "description");
xmlSerializer.cdsect(feature.getDescription());
xmlSerializer.endTag(null, "description");
StringBuilder pointBuilder = new StringBuilder();
for (int i = 0; i < feature.getPointCount(); ++i) {
if (i > 0) {
pointBuilder.append('\n');
}
pointBuilder.append(feature.getPoint(i).getLongitudeE6() / 1e6);
pointBuilder.append(',');
pointBuilder.append(feature.getPoint(i).getLatitudeE6() / 1e6);
pointBuilder.append(",0.000000");
}
String pointString = pointBuilder.toString();
if (feature.getType() == MapsFeature.MARKER) {
xmlSerializer.startTag(null, "Point");
xmlSerializer.startTag(null, "coordinates");
xmlSerializer.text(pointString);
xmlSerializer.endTag(null, "coordinates");
xmlSerializer.endTag(null, "Point");
} else if (feature.getType() == MapsFeature.LINE) {
xmlSerializer.startTag(null, "LineString");
xmlSerializer.startTag(null, "tessellate");
xmlSerializer.text("1");
xmlSerializer.endTag(null, "tessellate");
xmlSerializer.startTag(null, "coordinates");
xmlSerializer.text(pointString);
xmlSerializer.endTag(null, "coordinates");
xmlSerializer.endTag(null, "LineString");
} else {
xmlSerializer.startTag(null, "Polygon");
xmlSerializer.startTag(null, "outerBoundaryIs");
xmlSerializer.startTag(null, "LinearRing");
xmlSerializer.startTag(null, "tessellate");
xmlSerializer.text("1");
xmlSerializer.endTag(null, "tessellate");
xmlSerializer.startTag(null, "coordinates");
xmlSerializer.text(pointString + "\n"
+ Double.toString(feature.getPoint(0).getLongitudeE6() / 1e6)
+ ","
+ Double.toString(feature.getPoint(0).getLatitudeE6() / 1e6)
+ ",0.000000");
xmlSerializer.endTag(null, "coordinates");
xmlSerializer.endTag(null, "LinearRing");
xmlSerializer.endTag(null, "outerBoundaryIs");
xmlSerializer.endTag(null, "Polygon");
}
xmlSerializer.endTag(null, "Placemark");
xmlSerializer.flush();
entry.setContent(writer.toString());
Log.d("My Google Maps", "Edit URI: " + entry.getEditUri());
} catch (IOException e) {
e.printStackTrace();
}
return entry;
}
}
@@ -0,0 +1,52 @@
// Copyright 2009 Google Inc. All Rights Reserved.
package com.google.android.apps.mytracks.io.gdata.maps;
/**
* Metadata about a Google Maps map.
*/
public class MapsMapMetadata {
private String title;
private String description;
private String gdataEditUri;
private boolean searchable;
public MapsMapMetadata() {
title = "";
description = "";
gdataEditUri = "";
searchable = false;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public boolean getSearchable() {
return searchable;
}
public void setSearchable(boolean searchable) {
this.searchable = searchable;
}
public String getGDataEditUri() {
return gdataEditUri;
}
public void setGDataEditUri(String editUri) {
this.gdataEditUri = editUri;
}
}
@@ -0,0 +1,163 @@
// Copyright 2009 Google Inc. All Rights Reserved.
package com.google.android.apps.mytracks.io.gdata.maps;
import com.google.wireless.gdata.data.Entry;
import com.google.wireless.gdata.data.Feed;
import com.google.wireless.gdata.data.XmlUtils;
import com.google.wireless.gdata.parser.ParseException;
import com.google.wireless.gdata.parser.xml.XmlGDataParser;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException;
import java.io.InputStream;
/**
* Parser for XML gdata maps data.
*/
class XmlMapsGDataParser extends XmlGDataParser {
public XmlMapsGDataParser(InputStream is, XmlPullParser xpp)
throws ParseException {
super(is, xpp);
}
@Override
protected Feed createFeed() {
return new Feed();
}
@Override
protected Entry createEntry() {
return new MapFeatureEntry();
}
@Override
protected void handleExtraElementInFeed(Feed feed) {
// Do nothing
}
@Override
protected void handleExtraLinkInEntry(
String rel, String type, String href, Entry entry)
throws XmlPullParserException, IOException {
if (!(entry instanceof MapFeatureEntry)) {
throw new IllegalArgumentException("Expected MapFeatureEntry!");
}
if (rel.endsWith("#view")) {
return;
}
super.handleExtraLinkInEntry(rel, type, href, entry);
}
/**
* Parses the current entry in the XML document. Assumes that the parser is
* currently pointing just after an &lt;entry&gt;.
*
* @param plainEntry The entry that will be filled.
* @throws XmlPullParserException Thrown if the XML cannot be parsed.
* @throws IOException Thrown if the underlying inputstream cannot be read.
*/
@Override
protected void handleEntry(Entry plainEntry)
throws XmlPullParserException, IOException, ParseException {
XmlPullParser parser = getParser();
if (!(plainEntry instanceof MapFeatureEntry)) {
throw new IllegalArgumentException("Expected MapFeatureEntry!");
}
MapFeatureEntry entry = (MapFeatureEntry) plainEntry;
int eventType = parser.getEventType();
entry.setPrivacy("public");
while (eventType != XmlPullParser.END_DOCUMENT) {
switch (eventType) {
case XmlPullParser.START_TAG:
String name = parser.getName();
if ("entry".equals(name)) {
// stop parsing here.
return;
} else if ("id".equals(name)) {
entry.setId(XmlUtils.extractChildText(parser));
} else if ("title".equals(name)) {
entry.setTitle(XmlUtils.extractChildText(parser));
} else if ("link".equals(name)) {
String rel = parser.getAttributeValue(null /* ns */, "rel");
String type = parser.getAttributeValue(null /* ns */, "type");
String href = parser.getAttributeValue(null /* ns */, "href");
if ("edit".equals(rel)) {
entry.setEditUri(href);
} else if ("alternate".equals(rel) && "text/html".equals(type)) {
entry.setHtmlUri(href);
} else {
handleExtraLinkInEntry(rel, type, href, entry);
}
} else if ("summary".equals(name)) {
entry.setSummary(XmlUtils.extractChildText(parser));
} else if ("content".equals(name)) {
StringBuilder contentBuilder = new StringBuilder();
int parentDepth = parser.getDepth();
while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {
int etype = parser.next();
switch (etype) {
case XmlPullParser.START_TAG:
contentBuilder.append('<');
contentBuilder.append(parser.getName());
contentBuilder.append('>');
break;
case XmlPullParser.TEXT:
contentBuilder.append("<![CDATA[");
contentBuilder.append(parser.getText());
contentBuilder.append("]]>");
break;
case XmlPullParser.END_TAG:
if (parser.getDepth() > parentDepth) {
contentBuilder.append("</");
contentBuilder.append(parser.getName());
contentBuilder.append('>');
}
break;
}
if (etype == XmlPullParser.END_TAG
&& parser.getDepth() == parentDepth) {
break;
}
}
entry.setContent(contentBuilder.toString());
} else if ("category".equals(name)) {
String category = parser.getAttributeValue(null /* ns */, "term");
if (category != null && category.length() > 0) {
entry.setCategory(category);
}
String categoryScheme =
parser.getAttributeValue(null /* ns */, "scheme");
if (categoryScheme != null && category.length() > 0) {
entry.setCategoryScheme(categoryScheme);
}
} else if ("published".equals(name)) {
entry.setPublicationDate(XmlUtils.extractChildText(parser));
} else if ("updated".equals(name)) {
entry.setUpdateDate(XmlUtils.extractChildText(parser));
} else if ("deleted".equals(name)) {
entry.setDeleted(true);
} else if ("draft".equals(name)) {
String draft = XmlUtils.extractChildText(parser);
entry.setPrivacy("yes".equals(draft) ? "unlisted" : "public");
} else if ("customProperty".equals(name)) {
String attrName = parser.getAttributeValue(null, "name");
String attrValue = XmlUtils.extractChildText(parser);
entry.setAttribute(attrName, attrValue);
} else if ("deleted".equals(name)) {
entry.setDeleted(true);
} else {
handleExtraElementInEntry(entry);
}
break;
default:
break;
}
eventType = parser.next();
}
}
}
@@ -0,0 +1,94 @@
// Copyright 2010 Google Inc. All Rights Reserved.
package com.google.android.apps.mytracks.io.gdata.maps;
import com.google.wireless.gdata.client.GDataParserFactory;
import com.google.wireless.gdata.data.Entry;
import com.google.wireless.gdata.parser.GDataParser;
import com.google.wireless.gdata.parser.ParseException;
import com.google.wireless.gdata.parser.xml.XmlGDataParser;
import com.google.wireless.gdata.parser.xml.XmlParserFactory;
import com.google.wireless.gdata.serializer.GDataSerializer;
import com.google.wireless.gdata.serializer.xml.XmlEntryGDataSerializer;
import android.util.Log;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import org.xmlpull.v1.XmlPullParserException;
/**
* Factory of Xml parsers for gdata maps data.
*/
public class XmlMapsGDataParserFactory implements GDataParserFactory {
private XmlParserFactory xmlFactory;
public XmlMapsGDataParserFactory(XmlParserFactory xmlFactory) {
this.xmlFactory = xmlFactory;
}
@Override
public GDataParser createParser(InputStream is) throws ParseException {
is = maybeLogCommunication(is);
try {
return new XmlGDataParser(is, xmlFactory.createParser());
} catch (XmlPullParserException e) {
e.printStackTrace();
return null;
}
}
@SuppressWarnings({ "unchecked", "rawtypes" })
@Override
public GDataParser createParser(Class cls, InputStream is)
throws ParseException {
is = maybeLogCommunication(is);
try {
return createParserForClass(cls, is);
} catch (XmlPullParserException e) {
e.printStackTrace();
return null;
}
}
private InputStream maybeLogCommunication(InputStream is)
throws ParseException {
if (MapsClient.LOG_COMMUNICATION) {
StringBuilder builder = new StringBuilder();
byte[] buffer = new byte[2048];
try {
for (int n = is.read(buffer); n >= 0; n = is.read(buffer)) {
String part = new String(buffer, 0, n);
builder.append(part);
Log.d("Response part", part);
}
} catch (IOException e) {
throw new ParseException("Could not read stream", e);
}
String whole = builder.toString();
Log.d("Response", whole);
is = new ByteArrayInputStream(whole.getBytes());
}
return is;
}
private GDataParser createParserForClass(
Class<? extends Entry> cls, InputStream is)
throws ParseException, XmlPullParserException {
if (cls == MapFeatureEntry.class) {
return new XmlMapsGDataParser(is, xmlFactory.createParser());
} else {
return new XmlGDataParser(is, xmlFactory.createParser());
}
}
@Override
public GDataSerializer createSerializer(Entry en) {
if (en instanceof MapFeatureEntry) {
return new XmlMapsGDataSerializer(xmlFactory, (MapFeatureEntry) en);
} else {
return new XmlEntryGDataSerializer(xmlFactory, en);
}
}
}
@@ -0,0 +1,262 @@
// Copyright 2010 Google Inc. All Rights Reserved.
package com.google.android.apps.mytracks.io.gdata.maps;
import com.google.wireless.gdata.data.StringUtils;
import com.google.wireless.gdata.parser.ParseException;
import com.google.wireless.gdata.parser.xml.XmlGDataParser;
import com.google.wireless.gdata.parser.xml.XmlParserFactory;
import com.google.wireless.gdata.serializer.xml.XmlEntryGDataSerializer;
import android.util.Log;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Map;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlSerializer;
/**
* Serializer of maps data for GData.
*/
class XmlMapsGDataSerializer extends XmlEntryGDataSerializer {
private static final String APP_NAMESPACE = "http://www.w3.org/2007/app";
private MapFeatureEntry entry;
private XmlParserFactory factory;
private OutputStream stream;
public XmlMapsGDataSerializer(XmlParserFactory factory, MapFeatureEntry entry) {
super(factory, entry);
this.factory = factory;
this.entry = entry;
}
@Override
public void serialize(OutputStream out, int format)
throws IOException, ParseException {
XmlSerializer serializer = null;
try {
serializer = factory.createSerializer();
} catch (XmlPullParserException e) {
throw new ParseException("Unable to create XmlSerializer.", e);
}
ByteArrayOutputStream printStream;
if (MapsClient.LOG_COMMUNICATION) {
printStream = new ByteArrayOutputStream();
serializer.setOutput(printStream, "UTF-8");
} else {
serializer.setOutput(out, "UTF-8");
}
serializer.startDocument("UTF-8", Boolean.FALSE);
declareEntryNamespaces(serializer);
serializer.startTag(XmlGDataParser.NAMESPACE_ATOM_URI, "entry");
if (MapsClient.LOG_COMMUNICATION) {
stream = printStream;
} else {
stream = out;
}
serializeEntryContents(serializer, format);
serializer.endTag(XmlGDataParser.NAMESPACE_ATOM_URI, "entry");
serializer.endDocument();
serializer.flush();
if (MapsClient.LOG_COMMUNICATION) {
Log.d("Request", printStream.toString());
out.write(printStream.toByteArray());
stream = out;
}
}
private final void declareEntryNamespaces(XmlSerializer serializer)
throws IOException {
serializer.setPrefix(
"" /* default ns */, XmlGDataParser.NAMESPACE_ATOM_URI);
serializer.setPrefix(
XmlGDataParser.NAMESPACE_GD, XmlGDataParser.NAMESPACE_GD_URI);
declareExtraEntryNamespaces(serializer);
}
private final void serializeEntryContents(XmlSerializer serializer,
int format) throws IOException {
if (format != FORMAT_CREATE) {
serializeId(serializer, entry.getId());
}
serializeTitle(serializer, entry.getTitle());
if (format != FORMAT_CREATE) {
serializeLink(serializer,
"edit" /* rel */, entry.getEditUri(), null /* type */);
serializeLink(serializer,
"alternate" /* rel */, entry.getHtmlUri(), "text/html" /* type */);
}
serializeSummary(serializer, entry.getSummary());
serializeContent(serializer, entry.getContent());
serializeAuthor(serializer, entry.getAuthor(), entry.getEmail());
serializeCategory(serializer,
entry.getCategory(), entry.getCategoryScheme());
if (format == FORMAT_FULL) {
serializePublicationDate(serializer, entry.getPublicationDate());
}
if (format != FORMAT_CREATE) {
serializeUpdateDate(serializer, entry.getUpdateDate());
}
serializeExtraEntryContents(serializer, format);
}
private static void serializeId(XmlSerializer serializer, String id)
throws IOException {
if (StringUtils.isEmpty(id)) {
return;
}
serializer.startTag(null /* ns */, "id");
serializer.text(id);
serializer.endTag(null /* ns */, "id");
}
private static void serializeTitle(XmlSerializer serializer, String title)
throws IOException {
if (StringUtils.isEmpty(title)) {
return;
}
serializer.startTag(null /* ns */, "title");
serializer.text(title);
serializer.endTag(null /* ns */, "title");
}
public static void serializeLink(XmlSerializer serializer, String rel,
String href, String type) throws IOException {
if (StringUtils.isEmpty(href)) {
return;
}
serializer.startTag(null /* ns */, "link");
serializer.attribute(null /* ns */, "rel", rel);
serializer.attribute(null /* ns */, "href", href);
if (!StringUtils.isEmpty(type)) {
serializer.attribute(null /* ns */, "type", type);
}
serializer.endTag(null /* ns */, "link");
}
private static void serializeSummary(XmlSerializer serializer, String summary)
throws IOException {
if (StringUtils.isEmpty(summary)) {
return;
}
serializer.startTag(null /* ns */, "summary");
serializer.text(summary);
serializer.endTag(null /* ns */, "summary");
}
private void serializeContent(XmlSerializer serializer, String content)
throws IOException {
if (content == null) {
return;
}
serializer.startTag(null /* ns */, "content");
if (content.contains("</Placemark>")) {
serializer.attribute(
null /* ns */, "type", "application/vnd.google-earth.kml+xml");
serializer.flush();
stream.write(content.getBytes());
} else {
serializer.text(content);
}
serializer.endTag(null /* ns */, "content");
}
private static void serializeAuthor(XmlSerializer serializer, String author,
String email) throws IOException {
if (StringUtils.isEmpty(author) || StringUtils.isEmpty(email)) {
return;
}
serializer.startTag(null /* ns */, "author");
serializer.startTag(null /* ns */, "name");
serializer.text(author);
serializer.endTag(null /* ns */, "name");
serializer.startTag(null /* ns */, "email");
serializer.text(email);
serializer.endTag(null /* ns */, "email");
serializer.endTag(null /* ns */, "author");
}
private static void serializeCategory(XmlSerializer serializer,
String category, String categoryScheme) throws IOException {
if (StringUtils.isEmpty(category) && StringUtils.isEmpty(categoryScheme)) {
return;
}
serializer.startTag(null /* ns */, "category");
if (!StringUtils.isEmpty(category)) {
serializer.attribute(null /* ns */, "term", category);
}
if (!StringUtils.isEmpty(categoryScheme)) {
serializer.attribute(null /* ns */, "scheme", categoryScheme);
}
serializer.endTag(null /* ns */, "category");
}
private static void serializePublicationDate(XmlSerializer serializer,
String publicationDate) throws IOException {
if (StringUtils.isEmpty(publicationDate)) {
return;
}
serializer.startTag(null /* ns */, "published");
serializer.text(publicationDate);
serializer.endTag(null /* ns */, "published");
}
private static void serializeUpdateDate(XmlSerializer serializer,
String updateDate) throws IOException {
if (StringUtils.isEmpty(updateDate)) {
return;
}
serializer.startTag(null /* ns */, "updated");
serializer.text(updateDate);
serializer.endTag(null /* ns */, "updated");
}
@Override
protected void serializeExtraEntryContents(XmlSerializer serializer,
int format) throws IOException {
Map<String, String> attrs = entry.getAllAttributes();
for (Map.Entry<String, String> attr : attrs.entrySet()) {
serializer.startTag("http://schemas.google.com/g/2005", "customProperty");
serializer.attribute(null, "name", attr.getKey());
serializer.text(attr.getValue());
serializer.endTag("http://schemas.google.com/g/2005", "customProperty");
}
String privacy = entry.getPrivacy();
if (!StringUtils.isEmpty(privacy)) {
serializer.setPrefix("app", APP_NAMESPACE);
if ("public".equals(privacy)) {
serializer.startTag(APP_NAMESPACE, "control");
serializer.startTag(APP_NAMESPACE, "draft");
serializer.text("no");
serializer.endTag(APP_NAMESPACE, "draft");
serializer.endTag(APP_NAMESPACE, "control");
}
if ("unlisted".equals(privacy)) {
serializer.startTag(APP_NAMESPACE, "control");
serializer.startTag(APP_NAMESPACE, "draft");
serializer.text("yes");
serializer.endTag(APP_NAMESPACE, "draft");
serializer.endTag(APP_NAMESPACE, "control");
}
}
}
}
@@ -0,0 +1,248 @@
/*
* 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.apps.mytracks.util.DialogUtils;
import com.google.android.apps.mytracks.util.IntentUtils;
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.content.DialogInterface;
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 DIALOG_PROGRESS_ID = 0;
private static final int DIALOG_ERROR_ID = 1;
private SendRequest sendRequest;
private ChooseMapAsyncTask asyncTask;
@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_item_title);
title.setText(mapData.getTitle());
TextView description = (TextView) convertView.findViewById(
R.id.choose_map_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_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 DIALOG_PROGRESS_ID:
return DialogUtils.createSpinnerProgressDialog(
this, R.string.maps_list_progress_message, new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
asyncTask.cancel(true);
finish();
}
});
case DIALOG_ERROR_ID:
return new AlertDialog.Builder(this)
.setCancelable(true)
.setIcon(android.R.drawable.ic_dialog_alert)
.setMessage(R.string.maps_list_error)
.setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
finish();
}
})
.setPositiveButton(R.string.generic_ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int arg1) {
finish();
}
})
.setTitle(R.string.generic_error_title)
.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(DIALOG_PROGRESS_ID);
}
/**
* Shows the error dialog.
*/
@VisibleForTesting
void showErrorDialog() {
showDialog(DIALOG_ERROR_ID);
}
/**
* Remove the progress dialog.
*/
@VisibleForTesting
void removeProgressDialog() {
removeDialog(DIALOG_PROGRESS_ID);
}
/**
* Starts the next activity, {@link SendMapsActivity}.
*
* @param mapId the chosen map id
*/
private void startNextActivity(String mapId) {
sendRequest.setMapId(mapId);
Intent intent = IntentUtils.newIntent(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,247 @@
/*
* 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.common.annotations.VisibleForTesting;
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, account, activity.getApplicationContext(), GDataClientFactory
.getGDataClient(activity.getApplicationContext()), new MapsClient(
GDataClientFactory.getGDataClient(activity.getApplicationContext()),
new XmlMapsGDataParserFactory(new AndroidXmlParserFactory())));
}
/**
* Creates this constructor for test.
*/
public ChooseMapAsyncTask(ChooseMapActivity activity, Account account,
Context context, GDataClient gDataClient, MapsClient mapsClient) {
this.activity = activity;
this.account = account;
this.context = context;
this.gDataClient = gDataClient;
this.mapsClient = mapsClient;
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) {
try {
return getMaps();
} finally {
if (gDataClient != null) {
gDataClient.close();
}
}
}
@Override
protected void onPostExecute(Boolean result) {
success = result;
completed = true;
if (activity != null) {
activity.onAsyncTaskCompleted(success, mapIds, mapData);
}
}
/**
* Gets all the maps from Google Maps.
*
* @return true if success.
*/
@VisibleForTesting
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.
*/
@VisibleForTesting
boolean retryUpload() {
if (isCancelled()) {
return false;
}
AccountManager.get(context).invalidateAuthToken(Constants.ACCOUNT_TYPE, authToken);
if (canRetry) {
canRetry = false;
return getMaps();
}
return false;
}
/**
* Gets the complete status of task.
*/
@VisibleForTesting
boolean getCompleted() {
return completed;
}
/**
* Sets the complete status of task.
* @param completed
*/
@VisibleForTesting
void setCompleted(boolean completed) {
this.completed = completed;
}
/**
* Sets the status of canRetry.
* @param canRetry status of canRetry
*/
@VisibleForTesting
void setCanRetry(boolean canRetry) {
this.canRetry = canRetry;
}
/**
* Gets the status of canRetry.
*/
@VisibleForTesting
boolean getCanRetry() {
return canRetry;
}
}
@@ -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.apps.mytracks.util.IntentUtils;
import com.google.android.maps.mytracks.R;
import com.google.common.annotations.VisibleForTesting;
import android.content.Intent;
/**
* An activity to send a track to Google Maps.
*
* @author Jimmy Shih
*/
public class SendMapsActivity extends AbstractSendActivity {
@Override
protected AbstractSendAsyncTask createAsyncTask() {
return new SendMapsAsyncTask(
this, sendRequest.getTrackId(), sendRequest.getAccount(), 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 = IntentUtils.newIntent(this, next)
.putExtra(SendRequest.SEND_REQUEST_KEY, sendRequest);
startActivity(intent);
finish();
}
@VisibleForTesting
Class<?> getNextClass(SendRequest request, boolean isCancel) {
if (isCancel) {
return UploadResultActivity.class;
} else {
if (request.isSendFusionTables()) {
return SendFusionTablesActivity.class;
} else if (request.isSendDocs()) {
return SendDocsActivity.class;
} else {
return UploadResultActivity.class;
}
}
}
}
@@ -0,0 +1,543 @@
/*
* 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.PreferencesUtils;
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.common.annotations.VisibleForTesting;
import com.google.wireless.gdata.client.GDataClient;
import com.google.wireless.gdata.client.HttpException;
import com.google.wireless.gdata.parser.ParseException;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.accounts.AuthenticatorException;
import android.accounts.OperationCanceledException;
import android.content.Context;
import android.database.Cursor;
import android.location.Location;
import android.util.Log;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Vector;
import org.xmlpull.v1.XmlPullParserException;
/**
* AsyncTask to send a track to Google Maps.
* <p>
* IMPORTANT: While this code is Apache-licensed, please notice that usage of
* the Google Maps servers through this API is only allowed for the My Tracks
* application. Other applications looking to upload maps data should look into
* using the Google Fusion Tables API.
*
* @author Jimmy Shih
*/
public class SendMapsAsyncTask extends AbstractSendAsyncTask {
private static final String START_ICON_URL =
"http://maps.google.com/mapfiles/ms/micons/green-dot.png";
private static final String END_ICON_URL =
"http://maps.google.com/mapfiles/ms/micons/red-dot.png";
private static final int MAX_POINTS_PER_UPLOAD = 500;
private static final int PROGRESS_FETCH_MAP_ID = 5;
@VisibleForTesting
static final int PROGRESS_UPLOAD_DATA_MIN = 10;
@VisibleForTesting
static final int PROGRESS_UPLOAD_DATA_MAX = 90;
private static final int PROGRESS_UPLOAD_WAYPOINTS = 95;
private static final int PROGRESS_COMPLETE = 100;
private static final String TAG = SendMapsAsyncTask.class.getSimpleName();
private final long trackId;
private final Account account;
private final String chooseMapId;
private final MyTracksProviderUtils myTracksProviderUtils;
private final Context context;
private final GDataClient gDataClient;
private final MapsClient mapsClient;
// The following variables are for per upload states
private MapsGDataConverter mapsGDataConverter;
private String authToken;
private String mapId;
int currentSegment;
public SendMapsAsyncTask(SendMapsActivity activity, long trackId, Account account,
String chooseMapId) {
this(activity, trackId, account, chooseMapId, MyTracksProviderUtils.Factory.get(activity
.getApplicationContext()));
}
/**
* This constructor is created for test.
*/
@VisibleForTesting
public SendMapsAsyncTask (
SendMapsActivity activity, long trackId, Account account, String chooseMapId, MyTracksProviderUtils myTracksProviderUtils) {
super(activity);
this.trackId = trackId;
this.account = account;
this.chooseMapId = chooseMapId;
this.myTracksProviderUtils = myTracksProviderUtils;
context = activity.getApplicationContext();
gDataClient = GDataClientFactory.getGDataClient(context);
mapsClient = new MapsClient(
gDataClient, new XmlMapsGDataParserFactory(new AndroidXmlParserFactory()));
}
@Override
protected void closeConnection() {
if (gDataClient != null) {
gDataClient.close();
}
}
@Override
protected void saveResult() {
Track track = myTracksProviderUtils.getTrack(trackId);
if (track == null) {
Log.d(TAG, "No track for " + trackId);
return;
}
track.setMapId(mapId);
myTracksProviderUtils.updateTrack(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, "No track for " + trackId);
return false;
}
// Fetch the mapId, create a new map if necessary
publishProgress(PROGRESS_FETCH_MAP_ID);
if (!fetchSendMapId(track)) {
Log.d("TAG", "Unable to upload all track points");
return retryTask();
}
// Upload all the track points plus the start and end markers
publishProgress(PROGRESS_UPLOAD_DATA_MIN);
if (!uploadAllTrackPoints(track)) {
Log.d("TAG", "Unable to upload all track points");
return retryTask();
}
// Upload all the waypoints
publishProgress(PROGRESS_UPLOAD_WAYPOINTS);
if (!uploadWaypoints()) {
Log.d("TAG", "Unable to upload waypoints");
return false;
}
publishProgress(PROGRESS_COMPLETE);
return true;
}
@Override
protected void invalidateToken() {
AccountManager.get(context).invalidateAuthToken(Constants.ACCOUNT_TYPE, authToken);
}
/**
* Fetches the {@link SendMapsAsyncTask#mapId} instance variable for
* sending a track to Google Maps.
*
* @param track the Track
* @return true if able to fetch the mapId variable.
*/
@VisibleForTesting
boolean fetchSendMapId(Track track) {
if (isCancelled()) {
return false;
}
if (chooseMapId != null) {
mapId = chooseMapId;
return true;
} else {
boolean defaultMapPublic = PreferencesUtils.getBoolean(
context, R.string.default_map_public_key, PreferencesUtils.DEFAULT_MAP_PUBLIC_DEFAULT);
try {
String description = track.getCategory() + "\n" + track.getDescription() + "\n"
+ context.getString(R.string.send_google_by_my_tracks, "", "");
mapId = SendMapsUtils.createNewMap(
track.getName(), description, defaultMapPublic, mapsClient, authToken);
} catch (ParseException e) {
Log.d(TAG, "Unable to create a new map", e);
return false;
} catch (HttpException e) {
Log.d(TAG, "Unable to create a new map", e);
return false;
} catch (IOException e) {
Log.d(TAG, "Unable to create a new map", e);
return false;
}
return mapId != null;
}
}
/**
* Uploads all the points in a track.
*
* @param track the track
* @return true if success.
*/
@VisibleForTesting
boolean uploadAllTrackPoints(Track track) {
Cursor locationsCursor = null;
try {
boolean metricUnits = PreferencesUtils.getBoolean(
context, R.string.metric_units_key, PreferencesUtils.METRIC_UNITS_DEFAULT);
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.getTripStatistics().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());
track.setDescription(getTrackDescription(track, distances, elevations));
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();
}
}
}
/**
* Gets the description of a track.
*
* @param track the track
* @param distances distance vectors
* @param elevations elevation vectors
* @return the description of a track.
*/
@VisibleForTesting
String getTrackDescription(Track track, Vector<Double> distances, Vector<Double> elevations) {
DescriptionGenerator descriptionGenerator = new DescriptionGeneratorImpl(context);
return "<p>" + track.getDescription() + "</p><p>"
+ descriptionGenerator.generateTrackDescription(track, distances, elevations, true)
+ "</p>";
}
/**
* Prepares and uploads a list of locations from a track.
*
* @param track the track
* @param locations the locations from the track
* @param lastBatch true if it is the last batch of locations
* @return true if success.
*/
@VisibleForTesting
boolean prepareAndUploadPoints(Track track, List<Location> locations, boolean lastBatch) {
// Prepare locations
ArrayList<Track> splitTracks = SendToGoogleUtils.prepareLocations(track, locations);
// Upload segments
boolean onlyOneSegment = lastBatch && currentSegment == 1 && splitTracks.size() == 1;
for (Track segment : splitTracks) {
if (!onlyOneSegment) {
segment.setName(context.getString(
R.string.send_google_track_part_label, segment.getName(), currentSegment));
}
if (!uploadSegment(segment.getName(), segment.getLocations())) {
Log.d(TAG, "Unable to upload segment");
return false;
}
currentSegment++;
}
return true;
}
/**
* Uploads a marker.
*
* @param title marker title
* @param description marker description
* @param iconUrl marker marker icon
* @param location marker location
* @return true if success.
*/
@VisibleForTesting
boolean uploadMarker(
String title, String description, String iconUrl, Location location) {
if (isCancelled()) {
return false;
}
try {
if (!SendMapsUtils.uploadMarker(mapId, title, description, iconUrl, location, mapsClient,
authToken, mapsGDataConverter)) {
Log.d(TAG, "Unable to upload marker");
return false;
}
} catch (ParseException e) {
Log.d(TAG, "Unable to upload marker", e);
return false;
} catch (HttpException e) {
Log.d(TAG, "Unable to upload marker", e);
return false;
} catch (IOException e) {
Log.d(TAG, "Unable to upload marker", e);
return false;
}
return true;
}
/**
* Uploads a segment
*
* @param title segment title
* @param locations segment locations
* @return true if success
*/
private boolean uploadSegment(String title, ArrayList<Location> locations) {
if (isCancelled()) {
return false;
}
try {
if (!SendMapsUtils.uploadSegment(
mapId, title, locations, mapsClient, authToken, mapsGDataConverter)) {
Log.d(TAG, "Unable to upload track points");
return false;
}
} catch (ParseException e) {
Log.d(TAG, "Unable to upload track points", e);
return false;
} catch (HttpException e) {
Log.d(TAG, "Unable to upload track points", e);
return false;
} catch (IOException e) {
Log.d(TAG, "Unable to upload track points", e);
return false;
}
return true;
}
/**
* Uploads all the waypoints.
*
* @return true if success.
*/
@VisibleForTesting
boolean uploadWaypoints() {
Cursor cursor = null;
try {
cursor = myTracksProviderUtils.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
*/
@VisibleForTesting
void updateProgress(int uploaded, int total) {
publishProgress(getPercentage(uploaded, total));
}
/**
* Count the percentage of the number of locations uploaded.
*
* @param uploaded the number of uploaded locations
* @param total the number of total locations
*/
@VisibleForTesting
static int getPercentage(int uploaded, int total) {
double totalPercentage = (double) uploaded / total;
double scaledPercentage = totalPercentage
* (PROGRESS_UPLOAD_DATA_MAX - PROGRESS_UPLOAD_DATA_MIN) + PROGRESS_UPLOAD_DATA_MIN;
return (int) scaledPercentage;
}
/**
* Gets the mapID.
*
* @return mapId
*/
@VisibleForTesting
String getMapId() {
return mapId;
}
/**
* Sets the value of mapsGDataConverter.
*
* @param mapsGDataConverter new value of mapsGDataConverter
*/
@VisibleForTesting
void setMapsGDataConverter(MapsGDataConverter mapsGDataConverter) {
this.mapsGDataConverter = mapsGDataConverter;
}
}
@@ -0,0 +1,228 @@
/*
* 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
|| track.getMapId().length() == 0) {
Log.e(TAG, "Invalid track");
return null;
}
return MapsClient.buildMapUrl(track.getMapId());
}
/**
* Creates a new Google Map.
*
* @param title title of the map
* @param description description of the map
* @param isPublic true if the map can be public
* @param mapsClient the maps client
* @param authToken the auth token
* @return map id of the created map if successful.
*/
public static String createNewMap(
String title, String description, boolean isPublic, MapsClient mapsClient, String authToken)
throws ParseException, HttpException, IOException {
String mapFeed = MapsClient.getMapsFeed();
MapsMapMetadata metaData = new MapsMapMetadata();
metaData.setTitle(title);
metaData.setDescription(description);
metaData.setSearchable(isPublic);
Entry entry = MapsGDataConverter.getMapEntryForMetadata(metaData);
Entry result = mapsClient.createEntry(mapFeed, authToken, entry);
if (result == null) {
Log.d(TAG, "No result when creating a new map");
return null;
}
return MapsClient.getMapIdFromMapEntryId(result.getId());
}
/**
* Uploads a start/end marker to Google Maps.
*
* @param mapId the map id
* @param title the marker title
* @param description the marker description
* @param iconUrl the marker icon URL
* @param location the marker location
* @param mapsClient the maps client
* @param authToken the auth token
* @param mapsGDataConverter the maps gdata converter
* @return true if success.
*/
public static boolean uploadMarker(String mapId, String title, String description, String iconUrl,
Location location, MapsClient mapsClient, String authToken,
MapsGDataConverter mapsGDataConverter) throws ParseException, HttpException, IOException {
String featuresFeed = MapsClient.getFeaturesFeed(mapId);
MapsFeature mapsFeature = buildMapsMarkerFeature(
title, description, iconUrl, getGeoPoint(location));
Entry entry = mapsGDataConverter.getEntryForFeature(mapsFeature);
try {
mapsClient.createEntry(featuresFeed, authToken, entry);
} catch (IOException e) {
// Retry once (often IOException is thrown on a timeout)
Log.d(TAG, "Retry upload marker", e);
mapsClient.createEntry(featuresFeed, authToken, entry);
}
return true;
}
/**
* Uploads a waypoint as a marker feature to Google Maps.
*
* @param mapId the map id
* @param waypoint the waypoint
* @param mapsClient the maps client
* @param authToken the auth token
* @param mapsGDataConverter the maps gdata converter
* @return true if success.
*/
public static boolean uploadWaypoint(String mapId, Waypoint waypoint, MapsClient mapsClient,
String authToken, MapsGDataConverter mapsGDataConverter)
throws ParseException, HttpException, IOException {
String featuresFeed = MapsClient.getFeaturesFeed(mapId);
MapsFeature feature = buildMapsMarkerFeature(waypoint.getName(), waypoint.getDescription(),
waypoint.getIcon(), getGeoPoint(waypoint.getLocation()));
Entry entry = mapsGDataConverter.getEntryForFeature(feature);
try {
mapsClient.createEntry(featuresFeed, authToken, entry);
} catch (IOException e) {
// Retry once (often IOException is thrown on a timeout)
Log.d(TAG, "Retry upload waypoint", e);
mapsClient.createEntry(featuresFeed, authToken, entry);
}
return true;
}
/**
* Uploads a segment as a line feature to Google Maps.
*
* @param mapId the map id
* @param title the segment title
* @param locations the segment locations
* @param mapsClient the maps client
* @param authToken the auth token
* @param mapsGDataConverter the maps gdata converter
* @return true if success.
*/
public static boolean uploadSegment(String mapId, String title, ArrayList<Location> locations,
MapsClient mapsClient, String authToken, MapsGDataConverter mapsGDataConverter)
throws ParseException, HttpException, IOException {
String featuresFeed = MapsClient.getFeaturesFeed(mapId);
Entry entry = mapsGDataConverter.getEntryForFeature(buildMapsLineFeature(title, locations));
try {
mapsClient.createEntry(featuresFeed, authToken, entry);
} catch (IOException e) {
// Retry once (often IOException is thrown on a timeout)
Log.d(TAG, "Retry upload track points", e);
mapsClient.createEntry(featuresFeed, authToken, entry);
}
return true;
}
/**
* Builds a map marker feature.
*
* @param title feature title
* @param description the feature description
* @param iconUrl the feature icon URL
* @param geoPoint the marker
*/
@VisibleForTesting
static MapsFeature buildMapsMarkerFeature(
String title, String description, String iconUrl, GeoPoint geoPoint) {
MapsFeature mapsFeature = new MapsFeature();
mapsFeature.setType(MapsFeature.MARKER);
mapsFeature.generateAndroidId();
// Feature must have a name (otherwise GData upload may fail)
mapsFeature.setTitle(TextUtils.isEmpty(title) ? EMPTY_TITLE : title);
mapsFeature.setDescription(description.replaceAll("\n", "<br>"));
mapsFeature.setIconUrl(iconUrl);
mapsFeature.addPoint(geoPoint);
return mapsFeature;
}
/**
* Builds a maps line feature from a set of locations.
*
* @param title the feature title
* @param locations set of locations
*/
@VisibleForTesting
static MapsFeature buildMapsLineFeature(String title, ArrayList<Location> locations) {
MapsFeature mapsFeature = new MapsFeature();
mapsFeature.setType(MapsFeature.LINE);
mapsFeature.generateAndroidId();
// Feature must have a name (otherwise GData upload may fail)
mapsFeature.setTitle(TextUtils.isEmpty(title) ? EMPTY_TITLE : title);
mapsFeature.setColor(LINE_COLOR);
for (Location location : locations) {
mapsFeature.addPoint(getGeoPoint(location));
}
return mapsFeature;
}
/**
* Gets a {@link GeoPoint} from a {@link Location}.
*
* @param location the location
*/
@VisibleForTesting
static GeoPoint getGeoPoint(Location location) {
return new GeoPoint(
(int) (location.getLatitude() * 1E6), (int) (location.getLongitude() * 1E6));
}
}
@@ -0,0 +1,134 @@
/*
* Copyright 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.io.sendtogoogle;
import com.google.android.apps.mytracks.util.DialogUtils;
import com.google.android.maps.mytracks.R;
import android.app.Activity;
import android.app.Dialog;
import android.app.ProgressDialog;
import android.content.DialogInterface;
import android.os.Bundle;
/**
* The abstract class for activities sending a track to Google.
* <p>
* The activity gets recreated when the screen rotates. To support the activity
* displaying a progress dialog, we do the following:
* <ul>
* <li>use one instance of an AyncTask to send the track</li>
* <li>save that instance as the last non configuration instance of the activity
* </li>
* <li>when a new activity is created, pass the activity to the AsyncTask so
* that the AsyncTask can update the progress dialog of the activity</li>
* </ul>
*
* @author Jimmy Shih
*/
public abstract class AbstractSendActivity extends Activity {
private static final int DIALOG_PROGRESS_ID = 0;
protected SendRequest sendRequest;
private AbstractSendAsyncTask asyncTask;
private ProgressDialog progressDialog;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
sendRequest = getIntent().getParcelableExtra(SendRequest.SEND_REQUEST_KEY);
Object retained = getLastNonConfigurationInstance();
if (retained instanceof AbstractSendAsyncTask) {
asyncTask = (AbstractSendAsyncTask) retained;
asyncTask.setActivity(this);
} else {
asyncTask = createAsyncTask();
asyncTask.execute();
}
}
@Override
public Object onRetainNonConfigurationInstance() {
asyncTask.setActivity(null);
return asyncTask;
}
@Override
protected Dialog onCreateDialog(int id) {
if (id != DIALOG_PROGRESS_ID) {
return null;
}
progressDialog = DialogUtils.createHorizontalProgressDialog(
this, R.string.send_google_progress_message, new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
asyncTask.cancel(true);
startNextActivity(false, true);
}
}, getServiceName());
return progressDialog;
}
/**
* Invokes when the associated AsyncTask completes.
*
* @param success true if the AsyncTask is successful
*/
public void onAsyncTaskCompleted(boolean success) {
startNextActivity(success, false);
}
/**
* Shows the progress dialog.
*/
public void showProgressDialog() {
showDialog(DIALOG_PROGRESS_ID);
}
/**
* Sets the progress dialog value.
*
* @param value the dialog value
*/
public void setProgressDialogValue(int value) {
if (progressDialog != null) {
progressDialog.setIndeterminate(false);
progressDialog.setProgress(value);
progressDialog.setMax(100);
}
}
/**
* Creates the AsyncTask.
*/
protected abstract AbstractSendAsyncTask createAsyncTask();
/**
* Gets the service name.
*/
protected abstract String getServiceName();
/**
* Starts the next activity.
*
* @param success true if this activity is successful
* @param isCancel true if it is a cancel request
*/
protected abstract void startNextActivity(boolean success, boolean isCancel);
}

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