forked from upstream-mirrors/OpenTracks
Support uploading of tracks to fusion tables. Remove support for maps data api.
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
/*
|
||||
* 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.accounts.Account;
|
||||
import com.google.android.accounts.AccountManager;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.util.Log;
|
||||
|
||||
/**
|
||||
* Choose which account to upload track information to.
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public class AccountChooser {
|
||||
|
||||
/**
|
||||
* The last selected account.
|
||||
*/
|
||||
private int selectedAccountIndex = -1;
|
||||
private Account selectedAccount = null;
|
||||
|
||||
/**
|
||||
* An interface for receiving updates once the user has selected the account.
|
||||
*/
|
||||
public interface AccountHandler {
|
||||
/**
|
||||
* Handle the account being selected.
|
||||
* @param account The selected account or null if none could be found
|
||||
*/
|
||||
public void handleAccountSelected(Account account);
|
||||
}
|
||||
|
||||
/**
|
||||
* Chooses the best account to upload to.
|
||||
* If no account is found the user will be alerted.
|
||||
* If only one account is found that will be used.
|
||||
* If multiple accounts are found the user will be allowed to choose.
|
||||
*
|
||||
* @param activity The parent activity
|
||||
* @param handler The handler to be notified when an account has been selected
|
||||
*/
|
||||
public void chooseAccount(final Activity activity,
|
||||
final AccountHandler handler) {
|
||||
final Account[] accounts = AccountManager.get(activity)
|
||||
.getAccountsByType(MyTracksConstants.ACCOUNT_TYPE);
|
||||
if (accounts.length < 1) {
|
||||
alertNoAccounts(activity, handler);
|
||||
return;
|
||||
}
|
||||
if (accounts.length == 1) {
|
||||
handler.handleAccountSelected(accounts[0]);
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO This should be read out of a preference.
|
||||
if (selectedAccount != null) {
|
||||
handler.handleAccountSelected(selectedAccount);
|
||||
return;
|
||||
}
|
||||
|
||||
// Let the user choose.
|
||||
Log.e(MyTracksConstants.TAG, "Multiple matching accounts found.");
|
||||
final AlertDialog.Builder builder = new AlertDialog.Builder(activity);
|
||||
builder.setTitle(R.string.choose_account_title);
|
||||
builder.setCancelable(false);
|
||||
builder.setPositiveButton(R.string.ok,
|
||||
new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
selectedAccount = accounts[selectedAccountIndex];
|
||||
handler.handleAccountSelected(selectedAccount);
|
||||
}
|
||||
});
|
||||
builder.setNegativeButton(R.string.cancel,
|
||||
new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
handler.handleAccountSelected(null);
|
||||
}
|
||||
});
|
||||
String[] choices = new String[accounts.length];
|
||||
for (int i = 0; i < accounts.length; i++) {
|
||||
choices[i] = accounts[i].name;
|
||||
}
|
||||
builder.setSingleChoiceItems(choices, selectedAccountIndex,
|
||||
new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
selectedAccountIndex = which;
|
||||
}
|
||||
});
|
||||
builder.show();
|
||||
}
|
||||
|
||||
/**
|
||||
* Puts up a dialog alerting the user that no suitable account was found.
|
||||
*/
|
||||
private void alertNoAccounts(final Activity activity,
|
||||
final AccountHandler handler) {
|
||||
Log.e(MyTracksConstants.TAG, "No matching accounts found.");
|
||||
final AlertDialog.Builder builder = new AlertDialog.Builder(activity);
|
||||
builder.setTitle(R.string.no_account_found_title);
|
||||
builder.setMessage(R.string.no_account_found);
|
||||
builder.setCancelable(true);
|
||||
builder.setNegativeButton(R.string.ok,
|
||||
new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
handler.handleAccountSelected(null);
|
||||
}
|
||||
});
|
||||
builder.show();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
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.maps.mytracks.R;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
|
||||
import android.os.Bundle;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.Log;
|
||||
import android.view.Window;
|
||||
import android.widget.ScrollView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Activity for viewing the combined statistics for all the recorded tracks.
|
||||
*
|
||||
* Other features to add - menu items to change setings.
|
||||
*
|
||||
* @author Fergus Nelson
|
||||
*/
|
||||
public class AggregatedStatsActivity extends Activity implements
|
||||
OnSharedPreferenceChangeListener {
|
||||
|
||||
private final StatsUtilities utils;
|
||||
|
||||
private MyTracksProviderUtils tracksProvider;
|
||||
|
||||
private boolean metricUnits = true;
|
||||
|
||||
public AggregatedStatsActivity() {
|
||||
this.utils = new StatsUtilities(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences,
|
||||
String key) {
|
||||
Log.d(MyTracksConstants.TAG, "StatsActivity: onSharedPreferences changed "
|
||||
+ key);
|
||||
if (key != null) {
|
||||
if (key.equals(R.string.metric_units_key)) {
|
||||
metricUnits = sharedPreferences.getBoolean(
|
||||
getString(R.string.metric_units_key), true);
|
||||
utils.setMetricUnits(metricUnits);
|
||||
utils.updateUnits();
|
||||
loadAggregatedStats();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
this.tracksProvider = MyTracksProviderUtils.Factory.get(this);
|
||||
|
||||
// We don't need a window title bar:
|
||||
requestWindowFeature(Window.FEATURE_NO_TITLE);
|
||||
setContentView(R.layout.stats);
|
||||
|
||||
ScrollView sv = ((ScrollView) findViewById(R.id.scrolly));
|
||||
sv.setScrollBarStyle(ScrollView.SCROLLBARS_OUTSIDE_INSET);
|
||||
|
||||
SharedPreferences preferences = getSharedPreferences(
|
||||
MyTracksSettings.SETTINGS_NAME, 0);
|
||||
if (preferences != null) {
|
||||
metricUnits = preferences.getBoolean(getString(R.string.metric_units_key), true);
|
||||
preferences.registerOnSharedPreferenceChangeListener(this);
|
||||
}
|
||||
utils.setMetricUnits(metricUnits);
|
||||
utils.updateUnits();
|
||||
utils.setSpeedLabel(R.id.speed_label, R.string.speed, R.string.pace_label);
|
||||
utils.setSpeedLabels();
|
||||
|
||||
DisplayMetrics metrics = new DisplayMetrics();
|
||||
getWindowManager().getDefaultDisplay().getMetrics(metrics);
|
||||
if (metrics.heightPixels > 600) {
|
||||
((TextView) findViewById(R.id.speed_register)).setTextSize(80.0f);
|
||||
}
|
||||
loadAggregatedStats();
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Reads tracks from the db
|
||||
* 2. Merges the trip stats from the tracks
|
||||
* 3. Updates the view
|
||||
*/
|
||||
private void loadAggregatedStats() {
|
||||
List<Track> tracks = retrieveTracks();
|
||||
TripStatistics rollingStats = null;
|
||||
if (!tracks.isEmpty()) {
|
||||
rollingStats = new TripStatistics(tracks.iterator().next()
|
||||
.getStatistics());
|
||||
for (int i = 1; i < tracks.size(); i++) {
|
||||
rollingStats.merge(tracks.get(i).getStatistics());
|
||||
}
|
||||
}
|
||||
updateView(rollingStats);
|
||||
}
|
||||
|
||||
private List<Track> retrieveTracks() {
|
||||
return tracksProvider.getAllTracks();
|
||||
}
|
||||
|
||||
private void updateView(TripStatistics aggStats) {
|
||||
if (aggStats != null) {
|
||||
utils.setAllStats(aggStats);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,661 @@
|
||||
/*
|
||||
* 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.ChartView.Mode;
|
||||
import com.google.android.apps.mytracks.content.MyTracksLocation;
|
||||
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
|
||||
import com.google.android.apps.mytracks.content.Sensor;
|
||||
import com.google.android.apps.mytracks.content.Track;
|
||||
import com.google.android.apps.mytracks.content.TrackPointsColumns;
|
||||
import com.google.android.apps.mytracks.content.Waypoint;
|
||||
import com.google.android.apps.mytracks.content.WaypointsColumns;
|
||||
import com.google.android.apps.mytracks.content.Sensor.SensorDataSet;
|
||||
import com.google.android.apps.mytracks.services.StatusAnnouncerFactory;
|
||||
import com.google.android.apps.mytracks.stats.DoubleBuffer;
|
||||
import com.google.android.apps.mytracks.stats.TripStatisticsBuilder;
|
||||
import com.google.android.apps.mytracks.util.ApiFeatures;
|
||||
import com.google.android.apps.mytracks.util.MyTracksUtils;
|
||||
import com.google.android.apps.mytracks.util.UnitConversions;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.SharedPreferences;
|
||||
import android.database.ContentObserver;
|
||||
import android.database.Cursor;
|
||||
import android.location.Location;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.HandlerThread;
|
||||
import android.util.Log;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewGroup.LayoutParams;
|
||||
import android.view.Window;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.ZoomControls;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
/**
|
||||
* An activity that displays a chart from the track point provider.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public class ChartActivity extends Activity implements
|
||||
SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
|
||||
private double profileLength = 0;
|
||||
|
||||
private boolean metricUnits = true;
|
||||
private boolean reportSpeed = true;
|
||||
|
||||
/**
|
||||
* The track id that is displayed.
|
||||
*/
|
||||
private long selectedTrackId = -1;
|
||||
|
||||
/**
|
||||
* Id of the last location that was seen when reading tracks from the
|
||||
* provider. This is used to determine which locations are new compared to the
|
||||
* last time the chart was updated.
|
||||
*/
|
||||
private long lastSeenLocationId = -1;
|
||||
|
||||
private long startTime = -1;
|
||||
|
||||
private Location lastLocation;
|
||||
|
||||
/**
|
||||
* The id of the track currently being recorded.
|
||||
*/
|
||||
private long recordingTrackId = -1;
|
||||
|
||||
private final DoubleBuffer elevationBuffer =
|
||||
new DoubleBuffer(MyTracksConstants.ELEVATION_SMOOTHING_FACTOR);
|
||||
private final DoubleBuffer speedBuffer =
|
||||
new DoubleBuffer(MyTracksConstants.SPEED_SMOOTHING_FACTOR);
|
||||
|
||||
private Mode mode = Mode.BY_DISTANCE;
|
||||
|
||||
/**
|
||||
* Utilities to deal with the database.
|
||||
*/
|
||||
private MyTracksProviderUtils providerUtils;
|
||||
|
||||
/*
|
||||
* UI elements:
|
||||
*/
|
||||
private ChartView cv;
|
||||
private MenuItem chartSettingsMenuItem;
|
||||
private LinearLayout busyPane;
|
||||
private ZoomControls zoomControls;
|
||||
|
||||
/** Handler for callbacks to the UI thread */
|
||||
private final Handler uiHandler = new Handler();
|
||||
|
||||
/**
|
||||
* A runnable that can be posted to the UI thread. It will remove the spinner
|
||||
* (if any), enable/disable zoom controls and orange pointer as appropriate
|
||||
* and redraw.
|
||||
*/
|
||||
private final Runnable updateChart = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
busyPane.setVisibility(View.GONE);
|
||||
zoomControls.setIsZoomInEnabled(cv.canZoomIn());
|
||||
zoomControls.setIsZoomOutEnabled(cv.canZoomOut());
|
||||
cv.setShowPointer(selectedTrackIsRecording());
|
||||
cv.invalidate();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* A runnable that can be posted to the UI thread. It will show the spinner.
|
||||
*/
|
||||
private final Runnable showSpinner = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
busyPane.setVisibility(View.VISIBLE);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* An observer for the tracks provider. Will listen to new track points being
|
||||
* added and update the chart if necessary.
|
||||
*/
|
||||
private ContentObserver observer;
|
||||
|
||||
/**
|
||||
* An observer for the waypoints provider. Will listen to new way points being
|
||||
* added and update the chart if necessary.
|
||||
*/
|
||||
private ContentObserver waypointObserver;
|
||||
|
||||
/**
|
||||
* A thread with a looper. Post to updateTrackHandler to execute Runnables on
|
||||
* this thread.
|
||||
*/
|
||||
private final HandlerThread updateTrackThread =
|
||||
new HandlerThread("updateTrackThread");
|
||||
|
||||
/** Handler for updateTrackThread */
|
||||
private Handler updateTrackHandler;
|
||||
|
||||
/**
|
||||
* A runnable that update the profile from the provider.
|
||||
*/
|
||||
private final Runnable updateTrackRunnable = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Log.d(MyTracksConstants.TAG, "MyTracks: Updating chart.");
|
||||
Track track = providerUtils.getTrack(recordingTrackId);
|
||||
if (track == null) {
|
||||
Log.w(MyTracksConstants.TAG, "MyTracks: track not found");
|
||||
return;
|
||||
}
|
||||
Cursor cursor = null;
|
||||
try {
|
||||
cursor = providerUtils.getLocationsCursor(recordingTrackId,
|
||||
lastSeenLocationId + 1,
|
||||
MyTracksConstants.MAX_DISPLAYED_TRACK_POINTS - cv.getData().size(),
|
||||
true);
|
||||
if (cursor != null) {
|
||||
if (cursor.moveToLast()) {
|
||||
final int idColumnIdx =
|
||||
cursor.getColumnIndexOrThrow(TrackPointsColumns._ID);
|
||||
ArrayList<double[]> data = new ArrayList<double[]>();
|
||||
// Need two locations so we can keep track of the last location.
|
||||
Location location = new MyTracksLocation("");
|
||||
do {
|
||||
lastSeenLocationId = cursor.getLong(idColumnIdx);
|
||||
providerUtils.fillLocation(cursor, location);
|
||||
if (MyTracksUtils.isValidLocation(location)) {
|
||||
double[] point = new double[6];
|
||||
location = getDataPoint(location, track, point);
|
||||
data.add(point);
|
||||
}
|
||||
} while (cursor.moveToPrevious());
|
||||
cv.addDataPoints(data);
|
||||
}
|
||||
}
|
||||
} catch (RuntimeException e) {
|
||||
Log.w(MyTracksConstants.TAG, "Caught an unexpected exception.", e);
|
||||
} finally {
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
uiHandler.post(new Runnable() {
|
||||
public void run() {
|
||||
cv.invalidate();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences,
|
||||
String key) {
|
||||
if (key != null) {
|
||||
if (key.equals(getString(R.string.selected_track_key))) {
|
||||
selectedTrackId =
|
||||
sharedPreferences.getLong(getString(R.string.selected_track_key),
|
||||
-1);
|
||||
readProfileAsync();
|
||||
} else if (key.equals(getString(R.string.metric_units_key))) {
|
||||
metricUnits =
|
||||
sharedPreferences.getBoolean(getString(R.string.metric_units_key),
|
||||
true);
|
||||
cv.setMetricUnits(metricUnits);
|
||||
readProfileAsync();
|
||||
} else if (key.equals(getString(R.string.report_speed_key))) {
|
||||
reportSpeed =
|
||||
sharedPreferences.getBoolean(getString(R.string.report_speed_key),
|
||||
true);
|
||||
cv.setReportSpeed(reportSpeed, this);
|
||||
readProfileAsync();
|
||||
} else if (key.equals(getString(R.string.recording_track_key))) {
|
||||
recordingTrackId =
|
||||
sharedPreferences.getLong(getString(R.string.recording_track_key),
|
||||
-1);
|
||||
runOnUiThread(updateChart);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
Log.w(MyTracksConstants.TAG, "ChartActivity.onCreate");
|
||||
super.onCreate(savedInstanceState);
|
||||
MyTracks.getInstance().setChartActivity(this);
|
||||
providerUtils = MyTracksProviderUtils.Factory.get(this);
|
||||
|
||||
// The volume we want to control is the Text-To-Speech volume
|
||||
int volumeStream =
|
||||
new StatusAnnouncerFactory(ApiFeatures.getInstance()).getVolumeStream();
|
||||
setVolumeControlStream(volumeStream);
|
||||
|
||||
requestWindowFeature(Window.FEATURE_NO_TITLE);
|
||||
setContentView(R.layout.mytracks_elevation);
|
||||
ViewGroup layout = (ViewGroup) findViewById(R.id.elevation_chart);
|
||||
cv = new ChartView(this);
|
||||
cv.setMode(this.mode);
|
||||
LayoutParams params =
|
||||
new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT);
|
||||
layout.addView(cv, params);
|
||||
|
||||
SharedPreferences preferences =
|
||||
getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0);
|
||||
if (preferences != null) {
|
||||
selectedTrackId =
|
||||
preferences.getLong(getString(R.string.selected_track_key), -1);
|
||||
recordingTrackId =
|
||||
preferences.getLong(getString(R.string.recording_track_key), -1);
|
||||
metricUnits = preferences.getBoolean(getString(R.string.metric_units_key),
|
||||
true);
|
||||
cv.setMetricUnits(metricUnits);
|
||||
reportSpeed = preferences.getBoolean(getString(R.string.report_speed_key),
|
||||
true);
|
||||
cv.setReportSpeed(reportSpeed, this);
|
||||
preferences.registerOnSharedPreferenceChangeListener(this);
|
||||
}
|
||||
|
||||
busyPane = (LinearLayout) findViewById(R.id.elevation_busypane);
|
||||
zoomControls = (ZoomControls) findViewById(R.id.elevation_zoom);
|
||||
zoomControls.setOnZoomInClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
zoomIn();
|
||||
}
|
||||
});
|
||||
zoomControls.setOnZoomOutClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
zoomOut();
|
||||
}
|
||||
});
|
||||
|
||||
updateTrackThread.start();
|
||||
updateTrackHandler = new Handler(updateTrackThread.getLooper());
|
||||
|
||||
// Register observer for the track point provider:
|
||||
Handler contentHandler = new Handler();
|
||||
observer = new ContentObserver(contentHandler) {
|
||||
@Override
|
||||
public void onChange(boolean selfChange) {
|
||||
Log.d(MyTracksConstants.TAG, "ChartActivity: ContentObserver.onChange");
|
||||
// Check for any new locations and append them to the currently
|
||||
// recording track.
|
||||
if (recordingTrackId < 0) {
|
||||
// No track is being recorded. We should not be here.
|
||||
return;
|
||||
}
|
||||
if (selectedTrackId != recordingTrackId) {
|
||||
// No track, or one other than the recording track is selected, don't
|
||||
// bother.
|
||||
return;
|
||||
}
|
||||
// Update can potentially be lengthy, put it in its own thread:
|
||||
updateTrackHandler.post(updateTrackRunnable);
|
||||
super.onChange(selfChange);
|
||||
}
|
||||
};
|
||||
|
||||
waypointObserver = new ContentObserver(contentHandler) {
|
||||
@Override
|
||||
public void onChange(boolean selfChange) {
|
||||
Log.d(MyTracksConstants.TAG,
|
||||
"MyTracksMap: ContentObserver.onChange waypoints");
|
||||
if (selectedTrackId < 0) {
|
||||
return;
|
||||
}
|
||||
Thread t = new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
readWaypoints();
|
||||
ChartActivity.this.runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
cv.invalidate();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
t.start();
|
||||
super.onChange(selfChange);
|
||||
}
|
||||
};
|
||||
|
||||
readProfileAsync();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
unregisterContentObservers();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
// Make sure any updates that might have happened are propagated to this
|
||||
// activity:
|
||||
observer.onChange(false);
|
||||
waypointObserver.onChange(false);
|
||||
registerContentObservers();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the content observer for the map overlay.
|
||||
*/
|
||||
private void registerContentObservers() {
|
||||
getContentResolver().registerContentObserver(TrackPointsColumns.CONTENT_URI,
|
||||
false/* notifyForDescendents */, observer);
|
||||
getContentResolver().registerContentObserver(WaypointsColumns.CONTENT_URI,
|
||||
false/* notifyForDescendents */, waypointObserver);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister the content observer for the map overlay.
|
||||
*/
|
||||
private void unregisterContentObservers() {
|
||||
getContentResolver().unregisterContentObserver(observer);
|
||||
getContentResolver().unregisterContentObserver(waypointObserver);
|
||||
}
|
||||
|
||||
private boolean selectedTrackIsRecording() {
|
||||
return selectedTrackId == recordingTrackId;
|
||||
}
|
||||
|
||||
private void zoomIn() {
|
||||
cv.zoomIn();
|
||||
zoomControls.setIsZoomInEnabled(cv.canZoomIn());
|
||||
zoomControls.setIsZoomOutEnabled(cv.canZoomOut());
|
||||
}
|
||||
|
||||
private void zoomOut() {
|
||||
cv.zoomOut();
|
||||
zoomControls.setIsZoomInEnabled(cv.canZoomIn());
|
||||
zoomControls.setIsZoomOutEnabled(cv.canZoomOut());
|
||||
}
|
||||
|
||||
public void setMode(Mode newMode) {
|
||||
if (this.mode != newMode) {
|
||||
this.mode = newMode;
|
||||
cv.setMode(this.mode);
|
||||
readProfileAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public Mode getMode() {
|
||||
return mode;
|
||||
}
|
||||
|
||||
public void setSeriesEnabled(int index, boolean enabled) {
|
||||
cv.getChartValueSeries(index).setEnabled(enabled);
|
||||
runOnUiThread(updateChart);
|
||||
}
|
||||
|
||||
public boolean isSeriesEnabled(int index) {
|
||||
return cv.getChartValueSeries(index).isEnabled();
|
||||
}
|
||||
|
||||
private void readWaypoints() {
|
||||
if (selectedTrackId < 0) {
|
||||
return;
|
||||
}
|
||||
Cursor cursor = null;
|
||||
cv.clearWaypoints();
|
||||
try {
|
||||
// We will silently drop extra waypoints to make the app responsive.
|
||||
cursor =
|
||||
providerUtils.getWaypointsCursor(selectedTrackId, 0,
|
||||
MyTracksConstants.MAX_DISPLAYED_TRACK_POINTS);
|
||||
if (cursor != null) {
|
||||
if (cursor.moveToFirst()) {
|
||||
do {
|
||||
Waypoint wpt = providerUtils.createWaypoint(cursor);
|
||||
cv.addWaypoint(wpt);
|
||||
} while (cursor.moveToNext());
|
||||
}
|
||||
}
|
||||
} catch (RuntimeException e) {
|
||||
Log.w(MyTracksConstants.TAG, "Caught an unexpected exception.", e);
|
||||
} finally {
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
super.onCreateOptionsMenu(menu);
|
||||
chartSettingsMenuItem =
|
||||
menu.add(0, MyTracksConstants.MENU_CHART_SETTINGS, 0,
|
||||
R.string.chart_settings);
|
||||
chartSettingsMenuItem.setIcon(R.drawable.chart_settings);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case MyTracksConstants.MENU_CHART_SETTINGS:
|
||||
MyTracks.getInstance().getDialogManager().showDialogSafely(
|
||||
DialogManager.DIALOG_CHART_SETTINGS);
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a location, creates a new data point for the chart. A data point is
|
||||
* an array double[3 or 6], where:
|
||||
* data[0] = the time or distance
|
||||
* data[1] = the elevation
|
||||
* data[2] = the speed
|
||||
* and possibly:
|
||||
* data[3] = power
|
||||
* data[4] = cadence
|
||||
* data[5] = heart rate
|
||||
*
|
||||
* This must be called in order for each point.
|
||||
*
|
||||
* @param location the location to get data for (this method takes ownership of that location)
|
||||
* @param track the track to get data from
|
||||
* @param result the resulting point to fill out
|
||||
* @return the previous location, now available for reuse
|
||||
*/
|
||||
private Location getDataPoint(Location location, Track track, double[] result) {
|
||||
if (location instanceof MyTracksLocation &&
|
||||
((MyTracksLocation) location).getSensorDataSet() != null) {
|
||||
SensorDataSet sensorData = ((MyTracksLocation) location).getSensorDataSet();
|
||||
if (sensorData.hasPower()
|
||||
&& sensorData.getPower().getState() == Sensor.SensorState.SENDING
|
||||
&& sensorData.getPower().hasValue()) {
|
||||
result[3] = sensorData.getPower().getValue();
|
||||
} else {
|
||||
result[3] = Double.NaN;
|
||||
}
|
||||
if (sensorData.hasCadence()
|
||||
&& sensorData.getCadence().getState() == Sensor.SensorState.SENDING
|
||||
&& sensorData.getCadence().hasValue()) {
|
||||
result[4] = sensorData.getCadence().getValue();
|
||||
} else {
|
||||
result[4] = Double.NaN;
|
||||
}
|
||||
if (sensorData.hasHeartRate()
|
||||
&& sensorData.getHeartRate().getState() == Sensor.SensorState.SENDING
|
||||
&& sensorData.getHeartRate().hasValue()) {
|
||||
result[5] = sensorData.getHeartRate().getValue();
|
||||
} else {
|
||||
result[5] = Double.NaN;
|
||||
}
|
||||
} else {
|
||||
result[3] = Double.NaN;
|
||||
result[4] = Double.NaN;
|
||||
result[5] = Double.NaN;
|
||||
}
|
||||
switch (mode) {
|
||||
case BY_DISTANCE:
|
||||
result[0] = profileLength;
|
||||
if (lastLocation != null) {
|
||||
double d = lastLocation.distanceTo(location);
|
||||
if (metricUnits) {
|
||||
profileLength += d / 1000.0;
|
||||
} else {
|
||||
profileLength += d * UnitConversions.KM_TO_MI / 1000.0;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case BY_TIME:
|
||||
if (startTime == -1) {
|
||||
// Base case
|
||||
startTime = location.getTime();
|
||||
}
|
||||
result[0] = (location.getTime() - startTime);
|
||||
break;
|
||||
default:
|
||||
Log.w(MyTracksConstants.TAG, "ChartActivity unknown mode: " + mode);
|
||||
}
|
||||
|
||||
elevationBuffer.setNext(metricUnits
|
||||
? location.getAltitude()
|
||||
: location.getAltitude() * UnitConversions.M_TO_FT);
|
||||
result[1] = elevationBuffer.getAverage();
|
||||
|
||||
if (lastLocation == null) {
|
||||
if (Math.abs(location.getSpeed() - 128) > 1) {
|
||||
speedBuffer.setNext(location.getSpeed());
|
||||
}
|
||||
} else if (TripStatisticsBuilder.isValidSpeed(
|
||||
location.getTime(), location.getSpeed(), lastLocation.getTime(),
|
||||
lastLocation.getSpeed(), speedBuffer)
|
||||
&& (location.getSpeed() <= track.getStatistics().getMaxSpeed())) {
|
||||
speedBuffer.setNext(location.getSpeed());
|
||||
}
|
||||
result[2] = speedBuffer.getAverage() * 3.6;
|
||||
if (!metricUnits) {
|
||||
result[2] *= UnitConversions.KM_TO_MI;
|
||||
}
|
||||
if (!reportSpeed && (result[2] != 0)) {
|
||||
// Format as hours per unit
|
||||
result[2] = (60.0 / result[2]);
|
||||
}
|
||||
|
||||
Location oldLastLocation = lastLocation;
|
||||
lastLocation = location;
|
||||
|
||||
if (oldLastLocation == null) {
|
||||
// No previous location, but return a blank one for reuse
|
||||
return new MyTracksLocation("");
|
||||
}
|
||||
|
||||
return oldLastLocation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the chart data points reading from the provider. This is non-blocking.
|
||||
*/
|
||||
private void readProfileAsync() {
|
||||
cv.reset();
|
||||
updateTrackHandler.post(new Runnable() {
|
||||
public void run() {
|
||||
runOnUiThread(showSpinner);
|
||||
readProfile();
|
||||
readWaypoints();
|
||||
runOnUiThread(updateChart);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the track profile from the provider. This is a blocking function and
|
||||
* should not be run from the UI thread.
|
||||
*/
|
||||
private void readProfile() {
|
||||
profileLength = 0;
|
||||
lastLocation = null;
|
||||
startTime = -1;
|
||||
Cursor cursor = null;
|
||||
if (selectedTrackId < 0) {
|
||||
return;
|
||||
}
|
||||
Track track = providerUtils.getTrack(selectedTrackId);
|
||||
if (track == null) {
|
||||
return;
|
||||
}
|
||||
long lastLocationRead = track.getStartId();
|
||||
long totalLocations = track.getStopId() - track.getStartId();
|
||||
|
||||
// Limit the number of chart readings. Ideally we would want around 1024.
|
||||
int chartSamplingFrequency = Math.max(1, (int) (totalLocations / 1024.0));
|
||||
int bufferSize = 1024;
|
||||
try {
|
||||
final ArrayList<double[]> theData = new ArrayList<double[]>();
|
||||
int points = 0;
|
||||
// Need two locations so we can keep track of the last location.
|
||||
Location location = new MyTracksLocation("");
|
||||
while (lastLocationRead < track.getStopId()) {
|
||||
cursor = providerUtils.getLocationsCursor(
|
||||
selectedTrackId, lastLocationRead, bufferSize, false);
|
||||
if (cursor != null) {
|
||||
elevationBuffer.reset();
|
||||
speedBuffer.reset();
|
||||
if (cursor.moveToFirst()) {
|
||||
final int idColumnIdx =
|
||||
cursor.getColumnIndexOrThrow(TrackPointsColumns._ID);
|
||||
while (cursor.moveToNext()) {
|
||||
points++;
|
||||
providerUtils.fillLocation(cursor, location);
|
||||
if (MyTracksUtils.isValidLocation(location)) {
|
||||
lastLocationRead = lastSeenLocationId =
|
||||
cursor.getLong(idColumnIdx);
|
||||
// TODO Can we be smarter about choosing 3 or 6 entries?
|
||||
double[] point = new double[6];
|
||||
location = getDataPoint(location, track, point);
|
||||
if (points % chartSamplingFrequency == 0) {
|
||||
theData.add(point);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
lastLocationRead += bufferSize;
|
||||
}
|
||||
} else {
|
||||
lastLocationRead += bufferSize;
|
||||
}
|
||||
cursor.close();
|
||||
cursor = null;
|
||||
}
|
||||
runOnUiThread(new Runnable() {
|
||||
public void run() {
|
||||
cv.setDataPoints(theData);
|
||||
}
|
||||
});
|
||||
} catch (RuntimeException e) {
|
||||
Log.w(MyTracksConstants.TAG, "Caught unexpected exception.", e);
|
||||
} finally {
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
* 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.ChartView.Mode;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.view.Window;
|
||||
import android.widget.Button;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.RadioButton;
|
||||
import android.widget.RadioGroup;
|
||||
|
||||
/**
|
||||
* An activity that allows the user to set the chart settings.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public class ChartSettingsDialog extends Dialog {
|
||||
private RadioButton distance;
|
||||
private CheckBox[] series;
|
||||
|
||||
public ChartSettingsDialog(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
requestWindowFeature(Window.FEATURE_NO_TITLE);
|
||||
setContentView(R.layout.chart_settings);
|
||||
Button cancel = (Button) findViewById(R.id.chart_settings_cancel);
|
||||
cancel.setOnClickListener(new View.OnClickListener() {
|
||||
public void onClick(View v) {
|
||||
dismiss();
|
||||
}
|
||||
});
|
||||
Button ok = (Button) findViewById(R.id.chart_settings_ok);
|
||||
ok.setOnClickListener(new View.OnClickListener() {
|
||||
public void onClick(View v) {
|
||||
handleOk();
|
||||
}
|
||||
});
|
||||
|
||||
distance = (RadioButton) findViewById(R.id.chart_settings_by_distance);
|
||||
|
||||
series = new CheckBox[ChartView.NUM_SERIES];
|
||||
series[ChartView.ELEVATION_SERIES] =
|
||||
(CheckBox) findViewById(R.id.chart_settings_elevation);
|
||||
series[ChartView.SPEED_SERIES] =
|
||||
(CheckBox) findViewById(R.id.chart_settings_speed);
|
||||
series[ChartView.POWER_SERIES] =
|
||||
(CheckBox) findViewById(R.id.chart_settings_power);
|
||||
series[ChartView.CADENCE_SERIES] =
|
||||
(CheckBox) findViewById(R.id.chart_settings_cadence);
|
||||
series[ChartView.HEART_RATE_SERIES] =
|
||||
(CheckBox) findViewById(R.id.chart_settings_heart_rate);
|
||||
}
|
||||
|
||||
public void setup(ChartActivity chart) {
|
||||
if (chart == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
RadioGroup rd = (RadioGroup) findViewById(R.id.chart_settings_x);
|
||||
rd.check(chart.getMode() == Mode.BY_DISTANCE
|
||||
? R.id.chart_settings_by_distance
|
||||
: R.id.chart_settings_by_time);
|
||||
for (int i = 0; i < ChartView.NUM_SERIES; i++) {
|
||||
series[i].setChecked(chart.isSeriesEnabled(i));
|
||||
}
|
||||
}
|
||||
|
||||
private void handleOk() {
|
||||
ChartActivity chart = MyTracks.getInstance().getChartActivity();
|
||||
chart.setMode(distance.isChecked() ? Mode.BY_DISTANCE : Mode.BY_TIME);
|
||||
|
||||
// TODO: check that something is visible.
|
||||
for (int i = 0; i < ChartView.NUM_SERIES; i++) {
|
||||
chart.setSeriesEnabled(i, series[i].isChecked());
|
||||
}
|
||||
|
||||
dismiss();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
/*
|
||||
* 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 android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Path;
|
||||
|
||||
import java.text.DecimalFormat;
|
||||
|
||||
/**
|
||||
* This class encapsulates meta data about one series of values for a chart.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public class ChartValueSeries {
|
||||
|
||||
private final ExtremityMonitor monitor = new ExtremityMonitor();
|
||||
private final DecimalFormat format;
|
||||
private final Path path = new Path();
|
||||
private final Paint fillPaint;
|
||||
private final Paint strokePaint;
|
||||
private final int rounding;
|
||||
|
||||
private String title;
|
||||
private double min = 0;
|
||||
private double max = 1;
|
||||
private int effectiveMax = 0;
|
||||
private int absoluteMax = Integer.MIN_VALUE;
|
||||
private int effectiveMin = 0;
|
||||
private double spread = 0;
|
||||
private boolean enabled = true;
|
||||
|
||||
/**
|
||||
* Constructs a new chart value series.
|
||||
*
|
||||
* @param context The context for the chart
|
||||
* @param formatString The format of the decimal format for this series
|
||||
* @param fill The paint for filling the chart
|
||||
* @param stroke The paint for stroking the outside the chart, optional
|
||||
* @param rounding The factor to round the values by
|
||||
*/
|
||||
public ChartValueSeries(Context context,
|
||||
String formatString,
|
||||
Paint fill,
|
||||
Paint stroke,
|
||||
int rounding,
|
||||
int titleId) {
|
||||
this.format = new DecimalFormat(formatString);
|
||||
this.fillPaint = fill;
|
||||
this.strokePaint = stroke;
|
||||
this.rounding = rounding;
|
||||
this.title = context.getString(titleId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the path of the chart
|
||||
*/
|
||||
public void drawPath(Canvas c) {
|
||||
c.drawPath(path, fillPaint);
|
||||
if (strokePaint != null) {
|
||||
c.drawPath(path, strokePaint);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets this series
|
||||
*/
|
||||
public void reset() {
|
||||
monitor.reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates this series with a new value
|
||||
*/
|
||||
public void update(double d) {
|
||||
monitor.update(d);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The interval between markers
|
||||
*/
|
||||
public int getInterval() {
|
||||
// Try to find 5 even looking intervals.
|
||||
int interval = (int) (spread / 5);
|
||||
int minInterval = rounding / 4;
|
||||
if (interval < minInterval) {
|
||||
// We won't be able to find 5 even intervals.
|
||||
return minInterval;
|
||||
} else if (interval < rounding) {
|
||||
// The desired interval is less than the rounding value.
|
||||
// We will have less than 5 intervals.
|
||||
return rounding;
|
||||
} else {
|
||||
// Round the interval.
|
||||
return (interval / rounding) * rounding;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines what the min and max of the chart will be.
|
||||
* This will round down and up the min and max respectively.
|
||||
*/
|
||||
public void updateDimension() {
|
||||
if (monitor.getMax() == Double.NEGATIVE_INFINITY) {
|
||||
min = 0;
|
||||
max = 1;
|
||||
} else {
|
||||
min = monitor.getMin();
|
||||
max = monitor.getMax();
|
||||
}
|
||||
// Round it up.
|
||||
effectiveMax = ((int) (max / rounding)) * rounding + rounding;
|
||||
// Round it down.
|
||||
effectiveMin = ((int) (min / rounding)) * rounding;
|
||||
if (min < 0) {
|
||||
effectiveMin -= rounding;
|
||||
}
|
||||
spread = getMax() - effectiveMin;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The length of the longest string from the series
|
||||
*/
|
||||
public int getMaxLabelLength() {
|
||||
String minS = format.format(effectiveMin);
|
||||
String maxS = format.format(getMax());
|
||||
return Math.max(minS.length(), maxS.length());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The rounded down minimum value
|
||||
*/
|
||||
public int getMin() {
|
||||
return effectiveMin;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The minimum of the rounded up max value and the effective max.
|
||||
*/
|
||||
public int getMax() {
|
||||
// Return the effective max if no absolute is set
|
||||
// or the effective max is less than the absolute.
|
||||
if ((absoluteMax == Integer.MIN_VALUE) || (effectiveMax < absoluteMax)) {
|
||||
return effectiveMax;
|
||||
} else {
|
||||
return absoluteMax;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The difference between the min and max values in the series
|
||||
*/
|
||||
public double getSpread() {
|
||||
return spread;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The format for the decimal format for this series
|
||||
*/
|
||||
DecimalFormat getFormat() {
|
||||
return format;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The path for this series
|
||||
*/
|
||||
Path getPath() {
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The paint for this series
|
||||
*/
|
||||
Paint getPaint() {
|
||||
return (strokePaint == null)
|
||||
? fillPaint
|
||||
: strokePaint;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The title of the series
|
||||
*/
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return is this series enabled
|
||||
*/
|
||||
public boolean isEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the series enabled flag.
|
||||
*/
|
||||
public void setEnabled(boolean enabled) {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
public int getAbsoluteMax() {
|
||||
return absoluteMax;
|
||||
}
|
||||
|
||||
public void setAbsoluteMax(int absoluteMax) {
|
||||
this.absoluteMax = absoluteMax;
|
||||
}
|
||||
|
||||
public boolean hasData() {
|
||||
return monitor.hasData();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,834 @@
|
||||
/*
|
||||
* 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.Waypoint;
|
||||
import com.google.android.apps.mytracks.stats.ExtremityMonitor;
|
||||
import com.google.android.apps.mytracks.util.StringUtils;
|
||||
import com.google.android.apps.mytracks.util.UnitConversions;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Paint.Style;
|
||||
import android.graphics.Path;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.VelocityTracker;
|
||||
import android.view.View;
|
||||
import android.view.ViewConfiguration;
|
||||
import android.widget.Scroller;
|
||||
|
||||
import java.text.DecimalFormat;
|
||||
import java.text.NumberFormat;
|
||||
import java.util.ArrayList;
|
||||
|
||||
/**
|
||||
* Visualization of the chart.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
* @author Leif Hendrik Wilden
|
||||
*/
|
||||
public class ChartView extends View {
|
||||
|
||||
/*
|
||||
* Scrolling logic:
|
||||
*/
|
||||
private final Scroller scroller;
|
||||
private VelocityTracker velocityTracker = null;
|
||||
/** Position of the last motion event */
|
||||
private float lastMotionX;
|
||||
|
||||
/*
|
||||
* Zoom logic:
|
||||
*/
|
||||
private int zoomLevel = 1;
|
||||
private final int minZoomLevel = 1;
|
||||
private int maxZoomLevel = 5;
|
||||
|
||||
/*
|
||||
* Borders, margins, dimensions (in pixels):
|
||||
*/
|
||||
private int leftBorder = -1;
|
||||
private static final int TOP_BORDER = 15;
|
||||
private static final int BOTTOM_BORDER = 135;
|
||||
private static final int RIGHT_BORDER = 17;
|
||||
/** Space to leave for drawing the unit labels */
|
||||
private static final int UNIT_BORDER = 15;
|
||||
private static final int FONT_HEIGHT = 10;
|
||||
private int w = 0;
|
||||
private int h = 0;
|
||||
private int effectiveWidth = 0;
|
||||
private int effectiveHeight = 0;
|
||||
|
||||
/*
|
||||
* Ranges (in data units):
|
||||
*/
|
||||
private double maxX = 1;
|
||||
|
||||
/**
|
||||
* The various series.
|
||||
*/
|
||||
public static final int ELEVATION_SERIES = 0;
|
||||
public static final int SPEED_SERIES = 1;
|
||||
public static final int POWER_SERIES = 2;
|
||||
public static final int CADENCE_SERIES = 3;
|
||||
public static final int HEART_RATE_SERIES = 4;
|
||||
public static final int NUM_SERIES = 5;
|
||||
private ChartValueSeries[] series;
|
||||
|
||||
private final ExtremityMonitor xMonitor = new ExtremityMonitor();
|
||||
private final NumberFormat xFormat = new DecimalFormat("###,###");
|
||||
private final NumberFormat xShortFormat = new DecimalFormat("#.0");
|
||||
|
||||
/*
|
||||
* Paints etc. used when drawing the histogram:
|
||||
*/
|
||||
private final Paint borderPaint = new Paint();
|
||||
private final Paint labelPaint = new Paint();
|
||||
private final Paint gridPaint = new Paint();
|
||||
private final Paint clearPaint = new Paint();
|
||||
private final Drawable pointer;
|
||||
private final Drawable statsMarker;
|
||||
private final Drawable waypointMarker;
|
||||
private final int markerWidth, markerHeight;
|
||||
|
||||
/**
|
||||
* The chart data stored as an array of double arrays. Each one dimensional
|
||||
* array is composed of [x, y].
|
||||
*/
|
||||
private final ArrayList<double[]> data = new ArrayList<double[]>();
|
||||
|
||||
/**
|
||||
* List of way points to be displayed.
|
||||
*/
|
||||
private final ArrayList<Waypoint> waypoints = new ArrayList<Waypoint>();
|
||||
|
||||
private boolean metricUnits = true;
|
||||
private boolean showPointer = false;
|
||||
|
||||
private boolean yLabelMask[] = null;
|
||||
|
||||
/** Display chart versus distance or time */
|
||||
public enum Mode {
|
||||
BY_DISTANCE, BY_TIME
|
||||
}
|
||||
|
||||
private Mode mode = Mode.BY_DISTANCE;
|
||||
|
||||
public ChartView(Context context) {
|
||||
super(context);
|
||||
|
||||
setUpChartValueSeries(context);
|
||||
|
||||
labelPaint.setStyle(Style.STROKE);
|
||||
labelPaint.setColor(context.getResources().getColor(R.color.black));
|
||||
labelPaint.setAntiAlias(true);
|
||||
|
||||
borderPaint.setStyle(Style.STROKE);
|
||||
borderPaint.setColor(context.getResources().getColor(R.color.black));
|
||||
borderPaint.setAntiAlias(true);
|
||||
|
||||
gridPaint.setStyle(Style.STROKE);
|
||||
gridPaint.setColor(context.getResources().getColor(R.color.gray));
|
||||
gridPaint.setAntiAlias(false);
|
||||
|
||||
clearPaint.setStyle(Style.FILL);
|
||||
clearPaint.setColor(context.getResources().getColor(R.color.white));
|
||||
clearPaint.setAntiAlias(false);
|
||||
|
||||
pointer = context.getResources().getDrawable(R.drawable.arrow_180);
|
||||
pointer.setBounds(0, 0,
|
||||
pointer.getIntrinsicWidth(), pointer.getIntrinsicHeight());
|
||||
|
||||
statsMarker = getResources().getDrawable(R.drawable.ylw_pushpin);
|
||||
markerWidth = statsMarker.getIntrinsicWidth();
|
||||
markerHeight = statsMarker.getIntrinsicHeight();
|
||||
statsMarker.setBounds(0, 0, markerWidth, markerHeight);
|
||||
|
||||
waypointMarker = getResources().getDrawable(R.drawable.blue_pushpin);
|
||||
waypointMarker.setBounds(0, 0, markerWidth, markerHeight);
|
||||
|
||||
scroller = new Scroller(context);
|
||||
setFocusable(true);
|
||||
setClickable(true);
|
||||
updateDimensions();
|
||||
}
|
||||
|
||||
public void setUpChartValueSeries(Context context) {
|
||||
series = new ChartValueSeries[NUM_SERIES];
|
||||
|
||||
// Create the paints
|
||||
Paint fillPaint = new Paint();
|
||||
fillPaint.setStyle(Style.FILL);
|
||||
fillPaint.setColor(context.getResources().getColor(R.color.green));
|
||||
fillPaint.setAntiAlias(true);
|
||||
|
||||
// Create the value series
|
||||
series[ELEVATION_SERIES] =
|
||||
new ChartValueSeries(context,
|
||||
"###,###",
|
||||
fillPaint,
|
||||
null,
|
||||
100,
|
||||
R.string.elevation);
|
||||
|
||||
series[SPEED_SERIES] = getSeries(context, R.color.blue_transparent,
|
||||
R.color.blue, "###,###.0", R.string.speed);
|
||||
series[POWER_SERIES] = getSeries(context, R.color.red_transparent,
|
||||
R.color.red, "###,###", R.string.power);
|
||||
series[POWER_SERIES].setAbsoluteMax(1500);
|
||||
series[CADENCE_SERIES] = getSeries(context, R.color.purple_transparent,
|
||||
R.color.purple, "###,###", R.string.cadence);
|
||||
series[HEART_RATE_SERIES] = getSeries(context, R.color.yellow_transparent,
|
||||
R.color.yellow, "###,###", R.string.heart_rate);
|
||||
}
|
||||
|
||||
private ChartValueSeries getSeries(Context context,
|
||||
int fillColor, int strokeColor, String format, int seriesName) {
|
||||
Paint fillPaint = new Paint();
|
||||
fillPaint.setStyle(Style.FILL);
|
||||
fillPaint.setColor(context.getResources().getColor(fillColor));
|
||||
fillPaint.setAntiAlias(true);
|
||||
|
||||
Paint strokePaint = new Paint();
|
||||
strokePaint.setStyle(Style.STROKE);
|
||||
strokePaint.setColor(context.getResources().getColor(strokeColor));
|
||||
strokePaint.setAntiAlias(true);
|
||||
return new ChartValueSeries(context, format, fillPaint, strokePaint, 5,
|
||||
seriesName);
|
||||
}
|
||||
|
||||
public void clearWaypoints() {
|
||||
waypoints.clear();
|
||||
}
|
||||
|
||||
public void addWaypoint(Waypoint waypoint) {
|
||||
waypoints.add(waypoint);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the pointer icon is shown on the last data point.
|
||||
*/
|
||||
public void setShowPointer(boolean showPointer) {
|
||||
this.showPointer = showPointer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether metric units are used or not.
|
||||
*/
|
||||
public void setMetricUnits(boolean metricUnits) {
|
||||
this.metricUnits = metricUnits;
|
||||
}
|
||||
|
||||
public void setReportSpeed(boolean reportSpeed, Context c) {
|
||||
series[SPEED_SERIES].setTitle(c.getString(reportSpeed
|
||||
? R.string.speed
|
||||
: R.string.pace_label));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the data that is displayed by the chart.
|
||||
*
|
||||
* @return an array list with data points
|
||||
*/
|
||||
public ArrayList<double[]> getData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the data that is to be displayed by the chart.
|
||||
*
|
||||
* @param theData an array list of data points
|
||||
*/
|
||||
public synchronized void setDataPoints(ArrayList<double[]> theData) {
|
||||
scrollTo(0, 0);
|
||||
zoomLevel = 1;
|
||||
data.clear();
|
||||
xMonitor.reset();
|
||||
for (ChartValueSeries cvs : series) {
|
||||
cvs.reset();
|
||||
}
|
||||
addDataPoints(theData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new data point to the chart.
|
||||
*
|
||||
* @param theData a data point
|
||||
*/
|
||||
public synchronized void addDataPoint(double[] theData) {
|
||||
data.add(theData);
|
||||
addDataPointInternal(theData);
|
||||
updateDimensions();
|
||||
setupPath();
|
||||
}
|
||||
|
||||
private void addDataPointInternal(double[] theData) {
|
||||
xMonitor.update(theData[0]);
|
||||
int min = Math.min(series.length, theData.length - 1);
|
||||
for (int i = 0; i < min; i++) {
|
||||
if (!Double.isNaN(theData[i + 1])) {
|
||||
series[i].update(theData[i + 1]);
|
||||
}
|
||||
}
|
||||
// Fill in the extra's if needed.
|
||||
for (int i = theData.length; i < series.length; i++) {
|
||||
if (series[i].hasData()) {
|
||||
series[i].update(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds multiple data points to the chart.
|
||||
*
|
||||
* @param theData an array list of data points to be added
|
||||
*/
|
||||
public synchronized void addDataPoints(ArrayList<double[]> theData) {
|
||||
data.addAll(theData);
|
||||
for (int i = 0; i < theData.size(); i++) {
|
||||
double d[] = theData.get(i);
|
||||
addDataPointInternal(d);
|
||||
}
|
||||
updateDimensions();
|
||||
setupPath();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all data.
|
||||
* Call this only from the UI thread!
|
||||
*/
|
||||
public synchronized void reset() {
|
||||
data.clear();
|
||||
zoomLevel = 1;
|
||||
scrollTo(0, 0);
|
||||
updateDimensions();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if the chart can be zoomed into.
|
||||
*/
|
||||
public boolean canZoomIn() {
|
||||
return zoomLevel < maxZoomLevel;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if the chart can be zoomed out
|
||||
*/
|
||||
public boolean canZoomOut() {
|
||||
return zoomLevel > minZoomLevel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Zooms in one level (factor 2).
|
||||
*/
|
||||
public void zoomIn() {
|
||||
if (canZoomIn()) {
|
||||
zoomLevel++;
|
||||
setupPath();
|
||||
invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Zooms out one level (factor 2).
|
||||
*/
|
||||
public void zoomOut() {
|
||||
if (canZoomOut()) {
|
||||
zoomLevel--;
|
||||
scroller.abortAnimation();
|
||||
int scrollX = getScrollX();
|
||||
if (scrollX > effectiveWidth * (zoomLevel - 1)) {
|
||||
scrollX = effectiveWidth * (zoomLevel - 1);
|
||||
scrollTo(scrollX, 0);
|
||||
}
|
||||
setupPath();
|
||||
invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the current zoom level (1 equals to showing all data points)
|
||||
*/
|
||||
public int getZoomLevel() {
|
||||
return zoomLevel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiates flinging.
|
||||
*
|
||||
* @param velocityX start velocity (pixels per second)
|
||||
*/
|
||||
public void fling(int velocityX) {
|
||||
scroller.fling(getScrollX(), 0, velocityX, 0, 0,
|
||||
effectiveWidth * (zoomLevel - 1), 0, 0);
|
||||
invalidate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Scrolls the view horizontally by the given amount.
|
||||
*
|
||||
* @param deltaX number of pixels to scroll
|
||||
*/
|
||||
public void scrollBy(int deltaX) {
|
||||
int scrollX = getScrollX() + deltaX;
|
||||
if (scrollX < 0) {
|
||||
scrollX = 0;
|
||||
}
|
||||
int available = effectiveWidth * (zoomLevel - 1);
|
||||
if (scrollX > available) {
|
||||
scrollX = available;
|
||||
}
|
||||
scrollTo(scrollX, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the scroll position of the chart. This will trigger a redraw.
|
||||
*/
|
||||
@Override
|
||||
public void scrollTo(int x, int y) {
|
||||
super.scrollTo(x, y);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the current display mode (by distance, by time)
|
||||
*/
|
||||
public Mode getMode() {
|
||||
return mode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the display mode (by distance, by time).
|
||||
*/
|
||||
public void setMode(Mode mode) {
|
||||
this.mode = mode;
|
||||
}
|
||||
|
||||
private int getWaypointX(Waypoint waypoint) {
|
||||
return (mode == Mode.BY_DISTANCE)
|
||||
? getX(metricUnits
|
||||
? waypoint.getLength() / 1000.0
|
||||
: waypoint.getLength() * UnitConversions.KM_TO_MI / 1000.0)
|
||||
: getX(waypoint.getDuration());
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by the parent to indicate that the mScrollX/Y values need to be
|
||||
* updated. Triggers a redraw during flinging.
|
||||
*/
|
||||
@Override
|
||||
public void computeScroll() {
|
||||
if (scroller.computeScrollOffset()) {
|
||||
int oldX = getScrollX();
|
||||
int x = scroller.getCurrX();
|
||||
scrollTo(x, 0);
|
||||
if (oldX != x) {
|
||||
onScrollChanged(x, 0, oldX, 0);
|
||||
postInvalidate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTouchEvent(MotionEvent event) {
|
||||
if (velocityTracker == null) {
|
||||
velocityTracker = VelocityTracker.obtain();
|
||||
}
|
||||
velocityTracker.addMovement(event);
|
||||
final int action = event.getAction();
|
||||
final float x = event.getX();
|
||||
switch (action) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
/*
|
||||
* If being flinged and user touches, stop the fling. isFinished will be
|
||||
* false if being flinged.
|
||||
*/
|
||||
if (!scroller.isFinished()) {
|
||||
scroller.abortAnimation();
|
||||
}
|
||||
// Remember where the motion event started
|
||||
lastMotionX = x;
|
||||
break;
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
// Scroll to follow the motion event
|
||||
final int deltaX = (int) (lastMotionX - x);
|
||||
lastMotionX = x;
|
||||
if (deltaX < 0) {
|
||||
if (getScrollX() > 0) {
|
||||
scrollBy(deltaX);
|
||||
}
|
||||
} else if (deltaX > 0) {
|
||||
final int availableToScroll =
|
||||
effectiveWidth * (zoomLevel - 1) - getScrollX();
|
||||
if (availableToScroll > 0) {
|
||||
scrollBy(Math.min(availableToScroll, deltaX));
|
||||
}
|
||||
}
|
||||
break;
|
||||
case MotionEvent.ACTION_UP:
|
||||
// Check if top area with waypoint markers was touched and find the
|
||||
// touched marker if any:
|
||||
if (event.getY() < 100) {
|
||||
int dmin = Integer.MAX_VALUE;
|
||||
Waypoint nearestWaypoint = null;
|
||||
for (int i = 0; i < waypoints.size(); i++) {
|
||||
final Waypoint waypoint = waypoints.get(i);
|
||||
final int d = Math.abs(getWaypointX(waypoint) - (int) event.getX()
|
||||
- getScrollX());
|
||||
if (d < dmin) {
|
||||
dmin = d;
|
||||
nearestWaypoint = waypoint;
|
||||
}
|
||||
}
|
||||
if (nearestWaypoint != null && dmin < 100) {
|
||||
Intent intent =
|
||||
new Intent(getContext(), MyTracksWaypointDetails.class);
|
||||
intent.putExtra("waypointid", nearestWaypoint.getId());
|
||||
getContext().startActivity(intent);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
VelocityTracker myVelocityTracker = velocityTracker;
|
||||
myVelocityTracker.computeCurrentVelocity(1000);
|
||||
int initialVelocity = (int) myVelocityTracker.getXVelocity();
|
||||
if (Math.abs(initialVelocity) >
|
||||
ViewConfiguration.getMinimumFlingVelocity()) {
|
||||
fling(-initialVelocity);
|
||||
}
|
||||
if (velocityTracker != null) {
|
||||
velocityTracker.recycle();
|
||||
velocityTracker = null;
|
||||
}
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected synchronized void onDraw(Canvas c) {
|
||||
if (w != c.getWidth() || h != c.getHeight()) {
|
||||
// Dimensions have changed (for example due to orientation change):
|
||||
w = c.getWidth();
|
||||
h = c.getHeight();
|
||||
effectiveWidth = Math.max(0, w - leftBorder - RIGHT_BORDER);
|
||||
effectiveHeight = Math.max(0, h - TOP_BORDER - BOTTOM_BORDER);
|
||||
setupPath();
|
||||
}
|
||||
c.save();
|
||||
c.drawColor(Color.WHITE);
|
||||
if (data.size() < 1) {
|
||||
drawXAxis(c);
|
||||
drawYAxis(c);
|
||||
c.restore();
|
||||
return;
|
||||
}
|
||||
|
||||
// Draw the data series
|
||||
for (ChartValueSeries cvs : series) {
|
||||
if (cvs.isEnabled() && cvs.hasData()) {
|
||||
cvs.drawPath(c);
|
||||
}
|
||||
}
|
||||
drawGrid(c);
|
||||
|
||||
// Draw the waypoints:
|
||||
for (int i = 1; i < waypoints.size(); i++) {
|
||||
final Waypoint waypoint = waypoints.get(i);
|
||||
if (waypoint.getLocation() == null) {
|
||||
continue;
|
||||
}
|
||||
c.save();
|
||||
|
||||
final float x = getWaypointX(waypoint);
|
||||
c.drawLine(x, h - BOTTOM_BORDER, x, TOP_BORDER, gridPaint);
|
||||
c.translate(x - (markerWidth / 2), markerHeight);
|
||||
if (waypoints.get(i).getType() == Waypoint.TYPE_STATISTICS) {
|
||||
statsMarker.draw(c);
|
||||
} else {
|
||||
waypointMarker.draw(c);
|
||||
}
|
||||
c.restore();
|
||||
}
|
||||
|
||||
// Draw the axis and labels:
|
||||
drawXLabels(c);
|
||||
drawXAxis(c);
|
||||
drawSeriesTitles(c);
|
||||
|
||||
yLabelMask = new boolean[effectiveHeight / FONT_HEIGHT + 1];
|
||||
c.translate(getScrollX(), 0);
|
||||
drawYAxis(c);
|
||||
for (ChartValueSeries cvs : series) {
|
||||
if (cvs.isEnabled() && cvs.hasData()) {
|
||||
drawYLabels(cvs, c);
|
||||
}
|
||||
}
|
||||
c.restore();
|
||||
if (showPointer && data.size() > 0) {
|
||||
c.translate(getX(maxX) - pointer.getIntrinsicWidth() / 2,
|
||||
(getY(series[0], data.get(data.size() - 1)[1])
|
||||
- pointer.getIntrinsicHeight() / 2 - 12));
|
||||
pointer.draw(c);
|
||||
}
|
||||
}
|
||||
|
||||
private void drawSeriesTitles(Canvas c) {
|
||||
int sections = 1;
|
||||
for (ChartValueSeries cvs : series) {
|
||||
if (cvs.isEnabled() && cvs.hasData()) {
|
||||
sections++;
|
||||
}
|
||||
}
|
||||
int j = 0;
|
||||
for (ChartValueSeries cvs : series) {
|
||||
if (cvs.isEnabled() && cvs.hasData()) {
|
||||
int y = (int) (w * zoomLevel * ((double) (++j) / sections));
|
||||
c.drawText(cvs.getTitle(), y, TOP_BORDER, cvs.getPaint());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the path that is used to draw the histogram in onDraw(). The path
|
||||
* needs to be updated any time after the data or histogram dimensions change.
|
||||
*/
|
||||
private synchronized void setupPath() {
|
||||
for (ChartValueSeries cvs : series) {
|
||||
cvs.getPath().reset();
|
||||
}
|
||||
if (data.size() < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
// All of the data points to the respective series.
|
||||
for (int i = 0; i < data.size(); i++) {
|
||||
double[] d = data.get(i);
|
||||
int min = Math.min(series.length, d.length - 1);
|
||||
|
||||
for (int j = 0; j < min; j++) {
|
||||
ChartValueSeries cvs = series[j];
|
||||
Path path = cvs.getPath();
|
||||
path.lineTo(getX(d[0]), getY(cvs, d[j + 1]));
|
||||
}
|
||||
}
|
||||
|
||||
// Close the path.
|
||||
int yCorner = TOP_BORDER + effectiveHeight;
|
||||
int xCorner = getX(data.get(0)[0]);
|
||||
int min = series.length;
|
||||
for (int j = 0; j < min; j++) {
|
||||
ChartValueSeries cvs = series[j];
|
||||
Path path = cvs.getPath();
|
||||
int first = getFirstPointPopulatedIndex(j + 1);
|
||||
if (first != -1) {
|
||||
// Bottom right corner
|
||||
path.lineTo(getX(data.get(data.size() - 1)[0]), yCorner);
|
||||
// Bottom left corner
|
||||
path.lineTo(xCorner, yCorner);
|
||||
// Top right corner
|
||||
path.lineTo(xCorner, getY(cvs, data.get(first)[j + 1]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the index of the first point which has a series populated.
|
||||
* @param seriesIndex The index of the value series to search for.
|
||||
* @return The index in the first data for the point in the series that has series
|
||||
* index value populated or -1 if none is found.
|
||||
*/
|
||||
private int getFirstPointPopulatedIndex(int seriesIndex) {
|
||||
for (int i = 0; i < data.size(); i++) {
|
||||
if (data.get(i).length > seriesIndex) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the histogram dimensions.
|
||||
*/
|
||||
private void updateDimensions() {
|
||||
maxX = xMonitor.getMax();
|
||||
if (data.size() <= 1) {
|
||||
maxX = 1;
|
||||
}
|
||||
for (ChartValueSeries cvs : series) {
|
||||
cvs.updateDimension();
|
||||
}
|
||||
int maxLength = 0;
|
||||
for (ChartValueSeries cvs : series) {
|
||||
maxLength = Math.max(maxLength,
|
||||
cvs.getMaxLabelLength());
|
||||
}
|
||||
leftBorder = 4 + 7 * maxLength;
|
||||
effectiveWidth = w - leftBorder - RIGHT_BORDER;
|
||||
effectiveHeight = h - TOP_BORDER - BOTTOM_BORDER;
|
||||
}
|
||||
|
||||
private int getX(double distance) {
|
||||
return leftBorder + (int) ((distance * effectiveWidth / maxX) * zoomLevel);
|
||||
}
|
||||
|
||||
private int getY(ChartValueSeries cvs, double y) {
|
||||
return TOP_BORDER + effectiveHeight
|
||||
- (int) ((y - cvs.getMin()) * effectiveHeight / cvs.getSpread());
|
||||
}
|
||||
|
||||
private void drawXLabels(Canvas c) {
|
||||
int numLabels = 0;
|
||||
double interval = (int) (maxX / zoomLevel / 4);
|
||||
boolean shortFormat = false;
|
||||
if (interval < 1) {
|
||||
interval = .5;
|
||||
shortFormat = true;
|
||||
} else {
|
||||
if (interval < 5) {
|
||||
interval = 2;
|
||||
} else if (interval < 10) {
|
||||
interval = 5;
|
||||
} else {
|
||||
interval = (interval / 10) * 10;
|
||||
}
|
||||
}
|
||||
drawXLabel(c, 0, shortFormat);
|
||||
numLabels++;
|
||||
for (int i = 1; i * interval < maxX; i++) {
|
||||
drawXLabel(c, i * interval, shortFormat);
|
||||
numLabels++;
|
||||
}
|
||||
if (numLabels < 2) {
|
||||
drawXLabel(c, (int) maxX, shortFormat);
|
||||
}
|
||||
}
|
||||
|
||||
private void drawYLabels(ChartValueSeries cvs, Canvas c) {
|
||||
int numLabels = 0;
|
||||
drawYLabel(cvs, c, 0);
|
||||
final int y0 = getY(cvs, 0);
|
||||
numLabels++;
|
||||
int interval = cvs.getInterval();
|
||||
for (int i = 0; i * interval < cvs.getSpread(); i++) {
|
||||
final int y = i * interval + cvs.getMin();
|
||||
final int yi = getY(cvs, y);
|
||||
if (Math.abs(y0 - yi) > 15) {
|
||||
drawYLabel(cvs, c, y);
|
||||
numLabels++;
|
||||
}
|
||||
}
|
||||
if (numLabels < 3) {
|
||||
drawYLabel(cvs, c, cvs.getMax());
|
||||
}
|
||||
}
|
||||
|
||||
private void drawXLabel(Canvas c, double x, boolean shortFormat) {
|
||||
String s =
|
||||
(mode == Mode.BY_DISTANCE)
|
||||
? (shortFormat ? xShortFormat.format(x) : xFormat.format(x))
|
||||
: StringUtils.formatTime((long) x);
|
||||
c.drawText(s,
|
||||
getX(x),
|
||||
effectiveHeight + UNIT_BORDER + TOP_BORDER,
|
||||
labelPaint);
|
||||
}
|
||||
|
||||
private void drawYLabel(ChartValueSeries cvs, Canvas c, int y) {
|
||||
int desiredY =
|
||||
(int) ((y - cvs.getMin()) * effectiveHeight / cvs.getSpread());
|
||||
|
||||
// Make sure we don't write one label on top of another.
|
||||
int slot = desiredY / FONT_HEIGHT;
|
||||
if (slot < 0) {
|
||||
return;
|
||||
}
|
||||
if (yLabelMask[slot]) {
|
||||
desiredY = ++slot * FONT_HEIGHT;
|
||||
}
|
||||
desiredY = TOP_BORDER + effectiveHeight + 5 - desiredY;
|
||||
if (slot < yLabelMask.length) {
|
||||
yLabelMask[slot] = true;
|
||||
}
|
||||
c.drawText(cvs.getFormat().format(y),
|
||||
2, desiredY,
|
||||
cvs.getPaint());
|
||||
}
|
||||
|
||||
private void drawXAxis(Canvas canvas) {
|
||||
float rightEdge = getX(maxX);
|
||||
canvas.drawLine(leftBorder, effectiveHeight + TOP_BORDER, rightEdge,
|
||||
effectiveHeight + TOP_BORDER, borderPaint);
|
||||
Context c = getContext();
|
||||
String s =
|
||||
(mode == Mode.BY_DISTANCE)
|
||||
? (metricUnits ? c.getString(R.string.kilometer)
|
||||
: c.getString(R.string.mile)) : (c.getString(R.string.min));
|
||||
canvas.drawText(s,
|
||||
rightEdge,
|
||||
effectiveHeight + .2f * UNIT_BORDER + TOP_BORDER,
|
||||
labelPaint);
|
||||
}
|
||||
|
||||
private void drawYAxis(Canvas canvas) {
|
||||
canvas.drawLine(leftBorder, UNIT_BORDER + TOP_BORDER,
|
||||
leftBorder, effectiveHeight + TOP_BORDER,
|
||||
borderPaint);
|
||||
canvas.drawRect(0, 0,
|
||||
leftBorder - 1, effectiveHeight + TOP_BORDER + 1,
|
||||
clearPaint);
|
||||
Context c = getContext();
|
||||
// TODO: This should really show units for all series.
|
||||
String s = metricUnits
|
||||
? c.getString(R.string.meter)
|
||||
: c.getString(R.string.feet);
|
||||
canvas.drawText(s, leftBorder - UNIT_BORDER * .2f,
|
||||
UNIT_BORDER * .8f + TOP_BORDER, labelPaint);
|
||||
}
|
||||
|
||||
private synchronized void drawGrid(Canvas c) {
|
||||
if (data.size() < 1) {
|
||||
return;
|
||||
}
|
||||
for (ChartValueSeries cvs : series) {
|
||||
if (cvs.isEnabled()) {
|
||||
int interval = cvs.getInterval();
|
||||
for (int i = 1; i * interval < cvs.getSpread(); i++) {
|
||||
int y = getY(cvs, i * interval + cvs.getMin());
|
||||
c.drawLine(getX(data.get(0)[0]), y,
|
||||
getX(data.get(data.size() - 1)[0]), y,
|
||||
cvs.getPaint());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets one of the chart value series.
|
||||
* The index should be one of the following values:
|
||||
* ELEVATION_SERIES or SPEED_SERIES
|
||||
*
|
||||
* @param index of the value series
|
||||
* @return The chart series at the index
|
||||
*/
|
||||
public ChartValueSeries getChartValueSeries(int index) {
|
||||
return series[index];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* Copyright 2010 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
|
||||
package com.google.android.apps.mytracks;
|
||||
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* Check to see if the units are the default for the locale.
|
||||
* This comes down to warning Americans that the default is metric.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
class CheckUnits {
|
||||
private static final String PREFERENCE_UNITS_CHECKED = "checkunits.checked";
|
||||
private static final String PREFERENCES_CHECK_UNITS = "checkunits";
|
||||
|
||||
static void check(final Context context) {
|
||||
final SharedPreferences preferences =
|
||||
context.getSharedPreferences(PREFERENCES_CHECK_UNITS, Activity.MODE_PRIVATE);
|
||||
|
||||
// Has the user already warned about the default units?
|
||||
if (preferences.getBoolean(PREFERENCE_UNITS_CHECKED, false)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Is the user in the US?
|
||||
Locale current = Locale.getDefault();
|
||||
Locale enUs = new Locale(Locale.ENGLISH.getLanguage(),
|
||||
Locale.US.getCountry());
|
||||
if (!current.equals(enUs)) {
|
||||
return;
|
||||
}
|
||||
|
||||
final AlertDialog.Builder builder = new AlertDialog.Builder(context);
|
||||
builder.setTitle(R.string.check_units_title);
|
||||
builder.setCancelable(true);
|
||||
builder.setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
accept(context, preferences);
|
||||
}
|
||||
});
|
||||
builder.setNegativeButton(R.string.no, new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
recordCheckPerformed(preferences);
|
||||
}
|
||||
});
|
||||
builder.setOnCancelListener(new DialogInterface.OnCancelListener() {
|
||||
public void onCancel(DialogInterface dialog) {
|
||||
recordCheckPerformed(preferences);
|
||||
}
|
||||
});
|
||||
builder.setMessage(R.string.check_units_message);
|
||||
builder.show();
|
||||
}
|
||||
|
||||
private static void accept(Context context, SharedPreferences preferences) {
|
||||
recordCheckPerformed(preferences);
|
||||
Intent startIntent = new Intent(context, MyTracksSettings.class);
|
||||
context.startActivity(startIntent);
|
||||
}
|
||||
|
||||
private static void recordCheckPerformed(SharedPreferences preferences) {
|
||||
preferences.edit().putBoolean(PREFERENCE_UNITS_CHECKED, true).commit();
|
||||
}
|
||||
|
||||
private CheckUnits() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
/*
|
||||
* Copyright 2010 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package com.google.android.apps.mytracks;
|
||||
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.app.Dialog;
|
||||
import android.app.ProgressDialog;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.view.WindowManager.BadTokenException;
|
||||
|
||||
/**
|
||||
* A class to handle all dialog related events for My Tracks.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public class DialogManager {
|
||||
|
||||
public static final int DIALOG_CHART_SETTINGS = 1;
|
||||
public static final int DIALOG_IMPORT_PROGRESS = 2;
|
||||
public static final int DIALOG_PROGRESS = 3;
|
||||
public static final int DIALOG_SEND_TO_GOOGLE = 4;
|
||||
public static final int DIALOG_SEND_TO_GOOGLE_RESULT = 5;
|
||||
public static final int DIALOG_WRITE_PROGRESS = 6;
|
||||
|
||||
private ProgressDialog progressDialog;
|
||||
private ProgressDialog importProgressDialog;
|
||||
private ProgressDialog writeProgressDialog;
|
||||
private SendToGoogleDialog sendToGoogleDialog;
|
||||
private AlertDialog sendToGoogleResultDialog;
|
||||
private ChartSettingsDialog chartSettingsDialog;
|
||||
|
||||
private MyTracks activity;
|
||||
|
||||
public DialogManager(MyTracks activity) {
|
||||
this.activity = activity;
|
||||
}
|
||||
|
||||
protected Dialog onCreateDialog(int id, Bundle args) {
|
||||
switch (id) {
|
||||
case DIALOG_CHART_SETTINGS:
|
||||
chartSettingsDialog = new ChartSettingsDialog(activity);
|
||||
return chartSettingsDialog;
|
||||
case DIALOG_IMPORT_PROGRESS:
|
||||
importProgressDialog = new ProgressDialog(activity);
|
||||
importProgressDialog.setIcon(android.R.drawable.ic_dialog_info);
|
||||
importProgressDialog.setTitle(
|
||||
activity.getString(R.string.progress_title));
|
||||
importProgressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER);
|
||||
importProgressDialog.setMessage(
|
||||
activity.getString(R.string.import_progress_message));
|
||||
return importProgressDialog;
|
||||
case DIALOG_PROGRESS:
|
||||
progressDialog = new ProgressDialog(activity);
|
||||
progressDialog.setIcon(android.R.drawable.ic_dialog_info);
|
||||
progressDialog.setTitle(activity.getString(R.string.progress_title));
|
||||
progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
|
||||
progressDialog.setMessage("");
|
||||
progressDialog.setMax(100);
|
||||
progressDialog.setProgress(10);
|
||||
return progressDialog;
|
||||
case DIALOG_SEND_TO_GOOGLE:
|
||||
sendToGoogleDialog = new SendToGoogleDialog(activity);
|
||||
return sendToGoogleDialog;
|
||||
case DIALOG_SEND_TO_GOOGLE_RESULT:
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
|
||||
builder.setIcon(android.R.drawable.ic_dialog_info);
|
||||
builder.setTitle("Title");
|
||||
builder.setMessage("Message");
|
||||
builder.setPositiveButton(activity.getString(R.string.ok), null);
|
||||
sendToGoogleResultDialog = builder.create();
|
||||
return sendToGoogleResultDialog;
|
||||
case DIALOG_WRITE_PROGRESS:
|
||||
writeProgressDialog = new ProgressDialog(activity);
|
||||
writeProgressDialog.setIcon(android.R.drawable.ic_dialog_info);
|
||||
writeProgressDialog.setTitle(
|
||||
activity.getString(R.string.progress_title));
|
||||
writeProgressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER);
|
||||
writeProgressDialog.setMessage(
|
||||
activity.getString(R.string.write_progress_message));
|
||||
return writeProgressDialog;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected void onPrepareDialog(int id, Dialog dialog) {
|
||||
switch (id) {
|
||||
case DIALOG_SEND_TO_GOOGLE:
|
||||
activity.resetSendToGoogleStatus();
|
||||
break;
|
||||
case DIALOG_SEND_TO_GOOGLE_RESULT:
|
||||
boolean success = activity.getSendToGoogleSuccess();
|
||||
sendToGoogleResultDialog.setTitle(
|
||||
success ? R.string.success : R.string.error);
|
||||
sendToGoogleResultDialog.setIcon(success
|
||||
? android.R.drawable.ic_dialog_info
|
||||
: android.R.drawable.ic_dialog_alert);
|
||||
sendToGoogleResultDialog.setMessage(activity.getFusionTablesResultMessage());
|
||||
|
||||
boolean canShare = activity.getSendToFusionTablesTableId() != null;
|
||||
View share =
|
||||
sendToGoogleResultDialog.findViewById(android.R.id.button3);
|
||||
if (share != null) {
|
||||
share.setVisibility(canShare ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
break;
|
||||
case DIALOG_CHART_SETTINGS:
|
||||
Log.d(MyTracksConstants.TAG, "MyTracks.onPrepare chart dialog");
|
||||
chartSettingsDialog.setup(activity.getChartActivity());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public void setProgressMessage(final String message) {
|
||||
activity.runOnUiThread(new Runnable() {
|
||||
public void run() {
|
||||
synchronized (this) {
|
||||
if (progressDialog != null) {
|
||||
progressDialog.setMessage(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void setProgressValue(final int percent) {
|
||||
activity.runOnUiThread(new Runnable() {
|
||||
public void run() {
|
||||
synchronized (this) {
|
||||
if (progressDialog != null) {
|
||||
progressDialog.setProgress(percent);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the sendToGoogleDialog
|
||||
*/
|
||||
public SendToGoogleDialog getSendToGoogleDialog() {
|
||||
return sendToGoogleDialog;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a dialog with the given message.
|
||||
* Does it on the UI thread.
|
||||
*
|
||||
* @param success if true, displays an info icon/title, otherwise an error
|
||||
* icon/title
|
||||
* @param message resource string id
|
||||
*/
|
||||
public void showMessageDialog(final int message, final boolean success) {
|
||||
activity.runOnUiThread(new Runnable() {
|
||||
public void run() {
|
||||
AlertDialog dialog = null;
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
|
||||
builder.setMessage(activity.getString(message));
|
||||
builder.setNegativeButton(activity.getString(R.string.ok), null);
|
||||
builder.setIcon(success ? android.R.drawable.ic_dialog_info :
|
||||
android.R.drawable.ic_dialog_alert);
|
||||
builder.setTitle(success ? R.string.success : R.string.error);
|
||||
dialog = builder.create();
|
||||
dialog.show();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Just like showDialog, but will catch a BadTokenException that sometimes
|
||||
* (very rarely) gets thrown. This might happen if the user hits the "back"
|
||||
* button immediately after sending tracks to google.
|
||||
*
|
||||
* @param id the dialog id
|
||||
*/
|
||||
public void showDialogSafely(final int id) {
|
||||
activity.runOnUiThread(new Runnable() {
|
||||
public void run() {
|
||||
try {
|
||||
activity.showDialog(id);
|
||||
} catch (BadTokenException e) {
|
||||
Log.w(MyTracksConstants.TAG,
|
||||
"Could not display dialog with id " + id, e);
|
||||
} catch (IllegalStateException e) {
|
||||
Log.w(MyTracksConstants.TAG,
|
||||
"Could not display dialog with id " + id, e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Dismisses the progress dialog if it is showing. Executed on the UI thread.
|
||||
*/
|
||||
public void dismissDialogSafely(final int id) {
|
||||
activity.runOnUiThread(new Runnable() {
|
||||
public void run() {
|
||||
try {
|
||||
activity.dismissDialog(id);
|
||||
} catch (IllegalArgumentException e) {
|
||||
// This will be thrown if this dialog was not shown before.
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* 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.util.ResourceUtils;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
/**
|
||||
* Displays an EULA ("End User License Agreement") that the user has to accept
|
||||
* before using the application. Your application should call
|
||||
* {@link Eula#showEula(android.app.Activity)} in the onCreate() method of the
|
||||
* first activity. If the user accepts the EULA, it will never be shown again.
|
||||
* If the user refuses, {@link android.app.Activity#finish()} is invoked on your
|
||||
* activity.
|
||||
*/
|
||||
class Eula {
|
||||
private static final String PREFERENCE_EULA_ACCEPTED = "eula.accepted";
|
||||
private static final String PREFERENCES_EULA = "eula";
|
||||
|
||||
/**
|
||||
* Displays the EULA if necessary. This method should be called from the
|
||||
* onCreate() method of your main Activity.
|
||||
*
|
||||
* @param activity The Activity to finish if the user rejects the EULA
|
||||
*/
|
||||
static void showEula(final Activity activity) {
|
||||
final SharedPreferences preferences =
|
||||
activity.getSharedPreferences(PREFERENCES_EULA, Activity.MODE_PRIVATE);
|
||||
if (preferences.getBoolean(PREFERENCE_EULA_ACCEPTED, false)) {
|
||||
return;
|
||||
}
|
||||
|
||||
final AlertDialog.Builder builder = new AlertDialog.Builder(activity);
|
||||
builder.setTitle(R.string.eula_title);
|
||||
builder.setCancelable(true);
|
||||
builder.setPositiveButton(R.string.accept,
|
||||
new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
accept(activity, preferences);
|
||||
}
|
||||
});
|
||||
builder.setNegativeButton(R.string.decline,
|
||||
new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
refuse(activity);
|
||||
}
|
||||
});
|
||||
builder.setOnCancelListener(new DialogInterface.OnCancelListener() {
|
||||
@Override
|
||||
public void onCancel(DialogInterface dialog) {
|
||||
refuse(activity);
|
||||
}
|
||||
});
|
||||
builder.setMessage(ResourceUtils.readFile(activity, R.raw.eula));
|
||||
builder.show();
|
||||
}
|
||||
|
||||
private static void accept(Activity activity, SharedPreferences preferences) {
|
||||
preferences.edit().putBoolean(PREFERENCE_EULA_ACCEPTED, true).commit();
|
||||
Intent startIntent = new Intent(activity, WelcomeActivity.class);
|
||||
activity.startActivityForResult(startIntent, MyTracksConstants.WELCOME);
|
||||
}
|
||||
|
||||
private static void refuse(Activity activity) {
|
||||
activity.finish();
|
||||
}
|
||||
|
||||
private Eula() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
/*
|
||||
* 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.TracksColumns;
|
||||
import com.google.android.apps.mytracks.io.TrackWriter;
|
||||
import com.google.android.apps.mytracks.io.TrackWriterFactory;
|
||||
import com.google.android.apps.mytracks.io.TrackWriterFactory.TrackFileFormat;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.app.ProgressDialog;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.SharedPreferences;
|
||||
import android.database.Cursor;
|
||||
import android.os.Handler;
|
||||
import android.os.HandlerThread;
|
||||
import android.os.PowerManager;
|
||||
import android.os.PowerManager.WakeLock;
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
|
||||
/**
|
||||
* A class that will export all tracks to the sd card.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public class ExportAllTracks {
|
||||
// These must line up with the index in the array.
|
||||
public static final int GPX_OPTION_INDEX = 0;
|
||||
public static final int KML_OPTION_INDEX = 1;
|
||||
public static final int CSV_OPTION_INDEX = 2;
|
||||
|
||||
private final Activity activity;
|
||||
private WakeLock wakeLock;
|
||||
private ProgressDialog progress;
|
||||
|
||||
private TrackFileFormat format = TrackFileFormat.GPX;
|
||||
|
||||
private final DialogInterface.OnClickListener itemClick =
|
||||
new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
switch (which) {
|
||||
case GPX_OPTION_INDEX:
|
||||
format = TrackFileFormat.GPX;
|
||||
break;
|
||||
case KML_OPTION_INDEX:
|
||||
format = TrackFileFormat.KML;
|
||||
break;
|
||||
case CSV_OPTION_INDEX:
|
||||
format = TrackFileFormat.CSV;
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public ExportAllTracks(Activity activity) {
|
||||
this.activity = activity;
|
||||
Log.i(MyTracksConstants.TAG, "ExportAllTracks: Starting");
|
||||
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
|
||||
builder.setSingleChoiceItems(R.array.export_formats, 0, itemClick);
|
||||
builder.setPositiveButton(R.string.ok, positiveClick);
|
||||
builder.setNegativeButton(R.string.cancel, null);
|
||||
builder.show();
|
||||
}
|
||||
|
||||
private final DialogInterface.OnClickListener positiveClick =
|
||||
new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
HandlerThread handlerThread;
|
||||
Handler handler;
|
||||
handlerThread = new HandlerThread("SendToMyMaps");
|
||||
handlerThread.start();
|
||||
handler = new Handler(handlerThread.getLooper());
|
||||
handler.post(runner);
|
||||
}
|
||||
};
|
||||
|
||||
private final Runnable runner = new Runnable() {
|
||||
public void run() {
|
||||
aquireLocksAndExport();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Makes sure that we keep the phone from sleeping.
|
||||
* See if there is a current track. Aquire a wake lock if there is no
|
||||
* current track.
|
||||
*/
|
||||
private void aquireLocksAndExport() {
|
||||
SharedPreferences prefs =
|
||||
activity.getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0);
|
||||
long recordingTrackId = -1;
|
||||
if (prefs != null) {
|
||||
recordingTrackId =
|
||||
prefs.getLong(activity.getString(R.string.recording_track_key), -1);
|
||||
}
|
||||
if (recordingTrackId != -1) {
|
||||
acquireWakeLock();
|
||||
}
|
||||
|
||||
// Now we can safely export everything.
|
||||
exportAll();
|
||||
|
||||
// Release the wake lock if we recorded one.
|
||||
// TODO check what happens if we started recording after getting this lock.
|
||||
if (wakeLock != null && wakeLock.isHeld()) {
|
||||
wakeLock.release();
|
||||
Log.i(MyTracksConstants.TAG, "ExportAllTracks: Releasing wake lock.");
|
||||
}
|
||||
Log.i(MyTracksConstants.TAG, "ExportAllTracks: Done");
|
||||
Toast.makeText(activity, R.string.export_done, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
private void makeProgressDialog(final int trackCount) {
|
||||
String exportMsg = activity.getString(R.string.tracklist_btn_export_all);
|
||||
progress = new ProgressDialog(activity);
|
||||
progress.setIcon(android.R.drawable.ic_dialog_info);
|
||||
progress.setTitle(exportMsg);
|
||||
progress.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
|
||||
progress.setMax(trackCount);
|
||||
progress.setProgress(0);
|
||||
progress.show();
|
||||
}
|
||||
|
||||
/**
|
||||
* Actually export the tracks.
|
||||
* This should be called after the wake locks have been aquired.
|
||||
*/
|
||||
private void exportAll() {
|
||||
// Get a cursor over all tracks.
|
||||
Cursor cursor = null;
|
||||
try {
|
||||
MyTracksProviderUtils providerUtils =
|
||||
MyTracksProviderUtils.Factory.get(activity);
|
||||
cursor = providerUtils.getTracksCursor("");
|
||||
if (cursor == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final int trackCount = cursor.getCount();
|
||||
Log.i(MyTracksConstants.TAG,
|
||||
"ExportAllTracks: Exporting: " + cursor.getCount() + " tracks.");
|
||||
int idxTrackId = cursor.getColumnIndexOrThrow(TracksColumns._ID);
|
||||
activity.runOnUiThread(new Runnable() {
|
||||
public void run() {
|
||||
makeProgressDialog(trackCount);
|
||||
}
|
||||
});
|
||||
|
||||
for (int i = 0; cursor.moveToNext(); i++) {
|
||||
final int status = i;
|
||||
activity.runOnUiThread(new Runnable() {
|
||||
public void run() {
|
||||
synchronized (this) {
|
||||
if (progress == null) {
|
||||
return;
|
||||
}
|
||||
progress.setProgress(status);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
long id = cursor.getLong(idxTrackId);
|
||||
Log.i(MyTracksConstants.TAG, "ExportAllTracks: exporting: " + id);
|
||||
TrackWriter writer =
|
||||
TrackWriterFactory.newWriter(activity, providerUtils, id, format);
|
||||
writer.writeTrack();
|
||||
|
||||
if (!writer.wasSuccess()) {
|
||||
// Abort the whole export on the first error.
|
||||
int error = writer.getErrorMessage();
|
||||
Toast.makeText(activity, error, Toast.LENGTH_LONG).show();
|
||||
return;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
if (progress != null) {
|
||||
synchronized (this) {
|
||||
progress.dismiss();
|
||||
progress = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to acquire a partial wake lock if not already acquired. Logs errors
|
||||
* and gives up trying in case the wake lock cannot be acquired.
|
||||
*/
|
||||
private void acquireWakeLock() {
|
||||
Log.i(MyTracksConstants.TAG, "ExportAllTracks: Aquiring wake lock.");
|
||||
try {
|
||||
PowerManager pm =
|
||||
(PowerManager) activity.getSystemService(Context.POWER_SERVICE);
|
||||
if (pm == null) {
|
||||
Log.e(MyTracksConstants.TAG,
|
||||
"ExportAllTracks: Power manager not found!");
|
||||
return;
|
||||
}
|
||||
if (wakeLock == null) {
|
||||
wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
|
||||
MyTracksConstants.TAG);
|
||||
if (wakeLock == null) {
|
||||
Log.e(MyTracksConstants.TAG,
|
||||
"ExportAllTracks: Could not create wake lock (null).");
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!wakeLock.isHeld()) {
|
||||
wakeLock.acquire();
|
||||
if (!wakeLock.isHeld()) {
|
||||
Log.e(MyTracksConstants.TAG,
|
||||
"ExportAllTracks: Could not acquire wake lock.");
|
||||
}
|
||||
}
|
||||
} catch (RuntimeException e) {
|
||||
Log.e(MyTracksConstants.TAG,
|
||||
"ExportAllTracks: Caught unexpected exception: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,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;
|
||||
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
|
||||
/**
|
||||
* Manage the application menus.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
class MenuManager {
|
||||
|
||||
private final MyTracks activity;
|
||||
|
||||
public MenuManager(MyTracks activity) {
|
||||
this.activity = activity;
|
||||
}
|
||||
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
activity.getMenuInflater().inflate(R.menu.main, menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void onPrepareOptionsMenu(Menu menu, boolean hasRecorded,
|
||||
boolean isRecording, boolean hasSelectedTrack) {
|
||||
menu.findItem(R.id.menu_list_tracks).setEnabled(hasRecorded);
|
||||
menu.findItem(R.id.menu_list_markers)
|
||||
.setEnabled(hasRecorded && hasSelectedTrack);
|
||||
menu.findItem(R.id.menu_start_recording)
|
||||
.setEnabled(!isRecording)
|
||||
.setVisible(!isRecording);
|
||||
menu.findItem(R.id.menu_stop_recording)
|
||||
.setEnabled(isRecording)
|
||||
.setVisible(isRecording);
|
||||
}
|
||||
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.menu_start_recording: {
|
||||
activity.startRecording();
|
||||
return true;
|
||||
}
|
||||
case R.id.menu_stop_recording: {
|
||||
activity.stopRecording();
|
||||
return true;
|
||||
}
|
||||
case R.id.menu_list_tracks: {
|
||||
activity.startActivityForResult(new Intent(activity, MyTracksList.class),
|
||||
MyTracksConstants.SHOW_TRACK);
|
||||
return true;
|
||||
}
|
||||
case R.id.menu_list_markers: {
|
||||
Intent startIntent = new Intent(activity, MyTracksWaypointsList.class);
|
||||
startIntent.putExtra("trackid", activity.getSelectedTrackId());
|
||||
activity.startActivityForResult(startIntent, MyTracksConstants.SHOW_WAYPOINT);
|
||||
return true;
|
||||
}
|
||||
case R.id.menu_sensor_state: {
|
||||
return startActivity(SensorStateActivity.class);
|
||||
}
|
||||
case R.id.menu_settings: {
|
||||
return startActivity(MyTracksSettings.class);
|
||||
}
|
||||
case R.id.menu_aggregated_stats: {
|
||||
return startActivity(AggregatedStatsActivity.class);
|
||||
}
|
||||
case R.id.menu_help: {
|
||||
return startActivity(WelcomeActivity.class);
|
||||
}
|
||||
case MyTracksConstants.MENU_CLEAR_MAP: {
|
||||
activity.setSelectedTrackId(-1);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean startActivity(Class<? extends Activity> activityClass) {
|
||||
activity.startActivity(new Intent(activity, activityClass));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,220 @@
|
||||
/*
|
||||
* 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 MyTracksConstants {
|
||||
|
||||
/**
|
||||
* Should be used by all log statements
|
||||
*/
|
||||
public static final String TAG = "MyTracks";
|
||||
|
||||
/**
|
||||
* Name of the gps location provider:
|
||||
*/
|
||||
public static final String GPS_PROVIDER = "gps";
|
||||
|
||||
/**
|
||||
* 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";
|
||||
|
||||
/*
|
||||
* onActivityResult request codes:
|
||||
*/
|
||||
|
||||
public static final int GET_LOGIN = 0;
|
||||
public static final int GET_MAP = 1;
|
||||
public static final int CREATE_MAP = 2;
|
||||
public static final int SHOW_TRACK = 3;
|
||||
public static final int ADD_LIST = 4;
|
||||
public static final int FEATURE_DETAILS = 5;
|
||||
public static final int START_RECORDING = 6;
|
||||
public static final int STOP_RECORDING = 7;
|
||||
public static final int AUTHENTICATE_TO_DOCS = 8;
|
||||
public static final int AUTHENTICATE_TO_TRIX = 9;
|
||||
public static final int SEND_TO_DOCS = 10;
|
||||
public static final int DELETE_TRACK = 11;
|
||||
public static final int SEND_TO_GOOGLE = 12;
|
||||
public static final int SEND_TO_GOOGLE_DIALOG = 13;
|
||||
public static final int SHARE_LINK = 14;
|
||||
public static final int SHARE_GPX_FILE = 15;
|
||||
public static final int SHARE_KML_FILE = 16;
|
||||
public static final int SHARE_CSV_FILE = 17;
|
||||
public static final int SHARE_TCX_FILE = 18;
|
||||
public static final int EDIT_DETAILS = 19;
|
||||
public static final int SAVE_GPX_FILE = 20;
|
||||
public static final int SAVE_KML_FILE = 21;
|
||||
public static final int SAVE_CSV_FILE = 22;
|
||||
public static final int SAVE_TCX_FILE = 23;
|
||||
public static final int CLEAR_MAP = 24;
|
||||
public static final int SHOW_WAYPOINT = 25;
|
||||
public static final int EDIT_WAYPOINT = 26;
|
||||
public static final int WELCOME = 27;
|
||||
|
||||
/*
|
||||
* Menu ids:
|
||||
*/
|
||||
|
||||
public static final int MENU_MY_LOCATION = 1;
|
||||
public static final int MENU_TOGGLE_LAYERS = 2;
|
||||
public static final int MENU_CHART_SETTINGS = 3;
|
||||
public static final int MENU_CURRENT_SEGMENT = 4;
|
||||
|
||||
/*
|
||||
* Context menu ids:
|
||||
*/
|
||||
|
||||
public static final int MENU_EDIT = 100;
|
||||
public static final int MENU_DELETE = 101;
|
||||
public static final int MENU_SEND_TO_GOOGLE = 102;
|
||||
public static final int MENU_SHARE = 103;
|
||||
public static final int MENU_SHOW = 104;
|
||||
public static final int MENU_SHARE_LINK = 200;
|
||||
public static final int MENU_SHARE_GPX_FILE = 201;
|
||||
public static final int MENU_SHARE_KML_FILE = 202;
|
||||
public static final int MENU_SHARE_CSV_FILE = 203;
|
||||
public static final int MENU_SHARE_TCX_FILE = 204;
|
||||
public static final int MENU_WRITE_TO_SD_CARD = 205;
|
||||
public static final int MENU_SAVE_GPX_FILE = 206;
|
||||
public static final int MENU_SAVE_KML_FILE = 207;
|
||||
public static final int MENU_SAVE_CSV_FILE = 208;
|
||||
public static final int MENU_SAVE_TCX_FILE = 209;
|
||||
public static final int MENU_CLEAR_MAP = 210;
|
||||
|
||||
/**
|
||||
* 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 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;
|
||||
|
||||
/**
|
||||
* 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 int getActionFromMenuId(int menuId) {
|
||||
switch (menuId) {
|
||||
case MyTracksConstants.MENU_SEND_TO_GOOGLE:
|
||||
return MyTracksConstants.SEND_TO_GOOGLE_DIALOG;
|
||||
case MyTracksConstants.MENU_EDIT:
|
||||
return MyTracksConstants.EDIT_DETAILS;
|
||||
case MyTracksConstants.MENU_DELETE:
|
||||
return MyTracksConstants.DELETE_TRACK;
|
||||
case MyTracksConstants.MENU_SHARE_LINK:
|
||||
return MyTracksConstants.SHARE_LINK;
|
||||
case MyTracksConstants.MENU_SHARE_KML_FILE:
|
||||
return MyTracksConstants.SHARE_KML_FILE;
|
||||
case MyTracksConstants.MENU_SHARE_GPX_FILE:
|
||||
return MyTracksConstants.SHARE_GPX_FILE;
|
||||
case MyTracksConstants.MENU_SHARE_CSV_FILE:
|
||||
return MyTracksConstants.SHARE_CSV_FILE;
|
||||
case MyTracksConstants.MENU_SHARE_TCX_FILE:
|
||||
return MyTracksConstants.SHARE_TCX_FILE;
|
||||
case MyTracksConstants.MENU_SAVE_GPX_FILE:
|
||||
return MyTracksConstants.SAVE_GPX_FILE;
|
||||
case MyTracksConstants.MENU_SAVE_KML_FILE:
|
||||
return MyTracksConstants.SAVE_KML_FILE;
|
||||
case MyTracksConstants.MENU_SAVE_CSV_FILE:
|
||||
return MyTracksConstants.SAVE_CSV_FILE;
|
||||
case MyTracksConstants.MENU_SAVE_TCX_FILE:
|
||||
return MyTracksConstants.SAVE_TCX_FILE;
|
||||
case MyTracksConstants.MENU_CLEAR_MAP:
|
||||
return MyTracksConstants.CLEAR_MAP;
|
||||
default:
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This is an abstract utility class.
|
||||
*/
|
||||
protected MyTracksConstants() { }
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* 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.maps.mytracks.R;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Handler;
|
||||
import android.os.Message;
|
||||
import android.util.Log;
|
||||
|
||||
/**
|
||||
* A utility class that can be used to delete all tracks and track points
|
||||
* from the provider, including asking for confirmation from the user via
|
||||
* a dialog.
|
||||
*
|
||||
* @author Leif Hendrik Wilden
|
||||
*/
|
||||
public class MyTracksDeleteAllTracks extends Handler {
|
||||
|
||||
private final Context context;
|
||||
private final Runnable done;
|
||||
|
||||
public MyTracksDeleteAllTracks(Context context, Runnable done) {
|
||||
this.context = context;
|
||||
this.done = done;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
super.handleMessage(msg);
|
||||
|
||||
AlertDialog dialog = null;
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(context);
|
||||
builder.setMessage(
|
||||
context.getString(R.string.all_data_will_be_permanently_deleted));
|
||||
builder.setTitle(context.getString(R.string.are_you_sure_question));
|
||||
builder.setIcon(android.R.drawable.ic_dialog_alert);
|
||||
builder.setPositiveButton(context.getString(R.string.yes),
|
||||
new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface dialog, int i) {
|
||||
dialog.dismiss();
|
||||
Log.w(MyTracksConstants.TAG, "deleting all!");
|
||||
MyTracksProviderUtils.Factory.get(context).deleteAllTracks();
|
||||
SharedPreferences prefs =
|
||||
context.getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0);
|
||||
SharedPreferences.Editor editor = prefs.edit();
|
||||
editor.putLong(context.getString(R.string.selected_track_key), -1);
|
||||
editor.commit();
|
||||
if (done != null) {
|
||||
Handler h = new Handler();
|
||||
h.post(done);
|
||||
}
|
||||
}
|
||||
});
|
||||
builder.setNegativeButton(context.getString(R.string.no),
|
||||
new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface dialog, int i) {
|
||||
dialog.dismiss();
|
||||
}
|
||||
});
|
||||
dialog = builder.create();
|
||||
dialog.show();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
* 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.TracksColumns;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.ContentValues;
|
||||
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 that let's the user see and edit the user editable track meta
|
||||
* data such as name, activity type and description.
|
||||
*
|
||||
* @author Leif Hendrik Wilden
|
||||
*/
|
||||
public class MyTracksDetails extends Activity implements OnClickListener {
|
||||
|
||||
/**
|
||||
* The id of the track being edited (taken from bundle, "trackid")
|
||||
*/
|
||||
private Long trackId;
|
||||
|
||||
private EditText name;
|
||||
private EditText description;
|
||||
private AutoCompleteTextView category;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle bundle) {
|
||||
super.onCreate(bundle);
|
||||
setContentView(R.layout.mytracks_detail);
|
||||
|
||||
// Required extra when launching this intent:
|
||||
trackId = getIntent().getLongExtra("trackid", -1);
|
||||
if (trackId < 0) {
|
||||
Log.d(MyTracksConstants.TAG,
|
||||
"MyTracksDetails intent was launched w/o track id.");
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
// Optional extra that can be used to suppress the cancel button:
|
||||
boolean hasCancelButton =
|
||||
getIntent().getBooleanExtra("hasCancelButton", true);
|
||||
|
||||
name = (EditText) findViewById(R.id.trackdetails_name);
|
||||
description = (EditText) findViewById(R.id.trackdetails_description);
|
||||
category = (AutoCompleteTextView) findViewById(R.id.trackdetails_category);
|
||||
ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(
|
||||
this,
|
||||
R.array.activity_types,
|
||||
android.R.layout.simple_dropdown_item_1line);
|
||||
category.setAdapter(adapter);
|
||||
|
||||
Button cancel = (Button) findViewById(R.id.trackdetails_cancel);
|
||||
if (hasCancelButton) {
|
||||
cancel.setOnClickListener(this);
|
||||
cancel.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
cancel.setVisibility(View.GONE);
|
||||
}
|
||||
Button save = (Button) findViewById(R.id.trackdetails_save);
|
||||
save.setOnClickListener(this);
|
||||
|
||||
fillDialog();
|
||||
}
|
||||
|
||||
private void fillDialog() {
|
||||
Track track = MyTracksProviderUtils.Factory.get(this).getTrack(trackId);
|
||||
if (track != null) {
|
||||
name.setText(track.getName());
|
||||
description.setText(track.getDescription());
|
||||
category.setText(track.getCategory());
|
||||
}
|
||||
}
|
||||
|
||||
private void saveDialog() {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(TracksColumns.NAME, name.getText().toString());
|
||||
values.put(TracksColumns.DESCRIPTION, description.getText().toString());
|
||||
values.put(TracksColumns.CATEGORY, category.getText().toString());
|
||||
getContentResolver().update(
|
||||
TracksColumns.CONTENT_URI,
|
||||
values,
|
||||
"_id = " + trackId,
|
||||
null/*selectionArgs*/);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
switch (v.getId()) {
|
||||
case R.id.trackdetails_cancel:
|
||||
finish();
|
||||
break;
|
||||
case R.id.trackdetails_save:
|
||||
saveDialog();
|
||||
finish();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
/*
|
||||
* 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.util.StringUtils;
|
||||
import com.google.android.apps.mytracks.util.UnitConversions;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.app.ListActivity;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.database.Cursor;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.view.ContextMenu;
|
||||
import android.view.ContextMenu.ContextMenuInfo;
|
||||
import android.view.MenuItem;
|
||||
import android.view.SubMenu;
|
||||
import android.view.View;
|
||||
import android.view.View.OnCreateContextMenuListener;
|
||||
import android.view.Window;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.ListView;
|
||||
import android.widget.SimpleCursorAdapter;
|
||||
import android.widget.TextView;
|
||||
|
||||
/**
|
||||
* A list activity displaying all the recorded tracks. There's a context
|
||||
* menu (via long press) displaying various options such as showing, editing,
|
||||
* deleting, sending to MyMaps, or writing to SD card.
|
||||
*
|
||||
* @author Leif Hendrik Wilden
|
||||
*/
|
||||
public class MyTracksList extends ListActivity
|
||||
implements SharedPreferences.OnSharedPreferenceChangeListener,
|
||||
View.OnClickListener {
|
||||
|
||||
private int contextPosition = -1;
|
||||
private long trackId = -1;
|
||||
private ListView listView = null;
|
||||
private boolean metricUnits = true;
|
||||
|
||||
private Cursor tracksCursor = null;
|
||||
|
||||
/**
|
||||
* The id of the currently recording track.
|
||||
*/
|
||||
private long recordingTrackId = -1;
|
||||
|
||||
private final OnCreateContextMenuListener contextMenuListener =
|
||||
new OnCreateContextMenuListener() {
|
||||
@Override
|
||||
public void onCreateContextMenu(ContextMenu menu, View v,
|
||||
ContextMenuInfo menuInfo) {
|
||||
menu.setHeaderTitle(R.string.tracklist_this_track);
|
||||
AdapterView.AdapterContextMenuInfo info =
|
||||
(AdapterView.AdapterContextMenuInfo) menuInfo;
|
||||
contextPosition = info.position;
|
||||
trackId = MyTracksList.this.listView.getAdapter().getItemId(
|
||||
contextPosition);
|
||||
menu.add(0, MyTracksConstants.MENU_SHOW, 0,
|
||||
R.string.tracklist_show_track);
|
||||
menu.add(0, MyTracksConstants.MENU_EDIT, 0,
|
||||
R.string.tracklist_edit_track);
|
||||
if (!MyTracks.getInstance().isRecording()
|
||||
|| trackId != recordingTrackId) {
|
||||
menu.add(0, MyTracksConstants.MENU_SEND_TO_GOOGLE, 0,
|
||||
R.string.tracklist_send_to_google);
|
||||
SubMenu share = menu.addSubMenu(0, MyTracksConstants.MENU_SHARE, 0,
|
||||
R.string.tracklist_share_track);
|
||||
share.add(0, MyTracksConstants.MENU_SHARE_LINK, 0,
|
||||
R.string.tracklist_share_link);
|
||||
share.add(0, MyTracksConstants.MENU_SHARE_GPX_FILE, 0,
|
||||
R.string.tracklist_share_gpx_file);
|
||||
share.add(0, MyTracksConstants.MENU_SHARE_KML_FILE, 0,
|
||||
R.string.tracklist_share_kml_file);
|
||||
share.add(0, MyTracksConstants.MENU_SHARE_CSV_FILE, 0,
|
||||
R.string.tracklist_share_csv_file);
|
||||
share.add(0, MyTracksConstants.MENU_SHARE_TCX_FILE, 0,
|
||||
R.string.tracklist_share_tcx_file);
|
||||
SubMenu save = menu.addSubMenu(0,
|
||||
MyTracksConstants.MENU_WRITE_TO_SD_CARD, 0,
|
||||
R.string.tracklist_write_to_sd);
|
||||
save.add(0, MyTracksConstants.MENU_SAVE_GPX_FILE, 0,
|
||||
R.string.tracklist_save_as_gpx);
|
||||
save.add(0, MyTracksConstants.MENU_SAVE_KML_FILE, 0,
|
||||
R.string.tracklist_save_as_kml);
|
||||
save.add(0, MyTracksConstants.MENU_SAVE_CSV_FILE, 0,
|
||||
R.string.tracklist_save_as_csv);
|
||||
save.add(0, MyTracksConstants.MENU_SAVE_TCX_FILE, 0,
|
||||
R.string.tracklist_save_as_tcx);
|
||||
menu.add(0, MyTracksConstants.MENU_DELETE, 0,
|
||||
R.string.tracklist_delete_track);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(
|
||||
SharedPreferences sharedPreferences, String key) {
|
||||
if (key == null) {
|
||||
return;
|
||||
}
|
||||
if (key.equals(getString(R.string.metric_units_key))) {
|
||||
metricUnits = sharedPreferences.getBoolean(
|
||||
getString(R.string.metric_units_key), true);
|
||||
if (tracksCursor != null && !tracksCursor.isClosed()) {
|
||||
tracksCursor.requery();
|
||||
}
|
||||
}
|
||||
if (key.equals(getString(R.string.recording_track_key))) {
|
||||
recordingTrackId = sharedPreferences.getLong(
|
||||
getString(R.string.recording_track_key), -1);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onListItemClick(ListView l, View v, int position, long id) {
|
||||
Intent result = new Intent();
|
||||
result.putExtra("trackid", id);
|
||||
setResult(MyTracksConstants.SHOW_TRACK, result);
|
||||
finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onMenuItemSelected(int featureId, MenuItem item) {
|
||||
if (!super.onMenuItemSelected(featureId, item)) {
|
||||
switch (item.getItemId()) {
|
||||
case MyTracksConstants.MENU_SHOW: {
|
||||
onListItemClick(null, null, 0, trackId);
|
||||
return true;
|
||||
}
|
||||
case MyTracksConstants.MENU_EDIT: {
|
||||
Intent intent = new Intent(this, MyTracksDetails.class);
|
||||
intent.putExtra("trackid", trackId);
|
||||
startActivity(intent);
|
||||
return true;
|
||||
}
|
||||
case MyTracksConstants.MENU_SHARE:
|
||||
case MyTracksConstants.MENU_WRITE_TO_SD_CARD:
|
||||
return false;
|
||||
default: {
|
||||
Intent result = new Intent();
|
||||
result.putExtra("trackid", trackId);
|
||||
setResult(
|
||||
MyTracksConstants.getActionFromMenuId(item.getItemId()), result);
|
||||
finish();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
switch (v.getId()) {
|
||||
case R.id.tracklist_btn_delete_all: {
|
||||
Handler h = new MyTracksDeleteAllTracks(this, null);
|
||||
h.handleMessage(null);
|
||||
break;
|
||||
}
|
||||
case R.id.tracklist_btn_export_all: {
|
||||
new ExportAllTracks(this);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
// We don't need a window title bar:
|
||||
requestWindowFeature(Window.FEATURE_NO_TITLE);
|
||||
|
||||
setContentView(R.layout.mytracks_list);
|
||||
|
||||
listView = getListView();
|
||||
listView.setOnCreateContextMenuListener(contextMenuListener);
|
||||
|
||||
findViewById(R.id.tracklist_btn_delete_all).setOnClickListener(this);
|
||||
findViewById(R.id.tracklist_btn_export_all).setOnClickListener(this);
|
||||
|
||||
SharedPreferences preferences =
|
||||
getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0);
|
||||
preferences.registerOnSharedPreferenceChangeListener(this);
|
||||
metricUnits =
|
||||
preferences.getBoolean(getString(R.string.metric_units_key), true);
|
||||
recordingTrackId =
|
||||
preferences.getLong(getString(R.string.recording_track_key), -1);
|
||||
|
||||
tracksCursor = getContentResolver().query(
|
||||
TracksColumns.CONTENT_URI, null, null, null, "_id DESC");
|
||||
startManagingCursor(tracksCursor);
|
||||
setListAdapter();
|
||||
}
|
||||
|
||||
private void setListAdapter() {
|
||||
// Get a cursor with all tracks
|
||||
SimpleCursorAdapter adapter = new SimpleCursorAdapter(
|
||||
this,
|
||||
R.layout.mytracks_list_item,
|
||||
tracksCursor,
|
||||
new String[] { TracksColumns.NAME, TracksColumns.STARTTIME,
|
||||
TracksColumns.TOTALDISTANCE, TracksColumns.DESCRIPTION,
|
||||
TracksColumns.CATEGORY },
|
||||
new int[] { R.id.trackdetails_item_name, R.id.trackdetails_item_time,
|
||||
R.id.trackdetails_item_stats, R.id.trackdetails_item_description,
|
||||
R.id.trackdetails_item_category });
|
||||
|
||||
final int startTimeIdx =
|
||||
tracksCursor.getColumnIndexOrThrow(TracksColumns.STARTTIME);
|
||||
final int totalTimeIdx =
|
||||
tracksCursor.getColumnIndexOrThrow(TracksColumns.TOTALTIME);
|
||||
final int totalDistanceIdx =
|
||||
tracksCursor.getColumnIndexOrThrow(TracksColumns.TOTALDISTANCE);
|
||||
|
||||
adapter.setViewBinder(new SimpleCursorAdapter.ViewBinder() {
|
||||
@Override
|
||||
public boolean setViewValue(View view, Cursor cursor, int columnIndex) {
|
||||
TextView textView = (TextView) view;
|
||||
if (columnIndex == startTimeIdx) {
|
||||
long time = cursor.getLong(startTimeIdx);
|
||||
textView.setText(String.format("%tc", time));
|
||||
} else if (columnIndex == totalDistanceIdx) {
|
||||
double length = cursor.getDouble(totalDistanceIdx);
|
||||
String lengthUnit = null;
|
||||
if (metricUnits) {
|
||||
if (length > 1000) {
|
||||
length /= 1000;
|
||||
lengthUnit = getString(R.string.kilometer);
|
||||
} else {
|
||||
lengthUnit = getString(R.string.meter);
|
||||
}
|
||||
} else {
|
||||
if (length > UnitConversions.MI_TO_M) {
|
||||
length /= UnitConversions.MI_TO_M;
|
||||
lengthUnit = getString(R.string.mile);
|
||||
} else {
|
||||
length *= UnitConversions.M_TO_FT;
|
||||
lengthUnit = getString(R.string.feet);
|
||||
}
|
||||
}
|
||||
textView.setText(String.format("%s %.2f %s",
|
||||
StringUtils.formatTime(cursor.getLong(totalTimeIdx)),
|
||||
length,
|
||||
lengthUnit));
|
||||
} else {
|
||||
textView.setText(cursor.getString(columnIndex));
|
||||
if (textView.getText().length() < 1) {
|
||||
textView.setVisibility(View.GONE);
|
||||
} else {
|
||||
textView.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
setListAdapter(adapter);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,452 @@
|
||||
/*
|
||||
* 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.Waypoint;
|
||||
import com.google.android.apps.mytracks.util.MyTracksUtils;
|
||||
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 android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Path;
|
||||
import android.graphics.Point;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.location.Location;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ArrayBlockingQueue;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
|
||||
/**
|
||||
* A map overlay that displays a "MyLocation" arrow, an error circle, the
|
||||
* currently recording track and optionally a selected track.
|
||||
*
|
||||
* @author Leif Hendrik Wilden
|
||||
*/
|
||||
public class MyTracksOverlay extends Overlay {
|
||||
|
||||
private final Drawable[] arrows;
|
||||
private final int arrowWidth, arrowHeight;
|
||||
private final Drawable statsMarker;
|
||||
private final Drawable waypointMarker;
|
||||
private final Drawable startMarker;
|
||||
private final Drawable endMarker;
|
||||
private final int markerWidth, markerHeight;
|
||||
private final Paint selectedTrackPaint;
|
||||
private final Paint errorCirclePaint;
|
||||
private final Context context;
|
||||
private final List<Waypoint> waypoints;
|
||||
private final List<CachedLocation> points;
|
||||
private final BlockingQueue<CachedLocation> pendingPoints;
|
||||
|
||||
private boolean trackDrawingEnabled;
|
||||
private int lastHeading = 0;
|
||||
private Location myLocation;
|
||||
private boolean showEndMarker = true;
|
||||
// TODO: Remove it completely after completing performance tests.
|
||||
private boolean alwaysVisible = true;
|
||||
|
||||
private GeoPoint lastReferencePoint;
|
||||
private Rect lastViewRect;
|
||||
private Path lastPath;
|
||||
|
||||
/**
|
||||
* Represents a pre-processed {@code Location} to speed up drawing.
|
||||
* This class is more like a data object and doesn't provide accessors.
|
||||
*/
|
||||
private static class CachedLocation {
|
||||
public final boolean valid;
|
||||
public final GeoPoint geoPoint;
|
||||
|
||||
public CachedLocation(Location location) {
|
||||
this.valid = MyTracksUtils.isValidLocation(location);
|
||||
this.geoPoint = valid ? MyTracksUtils.getGeoPoint(location) : null;
|
||||
}
|
||||
};
|
||||
|
||||
public MyTracksOverlay(Context context) {
|
||||
this.context = context;
|
||||
|
||||
this.waypoints = new ArrayList<Waypoint>();
|
||||
this.points = new ArrayList<CachedLocation>(1024);
|
||||
this.pendingPoints = new ArrayBlockingQueue<CachedLocation>(
|
||||
MyTracksConstants.MAX_DISPLAYED_TRACK_POINTS, true);
|
||||
|
||||
// TODO: Can we use a FrameAnimation or similar here 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[lastHeading].getIntrinsicWidth();
|
||||
arrowHeight = arrows[lastHeading].getIntrinsicHeight();
|
||||
for (Drawable arrow : arrows) {
|
||||
arrow.setBounds(0, 0, arrowWidth, arrowHeight);
|
||||
}
|
||||
|
||||
statsMarker = resources.getDrawable(R.drawable.ylw_pushpin);
|
||||
markerWidth = statsMarker.getIntrinsicWidth();
|
||||
markerHeight = statsMarker.getIntrinsicHeight();
|
||||
statsMarker.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);
|
||||
|
||||
waypointMarker = resources.getDrawable(R.drawable.blue_pushpin);
|
||||
waypointMarker.setBounds(0, 0, markerWidth, markerHeight);
|
||||
|
||||
selectedTrackPaint = new Paint();
|
||||
selectedTrackPaint.setColor(resources.getColor(R.color.red));
|
||||
selectedTrackPaint.setStrokeWidth(3);
|
||||
selectedTrackPaint.setStyle(Paint.Style.STROKE);
|
||||
selectedTrackPaint.setAntiAlias(true);
|
||||
|
||||
errorCirclePaint = new Paint();
|
||||
errorCirclePaint.setColor(resources.getColor(R.color.blue));
|
||||
errorCirclePaint.setStyle(Paint.Style.STROKE);
|
||||
errorCirclePaint.setStrokeWidth(3);
|
||||
errorCirclePaint.setAlpha(127);
|
||||
errorCirclePaint.setAntiAlias(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a location to the map overlay.
|
||||
*
|
||||
* 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 l the location to add.
|
||||
*/
|
||||
public void addLocation(Location l) {
|
||||
// Queue up in the pending queue until it's merged with {@code #points}.
|
||||
pendingPoints.offer(new CachedLocation(l));
|
||||
}
|
||||
|
||||
public void addWaypoint(Waypoint wpt) {
|
||||
// Note: We don't cache waypoints, because it's not worth the effort.
|
||||
if (wpt != null && wpt.getLocation() != null) {
|
||||
synchronized (waypoints) {
|
||||
waypoints.add(wpt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int getNumLocations() {
|
||||
synchronized (points) {
|
||||
return points.size() + pendingPoints.size();
|
||||
}
|
||||
}
|
||||
|
||||
// Visible for testing.
|
||||
int getNumWaypoints() {
|
||||
synchronized (waypoints) {
|
||||
return waypoints.size();
|
||||
}
|
||||
}
|
||||
|
||||
public void clearPoints() {
|
||||
synchronized (points) {
|
||||
points.clear();
|
||||
pendingPoints.clear();
|
||||
lastPath = null;
|
||||
lastViewRect = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void clearWaypoints() {
|
||||
synchronized (waypoints) {
|
||||
waypoints.clear();
|
||||
}
|
||||
}
|
||||
|
||||
public void setTrackDrawingEnabled(boolean trackDrawingEnabled) {
|
||||
this.trackDrawingEnabled = trackDrawingEnabled;
|
||||
}
|
||||
|
||||
public void setShowEndMarker(boolean showEndMarker) {
|
||||
this.showEndMarker = showEndMarker;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(Canvas canvas, MapView mapView, boolean shadow) {
|
||||
if (shadow) {
|
||||
return;
|
||||
}
|
||||
|
||||
// It's safe to keep projection within a single draw operation.
|
||||
final Projection projection = getMapProjection(mapView);
|
||||
// Get the current viewing window.
|
||||
if (trackDrawingEnabled) {
|
||||
Rect viewRect = getMapViewRect(mapView);
|
||||
|
||||
// Draw the selected track:
|
||||
drawTrack(canvas, projection, viewRect);
|
||||
|
||||
// Draw the waypoints:
|
||||
drawWaypoints(canvas, projection);
|
||||
}
|
||||
|
||||
// Draw the current location
|
||||
drawMyLocation(canvas, projection);
|
||||
}
|
||||
|
||||
// Visible for testing.
|
||||
Projection getMapProjection(MapView mapView) {
|
||||
return mapView.getProjection();
|
||||
}
|
||||
|
||||
// Visible for testing.
|
||||
Rect getMapViewRect(MapView mapView) {
|
||||
int w = mapView.getLongitudeSpan();
|
||||
int h = mapView.getLatitudeSpan();
|
||||
int cx = mapView.getMapCenter().getLongitudeE6();
|
||||
int cy = mapView.getMapCenter().getLatitudeE6();
|
||||
return new Rect(cx - w / 2, cy - h / 2, cx + w / 2, cy + h / 2);
|
||||
}
|
||||
|
||||
// Visible for testing.
|
||||
Path newPath() {
|
||||
return new Path();
|
||||
}
|
||||
|
||||
// Visible for testing.
|
||||
Path getLastPath() {
|
||||
return lastPath;
|
||||
}
|
||||
|
||||
private void drawWaypoints(Canvas canvas, Projection projection) {
|
||||
synchronized (waypoints) {;
|
||||
for (Waypoint wpt : waypoints) {
|
||||
Location loc = wpt.getLocation();
|
||||
drawElement(canvas, projection, MyTracksUtils.getGeoPoint(loc),
|
||||
wpt.getType() == Waypoint.TYPE_STATISTICS ? statsMarker
|
||||
: waypointMarker, -(markerWidth / 2) + 3, -markerHeight);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void drawMyLocation(Canvas canvas, Projection projection) {
|
||||
// Draw the arrow icon.
|
||||
if (myLocation == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Point pt = drawElement(canvas, projection,
|
||||
MyTracksUtils.getGeoPoint(myLocation), arrows[lastHeading],
|
||||
-(arrowWidth / 2) + 3, -(arrowHeight / 2));
|
||||
// Draw the error circle.
|
||||
float radius = projection.metersToEquatorPixels(myLocation.getAccuracy());
|
||||
canvas.drawCircle(pt.x, pt.y, radius, errorCirclePaint);
|
||||
}
|
||||
|
||||
private void drawTrack(Canvas canvas, Projection projection, Rect viewRect) {
|
||||
Path path;
|
||||
synchronized (points) {
|
||||
// Merge the pending points with the list of cached locations.
|
||||
final GeoPoint referencePoint = projection.fromPixels(0, 0);
|
||||
int newPoints = pendingPoints.drainTo(points);
|
||||
boolean newProjection = !viewRect.equals(lastViewRect) ||
|
||||
!referencePoint.equals(lastReferencePoint);
|
||||
if (newPoints == 0 && lastPath != null && !newProjection) {
|
||||
// No need to recreate path (same points and viewing area).
|
||||
path = lastPath;
|
||||
} else {
|
||||
int numPoints = points.size();
|
||||
if (numPoints < 2) {
|
||||
// Not enough points to draw a path.
|
||||
path = null;
|
||||
} else if (lastPath != null && !newProjection) {
|
||||
// Incremental update of the path, without repositioning the view.
|
||||
path = lastPath;
|
||||
updatePath(projection, viewRect, path, numPoints - newPoints);
|
||||
} else {
|
||||
// The view has changed so we have to start from scratch.
|
||||
path = newPath();
|
||||
path.incReserve(numPoints);
|
||||
updatePath(projection, viewRect, path, 0);
|
||||
}
|
||||
lastPath = path;
|
||||
}
|
||||
lastReferencePoint = referencePoint;
|
||||
lastViewRect = viewRect;
|
||||
}
|
||||
if (path != null) {
|
||||
canvas.drawPath(path, selectedTrackPaint);
|
||||
}
|
||||
|
||||
// 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, -markerHeight);
|
||||
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, -markerHeight);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void updatePath(Projection projection, Rect viewRect, Path path,
|
||||
int startLocationIdx) {
|
||||
// Whether to start a new segment on new valid and visible point.
|
||||
boolean newSegment =
|
||||
startLocationIdx > 0 ? !points.get(startLocationIdx - 1).valid : true;
|
||||
boolean lastVisible = !newSegment;
|
||||
final Point pt = new Point();
|
||||
// Loop over track points.
|
||||
for (int i = startLocationIdx; i < points.size(); ++i) {
|
||||
CachedLocation loc = points.get(i);
|
||||
// Check if valid, if not then indicate a new segment.
|
||||
if (!loc.valid) {
|
||||
newSegment = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
final GeoPoint geoPoint = loc.geoPoint;
|
||||
// Check if this breaks the existing segment.
|
||||
boolean visible = alwaysVisible || viewRect.contains(
|
||||
geoPoint.getLongitudeE6(), geoPoint.getLatitudeE6());
|
||||
if (!visible && !lastVisible) {
|
||||
// This is a point outside view not connected to a visible one.
|
||||
newSegment = true;
|
||||
}
|
||||
lastVisible = visible;
|
||||
|
||||
// Either move to beginning of a new segment or continue the old one.
|
||||
projection.toPixels(geoPoint, pt);
|
||||
if (newSegment) {
|
||||
path.moveTo(pt.x, pt.y);
|
||||
newSegment = false;
|
||||
} else {
|
||||
path.lineTo(pt.x, pt.y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Visible for testing.
|
||||
Point drawElement(Canvas canvas, Projection projection, GeoPoint geoPoint,
|
||||
Drawable element, int offsetX, int offsetY) {
|
||||
Point pt = new Point();
|
||||
projection.toPixels(geoPoint, pt);
|
||||
canvas.save();
|
||||
canvas.translate(pt.x + offsetX, pt.y + offsetY);
|
||||
element.draw(canvas);
|
||||
canvas.restore();
|
||||
return pt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the pointer location (will be drawn on next invalidate).
|
||||
*/
|
||||
public void setMyLocation(Location myLocation) {
|
||||
this.myLocation = myLocation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the pointer heading in degrees (will be drawn on next invalidate).
|
||||
*
|
||||
* @return true if the visible heading changed (i.e. a redraw of pointer is
|
||||
* potentially necessary)
|
||||
*/
|
||||
public boolean setHeading(float heading) {
|
||||
int newhdg = Math.round(-heading / 360 * 18 + 180);
|
||||
while (newhdg < 0)
|
||||
newhdg += 18;
|
||||
while (newhdg > 17)
|
||||
newhdg -= 18;
|
||||
if (newhdg != lastHeading) {
|
||||
lastHeading = newhdg;
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTap(GeoPoint p, MapView mapView) {
|
||||
if (p.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 = MyTracksUtils.getLocation(p);
|
||||
double dmin = 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 d = waypointLocation.distanceTo(tapLocation);
|
||||
if (d < dmin) {
|
||||
dmin = d;
|
||||
waypoint = waypoints.get(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (waypoint != null &&
|
||||
dmin < 15000000 / Math.pow(2, mapView.getZoomLevel())) {
|
||||
Intent intent = new Intent(context, MyTracksWaypointDetails.class);
|
||||
intent.putExtra("waypointid", waypoint.getId());
|
||||
context.startActivity(intent);
|
||||
return true;
|
||||
}
|
||||
return super.onTap(p, mapView);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* 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.MyTracksConstants.RESUME_TRACK_EXTRA_NAME;
|
||||
import static com.google.android.apps.mytracks.MyTracksConstants.TAG;
|
||||
|
||||
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 MyTracksReceiver extends BroadcastReceiver {
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
Log.d(TAG, "MyTracksReceiver.onReceive: " + intent.getAction());
|
||||
if (ACTION_BOOT_COMPLETED.equals(intent.getAction())) {
|
||||
Intent startIntent = new Intent(context, TrackRecordingService.class);
|
||||
startIntent.putExtra(RESUME_TRACK_EXTRA_NAME, true);
|
||||
context.startService(startIntent);
|
||||
} else {
|
||||
Log.w(TAG, "MyTracksReceiver: unsupported action");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
/*
|
||||
* 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 java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import com.google.android.apps.mytracks.io.backup.BackupActivityHelper;
|
||||
import com.google.android.apps.mytracks.io.backup.BackupPreferencesListener;
|
||||
import com.google.android.apps.mytracks.services.StatusAnnouncerFactory;
|
||||
import com.google.android.apps.mytracks.util.ApiFeatures;
|
||||
import com.google.android.apps.mytracks.util.BluetoothDeviceUtils;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Bundle;
|
||||
import android.preference.CheckBoxPreference;
|
||||
import android.preference.ListPreference;
|
||||
import android.preference.Preference;
|
||||
import android.preference.PreferenceActivity;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.preference.Preference.OnPreferenceChangeListener;
|
||||
import android.preference.Preference.OnPreferenceClickListener;
|
||||
import android.provider.Settings;
|
||||
|
||||
/**
|
||||
* An activity that let's the user see and edit the settings.
|
||||
*
|
||||
* @author Leif Hendrik Wilden
|
||||
* @author Rodrigo Damazio
|
||||
*/
|
||||
public class MyTracksSettings extends PreferenceActivity {
|
||||
|
||||
public static final String SETTINGS_NAME = "MyTracksSettings";
|
||||
|
||||
/*
|
||||
* Default values - keep in sync with those in preferences.xml.
|
||||
*/
|
||||
public static final int DEFAULT_AUTO_RESUME_TRACK_TIMEOUT = 10; // In min.
|
||||
public static final int DEFAULT_ANNOUNCEMENT_FREQUENCY = -1;
|
||||
public static final int DEFAULT_MAX_RECORDING_DISTANCE = 200;
|
||||
public static final int DEFAULT_MIN_RECORDING_DISTANCE = 5;
|
||||
public static final int DEFAULT_MIN_RECORDING_INTERVAL = 0;
|
||||
public static final int DEFAULT_MIN_REQUIRED_ACCURACY = 200;
|
||||
public static final int DEFAULT_SPLIT_FREQUENCY = 0;
|
||||
|
||||
private BackupPreferencesListener backupListener;
|
||||
|
||||
private SharedPreferences preferences;
|
||||
|
||||
/** Called when the activity is first created. */
|
||||
@Override
|
||||
public void onCreate(Bundle icicle) {
|
||||
super.onCreate(icicle);
|
||||
|
||||
// The volume we want to control is the Text-To-Speech volume
|
||||
ApiFeatures apiFeatures = ApiFeatures.getInstance();
|
||||
int volumeStream =
|
||||
new StatusAnnouncerFactory(apiFeatures).getVolumeStream();
|
||||
setVolumeControlStream(volumeStream);
|
||||
|
||||
// Tell it where to read/write preferences
|
||||
PreferenceManager preferenceManager = getPreferenceManager();
|
||||
preferenceManager.setSharedPreferencesName(SETTINGS_NAME);
|
||||
preferenceManager.setSharedPreferencesMode(0);
|
||||
|
||||
// Set up automatic preferences backup
|
||||
backupListener = BackupPreferencesListener.create(this, apiFeatures);
|
||||
preferences = preferenceManager.getSharedPreferences();
|
||||
preferences.registerOnSharedPreferenceChangeListener(backupListener);
|
||||
|
||||
// Load the preferences to be displayed
|
||||
addPreferencesFromResource(R.xml.preferences);
|
||||
|
||||
// Hook up switching of displayed list entries between metric and imperial
|
||||
// units
|
||||
CheckBoxPreference metricUnitsPreference =
|
||||
(CheckBoxPreference) findPreference(
|
||||
getString(R.string.metric_units_key));
|
||||
metricUnitsPreference.setOnPreferenceChangeListener(
|
||||
new OnPreferenceChangeListener() {
|
||||
@Override
|
||||
public boolean onPreferenceChange(Preference preference,
|
||||
Object newValue) {
|
||||
boolean isMetric = (Boolean) newValue;
|
||||
updatePreferenceUnits(isMetric);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
updatePreferenceUnits(metricUnitsPreference.isChecked());
|
||||
|
||||
ListPreference sensorTypePreference =
|
||||
(ListPreference) findPreference(getString(R.string.sensor_type_key));
|
||||
sensorTypePreference.setOnPreferenceChangeListener(
|
||||
new OnPreferenceChangeListener() {
|
||||
@Override
|
||||
public boolean onPreferenceChange(Preference preference,
|
||||
Object newValue) {
|
||||
updateSensorSettings((String) newValue);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
updateSensorSettings(sensorTypePreference.getValue());
|
||||
|
||||
// Disable TTS announcement preference if not available
|
||||
if (!apiFeatures.hasTextToSpeech()) {
|
||||
IntegerListPreference announcementFrequency =
|
||||
(IntegerListPreference) findPreference(
|
||||
getString(R.string.announcement_frequency_key));
|
||||
announcementFrequency.setEnabled(false);
|
||||
announcementFrequency.setValue("-1");
|
||||
announcementFrequency.setSummary(
|
||||
R.string.settings_not_available_summary);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
|
||||
configureBluetoothPreferences();
|
||||
Preference backupNowPreference =
|
||||
findPreference(getString(R.string.backup_to_sd_key));
|
||||
Preference restoreNowPreference =
|
||||
findPreference(getString(R.string.restore_from_sd_key));
|
||||
|
||||
// If recording, disable backup/restore
|
||||
// (we don't want to get to inconsistent states)
|
||||
boolean recording =
|
||||
preferences.getLong(getString(R.string.recording_track_key), -1) != -1;
|
||||
backupNowPreference.setEnabled(!recording);
|
||||
restoreNowPreference.setEnabled(!recording);
|
||||
backupNowPreference.setSummary(
|
||||
recording ? R.string.settings_no_backup_while_recording
|
||||
: R.string.settings_backup_to_sd_summary);
|
||||
restoreNowPreference.setSummary(
|
||||
recording ? R.string.settings_no_backup_while_recording
|
||||
: R.string.settings_restore_from_sd_summary);
|
||||
|
||||
// Add actions to the backup preferences
|
||||
backupNowPreference.setOnPreferenceClickListener(
|
||||
new OnPreferenceClickListener() {
|
||||
@Override
|
||||
public boolean onPreferenceClick(Preference preference) {
|
||||
BackupActivityHelper backupHelper =
|
||||
new BackupActivityHelper(MyTracksSettings.this);
|
||||
backupHelper.writeBackup();
|
||||
return true;
|
||||
}
|
||||
});
|
||||
restoreNowPreference.setOnPreferenceClickListener(
|
||||
new OnPreferenceClickListener() {
|
||||
@Override
|
||||
public boolean onPreferenceClick(Preference preference) {
|
||||
BackupActivityHelper backupHelper =
|
||||
new BackupActivityHelper(MyTracksSettings.this);
|
||||
backupHelper.restoreBackup();
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
getPreferenceManager().getSharedPreferences()
|
||||
.unregisterOnSharedPreferenceChangeListener(backupListener);
|
||||
|
||||
super.onPause();
|
||||
}
|
||||
private void updateSensorSettings(String sensorType) {
|
||||
boolean usesBluetooth =
|
||||
getString(R.string.zephyr_sensor_type).equals(sensorType);
|
||||
findPreference(
|
||||
getString(R.string.bluetooth_sensor_key)).setEnabled(usesBluetooth);
|
||||
findPreference(
|
||||
getString(R.string.bluetooth_pairing_key)).setEnabled(usesBluetooth);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates all the preferences which give options with distance units to use
|
||||
* the proper unit the user has selected.
|
||||
*
|
||||
* @param isMetric true to use metric units, false to use imperial
|
||||
*/
|
||||
private void updatePreferenceUnits(boolean isMetric) {
|
||||
final ListPreference minRecordingDistance =
|
||||
(ListPreference) findPreference(
|
||||
getString(R.string.min_recording_distance_key));
|
||||
final ListPreference maxRecordingDistance =
|
||||
(ListPreference) findPreference(
|
||||
getString(R.string.max_recording_distance_key));
|
||||
final ListPreference minRequiredAccuracy =
|
||||
(ListPreference) findPreference(
|
||||
getString(R.string.min_required_accuracy_key));
|
||||
final ListPreference splitFrequency =
|
||||
(ListPreference) findPreference(
|
||||
getString(R.string.split_frequency_key));
|
||||
|
||||
minRecordingDistance.setEntries(isMetric
|
||||
? R.array.min_recording_distance_options
|
||||
: R.array.min_recording_distance_options_ft);
|
||||
maxRecordingDistance.setEntries(isMetric
|
||||
? R.array.max_recording_distance_options
|
||||
: R.array.max_recording_distance_options_ft);
|
||||
minRequiredAccuracy.setEntries(isMetric
|
||||
? R.array.min_required_accuracy_options
|
||||
: R.array.min_required_accuracy_options_ft);
|
||||
splitFrequency.setEntries(isMetric
|
||||
? R.array.split_frequency_options
|
||||
: R.array.split_frequency_options_ft);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Configures preference actions related to bluetooth.
|
||||
*/
|
||||
private void configureBluetoothPreferences() {
|
||||
if (BluetoothDeviceUtils.isBluetoothMethodSupported()) {
|
||||
// Populate the list of bluetooth devices
|
||||
populateBluetoothDeviceList();
|
||||
|
||||
// Make the pair devices preference go to the system preferences
|
||||
findPreference(getString(R.string.bluetooth_pairing_key))
|
||||
.setOnPreferenceClickListener(new OnPreferenceClickListener() {
|
||||
public boolean onPreferenceClick(Preference preference) {
|
||||
Intent settingsIntent = new Intent(Settings.ACTION_BLUETOOTH_SETTINGS);
|
||||
startActivity(settingsIntent);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Populates the list preference with all available bluetooth devices.
|
||||
*/
|
||||
private void populateBluetoothDeviceList() {
|
||||
// Build the list of entries and their values
|
||||
List<String> entries = new ArrayList<String>();
|
||||
List<String> entryValues = new ArrayList<String>();
|
||||
|
||||
// The actual devices
|
||||
BluetoothDeviceUtils.getInstance().populateDeviceLists(entries, entryValues);
|
||||
|
||||
CharSequence[] entriesArray = entries.toArray(new CharSequence[entries.size()]);
|
||||
CharSequence[] entryValuesArray = entryValues.toArray(new CharSequence[entryValues.size()]);
|
||||
ListPreference devicesPreference =
|
||||
(ListPreference) findPreference(getString(R.string.bluetooth_sensor_key));
|
||||
devicesPreference.setEntryValues(entryValuesArray);
|
||||
devicesPreference.setEntries(entriesArray);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
/*
|
||||
* 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.stats.TripStatistics;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.ContentValues;
|
||||
import android.content.SharedPreferences;
|
||||
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;
|
||||
import android.widget.ImageView;
|
||||
|
||||
/**
|
||||
* Screen in which the user enters details about a waypoint.
|
||||
*
|
||||
* @author Leif Hendrik Wilden
|
||||
*/
|
||||
public class MyTracksWaypointDetails extends Activity
|
||||
implements OnClickListener {
|
||||
|
||||
/**
|
||||
* The id of the way point being edited (taken from bundle, "waypointid")
|
||||
*/
|
||||
private Long waypointId;
|
||||
|
||||
private EditText name;
|
||||
private EditText description;
|
||||
private AutoCompleteTextView category;
|
||||
private View detailsView;
|
||||
private View statsView;
|
||||
|
||||
private StatsUtilities utils;
|
||||
private Waypoint waypoint;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle bundle) {
|
||||
super.onCreate(bundle);
|
||||
setContentView(R.layout.mytracks_waypoint_details);
|
||||
|
||||
utils = new StatsUtilities(this);
|
||||
SharedPreferences preferences =
|
||||
getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0);
|
||||
if (preferences != null) {
|
||||
boolean useMetric =
|
||||
preferences.getBoolean(getString(R.string.metric_units_key), true);
|
||||
utils.setMetricUnits(useMetric);
|
||||
|
||||
boolean displaySpeed =
|
||||
preferences.getBoolean(getString(R.string.report_speed_key), true);
|
||||
utils.setReportSpeed(displaySpeed);
|
||||
|
||||
utils.updateWaypointUnits();
|
||||
utils.setSpeedLabels();
|
||||
}
|
||||
|
||||
// Required extra when launching this intent:
|
||||
waypointId = getIntent().getLongExtra("waypointid", -1);
|
||||
if (waypointId < 0) {
|
||||
Log.d(MyTracksConstants.TAG,
|
||||
"MyTracksWaypointsDetails intent was launched w/o waypoint id.");
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
// Optional extra that can be used to suppress the cancel button:
|
||||
boolean hasCancelButton =
|
||||
getIntent().getBooleanExtra("hasCancelButton", true);
|
||||
|
||||
name = (EditText) findViewById(R.id.waypointdetails_name);
|
||||
description = (EditText) findViewById(R.id.waypointdetails_description);
|
||||
category =
|
||||
(AutoCompleteTextView) findViewById(R.id.waypointdetails_category);
|
||||
statsView = findViewById(R.id.waypoint_stats);
|
||||
ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(
|
||||
this,
|
||||
R.array.waypoint_types,
|
||||
android.R.layout.simple_dropdown_item_1line);
|
||||
category.setAdapter(adapter);
|
||||
detailsView = findViewById(R.id.waypointdetails_description_layout);
|
||||
|
||||
Button cancel = (Button) findViewById(R.id.waypointdetails_cancel);
|
||||
if (hasCancelButton) {
|
||||
cancel.setOnClickListener(this);
|
||||
cancel.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
cancel.setVisibility(View.GONE);
|
||||
}
|
||||
Button save = (Button) findViewById(R.id.waypointdetails_save);
|
||||
save.setOnClickListener(this);
|
||||
|
||||
fillDialog();
|
||||
}
|
||||
|
||||
private void fillDialog() {
|
||||
waypoint = MyTracksProviderUtils.Factory.get(this).getWaypoint(waypointId);
|
||||
if (waypoint != null) {
|
||||
name.setText(waypoint.getName());
|
||||
ImageView icon = (ImageView) findViewById(R.id.waypointdetails_icon);
|
||||
int iconId = -1;
|
||||
switch(waypoint.getType()) {
|
||||
case Waypoint.TYPE_WAYPOINT:
|
||||
description.setText(waypoint.getDescription());
|
||||
detailsView.setVisibility(View.VISIBLE);
|
||||
category.setText(waypoint.getCategory());
|
||||
statsView.setVisibility(View.GONE);
|
||||
iconId = R.drawable.blue_pushpin;
|
||||
break;
|
||||
case Waypoint.TYPE_STATISTICS:
|
||||
detailsView.setVisibility(View.GONE);
|
||||
statsView.setVisibility(View.VISIBLE);
|
||||
iconId = R.drawable.ylw_pushpin;
|
||||
TripStatistics waypointStats = waypoint.getStatistics();
|
||||
utils.setAllStats(waypointStats);
|
||||
utils.setAltitude(
|
||||
R.id.elevation_register, waypoint.getLocation().getAltitude());
|
||||
break;
|
||||
}
|
||||
icon.setImageDrawable(getResources().getDrawable(iconId));
|
||||
}
|
||||
}
|
||||
|
||||
private void saveDialog() {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(WaypointsColumns.NAME, name.getText().toString());
|
||||
if (waypoint != null && waypoint.getType() == Waypoint.TYPE_WAYPOINT) {
|
||||
values.put(WaypointsColumns.DESCRIPTION,
|
||||
description.getText().toString());
|
||||
values.put(WaypointsColumns.CATEGORY, category.getText().toString());
|
||||
}
|
||||
getContentResolver().update(
|
||||
WaypointsColumns.CONTENT_URI,
|
||||
values,
|
||||
"_id = " + waypointId,
|
||||
null /*selectionArgs*/);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
switch (v.getId()) {
|
||||
case R.id.waypointdetails_cancel:
|
||||
finish();
|
||||
break;
|
||||
case R.id.waypointdetails_save:
|
||||
saveDialog();
|
||||
finish();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
/*
|
||||
* 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.util.StringUtils;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.app.ListActivity;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.database.Cursor;
|
||||
import android.os.Bundle;
|
||||
import android.view.ContextMenu;
|
||||
import android.view.ContextMenu.ContextMenuInfo;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.View.OnCreateContextMenuListener;
|
||||
import android.view.Window;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.ListView;
|
||||
import android.widget.SimpleCursorAdapter;
|
||||
import android.widget.TextView;
|
||||
|
||||
/**
|
||||
* Activity which shows the list of waypoints in a track.
|
||||
*
|
||||
* @author Leif Hendrik Wilden
|
||||
*/
|
||||
public class MyTracksWaypointsList extends ListActivity
|
||||
implements View.OnClickListener {
|
||||
|
||||
private int contextPosition = -1;
|
||||
private long trackId = -1;
|
||||
private long waypointId = -1;
|
||||
private ListView listView = null;
|
||||
private Button insertWaypointButton = null;
|
||||
private Button insertStatisticsButton = null;
|
||||
private long recordingTrackId = -1;
|
||||
private long selectedTrackId = -1;
|
||||
private MyTracksProviderUtils providerUtils;
|
||||
|
||||
private Cursor waypointsCursor = null;
|
||||
|
||||
private final OnCreateContextMenuListener contextMenuListener =
|
||||
new OnCreateContextMenuListener() {
|
||||
public void onCreateContextMenu(ContextMenu menu, View v,
|
||||
ContextMenuInfo menuInfo) {
|
||||
menu.setHeaderTitle(R.string.waypointslist_this_waypoint);
|
||||
AdapterView.AdapterContextMenuInfo info =
|
||||
(AdapterView.AdapterContextMenuInfo) menuInfo;
|
||||
contextPosition = info.position;
|
||||
waypointId = MyTracksWaypointsList.this.listView.getAdapter()
|
||||
.getItemId(contextPosition);
|
||||
int type = providerUtils.getWaypoint(info.id).getType();
|
||||
menu.add(0, MyTracksConstants.MENU_SHOW, 0,
|
||||
R.string.waypointslist_show_waypoint);
|
||||
menu.add(0, MyTracksConstants.MENU_EDIT, 0,
|
||||
R.string.waypointslist_edit_waypoint);
|
||||
menu.add(0, MyTracksConstants.MENU_DELETE, 0,
|
||||
R.string.waypointslist_delete_waypoint).setEnabled(
|
||||
recordingTrackId < 0 || type == Waypoint.TYPE_WAYPOINT ||
|
||||
info.id != providerUtils.getLastWaypointId(recordingTrackId));
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
protected void onListItemClick(ListView l, View v, int position, long id) {
|
||||
Intent result = new Intent();
|
||||
result.putExtra("trackid", trackId);
|
||||
result.putExtra("waypointid", id);
|
||||
setResult(MyTracksConstants.EDIT_WAYPOINT, result);
|
||||
finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onMenuItemSelected(int featureId, MenuItem item) {
|
||||
if (!super.onMenuItemSelected(featureId, item)) {
|
||||
switch (item.getItemId()) {
|
||||
case MyTracksConstants.MENU_SHOW: {
|
||||
onListItemClick(null, null, 0, waypointId);
|
||||
return true;
|
||||
}
|
||||
case MyTracksConstants.MENU_EDIT: {
|
||||
Intent intent = new Intent(this, MyTracksWaypointDetails.class);
|
||||
intent.putExtra("trackid", trackId);
|
||||
intent.putExtra("waypointid", waypointId);
|
||||
startActivity(intent);
|
||||
return true;
|
||||
}
|
||||
case MyTracksConstants.MENU_DELETE: {
|
||||
deleteWaypoint(waypointId);
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
providerUtils = MyTracksProviderUtils.Factory.get(this);
|
||||
|
||||
// We don't need a window title bar:
|
||||
requestWindowFeature(Window.FEATURE_NO_TITLE);
|
||||
|
||||
setContentView(R.layout.mytracks_waypoints_list);
|
||||
|
||||
listView = getListView();
|
||||
listView.setOnCreateContextMenuListener(contextMenuListener);
|
||||
|
||||
insertWaypointButton =
|
||||
(Button) findViewById(R.id.waypointslist_btn_insert_waypoint);
|
||||
insertWaypointButton.setOnClickListener(this);
|
||||
insertStatisticsButton =
|
||||
(Button) findViewById(R.id.waypointslist_btn_insert_statistics);
|
||||
insertStatisticsButton.setOnClickListener(this);
|
||||
SharedPreferences preferences =
|
||||
getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0);
|
||||
if (preferences != null) {
|
||||
recordingTrackId =
|
||||
preferences.getLong(getString(R.string.recording_track_key), -1);
|
||||
selectedTrackId =
|
||||
preferences.getLong(getString(R.string.selected_track_key), -1);
|
||||
}
|
||||
boolean selectedRecording = selectedTrackId > 0
|
||||
&& selectedTrackId == recordingTrackId;
|
||||
insertWaypointButton.setEnabled(selectedRecording);
|
||||
insertStatisticsButton.setEnabled(selectedRecording);
|
||||
|
||||
if (getIntent() != null && getIntent().getExtras() != null) {
|
||||
trackId = getIntent().getExtras().getLong("trackid", -1);
|
||||
} else {
|
||||
trackId = -1;
|
||||
}
|
||||
|
||||
final long firstWaypointId = providerUtils.getFirstWaypointId(trackId);
|
||||
waypointsCursor = getContentResolver().query(
|
||||
WaypointsColumns.CONTENT_URI, null,
|
||||
WaypointsColumns.TRACKID + "=" + trackId + " AND "
|
||||
+ WaypointsColumns._ID + "!=" + firstWaypointId, null, null);
|
||||
startManagingCursor(waypointsCursor);
|
||||
setListAdapter();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
switch (v.getId()) {
|
||||
case R.id.waypointslist_btn_insert_waypoint: {
|
||||
long id = MyTracks.getInstance().insertWaypointMarker();
|
||||
if (id >= 0) {
|
||||
Intent intent = new Intent(this, MyTracksWaypointDetails.class);
|
||||
intent.putExtra("waypointid", id);
|
||||
startActivity(intent);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case R.id.waypointslist_btn_insert_statistics: {
|
||||
long id = MyTracks.getInstance().insertStatisticsMarker();
|
||||
if (id >= 0) {
|
||||
Intent intent = new Intent(this, MyTracksWaypointDetails.class);
|
||||
intent.putExtra("waypointid", id);
|
||||
startActivity(intent);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private void setListAdapter() {
|
||||
// Get a cursor with all tracks
|
||||
SimpleCursorAdapter adapter = new SimpleCursorAdapter(
|
||||
this,
|
||||
R.layout.mytracks_marker_item,
|
||||
waypointsCursor,
|
||||
new String[] { WaypointsColumns.NAME, WaypointsColumns.TIME,
|
||||
WaypointsColumns.CATEGORY, WaypointsColumns.TYPE },
|
||||
new int[] { R.id.waypointslist_item_name,
|
||||
R.id.waypointslist_item_time,
|
||||
R.id.waypointslist_item_category,
|
||||
R.id.waypointslist_item_icon });
|
||||
|
||||
final int timeIdx =
|
||||
waypointsCursor.getColumnIndexOrThrow(WaypointsColumns.TIME);
|
||||
final int typeIdx =
|
||||
waypointsCursor.getColumnIndexOrThrow(WaypointsColumns.TYPE);
|
||||
adapter.setViewBinder(new SimpleCursorAdapter.ViewBinder() {
|
||||
@Override
|
||||
public boolean setViewValue(View view, Cursor cursor, int columnIndex) {
|
||||
if (columnIndex == timeIdx) {
|
||||
long time = cursor.getLong(timeIdx);
|
||||
TextView textView = (TextView) view;
|
||||
textView.setText(String.format("%tc", time));
|
||||
textView.setVisibility(
|
||||
textView.getText().length() < 1 ? View.GONE : View.VISIBLE);
|
||||
} else if (columnIndex == typeIdx) {
|
||||
int type = cursor.getInt(typeIdx);
|
||||
ImageView imageView = (ImageView) view;
|
||||
imageView.setImageDrawable(getResources().getDrawable(
|
||||
type == Waypoint.TYPE_STATISTICS
|
||||
? R.drawable.ylw_pushpin
|
||||
: R.drawable.blue_pushpin));
|
||||
} else {
|
||||
TextView textView = (TextView) view;
|
||||
textView.setText(cursor.getString(columnIndex));
|
||||
textView.setVisibility(
|
||||
textView.getText().length() < 1 ? View.GONE : View.VISIBLE);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
setListAdapter(adapter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the way point with the given id.
|
||||
* Prompts the user if he want to really delete it.
|
||||
*/
|
||||
public void deleteWaypoint(final long waypointId) {
|
||||
AlertDialog dialog = null;
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
||||
builder.setMessage(getString(R.string.marker_will_be_permanently_deleted));
|
||||
builder.setTitle(getString(R.string.are_you_sure_question));
|
||||
builder.setIcon(android.R.drawable.ic_dialog_alert);
|
||||
builder.setPositiveButton(getString(R.string.yes),
|
||||
new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int i) {
|
||||
dialog.dismiss();
|
||||
providerUtils.deleteWaypoint(waypointId,
|
||||
new StringUtils(MyTracksWaypointsList.this));
|
||||
}
|
||||
});
|
||||
builder.setNegativeButton(getString(R.string.no),
|
||||
new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int i) {
|
||||
dialog.dismiss();
|
||||
}
|
||||
});
|
||||
dialog = builder.create();
|
||||
dialog.show();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
/*
|
||||
* 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 android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Handler;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.animation.AlphaAnimation;
|
||||
import android.view.animation.Animation;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.RelativeLayout;
|
||||
import android.widget.RelativeLayout.LayoutParams;
|
||||
|
||||
/**
|
||||
* Creates previous and next arrows for a given activity.
|
||||
*
|
||||
* @author Leif Hendrik Wilden
|
||||
*/
|
||||
public class NavControls {
|
||||
|
||||
private static final int KEEP_VISIBLE_MILLIS = 4000;
|
||||
private static final boolean FADE_CONTROLS = true;
|
||||
|
||||
private static final Animation SHOW_NEXT_ANIMATION =
|
||||
new AlphaAnimation(0F, 1F);
|
||||
private static final Animation HIDE_NEXT_ANIMATION =
|
||||
new AlphaAnimation(1F, 0F);
|
||||
private static final Animation SHOW_PREV_ANIMATION =
|
||||
new AlphaAnimation(0F, 1F);
|
||||
private static final Animation HIDE_PREV_ANIMATION =
|
||||
new AlphaAnimation(1F, 0F);
|
||||
|
||||
/**
|
||||
* A touchable image view.
|
||||
* When touched it changes the navigation control icons accordingly.
|
||||
*/
|
||||
private class TouchLayout extends RelativeLayout implements Runnable {
|
||||
private final boolean isLeft;
|
||||
private final ImageView icon;
|
||||
|
||||
public TouchLayout(Context context, boolean isLeft) {
|
||||
super(context);
|
||||
this.isLeft = isLeft;
|
||||
this.icon = new ImageView(context);
|
||||
icon.setVisibility(View.GONE);
|
||||
addView(icon);
|
||||
}
|
||||
|
||||
public void setIcon(Drawable drawable) {
|
||||
icon.setImageDrawable(drawable);
|
||||
icon.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTouchEvent(MotionEvent event) {
|
||||
switch (event.getAction()) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
setPressed(true);
|
||||
shiftIcons(isLeft);
|
||||
|
||||
// Call the user back
|
||||
handler.post(this);
|
||||
|
||||
break;
|
||||
case MotionEvent.ACTION_UP:
|
||||
setPressed(false);
|
||||
break;
|
||||
}
|
||||
return super.onTouchEvent(event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
touchRunnable.run();
|
||||
setPressed(false);
|
||||
}
|
||||
}
|
||||
|
||||
private final Handler handler = new Handler();
|
||||
private final Runnable dismissControls = new Runnable() {
|
||||
public void run() {
|
||||
hide();
|
||||
}
|
||||
};
|
||||
|
||||
private final TouchLayout prevImage;
|
||||
private final TouchLayout nextImage;
|
||||
private final TypedArray leftIcons;
|
||||
private final TypedArray rightIcons;
|
||||
private final Runnable touchRunnable;
|
||||
|
||||
private boolean isVisible = false;
|
||||
private int currentIcons;
|
||||
|
||||
public NavControls(Context context, ViewGroup container,
|
||||
TypedArray leftIcons, TypedArray rightIcons,
|
||||
Runnable touchRunnable) {
|
||||
this.leftIcons = leftIcons;
|
||||
this.rightIcons = rightIcons;
|
||||
this.touchRunnable = touchRunnable;
|
||||
|
||||
if (leftIcons.length() != rightIcons.length() || leftIcons.length() < 1) {
|
||||
throw new IllegalArgumentException("Invalid icons specified");
|
||||
}
|
||||
if (touchRunnable == null) {
|
||||
throw new NullPointerException("Runnable cannot be null");
|
||||
}
|
||||
|
||||
LayoutParams prevParams = new LayoutParams(
|
||||
LayoutParams.WRAP_CONTENT,
|
||||
LayoutParams.WRAP_CONTENT);
|
||||
LayoutParams nextParams = new LayoutParams(
|
||||
LayoutParams.WRAP_CONTENT,
|
||||
LayoutParams.WRAP_CONTENT);
|
||||
|
||||
prevParams.addRule(RelativeLayout.ALIGN_PARENT_LEFT);
|
||||
nextParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT);
|
||||
prevParams.addRule(RelativeLayout.CENTER_VERTICAL);
|
||||
nextParams.addRule(RelativeLayout.CENTER_VERTICAL);
|
||||
|
||||
nextImage = new TouchLayout(context, false);
|
||||
prevImage = new TouchLayout(context, true);
|
||||
nextImage.setLayoutParams(nextParams);
|
||||
prevImage.setLayoutParams(prevParams);
|
||||
nextImage.setVisibility(View.INVISIBLE);
|
||||
prevImage.setVisibility(View.INVISIBLE);
|
||||
|
||||
container.addView(prevImage);
|
||||
container.addView(nextImage);
|
||||
|
||||
prevImage.setIcon(leftIcons.getDrawable(0));
|
||||
nextImage.setIcon(rightIcons.getDrawable(0));
|
||||
this.currentIcons = 0;
|
||||
}
|
||||
|
||||
private void keepVisible() {
|
||||
if (isVisible && FADE_CONTROLS) {
|
||||
handler.removeCallbacks(dismissControls);
|
||||
handler.postDelayed(dismissControls, KEEP_VISIBLE_MILLIS);
|
||||
}
|
||||
}
|
||||
|
||||
public void show() {
|
||||
if (!isVisible) {
|
||||
SHOW_PREV_ANIMATION.setDuration(500);
|
||||
SHOW_PREV_ANIMATION.startNow();
|
||||
prevImage.setPressed(false);
|
||||
prevImage.setAnimation(SHOW_PREV_ANIMATION);
|
||||
prevImage.setVisibility(View.VISIBLE);
|
||||
|
||||
SHOW_NEXT_ANIMATION.setDuration(500);
|
||||
SHOW_NEXT_ANIMATION.startNow();
|
||||
nextImage.setPressed(false);
|
||||
nextImage.setAnimation(SHOW_NEXT_ANIMATION);
|
||||
nextImage.setVisibility(View.VISIBLE);
|
||||
|
||||
isVisible = true;
|
||||
keepVisible();
|
||||
} else {
|
||||
keepVisible();
|
||||
}
|
||||
}
|
||||
|
||||
public void hideNow() {
|
||||
handler.removeCallbacks(dismissControls);
|
||||
isVisible = false;
|
||||
prevImage.clearAnimation();
|
||||
prevImage.setVisibility(View.INVISIBLE);
|
||||
nextImage.clearAnimation();
|
||||
nextImage.setVisibility(View.INVISIBLE);
|
||||
}
|
||||
|
||||
public void hide() {
|
||||
isVisible = false;
|
||||
prevImage.setAnimation(HIDE_PREV_ANIMATION);
|
||||
HIDE_PREV_ANIMATION.setDuration(500);
|
||||
HIDE_PREV_ANIMATION.startNow();
|
||||
prevImage.setVisibility(View.INVISIBLE);
|
||||
|
||||
nextImage.setAnimation(HIDE_NEXT_ANIMATION);
|
||||
HIDE_NEXT_ANIMATION.setDuration(500);
|
||||
HIDE_NEXT_ANIMATION.startNow();
|
||||
nextImage.setVisibility(View.INVISIBLE);
|
||||
}
|
||||
|
||||
public int getCurrentIcons() {
|
||||
return currentIcons;
|
||||
}
|
||||
|
||||
private void shiftIcons(boolean isLeft) {
|
||||
// Increment or decrement by one, with wrap around
|
||||
currentIcons = (currentIcons + leftIcons.length() +
|
||||
(isLeft ? -1 : 1)) % leftIcons.length();
|
||||
prevImage.setIcon(leftIcons.getDrawable(currentIcons));
|
||||
nextImage.setIcon(rightIcons.getDrawable(currentIcons));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* Interface that allows to set a progress dialog message and progress value
|
||||
* in percent.
|
||||
*
|
||||
* @author Leif Hendrik Wilden
|
||||
*/
|
||||
public interface ProgressIndicator {
|
||||
|
||||
public void setProgressMessage(int resId);
|
||||
public void clearProgressMessage();
|
||||
|
||||
public void setProgressValue(int percent);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
/*
|
||||
* 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.maps.mytracks.R;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.SharedPreferences.Editor;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.view.Window;
|
||||
import android.widget.Button;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.CompoundButton;
|
||||
import android.widget.CompoundButton.OnCheckedChangeListener;
|
||||
import android.widget.RadioButton;
|
||||
import android.widget.RadioGroup;
|
||||
|
||||
/**
|
||||
* A dialog where the user can choose where to send the tracks to, i.e.
|
||||
* to Google My Maps, Google Docs, etc.
|
||||
*
|
||||
* @author Leif Hendrik Wilden
|
||||
*/
|
||||
public class SendToGoogleDialog extends Dialog {
|
||||
|
||||
private CheckBox sendToFusionTablesCheckBox;
|
||||
private CheckBox sendToDocsCheckBox;
|
||||
private RadioButton sendStatsRadioButton;
|
||||
private RadioButton sendStatsAndPointsRadioButton;
|
||||
private Button sendButton;
|
||||
|
||||
public SendToGoogleDialog(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
requestWindowFeature(Window.FEATURE_NO_TITLE);
|
||||
setContentView(R.layout.mytracks_send_to_google);
|
||||
|
||||
Button cancel = (Button) findViewById(R.id.sendtogoogle_cancel);
|
||||
cancel.setOnClickListener(new View.OnClickListener() {
|
||||
public void onClick(View v) {
|
||||
MyTracks.getInstance().dismissDialog(DialogManager.DIALOG_SEND_TO_GOOGLE);
|
||||
}
|
||||
});
|
||||
|
||||
Button send = (Button) findViewById(R.id.sendtogoogle_send_now);
|
||||
send.setOnClickListener(new View.OnClickListener() {
|
||||
public void onClick(View v) {
|
||||
dismiss();
|
||||
MyTracks.getInstance().sendToGoogle();
|
||||
}
|
||||
});
|
||||
|
||||
OnCheckedChangeListener checkBoxListener = new OnCheckedChangeListener() {
|
||||
public void onCheckedChanged(CompoundButton button, boolean checked) {
|
||||
sendButton.setEnabled(
|
||||
sendToFusionTablesCheckBox.isChecked() ||
|
||||
sendToDocsCheckBox.isChecked());
|
||||
//groupDocs.setVisibility(
|
||||
// sendToDocsCheckBox.isChecked() ? View.VISIBLE : View.INVISIBLE);
|
||||
}
|
||||
};
|
||||
|
||||
sendButton = (Button) findViewById(R.id.sendtogoogle_send_now);
|
||||
sendToFusionTablesCheckBox =
|
||||
(CheckBox) findViewById(R.id.sendtogoogle_google_fusiontables);
|
||||
sendToFusionTablesCheckBox.setOnCheckedChangeListener(checkBoxListener);
|
||||
sendToDocsCheckBox = (CheckBox) findViewById(R.id.sendtogoogle_google_docs);
|
||||
sendToDocsCheckBox.setOnCheckedChangeListener(checkBoxListener);
|
||||
sendStatsRadioButton =
|
||||
(RadioButton) findViewById(R.id.sendtogoogle_send_stats);
|
||||
sendStatsAndPointsRadioButton = (RadioButton) findViewById(
|
||||
R.id.sendtogoogle_send_stats_and_points);
|
||||
|
||||
SharedPreferences prefs =
|
||||
getContext().getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0);
|
||||
if (prefs != null) {
|
||||
sendToFusionTablesCheckBox.setChecked(
|
||||
prefs.getBoolean(
|
||||
getContext().getString(R.string.send_to_my_maps_key), true));
|
||||
sendToDocsCheckBox.setChecked(
|
||||
prefs.getBoolean(
|
||||
getContext().getString(R.string.send_to_docs_key), true));
|
||||
sendStatsAndPointsRadioButton.setChecked(
|
||||
prefs.getBoolean(
|
||||
getContext().getString(R.string.send_stats_and_points_key),
|
||||
false));
|
||||
sendStatsRadioButton.setChecked(
|
||||
!prefs.getBoolean(
|
||||
getContext().getString(R.string.send_stats_and_points_key),
|
||||
false));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStop() {
|
||||
SharedPreferences prefs =
|
||||
getContext().getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0);
|
||||
if (prefs != null) {
|
||||
Editor editor = prefs.edit();
|
||||
if (editor != null) {
|
||||
editor.putBoolean(
|
||||
getContext().getString(R.string.send_to_my_maps_key),
|
||||
sendToFusionTablesCheckBox.isChecked());
|
||||
editor.putBoolean(
|
||||
getContext().getString(R.string.send_to_docs_key),
|
||||
sendToDocsCheckBox.isChecked());
|
||||
editor.putBoolean(
|
||||
getContext().getString(R.string.send_stats_and_points_key),
|
||||
sendStatsAndPointsRadioButton.isChecked());
|
||||
editor.commit();
|
||||
}
|
||||
}
|
||||
super.onStop();
|
||||
}
|
||||
|
||||
public boolean getSendToFusionTables() {
|
||||
return sendToFusionTablesCheckBox.isChecked();
|
||||
}
|
||||
|
||||
public boolean getSendToDocs() {
|
||||
return sendToDocsCheckBox.isChecked();
|
||||
}
|
||||
|
||||
public boolean getSendStatsAndPoints() {
|
||||
return sendStatsAndPointsRadioButton.isChecked();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
/*
|
||||
* Copyright 2010 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package com.google.android.apps.mytracks;
|
||||
|
||||
import static com.google.android.apps.mytracks.MyTracksConstants.TAG;
|
||||
import com.google.android.apps.mytracks.content.Sensor;
|
||||
import com.google.android.apps.mytracks.services.ITrackRecordingService;
|
||||
import com.google.android.apps.mytracks.services.sensors.SensorUtils;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
import com.google.protobuf.InvalidProtocolBufferException;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.os.Bundle;
|
||||
import android.os.RemoteException;
|
||||
import android.util.Log;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
|
||||
/**
|
||||
* An activity that displays information about sensors.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public class SensorStateActivity extends Activity {
|
||||
|
||||
private static final SimpleDateFormat TIMESTAMP_FORMAT =
|
||||
new SimpleDateFormat("HH:mm:ss");
|
||||
private static final long REFRESH_PERIOD_MS = 250;
|
||||
|
||||
private final StatsUtilities utils;
|
||||
|
||||
/**
|
||||
* This timer periodically invokes the refresh timer task.
|
||||
*/
|
||||
private Timer timer;
|
||||
|
||||
private final Runnable stateUpdater = new Runnable() {
|
||||
public void run() {
|
||||
updateState();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* A task which will update the U/I.
|
||||
*/
|
||||
private class RefreshTask extends TimerTask {
|
||||
@Override
|
||||
public void run() {
|
||||
runOnUiThread(stateUpdater);
|
||||
}
|
||||
};
|
||||
|
||||
public SensorStateActivity() {
|
||||
utils = new StatsUtilities(this);
|
||||
Log.w(TAG, "SensorStateActivity()");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
Log.w(TAG, "SensorStateActivity.onCreate");
|
||||
|
||||
setContentView(R.layout.sensor_state);
|
||||
|
||||
updateState();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
timer = new Timer();
|
||||
timer.schedule(new RefreshTask(), REFRESH_PERIOD_MS, REFRESH_PERIOD_MS);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
timer.cancel();
|
||||
timer.purge();
|
||||
timer = null;
|
||||
}
|
||||
|
||||
protected void updateState() {
|
||||
MyTracks mt = MyTracks.getInstance();
|
||||
|
||||
ITrackRecordingService service =
|
||||
mt == null ? null : mt.getTrackRecordingService();
|
||||
if (service == null) {
|
||||
Log.d(MyTracksConstants.TAG, "Could not get track recording service.");
|
||||
updateSensorState(Sensor.SensorState.NONE);
|
||||
updateSensorData(null);
|
||||
return;
|
||||
}
|
||||
Sensor.SensorDataSet sds = null;
|
||||
try {
|
||||
byte[] buff = service.getSensorData();
|
||||
if (buff != null) {
|
||||
sds = Sensor.SensorDataSet.parseFrom(buff);
|
||||
updateSensorData(sds);
|
||||
}
|
||||
} catch (RemoteException e) {
|
||||
Log.e(MyTracksConstants.TAG, "Could not read sensor data.", e);
|
||||
} catch (InvalidProtocolBufferException e) {
|
||||
Log.e(MyTracksConstants.TAG, "Could not read sensor data.", e);
|
||||
}
|
||||
updateSensorData(sds);
|
||||
|
||||
try {
|
||||
int i = service.getSensorState();
|
||||
updateSensorState(Sensor.SensorState.valueOf(i));
|
||||
} catch (RemoteException e) {
|
||||
Log.e(MyTracksConstants.TAG, "Could not read sensor state.", e);
|
||||
updateSensorState(Sensor.SensorState.NONE);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateSensorState(Sensor.SensorState state) {
|
||||
TextView sensorTime =
|
||||
((TextView) findViewById(R.id.sensor_state_register));
|
||||
sensorTime.setText(SensorUtils.getStateAsString(state, this));
|
||||
}
|
||||
|
||||
protected void updateSensorData(Sensor.SensorDataSet sds) {
|
||||
if (sds == null) {
|
||||
utils.setUnknown(R.id.sensor_data_time_register);
|
||||
utils.setUnknown(R.id.cadence_state_register);
|
||||
utils.setUnknown(R.id.power_state_register);
|
||||
utils.setUnknown(R.id.heart_rate_register);
|
||||
return;
|
||||
}
|
||||
|
||||
TextView sensorTime =
|
||||
((TextView) findViewById(R.id.sensor_data_time_register));
|
||||
sensorTime.setText(
|
||||
TIMESTAMP_FORMAT.format(new Date(sds.getCreationTime())));
|
||||
|
||||
if (sds.hasPower() && sds.getPower().hasValue()
|
||||
&& sds.getPower().getState() == Sensor.SensorState.SENDING) {
|
||||
utils.setText(R.id.power_state_register,
|
||||
Integer.toString(sds.getPower().getValue()));
|
||||
} else {
|
||||
utils.setText(R.id.power_state_register,
|
||||
SensorUtils.getStateAsString(
|
||||
sds.hasPower()
|
||||
? sds.getPower().getState()
|
||||
: Sensor.SensorState.NONE,
|
||||
this));
|
||||
}
|
||||
|
||||
if (sds.hasCadence() && sds.getCadence().hasValue()
|
||||
&& sds.getCadence().getState() == Sensor.SensorState.SENDING) {
|
||||
utils.setText(R.id.cadence_state_register,
|
||||
Integer.toString(sds.getCadence().getValue()));
|
||||
} else {
|
||||
utils.setText(R.id.cadence_state_register,
|
||||
SensorUtils.getStateAsString(
|
||||
sds.hasCadence()
|
||||
? sds.getCadence().getState()
|
||||
: Sensor.SensorState.NONE,
|
||||
this));
|
||||
}
|
||||
|
||||
if (sds.hasHeartRate() && sds.getHeartRate().hasValue()
|
||||
&& sds.getHeartRate().getState() == Sensor.SensorState.SENDING) {
|
||||
utils.setText(R.id.heart_rate_register,
|
||||
Integer.toString(sds.getHeartRate().getValue()));
|
||||
} else {
|
||||
utils.setText(R.id.heart_rate_register,
|
||||
SensorUtils.getStateAsString(
|
||||
sds.hasHeartRate()
|
||||
? sds.getHeartRate().getState()
|
||||
: Sensor.SensorState.NONE,
|
||||
this));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,442 @@
|
||||
/*
|
||||
* 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.MyTracksProviderUtilsImpl;
|
||||
import com.google.android.apps.mytracks.content.Track;
|
||||
import com.google.android.apps.mytracks.content.TracksColumns;
|
||||
import com.google.android.apps.mytracks.content.WaypointsColumns;
|
||||
import com.google.android.apps.mytracks.services.StatusAnnouncerFactory;
|
||||
import com.google.android.apps.mytracks.util.ApiFeatures;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
|
||||
import android.database.ContentObserver;
|
||||
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.DisplayMetrics;
|
||||
import android.util.Log;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.Window;
|
||||
import android.widget.ScrollView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
/**
|
||||
* An activity that displays track statistics to the user.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public class StatsActivity extends Activity
|
||||
implements OnSharedPreferenceChangeListener {
|
||||
|
||||
private StatsUtilities utils;
|
||||
private UIUpdateThread thread;
|
||||
|
||||
private ContentObserver observer;
|
||||
|
||||
/**
|
||||
* The id of the currently selected track.
|
||||
*/
|
||||
private long selectedTrackId = -1;
|
||||
|
||||
/**
|
||||
* The id of the currently recording track.
|
||||
*/
|
||||
private long recordingTrackId = -1;
|
||||
|
||||
/**
|
||||
* The start time of the selected track.
|
||||
*/
|
||||
private long startTime = -1;
|
||||
|
||||
/**
|
||||
* True if distances should be displayed in metric units (from shared
|
||||
* preferences).
|
||||
*/
|
||||
private boolean metricUnits = true;
|
||||
|
||||
/**
|
||||
* True if pace should be displayed as dist/time (from shared preferences).
|
||||
*/
|
||||
private boolean displaySpeed = true;
|
||||
|
||||
/**
|
||||
* true if activity has resumed and is on top
|
||||
*/
|
||||
private boolean activityOnTop = false;
|
||||
|
||||
/**
|
||||
* If true, the statistics for the current segment are shown, otherwise
|
||||
* for the full track.
|
||||
*/
|
||||
private boolean showCurrentSegment = false;
|
||||
|
||||
private MyTracksProviderUtils providerUtils;
|
||||
|
||||
/**
|
||||
* A runnable for posting to the UI thread. Will update the total time field.
|
||||
*/
|
||||
private final Runnable updateResults = new Runnable() {
|
||||
public void run() {
|
||||
updateTotalTime();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* A thread that updates the total time field every second.
|
||||
*/
|
||||
private class UIUpdateThread extends Thread {
|
||||
|
||||
public UIUpdateThread() {
|
||||
super();
|
||||
Log.i(MyTracksConstants.TAG, "Created UI update thread");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
Log.i(MyTracksConstants.TAG, "Started UI update thread");
|
||||
while (MyTracks.getInstance().isRecording()) {
|
||||
long sleeptime = 1000;
|
||||
runOnUiThread(updateResults);
|
||||
try {
|
||||
Thread.sleep(sleeptime);
|
||||
} catch (InterruptedException e) {
|
||||
Log.w(MyTracksConstants.TAG,
|
||||
"StatsActivity: Caught exception on sleep.", e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
Log.w(MyTracksConstants.TAG, "UIUpdateThread finished.");
|
||||
}
|
||||
}
|
||||
|
||||
/** Called when the activity is first created. */
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
utils = new StatsUtilities(this);
|
||||
providerUtils = new MyTracksProviderUtilsImpl(getContentResolver());
|
||||
|
||||
// The volume we want to control is the Text-To-Speech volume
|
||||
int volumeStream =
|
||||
new StatusAnnouncerFactory(ApiFeatures.getInstance()).getVolumeStream();
|
||||
setVolumeControlStream(volumeStream);
|
||||
|
||||
// We don't need a window title bar:
|
||||
requestWindowFeature(Window.FEATURE_NO_TITLE);
|
||||
|
||||
setContentView(R.layout.stats);
|
||||
|
||||
Handler contentHandler = new Handler();
|
||||
observer = new ContentObserver(contentHandler) {
|
||||
@Override
|
||||
public void onChange(boolean selfChange) {
|
||||
Log.d(MyTracksConstants.TAG, "StatsActivity: ContentObserver.onChange");
|
||||
restoreStats();
|
||||
super.onChange(selfChange);
|
||||
}
|
||||
};
|
||||
|
||||
ScrollView sv = ((ScrollView) findViewById(R.id.scrolly));
|
||||
sv.setScrollBarStyle(ScrollView.SCROLLBARS_OUTSIDE_INSET);
|
||||
|
||||
SharedPreferences preferences =
|
||||
getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0);
|
||||
if (preferences != null) {
|
||||
selectedTrackId = preferences.getLong(
|
||||
getString(R.string.selected_track_key), -1);
|
||||
recordingTrackId = preferences.getLong(
|
||||
getString(R.string.recording_track_key), -1);
|
||||
metricUnits = preferences.getBoolean(
|
||||
getString(R.string.metric_units_key), true);
|
||||
displaySpeed =
|
||||
preferences.getBoolean(getString(R.string.report_speed_key), true);
|
||||
checkLiveTrack();
|
||||
restoreStats();
|
||||
showUnknownLocation();
|
||||
preferences.registerOnSharedPreferenceChangeListener(this);
|
||||
}
|
||||
utils.setMetricUnits(metricUnits);
|
||||
utils.setReportSpeed(displaySpeed);
|
||||
utils.updateUnits();
|
||||
utils.setSpeedLabel(R.id.speed_label, R.string.speed, R.string.pace_label);
|
||||
utils.setSpeedLabels();
|
||||
|
||||
DisplayMetrics metrics = new DisplayMetrics();
|
||||
getWindowManager().getDefaultDisplay().getMetrics(metrics);
|
||||
if (metrics.heightPixels > 600) {
|
||||
((TextView) findViewById(R.id.speed_register)).setTextSize(80.0f);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
unregisterLocationListener();
|
||||
if (thread != null) {
|
||||
thread.interrupt();
|
||||
thread = null;
|
||||
}
|
||||
getContentResolver().unregisterContentObserver(observer);
|
||||
activityOnTop = false;
|
||||
super.onPause();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
activityOnTop = true;
|
||||
checkLiveTrack();
|
||||
restoreStats();
|
||||
showUnknownLocation();
|
||||
super.onResume();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(
|
||||
final SharedPreferences sharedPreferences, final String key) {
|
||||
Log.d(MyTracksConstants.TAG,
|
||||
"StatsActivity: onSharedPreferences changed " + key);
|
||||
if (key != null) {
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (key.equals(getString(R.string.selected_track_key))) {
|
||||
selectedTrackId =
|
||||
sharedPreferences.getLong(
|
||||
getString(R.string.selected_track_key),
|
||||
-1);
|
||||
checkLiveTrack();
|
||||
restoreStats();
|
||||
showUnknownLocation();
|
||||
} else if (key.equals(getString(R.string.recording_track_key))) {
|
||||
recordingTrackId =
|
||||
sharedPreferences.getLong(
|
||||
getString(R.string.recording_track_key),
|
||||
-1);
|
||||
checkLiveTrack();
|
||||
restoreStats();
|
||||
showUnknownLocation();
|
||||
} else if (key.equals(getString(R.string.metric_units_key))) {
|
||||
metricUnits =
|
||||
sharedPreferences.getBoolean(
|
||||
getString(R.string.metric_units_key), true);
|
||||
utils.setMetricUnits(metricUnits);
|
||||
utils.updateUnits();
|
||||
restoreStats();
|
||||
} else if (key.equals(getString(R.string.report_speed_key))) {
|
||||
displaySpeed =
|
||||
sharedPreferences.getBoolean(
|
||||
getString(R.string.report_speed_key),
|
||||
true);
|
||||
utils.setReportSpeed(displaySpeed);
|
||||
utils.updateUnits();
|
||||
utils.setSpeedLabel(
|
||||
R.id.speed_label, R.string.speed, R.string.pace_label);
|
||||
Log.w(MyTracksConstants.TAG, "Setting speed labels");
|
||||
utils.setSpeedLabels();
|
||||
restoreStats();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
super.onCreateOptionsMenu(menu);
|
||||
MenuItem currentSegment = menu.add(0,
|
||||
MyTracksConstants.MENU_CURRENT_SEGMENT, 0, R.string.current_segment);
|
||||
currentSegment.setIcon(R.drawable.menu_by_distance);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPrepareOptionsMenu(Menu menu) {
|
||||
MenuItem item = menu.findItem(MyTracksConstants.MENU_CURRENT_SEGMENT);
|
||||
if (item != null) {
|
||||
item.setTitle(showCurrentSegment
|
||||
? getString(R.string.current_track)
|
||||
: getString(R.string.current_segment));
|
||||
}
|
||||
return super.onPrepareOptionsMenu(menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case MyTracksConstants.MENU_CURRENT_SEGMENT:
|
||||
showCurrentSegment = !showCurrentSegment;
|
||||
restoreStats();
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
private final LocationListener locationListener = new LocationListener() {
|
||||
@Override
|
||||
public void onLocationChanged(Location l) {
|
||||
if (selectedTrackIsRecording()) {
|
||||
showLocation(l);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProviderDisabled(String provider) {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProviderEnabled(String provider) {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStatusChanged(String provider, int status, Bundle extras) {
|
||||
// Do nothing
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Registers to receive location updates from the GPS location provider.
|
||||
*/
|
||||
private void registerLocationListener() {
|
||||
LocationManager locationManager =
|
||||
(LocationManager) getSystemService(Context.LOCATION_SERVICE);
|
||||
if (locationManager != null) {
|
||||
LocationProvider gpsProvider =
|
||||
locationManager.getProvider(MyTracksConstants.GPS_PROVIDER);
|
||||
if (gpsProvider == null) {
|
||||
Toast.makeText(this, getString(R.string.error_no_gps_location_provider),
|
||||
Toast.LENGTH_LONG).show();
|
||||
return;
|
||||
} else {
|
||||
Log.d(MyTracksConstants.TAG, "StatsActivity: Using location provider "
|
||||
+ gpsProvider.getName());
|
||||
}
|
||||
locationManager.requestLocationUpdates(gpsProvider.getName(),
|
||||
0/*minTime*/, 0/*minDist*/, locationListener);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters all location listener.
|
||||
*/
|
||||
private void unregisterLocationListener() {
|
||||
LocationManager locationManager =
|
||||
(LocationManager) getSystemService(Context.LOCATION_SERVICE);
|
||||
if (locationManager != null) {
|
||||
locationManager.removeUpdates(locationListener);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if the selected track is the currently recording track
|
||||
*/
|
||||
private boolean selectedTrackIsRecording() {
|
||||
return MyTracks.getInstance().isRecording()
|
||||
&& selectedTrackId == recordingTrackId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads values for selected tracks from provider and update the UI.
|
||||
*/
|
||||
private void restoreStats() {
|
||||
if (selectedTrackId < 0) {
|
||||
utils.setAllToUnknown();
|
||||
return;
|
||||
}
|
||||
|
||||
Track track = providerUtils.getTrack(selectedTrackId);
|
||||
if (track == null || track.getStatistics() == null) {
|
||||
utils.setAllToUnknown();
|
||||
return;
|
||||
}
|
||||
|
||||
startTime = track.getStatistics().getStartTime();
|
||||
if (!selectedTrackIsRecording()) {
|
||||
utils.setTime(R.id.total_time_register,
|
||||
track.getStatistics().getTotalTime());
|
||||
}
|
||||
utils.setAllStats(track.getStatistics());
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this activity needs to update live track data or not.
|
||||
* If so, make sure that:
|
||||
* a) a thread keeps updating the total time
|
||||
* b) a location listener is registered
|
||||
* c) a content observer is registered
|
||||
* Otherwise unregister listeners, observers, and kill update thread.
|
||||
*/
|
||||
private void checkLiveTrack() {
|
||||
final boolean isRecording = selectedTrackIsRecording();
|
||||
final boolean startThread =
|
||||
(thread == null) && isRecording && activityOnTop;
|
||||
final boolean killThread =
|
||||
(thread != null) && (!isRecording || !activityOnTop);
|
||||
if (startThread) {
|
||||
thread = new UIUpdateThread();
|
||||
thread.start();
|
||||
getContentResolver().registerContentObserver(
|
||||
TracksColumns.CONTENT_URI, false, observer);
|
||||
getContentResolver().registerContentObserver(
|
||||
WaypointsColumns.CONTENT_URI, false, observer);
|
||||
registerLocationListener();
|
||||
} else if (killThread) {
|
||||
thread.interrupt();
|
||||
thread = null;
|
||||
getContentResolver().unregisterContentObserver(observer);
|
||||
unregisterLocationListener();
|
||||
}
|
||||
}
|
||||
|
||||
public void updateTotalTime() {
|
||||
if (selectedTrackIsRecording()) {
|
||||
utils.setTime(R.id.total_time_register,
|
||||
System.currentTimeMillis() - startTime);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the given location fields (latitude, longitude, altitude) and all
|
||||
* other fields.
|
||||
*
|
||||
* @param l may be null (will set location fields to unknown)
|
||||
*/
|
||||
private void showLocation(Location l) {
|
||||
utils.setAltitude(R.id.elevation_register, l.getAltitude());
|
||||
utils.setLatLong(R.id.latitude_register, l.getLatitude());
|
||||
utils.setLatLong(R.id.longitude_register, l.getLongitude());
|
||||
utils.setSpeed(R.id.speed_register, l.getSpeed() * 3.6);
|
||||
}
|
||||
|
||||
private void showUnknownLocation() {
|
||||
utils.setUnknown(R.id.elevation_register);
|
||||
utils.setUnknown(R.id.latitude_register);
|
||||
utils.setUnknown(R.id.longitude_register);
|
||||
utils.setUnknown(R.id.speed_register);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,261 @@
|
||||
/*
|
||||
* 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.TripStatistics;
|
||||
import com.google.android.apps.mytracks.util.StringUtils;
|
||||
import com.google.android.apps.mytracks.util.UnitConversions;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import java.text.DecimalFormat;
|
||||
import java.text.NumberFormat;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.util.Log;
|
||||
import android.widget.TextView;
|
||||
|
||||
/**
|
||||
* Various utility functions for views that display statistics information.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public class StatsUtilities {
|
||||
|
||||
private final Activity activity;
|
||||
private static final NumberFormat LAT_LONG_FORMAT =
|
||||
new DecimalFormat("##,###.00000");
|
||||
private static final NumberFormat ALTITUDE_FORMAT =
|
||||
new DecimalFormat("###,###");
|
||||
private static final NumberFormat SPEED_FORMAT =
|
||||
new DecimalFormat("#,###,###.00");
|
||||
private static final NumberFormat GRADE_FORMAT =
|
||||
new DecimalFormat("##.0%");
|
||||
|
||||
/**
|
||||
* True if distances should be displayed in metric units (from shared
|
||||
* preferences).
|
||||
*/
|
||||
private boolean metricUnits = true;
|
||||
|
||||
/**
|
||||
* True - report speed
|
||||
* False - report pace
|
||||
*/
|
||||
private boolean reportSpeed = true;
|
||||
|
||||
public StatsUtilities(Activity a) {
|
||||
this.activity = a;
|
||||
}
|
||||
|
||||
public boolean isMetricUnits() {
|
||||
return metricUnits;
|
||||
}
|
||||
|
||||
public void setMetricUnits(boolean metricUnits) {
|
||||
this.metricUnits = metricUnits;
|
||||
}
|
||||
|
||||
public boolean isReportSpeed() {
|
||||
return reportSpeed;
|
||||
}
|
||||
|
||||
public void setReportSpeed(boolean reportSpeed) {
|
||||
this.reportSpeed = reportSpeed;
|
||||
}
|
||||
|
||||
public void setUnknown(int id) {
|
||||
((TextView) activity.findViewById(id)).setText(R.string.unknown);
|
||||
}
|
||||
|
||||
public void setText(int id, double d, NumberFormat format) {
|
||||
if (!Double.isNaN(d) && !Double.isInfinite(d)) {
|
||||
setText(id, format.format(d));
|
||||
}
|
||||
}
|
||||
|
||||
public void setText(int id, String s) {
|
||||
int lengthLimit = 8;
|
||||
String displayString = s.length() > lengthLimit
|
||||
? s.substring(0, lengthLimit - 3) + "..."
|
||||
: s;
|
||||
((TextView) activity.findViewById(id)).setText(displayString);
|
||||
}
|
||||
|
||||
public void setLatLong(int id, double d) {
|
||||
TextView msgTextView = (TextView) activity.findViewById(id);
|
||||
msgTextView.setText(LAT_LONG_FORMAT.format(d));
|
||||
}
|
||||
|
||||
public void setAltitude(int id, double d) {
|
||||
setText(id, (metricUnits ? d : (d * UnitConversions.M_TO_FT)),
|
||||
ALTITUDE_FORMAT);
|
||||
}
|
||||
|
||||
public void setDistance(int id, double d) {
|
||||
setText(id, (metricUnits ? d : (d * UnitConversions.KM_TO_MI)),
|
||||
SPEED_FORMAT);
|
||||
}
|
||||
|
||||
public void setSpeed(int id, double d) {
|
||||
if (d == 0) {
|
||||
setUnknown(id);
|
||||
return;
|
||||
}
|
||||
double speed = metricUnits ? d : d * UnitConversions.KM_TO_MI;
|
||||
if (reportSpeed) {
|
||||
setText(id, speed, SPEED_FORMAT);
|
||||
} else {
|
||||
// Format as milliseconds per unit
|
||||
long pace = (long) (3600000.0 / speed);
|
||||
setTime(id, pace);
|
||||
}
|
||||
}
|
||||
|
||||
public void setAltitudeUnits(int unitLabelId) {
|
||||
TextView unitTextView = (TextView) activity.findViewById(unitLabelId);
|
||||
unitTextView.setText(metricUnits ? R.string.meter : R.string.feet);
|
||||
}
|
||||
|
||||
public void setDistanceUnits(int unitLabelId) {
|
||||
TextView unitTextView = (TextView) activity.findViewById(unitLabelId);
|
||||
unitTextView.setText(metricUnits ? R.string.kilometer : R.string.mile);
|
||||
}
|
||||
|
||||
public void setSpeedUnits(int unitLabelId, int unitLabelBottomId) {
|
||||
TextView unitTextView = (TextView) activity.findViewById(unitLabelId);
|
||||
unitTextView.setText(reportSpeed
|
||||
? (metricUnits ? R.string.kilometer : R.string.mile)
|
||||
: R.string.min);
|
||||
|
||||
unitTextView = (TextView) activity.findViewById(unitLabelBottomId);
|
||||
unitTextView.setText(reportSpeed
|
||||
? R.string.hr
|
||||
: (metricUnits ? R.string.kilometer : R.string.mile));
|
||||
}
|
||||
|
||||
public void setTime(int id, long l) {
|
||||
setText(id, StringUtils.formatTime(l));
|
||||
}
|
||||
|
||||
public void setGrade(int id, double d) {
|
||||
setText(id, d, GRADE_FORMAT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the unit fields.
|
||||
*/
|
||||
public void updateUnits() {
|
||||
setSpeedUnits(R.id.speed_unit_label_top, R.id.speed_unit_label_bottom);
|
||||
updateWaypointUnits();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the units fields used by waypoints.
|
||||
*/
|
||||
public void updateWaypointUnits() {
|
||||
setSpeedUnits(R.id.average_moving_speed_unit_label_top,
|
||||
R.id.average_moving_speed_unit_label_bottom);
|
||||
setSpeedUnits(R.id.average_speed_unit_label_top,
|
||||
R.id.average_speed_unit_label_bottom);
|
||||
setDistanceUnits(R.id.total_distance_unit_label);
|
||||
setSpeedUnits(R.id.max_speed_unit_label_top,
|
||||
R.id.max_speed_unit_label_bottom);
|
||||
setAltitudeUnits(R.id.elevation_unit_label);
|
||||
setAltitudeUnits(R.id.elevation_gain_unit_label);
|
||||
setAltitudeUnits(R.id.min_elevation_unit_label);
|
||||
setAltitudeUnits(R.id.max_elevation_unit_label);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets all fields to "-" (unknown).
|
||||
*/
|
||||
public void setAllToUnknown() {
|
||||
// "Instant" values:
|
||||
setUnknown(R.id.elevation_register);
|
||||
setUnknown(R.id.latitude_register);
|
||||
setUnknown(R.id.longitude_register);
|
||||
setUnknown(R.id.speed_register);
|
||||
// Values from provider:
|
||||
setUnknown(R.id.total_time_register);
|
||||
setUnknown(R.id.moving_time_register);
|
||||
setUnknown(R.id.total_distance_register);
|
||||
setUnknown(R.id.average_speed_register);
|
||||
setUnknown(R.id.average_moving_speed_register);
|
||||
setUnknown(R.id.max_speed_register);
|
||||
setUnknown(R.id.min_elevation_register);
|
||||
setUnknown(R.id.max_elevation_register);
|
||||
setUnknown(R.id.elevation_gain_register);
|
||||
setUnknown(R.id.min_grade_register);
|
||||
setUnknown(R.id.max_grade_register);
|
||||
}
|
||||
|
||||
public void setAllStats(long movingTime, double totalDistance,
|
||||
double averageSpeed, double averageMovingSpeed, double maxSpeed,
|
||||
double minElevation, double maxElevation, double elevationGain,
|
||||
double minGrade, double maxGrade) {
|
||||
setTime(R.id.moving_time_register, movingTime);
|
||||
setDistance(R.id.total_distance_register, totalDistance / 1000);
|
||||
setSpeed(R.id.average_speed_register, averageSpeed * 3.6);
|
||||
setSpeed(R.id.average_moving_speed_register, averageMovingSpeed * 3.6);
|
||||
setSpeed(R.id.max_speed_register, maxSpeed * 3.6);
|
||||
setAltitude(R.id.min_elevation_register, minElevation);
|
||||
setAltitude(R.id.max_elevation_register, maxElevation);
|
||||
setAltitude(R.id.elevation_gain_register, elevationGain);
|
||||
setGrade(R.id.min_grade_register, minGrade);
|
||||
setGrade(R.id.max_grade_register, maxGrade);
|
||||
}
|
||||
|
||||
public void setAllStats(TripStatistics stats) {
|
||||
setTime(R.id.moving_time_register, stats.getMovingTime());
|
||||
setDistance(R.id.total_distance_register, stats.getTotalDistance() / 1000);
|
||||
setSpeed(R.id.average_speed_register, stats.getAverageSpeed() * 3.6);
|
||||
setSpeed(R.id.average_moving_speed_register,
|
||||
stats.getAverageMovingSpeed() * 3.6);
|
||||
setSpeed(R.id.max_speed_register, stats.getMaxSpeed() * 3.6);
|
||||
setAltitude(R.id.min_elevation_register, stats.getMinElevation());
|
||||
setAltitude(R.id.max_elevation_register, stats.getMaxElevation());
|
||||
setAltitude(R.id.elevation_gain_register, stats.getTotalElevationGain());
|
||||
setGrade(R.id.min_grade_register, stats.getMinGrade());
|
||||
setGrade(R.id.max_grade_register, stats.getMaxGrade());
|
||||
setTime(R.id.total_time_register, stats.getTotalTime());
|
||||
|
||||
}
|
||||
|
||||
public void setSpeedLabel(int id, int speedString, int paceString) {
|
||||
Log.w(MyTracksConstants.TAG, "Setting view " + id +
|
||||
" to " + reportSpeed +
|
||||
" speed: " + speedString +
|
||||
" pace: " + paceString);
|
||||
TextView tv = ((TextView) activity.findViewById(id));
|
||||
if (tv != null) {
|
||||
tv.setText(reportSpeed ? speedString : paceString);
|
||||
} else {
|
||||
Log.w(MyTracksConstants.TAG, "Could not find id: " + id);
|
||||
}
|
||||
}
|
||||
|
||||
public void setSpeedLabels() {
|
||||
setSpeedLabel(R.id.average_speed_label,
|
||||
R.string.average_speed_label,
|
||||
R.string.average_pace_label);
|
||||
setSpeedLabel(R.id.average_moving_speed_label,
|
||||
R.string.average_moving_speed_label,
|
||||
R.string.average_moving_pace_label);
|
||||
setSpeedLabel(R.id.max_speed_label,
|
||||
R.string.max_speed_label,
|
||||
R.string.min_pace_label);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* 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.maps.mytracks.R;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.PackageManager.NameNotFoundException;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
import android.view.Window;
|
||||
import android.webkit.WebView;
|
||||
import android.widget.TextView;
|
||||
|
||||
/**
|
||||
* An activity that displays a welcome screen.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public class WelcomeActivity extends Activity {
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
requestWindowFeature(Window.FEATURE_NO_TITLE);
|
||||
requestWindowFeature(Window.FEATURE_PROGRESS);
|
||||
|
||||
setContentView(R.layout.welcome);
|
||||
|
||||
WebView web = (WebView) findViewById(R.id.welcome_web);
|
||||
web.loadUrl("file:///android_asset/welcome.html");
|
||||
|
||||
findViewById(R.id.welcome_read_later).setOnClickListener(
|
||||
new OnClickListener() {
|
||||
public void onClick(View v) {
|
||||
finish();
|
||||
}
|
||||
});
|
||||
|
||||
findViewById(R.id.welcome_about).setOnClickListener(new OnClickListener() {
|
||||
public void onClick(View v) {
|
||||
about();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the "about" dialog.
|
||||
*
|
||||
* TODO: Add a menu option for showing the same thing.
|
||||
*/
|
||||
public void about() {
|
||||
LayoutInflater li = LayoutInflater.from(this);
|
||||
View view = li.inflate(R.layout.about, null);
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
||||
builder.setView(view);
|
||||
builder.setPositiveButton(R.string.ok, null);
|
||||
builder.setIcon(R.drawable.arrow_icon);
|
||||
AlertDialog dialog = builder.create();
|
||||
dialog.show();
|
||||
((TextView) dialog.findViewById(R.id.about_version_register)).
|
||||
setText(MyTracks.getMyTracksVersion(this));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* 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.content;
|
||||
|
||||
/**
|
||||
* Utilities for serializing primitive types.
|
||||
*
|
||||
* @author Rodrigo Damazio
|
||||
*/
|
||||
public class ContentTypeIds {
|
||||
public static final byte BOOLEAN_TYPE_ID = 0;
|
||||
public static final byte LONG_TYPE_ID = 1;
|
||||
public static final byte INT_TYPE_ID = 2;
|
||||
public static final byte FLOAT_TYPE_ID = 3;
|
||||
public static final byte DOUBLE_TYPE_ID = 4;
|
||||
public static final byte STRING_TYPE_ID = 5;
|
||||
|
||||
private ContentTypeIds() { /* Not instantiable */ }
|
||||
}
|
||||
@@ -0,0 +1,416 @@
|
||||
/*
|
||||
* 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.MyTracksConstants;
|
||||
|
||||
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.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
/**
|
||||
* A provider that handles recorded (GPS) tracks and their track points.
|
||||
*
|
||||
* @author Leif Hendrik Wilden
|
||||
*/
|
||||
public class MyTracksProvider extends ContentProvider {
|
||||
|
||||
private static final String DATABASE_NAME = "mytracks.db";
|
||||
private static final int DATABASE_VERSION = 19;
|
||||
private static final int TRACKPOINTS = 1;
|
||||
private static final int TRACKPOINTS_ID = 2;
|
||||
private static final int TRACKS = 3;
|
||||
private static final int TRACKS_ID = 4;
|
||||
private static final int WAYPOINTS = 5;
|
||||
private static final int WAYPOINTS_ID = 6;
|
||||
private static final String TRACKPOINTS_TABLE = "trackpoints";
|
||||
private static final String TRACKS_TABLE = "tracks";
|
||||
private static final String WAYPOINTS_TABLE = "waypoints";
|
||||
public static final String TAG = "MyTracksProvider";
|
||||
|
||||
/**
|
||||
* Helper which creates or upgrades the database if necessary.
|
||||
*/
|
||||
private static class DatabaseHelper extends SQLiteOpenHelper {
|
||||
|
||||
public DatabaseHelper(Context context) {
|
||||
super(context, DATABASE_NAME, null, DATABASE_VERSION);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(SQLiteDatabase db) {
|
||||
db.execSQL("CREATE TABLE " + TRACKPOINTS_TABLE + " ("
|
||||
+ TrackPointsColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, "
|
||||
+ TrackPointsColumns.TRACKID + " INTEGER, "
|
||||
+ TrackPointsColumns.LONGITUDE + " INTEGER, "
|
||||
+ TrackPointsColumns.LATITUDE + " INTEGER, "
|
||||
+ TrackPointsColumns.TIME + " INTEGER, "
|
||||
+ TrackPointsColumns.ALTITUDE + " FLOAT, "
|
||||
+ TrackPointsColumns.ACCURACY + " FLOAT, "
|
||||
+ TrackPointsColumns.SPEED + " FLOAT, "
|
||||
+ TrackPointsColumns.BEARING + " FLOAT, "
|
||||
+ TrackPointsColumns.SENSOR + " BLOB);");
|
||||
db.execSQL("CREATE TABLE " + TRACKS_TABLE + " ("
|
||||
+ TracksColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, "
|
||||
+ TracksColumns.NAME + " STRING, "
|
||||
+ TracksColumns.DESCRIPTION + " STRING, "
|
||||
+ TracksColumns.CATEGORY + " STRING, "
|
||||
+ TracksColumns.STARTID + " INTEGER, "
|
||||
+ TracksColumns.STOPID + " INTEGER, "
|
||||
+ TracksColumns.STARTTIME + " INTEGER, "
|
||||
+ TracksColumns.STOPTIME + " INTEGER, "
|
||||
+ TracksColumns.NUMPOINTS + " INTEGER, "
|
||||
+ TracksColumns.TOTALDISTANCE + " FLOAT, "
|
||||
+ TracksColumns.TOTALTIME + " INTEGER, "
|
||||
+ TracksColumns.MOVINGTIME + " INTEGER, "
|
||||
+ TracksColumns.MINLAT + " INTEGER, "
|
||||
+ TracksColumns.MAXLAT + " INTEGER, "
|
||||
+ TracksColumns.MINLON + " INTEGER, "
|
||||
+ TracksColumns.MAXLON + " INTEGER, "
|
||||
+ TracksColumns.AVGSPEED + " FLOAT, "
|
||||
+ TracksColumns.AVGMOVINGSPEED + " FLOAT, "
|
||||
+ TracksColumns.MAXSPEED + " FLOAT, "
|
||||
+ TracksColumns.MINELEVATION + " FLOAT, "
|
||||
+ TracksColumns.MAXELEVATION + " FLOAT, "
|
||||
+ TracksColumns.ELEVATIONGAIN + " FLOAT, "
|
||||
+ TracksColumns.MINGRADE + " FLOAT, "
|
||||
+ TracksColumns.MAXGRADE + " FLOAT, "
|
||||
+ TracksColumns.MAPID + " STRING, "
|
||||
+ TracksColumns.TABLEID + " STRING);");
|
||||
db.execSQL("CREATE TABLE " + WAYPOINTS_TABLE + " ("
|
||||
+ WaypointsColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, "
|
||||
+ WaypointsColumns.NAME + " STRING, "
|
||||
+ WaypointsColumns.DESCRIPTION + " STRING, "
|
||||
+ WaypointsColumns.CATEGORY + " STRING, "
|
||||
+ WaypointsColumns.ICON + " STRING, "
|
||||
+ WaypointsColumns.TRACKID + " INTEGER, "
|
||||
+ WaypointsColumns.TYPE + " INTEGER, "
|
||||
+ WaypointsColumns.LENGTH + " FLOAT, "
|
||||
+ WaypointsColumns.DURATION + " INTEGER, "
|
||||
+ WaypointsColumns.STARTTIME + " INTEGER, "
|
||||
+ WaypointsColumns.STARTID + " INTEGER, "
|
||||
+ WaypointsColumns.STOPID + " INTEGER, "
|
||||
+ WaypointsColumns.LONGITUDE + " INTEGER, "
|
||||
+ WaypointsColumns.LATITUDE + " INTEGER, "
|
||||
+ WaypointsColumns.TIME + " INTEGER, "
|
||||
+ WaypointsColumns.ALTITUDE + " FLOAT, "
|
||||
+ WaypointsColumns.ACCURACY + " FLOAT, "
|
||||
+ WaypointsColumns.SPEED + " FLOAT, "
|
||||
+ WaypointsColumns.BEARING + " FLOAT, "
|
||||
+ WaypointsColumns.TOTALDISTANCE + " FLOAT, "
|
||||
+ WaypointsColumns.TOTALTIME + " INTEGER, "
|
||||
+ WaypointsColumns.MOVINGTIME + " INTEGER, "
|
||||
+ WaypointsColumns.AVGSPEED + " FLOAT, "
|
||||
+ WaypointsColumns.AVGMOVINGSPEED + " FLOAT, "
|
||||
+ WaypointsColumns.MAXSPEED + " FLOAT, "
|
||||
+ WaypointsColumns.MINELEVATION + " FLOAT, "
|
||||
+ WaypointsColumns.MAXELEVATION + " FLOAT, "
|
||||
+ WaypointsColumns.ELEVATIONGAIN + " FLOAT, "
|
||||
+ WaypointsColumns.MINGRADE + " FLOAT, "
|
||||
+ WaypointsColumns.MAXGRADE + " FLOAT);");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
|
||||
if (oldVersion < 17) {
|
||||
Log.w(TAG, "Upgrading database from version " + oldVersion + " to "
|
||||
+ newVersion + ", which will destroy all old data");
|
||||
db.execSQL("DROP TABLE IF EXISTS " + TRACKPOINTS_TABLE);
|
||||
db.execSQL("DROP TABLE IF EXISTS " + TRACKS_TABLE);
|
||||
db.execSQL("DROP TABLE IF EXISTS " + WAYPOINTS_TABLE);
|
||||
onCreate(db);
|
||||
} else {
|
||||
if (oldVersion == 17) {
|
||||
Log.w(TAG, "Upgrading database from version " + oldVersion + " to "
|
||||
+ newVersion + ", adding sensor column.");
|
||||
db.execSQL("ALTER TABLE " + TRACKPOINTS_TABLE
|
||||
+ " ADD " + TrackPointsColumns.SENSOR + " BLOB");
|
||||
};
|
||||
if (oldVersion >= 17) {
|
||||
Log.w(TAG, "Upgrading database from version " + oldVersion + " to "
|
||||
+ newVersion + ", adding tableid column.");
|
||||
db.execSQL("ALTER TABLE " + TRACKS_TABLE
|
||||
+ " ADD " + TracksColumns.TABLEID + " STRING");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final UriMatcher urlMatcher;
|
||||
|
||||
private SQLiteDatabase db;
|
||||
|
||||
public MyTracksProvider() {
|
||||
urlMatcher = new UriMatcher(UriMatcher.NO_MATCH);
|
||||
urlMatcher.addURI(MyTracksProviderUtils.AUTHORITY,
|
||||
"trackpoints", TRACKPOINTS);
|
||||
urlMatcher.addURI(MyTracksProviderUtils.AUTHORITY,
|
||||
"trackpoints/#", TRACKPOINTS_ID);
|
||||
urlMatcher.addURI(MyTracksProviderUtils.AUTHORITY, "tracks", TRACKS);
|
||||
urlMatcher.addURI(MyTracksProviderUtils.AUTHORITY, "tracks/#", TRACKS_ID);
|
||||
urlMatcher.addURI(MyTracksProviderUtils.AUTHORITY, "waypoints", WAYPOINTS);
|
||||
urlMatcher.addURI(MyTracksProviderUtils.AUTHORITY,
|
||||
"waypoints/#", WAYPOINTS_ID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreate() {
|
||||
DatabaseHelper dbHelper = new DatabaseHelper(getContext());
|
||||
db = dbHelper.getWritableDatabase();
|
||||
return db != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int delete(Uri url, String where, String[] selectionArgs) {
|
||||
String table;
|
||||
switch (urlMatcher.match(url)) {
|
||||
case TRACKPOINTS:
|
||||
table = TRACKPOINTS_TABLE;
|
||||
break;
|
||||
case TRACKS:
|
||||
table = TRACKS_TABLE;
|
||||
break;
|
||||
case WAYPOINTS:
|
||||
table = WAYPOINTS_TABLE;
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown URL " + url);
|
||||
}
|
||||
|
||||
Log.w(MyTracksProvider.TAG, "provider delete in " + table + "!");
|
||||
int count = db.delete(table, where, selectionArgs);
|
||||
getContext().getContentResolver().notifyChange(url, null, true);
|
||||
return count;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getType(Uri url) {
|
||||
switch (urlMatcher.match(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) {
|
||||
Log.d(MyTracksProvider.TAG, "MyTracksProvider.insert");
|
||||
ContentValues values;
|
||||
if (initialValues != null) {
|
||||
values = initialValues;
|
||||
} else {
|
||||
values = new ContentValues();
|
||||
}
|
||||
|
||||
int urlMatchType = urlMatcher.match(url);
|
||||
return insertType(url, urlMatchType, values);
|
||||
}
|
||||
|
||||
private Uri insertType(Uri url, int urlMatchType, ContentValues values) {
|
||||
switch (urlMatchType) {
|
||||
case TRACKPOINTS:
|
||||
return insertTrackPoint(url, values);
|
||||
case TRACKS:
|
||||
return insertTrack(url, values);
|
||||
case WAYPOINTS:
|
||||
return insertWaypoint(url, values);
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown URL " + url);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public int bulkInsert(Uri url, ContentValues[] valuesBulk) {
|
||||
Log.d(MyTracksProvider.TAG, "MyTracksProvider.bulkInsert");
|
||||
int numInserted = 0;
|
||||
try {
|
||||
// Use a transaction in order to make the insertions run as a single batch
|
||||
db.beginTransaction();
|
||||
|
||||
int urlMatch = urlMatcher.match(url);
|
||||
for (numInserted = 0; numInserted < valuesBulk.length; numInserted++) {
|
||||
ContentValues values = valuesBulk[numInserted];
|
||||
if (values == null) { values = new ContentValues(); }
|
||||
|
||||
insertType(url, urlMatch, values);
|
||||
}
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
|
||||
return numInserted;
|
||||
}
|
||||
|
||||
private Uri insertTrackPoint(Uri url, ContentValues values) {
|
||||
boolean hasLat = values.containsKey(TrackPointsColumns.LATITUDE);
|
||||
boolean hasLong = values.containsKey(TrackPointsColumns.LONGITUDE);
|
||||
boolean hasTime = values.containsKey(TrackPointsColumns.TIME);
|
||||
if (!hasLat || !hasLong || !hasTime) {
|
||||
throw new IllegalArgumentException(
|
||||
"Latitude, longitude, and time values are required.");
|
||||
}
|
||||
long rowId = db.insert(TRACKPOINTS_TABLE, 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 row into " + url);
|
||||
}
|
||||
|
||||
private Uri insertTrack(Uri url, ContentValues values) {
|
||||
boolean hasStartTime = values.containsKey(TracksColumns.STARTTIME);
|
||||
boolean hasStartId = values.containsKey(TracksColumns.STARTID);
|
||||
if (!hasStartTime || !hasStartId) {
|
||||
throw new IllegalArgumentException(
|
||||
"Both start time and start id values are required.");
|
||||
}
|
||||
long rowId = db.insert(TRACKS_TABLE, TracksColumns._ID, values);
|
||||
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 row into " + url);
|
||||
}
|
||||
|
||||
private Uri insertWaypoint(Uri url, ContentValues values) {
|
||||
long rowId = db.insert(WAYPOINTS_TABLE, WaypointsColumns._ID, values);
|
||||
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 row into " + url);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cursor query(
|
||||
Uri url, String[] projection, String selection, String[] selectionArgs,
|
||||
String sort) {
|
||||
SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
|
||||
int match = urlMatcher.match(url);
|
||||
String sortOrder = null;
|
||||
if (match == TRACKPOINTS) {
|
||||
qb.setTables(TRACKPOINTS_TABLE);
|
||||
if (sort != null) {
|
||||
sortOrder = sort;
|
||||
} else {
|
||||
sortOrder = TrackPointsColumns.DEFAULT_SORT_ORDER;
|
||||
}
|
||||
} else if (match == TRACKPOINTS_ID) {
|
||||
qb.setTables(TRACKPOINTS_TABLE);
|
||||
qb.appendWhere("_id=" + url.getPathSegments().get(1));
|
||||
} else if (match == TRACKS) {
|
||||
qb.setTables(TRACKS_TABLE);
|
||||
if (sort != null) {
|
||||
sortOrder = sort;
|
||||
} else {
|
||||
sortOrder = TracksColumns.DEFAULT_SORT_ORDER;
|
||||
}
|
||||
} else if (match == TRACKS_ID) {
|
||||
qb.setTables(TRACKS_TABLE);
|
||||
qb.appendWhere("_id=" + url.getPathSegments().get(1));
|
||||
} else if (match == WAYPOINTS) {
|
||||
qb.setTables(WAYPOINTS_TABLE);
|
||||
if (sort != null) {
|
||||
sortOrder = sort;
|
||||
} else {
|
||||
sortOrder = WaypointsColumns.DEFAULT_SORT_ORDER;
|
||||
}
|
||||
} else if (match == WAYPOINTS_ID) {
|
||||
qb.setTables(WAYPOINTS_TABLE);
|
||||
qb.appendWhere("_id=" + url.getPathSegments().get(1));
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unknown URL " + url);
|
||||
}
|
||||
|
||||
Log.i(MyTracksConstants.TAG,
|
||||
"Build query: " + qb.buildQuery(projection, selection, selectionArgs, null, null, sortOrder, null));
|
||||
Cursor c = qb.query(db, projection, selection, selectionArgs, null, null,
|
||||
sortOrder);
|
||||
c.setNotificationUri(getContext().getContentResolver(), url);
|
||||
return c;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int update(Uri url, ContentValues values, String where,
|
||||
String[] selectionArgs) {
|
||||
int count;
|
||||
int match = urlMatcher.match(url);
|
||||
if (match == TRACKPOINTS) {
|
||||
count = db.update(TRACKPOINTS_TABLE, values, where, selectionArgs);
|
||||
} else if (match == TRACKPOINTS_ID) {
|
||||
String segment = url.getPathSegments().get(1);
|
||||
count = db.update(TRACKPOINTS_TABLE, values, "_id=" + segment
|
||||
+ (!TextUtils.isEmpty(where)
|
||||
? " AND (" + where + ')'
|
||||
: ""),
|
||||
selectionArgs);
|
||||
} else if (match == TRACKS) {
|
||||
count = db.update(TRACKS_TABLE, values, where, selectionArgs);
|
||||
} else if (match == TRACKS_ID) {
|
||||
String segment = url.getPathSegments().get(1);
|
||||
count = db.update(TRACKS_TABLE, values, "_id=" + segment
|
||||
+ (!TextUtils.isEmpty(where)
|
||||
? " AND (" + where + ')'
|
||||
: ""),
|
||||
selectionArgs);
|
||||
} else if (match == WAYPOINTS) {
|
||||
count = db.update(WAYPOINTS_TABLE, values, where, selectionArgs);
|
||||
} else if (match == WAYPOINTS_ID) {
|
||||
String segment = url.getPathSegments().get(1);
|
||||
count = db.update(WAYPOINTS_TABLE, values, "_id=" + segment
|
||||
+ (!TextUtils.isEmpty(where)
|
||||
? " AND (" + where + ')'
|
||||
: ""),
|
||||
selectionArgs);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unknown URL " + url);
|
||||
}
|
||||
getContext().getContentResolver().notifyChange(url, null, true);
|
||||
return count;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* Copyright 2010 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package com.google.android.apps.mytracks.io;
|
||||
|
||||
import android.content.Intent;
|
||||
|
||||
/**
|
||||
* This interface describes a class that will fetch and maintain a Google
|
||||
* authentication token.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public interface AuthManager {
|
||||
|
||||
/**
|
||||
* Initializes the login process. The user should be asked to login if they
|
||||
* haven't already. The {@link Runnable} provided will be executed when the
|
||||
* auth token is successfully fetched.
|
||||
*
|
||||
* @param whenFinished A {@link Runnable} to execute when the auth token
|
||||
* has been successfully fetched and is available via
|
||||
* {@link #getAuthToken()}
|
||||
*/
|
||||
public abstract void doLogin(Runnable whenFinished, Object o);
|
||||
|
||||
/**
|
||||
* The {@link android.app.Activity} owner of this class should call this
|
||||
* function when it gets {@link android.app.Activity#onActivityResult} with
|
||||
* the request code passed into the constructor. The resultCode and results
|
||||
* should come directly from the {@link android.app.Activity#onActivityResult}
|
||||
* function. This function will return true if an auth token was successfully
|
||||
* fetched or the process is not finished.
|
||||
*
|
||||
* @param resultCode The result code passed in to the
|
||||
* {@link android.app.Activity}'s
|
||||
* {@link android.app.Activity#onActivityResult} function
|
||||
* @param results The data passed in to the {@link android.app.Activity}'s
|
||||
* {@link android.app.Activity#onActivityResult} function
|
||||
* @return True if the auth token was fetched or we aren't done fetching
|
||||
* the auth token, or False if there was an error or the request was
|
||||
* canceled
|
||||
*/
|
||||
public abstract boolean authResult(int resultCode, Intent results);
|
||||
|
||||
/**
|
||||
* Returns the current auth token. Response may be null if no valid auth
|
||||
* token has been fetched.
|
||||
*
|
||||
* @return The current auth token or null if no auth token has been
|
||||
* fetched
|
||||
*/
|
||||
public abstract String getAuthToken();
|
||||
|
||||
/**
|
||||
* Invalidates the existing auth token and request a new one. The
|
||||
* {@link Runnable} provided will be executed when the new auth token is
|
||||
* successfully fetched.
|
||||
*
|
||||
* @param whenFinished A {@link Runnable} to execute when a new auth token
|
||||
* is successfully fetched
|
||||
*/
|
||||
public abstract void invalidateAndRefresh(Runnable whenFinished);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright 2010 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package com.google.android.apps.mytracks.io;
|
||||
|
||||
import com.google.android.apps.mytracks.MyTracksConstants;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
|
||||
/**
|
||||
* A factory for getting the platform specific AuthManager.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public class AuthManagerFactory {
|
||||
|
||||
private AuthManagerFactory() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the modern AuthManager should be used
|
||||
*/
|
||||
public static boolean useModernAuthManager() {
|
||||
return Integer.parseInt(Build.VERSION.SDK) >= 7;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a right {@link AuthManager} for the platform.
|
||||
* @return A new AuthManager
|
||||
*/
|
||||
public static AuthManager getAuthManager(Activity activity, int code,
|
||||
Bundle extras, boolean requireGoogle, String service) {
|
||||
if (useModernAuthManager()) {
|
||||
Log.i(MyTracksConstants.TAG,
|
||||
"Creating modern auth manager: " + service);
|
||||
return new ModernAuthManager(activity, code, extras, requireGoogle, service);
|
||||
} else {
|
||||
Log.i(MyTracksConstants.TAG,
|
||||
"Creating legacy auth manager: " + service);
|
||||
return new AuthManagerOld(activity, code, extras, requireGoogle, service);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
/*
|
||||
* Copyright 2009 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package com.google.android.apps.mytracks.io;
|
||||
|
||||
import com.google.android.googlelogindist.GoogleLoginServiceConstants;
|
||||
import com.google.android.googlelogindist.GoogleLoginServiceHelper;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.Vector;
|
||||
|
||||
/**
|
||||
* AuthManager keeps track of the current auth token for a user. The advantage
|
||||
* over just passing around a String is that this class can renew the auth
|
||||
* token if necessary, and it will change for all classes using this
|
||||
* AuthManager.
|
||||
*/
|
||||
public class AuthManagerOld implements AuthManager {
|
||||
/** The activity that will handle auth result callbacks. */
|
||||
private final Activity activity;
|
||||
|
||||
/** The code used to tell the activity that it is an auth result. */
|
||||
private final int code;
|
||||
|
||||
/** Extras to pass into the getCredentials function. */
|
||||
private final Bundle extras;
|
||||
|
||||
/** True if the account must be a Google account (not a domain account). */
|
||||
private final boolean requireGoogle;
|
||||
|
||||
/** The name of the service to authorize for. */
|
||||
private final String service;
|
||||
|
||||
/** A list of handlers to call when a new auth token is fetched. */
|
||||
private final Vector<Runnable> newTokenListeners = new Vector<Runnable>();
|
||||
|
||||
/** The most recently fetched auth token or null if none is available. */
|
||||
private String authToken;
|
||||
|
||||
/**
|
||||
* The number of handlers at the beginning of the above list that shouldn't
|
||||
* be removed after they are called.
|
||||
*/
|
||||
private int stickyNewTokenListenerCount;
|
||||
|
||||
/**
|
||||
* AuthManager requires many of the same parameters as
|
||||
* {@link GoogleLoginServiceHelper#getCredentials(Activity, int, Bundle,
|
||||
* boolean, String, boolean)}. The activity must have
|
||||
* a handler in {@link Activity#onActivityResult} that calls
|
||||
* {@link #authResult(int, Intent)} if the request code is the code given
|
||||
* here.
|
||||
*
|
||||
* @param activity An activity with a handler in
|
||||
* {@link Activity#onActivityResult} that calls
|
||||
* {@link #authResult(int, Intent)} when {@literal code} is the request
|
||||
* code
|
||||
* @param code The request code to pass to
|
||||
* {@link Activity#onActivityResult} when
|
||||
* {@link #authResult(int, Intent)} should be called
|
||||
* @param extras A {@link Bundle} of extras for
|
||||
* {@link GoogleLoginServiceHelper}
|
||||
* @param requireGoogle True if the account must be a Google account
|
||||
* @param service The name of the service to authenticate as
|
||||
*/
|
||||
public AuthManagerOld(Activity activity, int code, Bundle extras,
|
||||
boolean requireGoogle, String service) {
|
||||
this.activity = activity;
|
||||
this.code = code;
|
||||
this.extras = extras;
|
||||
this.requireGoogle = requireGoogle;
|
||||
this.service = service;
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see com.google.android.apps.mytracks.io.AuthManager#doLogin(java.lang.Runnable)
|
||||
*/
|
||||
public void doLogin(Runnable whenFinished, Object o) {
|
||||
synchronized (newTokenListeners) {
|
||||
if (whenFinished != null) {
|
||||
newTokenListeners.add(whenFinished);
|
||||
}
|
||||
}
|
||||
activity.runOnUiThread(new LoginRunnable());
|
||||
}
|
||||
|
||||
/**
|
||||
* Runnable which actually gets login credentials.
|
||||
*/
|
||||
private class LoginRunnable implements Runnable {
|
||||
@Override
|
||||
public void run() {
|
||||
GoogleLoginServiceHelper.getCredentials(
|
||||
activity, code, extras, requireGoogle, service, true);
|
||||
}
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see com.google.android.apps.mytracks.io.AuthManager#authResult(int, android.content.Intent)
|
||||
*/
|
||||
public boolean authResult(int resultCode, Intent results) {
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
authToken = results.getStringExtra(
|
||||
GoogleLoginServiceConstants.AUTHTOKEN_KEY);
|
||||
if (authToken == null) {
|
||||
GoogleLoginServiceHelper.getCredentials(
|
||||
activity, code, extras, requireGoogle, service, false);
|
||||
return true;
|
||||
} else {
|
||||
// Notify all active listeners that we have a new auth token.
|
||||
synchronized (newTokenListeners) {
|
||||
Iterator<Runnable> iter = newTokenListeners.iterator();
|
||||
while (iter.hasNext()) {
|
||||
iter.next().run();
|
||||
}
|
||||
iter = null;
|
||||
// Remove anything not in the sticky part of the list.
|
||||
newTokenListeners.setSize(stickyNewTokenListenerCount);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see com.google.android.apps.mytracks.io.AuthManager#getAuthToken()
|
||||
*/
|
||||
public String getAuthToken() {
|
||||
return authToken;
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see com.google.android.apps.mytracks.io.AuthManager#invalidateAndRefresh(java.lang.Runnable)
|
||||
*/
|
||||
public void invalidateAndRefresh(Runnable whenFinished) {
|
||||
synchronized (newTokenListeners) {
|
||||
if (whenFinished != null) {
|
||||
newTokenListeners.add(whenFinished);
|
||||
}
|
||||
}
|
||||
activity.runOnUiThread(new Runnable() {
|
||||
public void run() {
|
||||
GoogleLoginServiceHelper.invalidateAuthToken(activity, code, authToken);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a {@link Runnable} to be executed every time the auth token is
|
||||
* updated. The {@link Runnable} will not be removed until manually removed
|
||||
* with {@link #removeStickyNewTokenListener(Runnable)}.
|
||||
*
|
||||
* @param listener The {@link Runnable} to execute every time a new auth
|
||||
* token is fetched
|
||||
*/
|
||||
public void addStickyNewTokenListener(Runnable listener) {
|
||||
synchronized (newTokenListeners) {
|
||||
newTokenListeners.add(0, listener);
|
||||
stickyNewTokenListenerCount++;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops executing the given {@link Runnable} every time the auth token is
|
||||
* updated. This {@link Runnable} must have been added with
|
||||
* {@link #addStickyNewTokenListener(Runnable)} above. If the
|
||||
* {@link Runnable} was added more than once, only the first occurrence
|
||||
* will be removed.
|
||||
*
|
||||
* @param listener The {@link Runnable} to stop executing every time a new
|
||||
* auth token is fetched
|
||||
*/
|
||||
public void removeStickyNewTokenListener(Runnable listener) {
|
||||
synchronized (newTokenListeners) {
|
||||
if (stickyNewTokenListenerCount > 0
|
||||
&& newTokenListeners.remove(listener)) {
|
||||
stickyNewTokenListenerCount--;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
/*
|
||||
* Copyright 2010 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package com.google.android.apps.mytracks.io;
|
||||
|
||||
import com.google.android.apps.mytracks.content.Track;
|
||||
import com.google.android.apps.mytracks.content.Waypoint;
|
||||
import com.google.android.apps.mytracks.io.TrackWriterFactory.TrackFileFormat;
|
||||
|
||||
import android.location.Location;
|
||||
|
||||
import java.io.OutputStream;
|
||||
import java.io.PrintWriter;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.TimeZone;
|
||||
|
||||
/**
|
||||
* Exports a track as a CSV file, according to RFC 4180.
|
||||
*
|
||||
* The first field is a type:
|
||||
* TRACK - track description
|
||||
* P - point
|
||||
* WAYPOINT - waypoint
|
||||
*
|
||||
* For each type, the fields are:
|
||||
*
|
||||
* TRACK,,,,,,,,name,description,
|
||||
* P,time,lat,lon,alt,bearing,accurancy,speed,,,segmentIdx
|
||||
* WAYPOINT,time,lat,lon,alt,bearing,accuracy,speed,name,description,
|
||||
*
|
||||
* @author Rodrigo Damazio
|
||||
*/
|
||||
public class CsvTrackWriter implements TrackFormatWriter {
|
||||
static final SimpleDateFormat TIMESTAMP_FORMAT =
|
||||
new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
|
||||
static {
|
||||
TIMESTAMP_FORMAT.setTimeZone(TimeZone.getTimeZone("UTC"));
|
||||
}
|
||||
|
||||
private int segmentIdx = 0;
|
||||
private int numFields = -1;
|
||||
private PrintWriter pw;
|
||||
private Track track;
|
||||
|
||||
@Override
|
||||
public String getExtension() {
|
||||
return TrackFileFormat.CSV.getExtension();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void prepare(Track track, OutputStream out) {
|
||||
this.track = track;
|
||||
this.pw = new PrintWriter(out);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeHeader() {
|
||||
writeCommaSeparatedLine("TYPE", "TIME", "LAT", "LON", "ALT", "BEARING",
|
||||
"ACCURACY", "SPEED", "NAME", "DESCRIPTION", "SEGMENT");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeBeginTrack(Location firstPoint) {
|
||||
writeCommaSeparatedLine("TRACK",
|
||||
null, null, null, null, null, null, null,
|
||||
track.getName(), track.getDescription(),
|
||||
null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeOpenSegment() {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeLocation(Location location) {
|
||||
String timeStr = TIMESTAMP_FORMAT.format(new Date(location.getTime()));
|
||||
writeCommaSeparatedLine("P",
|
||||
timeStr,
|
||||
Double.toString(location.getLatitude()),
|
||||
Double.toString(location.getLongitude()),
|
||||
Double.toString(location.getAltitude()),
|
||||
Double.toString(location.getBearing()),
|
||||
Double.toString(location.getAccuracy()),
|
||||
Double.toString(location.getSpeed()),
|
||||
null, null,
|
||||
Integer.toString(segmentIdx));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeWaypoint(Waypoint waypoint) {
|
||||
Location location = waypoint.getLocation();
|
||||
String timeStr = TIMESTAMP_FORMAT.format(new Date(location.getTime()));
|
||||
writeCommaSeparatedLine("WAYPOINT",
|
||||
timeStr,
|
||||
Double.toString(location.getLatitude()),
|
||||
Double.toString(location.getLongitude()),
|
||||
Double.toString(location.getAltitude()),
|
||||
Double.toString(location.getBearing()),
|
||||
Double.toString(location.getAccuracy()),
|
||||
Double.toString(location.getSpeed()),
|
||||
waypoint.getName(),
|
||||
waypoint.getDescription(),
|
||||
null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a single line of a comma-separated-value file.
|
||||
*
|
||||
* @param strs the values to be written as comma-separated
|
||||
*/
|
||||
private void writeCommaSeparatedLine(String... strs) {
|
||||
if (numFields == -1) {
|
||||
numFields = strs.length;
|
||||
} else if (strs.length != numFields) {
|
||||
throw new IllegalArgumentException(
|
||||
"CSV lines with different number of fields");
|
||||
}
|
||||
|
||||
boolean isFirst = true;
|
||||
for (String str : strs) {
|
||||
if (!isFirst) {
|
||||
pw.print(',');
|
||||
}
|
||||
isFirst = false;
|
||||
|
||||
if (str != null) {
|
||||
pw.print('"');
|
||||
pw.print(str.replaceAll("\"", "\"\""));
|
||||
pw.print('"');
|
||||
}
|
||||
}
|
||||
pw.println();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeCloseSegment() {
|
||||
segmentIdx++;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeEndTrack(Location lastPoint) {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeFooter() {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
pw.close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,541 @@
|
||||
/*
|
||||
* Copyright 2010 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package com.google.android.apps.mytracks.io;
|
||||
|
||||
import com.google.android.apps.mytracks.MyTracksConstants;
|
||||
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.MyTracksUtils;
|
||||
import com.google.android.apps.mytracks.util.StringUtils;
|
||||
|
||||
import android.location.Location;
|
||||
import android.location.LocationManager;
|
||||
import android.net.Uri;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import javax.xml.parsers.ParserConfigurationException;
|
||||
import javax.xml.parsers.SAXParser;
|
||||
import javax.xml.parsers.SAXParserFactory;
|
||||
|
||||
import org.xml.sax.Attributes;
|
||||
import org.xml.sax.Locator;
|
||||
import org.xml.sax.SAXException;
|
||||
import org.xml.sax.helpers.DefaultHandler;
|
||||
|
||||
/**
|
||||
* Imports GPX XML files to the my tracks provider.
|
||||
*
|
||||
* TODO: Show progress indication to the user.
|
||||
*
|
||||
* @author Leif Hendrik Wilden
|
||||
* @author Steffen Horlacher
|
||||
* @author Rodrigo Damazio
|
||||
*/
|
||||
public class GpxImporter extends DefaultHandler {
|
||||
|
||||
/*
|
||||
* GPX-XML tag names and attributes.
|
||||
*/
|
||||
private static final String TAG_TRACK = "trk";
|
||||
private static final String TAG_TRACK_POINT = "trkpt";
|
||||
private static final Object TAG_TRACK_SEGMENT = "trkseg";
|
||||
private static final String TAG_NAME = "name";
|
||||
private static final String TAG_DESCRIPTION = "desc";
|
||||
private static final String TAG_ALTITUDE = "ele";
|
||||
private static final String TAG_TIME = "time";
|
||||
private static final String ATT_LAT = "lat";
|
||||
private static final String ATT_LON = "lon";
|
||||
|
||||
/**
|
||||
* The maximum number of locations to buffer for bulk-insertion into the database.
|
||||
*/
|
||||
private static final int MAX_BUFFERED_LOCATIONS = 512;
|
||||
|
||||
/**
|
||||
* Utilities for accessing the contnet provider.
|
||||
*/
|
||||
private final MyTracksProviderUtils providerUtils;
|
||||
|
||||
/**
|
||||
* List of track ids written in the database. Only contains successfully
|
||||
* written tracks.
|
||||
*/
|
||||
private final List<Long> tracksWritten;
|
||||
|
||||
/**
|
||||
* Contains the current elements content.
|
||||
*/
|
||||
private String content;
|
||||
|
||||
/**
|
||||
* Currently reading location.
|
||||
*/
|
||||
private Location location;
|
||||
|
||||
/**
|
||||
* Previous location, required for calculations.
|
||||
*/
|
||||
private Location lastLocation;
|
||||
|
||||
/**
|
||||
* Currently reading track.
|
||||
*/
|
||||
private Track track;
|
||||
|
||||
/**
|
||||
* Statistics builder for the current track.
|
||||
*/
|
||||
private TripStatisticsBuilder statsBuilder;
|
||||
|
||||
/**
|
||||
* Buffer of locations to be bulk-inserted into the database.
|
||||
*/
|
||||
private Location[] bufferedPointInserts = new Location[MAX_BUFFERED_LOCATIONS];
|
||||
|
||||
/**
|
||||
* Number of locations buffered to be inserted into the database.
|
||||
*/
|
||||
private int numBufferedPointInserts = 0;
|
||||
|
||||
/**
|
||||
* Number of locations already processed.
|
||||
*/
|
||||
private int numberOfLocations;
|
||||
|
||||
/**
|
||||
* Number of segments already processed.
|
||||
*/
|
||||
private int numberOfSegments;
|
||||
|
||||
/**
|
||||
* Used to identify if a track was written to the database but not yet
|
||||
* finished successfully.
|
||||
*/
|
||||
private boolean isCurrentTrackRollbackable;
|
||||
|
||||
/**
|
||||
* Flag to indicate if we're inside a track's xml element.
|
||||
* Some sub elements like name may be used in other parts of the gpx file,
|
||||
* and we use this to ignore them.
|
||||
*/
|
||||
private boolean isInTrackElement;
|
||||
|
||||
/**
|
||||
* Counter to find out which child level of track we are processing.
|
||||
*/
|
||||
private int trackChildDepth;
|
||||
|
||||
/**
|
||||
* SAX-Locator to get current line information.
|
||||
*/
|
||||
private Locator locator;
|
||||
private Location lastSegmentLocation;
|
||||
|
||||
/**
|
||||
* Reads GPS tracks from a GPX file and writes tracks and their coordinates to
|
||||
* the database.
|
||||
*
|
||||
* @param is a input steam with gpx-xml data
|
||||
* @return long[] array of track ids written in the database
|
||||
* @throws SAXException a parsing error
|
||||
* @throws ParserConfigurationException internal error
|
||||
* @throws IOException a file reading problem
|
||||
*/
|
||||
public static long[] importGPXFile(final InputStream is,
|
||||
final MyTracksProviderUtils providerUtils)
|
||||
throws ParserConfigurationException, SAXException, IOException {
|
||||
SAXParserFactory factory = SAXParserFactory.newInstance();
|
||||
GpxImporter handler = new GpxImporter(providerUtils);
|
||||
SAXParser parser = factory.newSAXParser();
|
||||
long[] trackIds = null;
|
||||
|
||||
try {
|
||||
long start = System.currentTimeMillis();
|
||||
|
||||
parser.parse(is, handler);
|
||||
|
||||
long end = System.currentTimeMillis();
|
||||
Log.d(MyTracksConstants.TAG, "Total import time: " + (end - start) + "ms");
|
||||
|
||||
trackIds = handler.getImportedTrackIds();
|
||||
} finally {
|
||||
// delete track if not finished
|
||||
handler.rollbackUnfinishedTracks();
|
||||
}
|
||||
|
||||
return trackIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor, requires providerUtils for writing tracks the database.
|
||||
*/
|
||||
public GpxImporter(MyTracksProviderUtils providerUtils) {
|
||||
this.providerUtils = providerUtils;
|
||||
tracksWritten = new ArrayList<Long>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void characters(char[] ch, int start, int length) throws SAXException {
|
||||
String newContent = new String(ch, start, length);
|
||||
if (content == null) {
|
||||
content = newContent;
|
||||
} else {
|
||||
// In 99% of the cases, a single call to this method will be made for each
|
||||
// sequence of characters we're interested in, so we'll rarely be
|
||||
// concatenating strings, thus not justifying the use of a StringBuilder.
|
||||
content += newContent;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startElement(String uri, String localName, String name,
|
||||
Attributes attributes) throws SAXException {
|
||||
if (isInTrackElement) {
|
||||
trackChildDepth++;
|
||||
if (localName.equals(TAG_TRACK_POINT)) {
|
||||
onTrackPointElementStart(attributes);
|
||||
} else if (localName.equals(TAG_TRACK_SEGMENT)) {
|
||||
onTrackSegmentElementStart();
|
||||
} else if (localName.equals(TAG_TRACK)) {
|
||||
String msg = createErrorMessage("Invalid GPX-XML detected");
|
||||
throw new SAXException(msg);
|
||||
}
|
||||
} else if (localName.equals(TAG_TRACK)) {
|
||||
isInTrackElement = true;
|
||||
trackChildDepth = 0;
|
||||
onTrackElementStart();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void endElement(String uri, String localName, String name)
|
||||
throws SAXException {
|
||||
if (!isInTrackElement) { return; }
|
||||
|
||||
// process these elements only as sub-elements of track
|
||||
if (localName.equals(TAG_TRACK_POINT)) {
|
||||
onTrackPointElementEnd();
|
||||
} else if (localName.equals(TAG_ALTITUDE)) {
|
||||
onAltitudeElementEnd();
|
||||
} else if (localName.equals(TAG_TIME)) {
|
||||
onTimeElementEnd();
|
||||
} else if (localName.equals(TAG_NAME)) {
|
||||
// we are only interested in the first level name element
|
||||
if (trackChildDepth == 1) {
|
||||
onNameElementEnd();
|
||||
}
|
||||
} else if (localName.equals(TAG_DESCRIPTION)) {
|
||||
// we are only interested in the first level description element
|
||||
if (trackChildDepth == 1) {
|
||||
onDescriptionElementEnd();
|
||||
}
|
||||
} else if (localName.equals(TAG_TRACK_SEGMENT)) {
|
||||
onTrackSegmentElementEnd();
|
||||
} else if (localName.equals(TAG_TRACK)) {
|
||||
onTrackElementEnd();
|
||||
isInTrackElement = false;
|
||||
trackChildDepth = 0;
|
||||
}
|
||||
trackChildDepth--;
|
||||
|
||||
// reset element content
|
||||
content = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setDocumentLocator(Locator locator) {
|
||||
this.locator = locator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new Track object and insert empty track in database. Track will be
|
||||
* updated with missing values later.
|
||||
*/
|
||||
private void onTrackElementStart() {
|
||||
track = new Track();
|
||||
numberOfLocations = 0;
|
||||
|
||||
Uri trackUri = providerUtils.insertTrack(track);
|
||||
long trackId = Long.parseLong(trackUri.getLastPathSegment());
|
||||
track.setId(trackId);
|
||||
isCurrentTrackRollbackable = true;
|
||||
}
|
||||
|
||||
private void onDescriptionElementEnd() {
|
||||
track.setDescription(content.toString().trim());
|
||||
}
|
||||
|
||||
private void onNameElementEnd() {
|
||||
track.setName(content.toString().trim());
|
||||
}
|
||||
|
||||
/**
|
||||
* Track segment started.
|
||||
*/
|
||||
private void onTrackSegmentElementStart() {
|
||||
if (numberOfSegments > 0) {
|
||||
// Add a segment separator:
|
||||
location = new Location(LocationManager.GPS_PROVIDER);
|
||||
location.setLatitude(100.0);
|
||||
location.setLongitude(100.0);
|
||||
location.setAltitude(0);
|
||||
if (lastLocation != null) {
|
||||
location.setTime(lastLocation.getTime());
|
||||
}
|
||||
insertTrackPoint(location);
|
||||
lastLocation = location;
|
||||
lastSegmentLocation = null;
|
||||
location = null;
|
||||
}
|
||||
|
||||
numberOfSegments++;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads trackpoint attributes and assigns them to the current location.
|
||||
*
|
||||
* @param attributes xml attributes
|
||||
*/
|
||||
private void onTrackPointElementStart(Attributes attributes) throws SAXException {
|
||||
if (location != null) {
|
||||
String errorMsg = createErrorMessage("Found a track point inside another one.");
|
||||
throw new SAXException(errorMsg);
|
||||
}
|
||||
|
||||
location = createLocationFromAttributes(attributes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and returns a location with the position parsed from the given
|
||||
* attributes.
|
||||
*
|
||||
* @param attributes the attributes to parse
|
||||
* @return the created location
|
||||
* @throws SAXException if the attributes cannot be parsed
|
||||
*/
|
||||
private Location createLocationFromAttributes(Attributes attributes) throws SAXException {
|
||||
String latitude = attributes.getValue(ATT_LAT);
|
||||
String longitude = attributes.getValue(ATT_LON);
|
||||
|
||||
if (latitude == null || longitude == null) {
|
||||
throw new SAXException(createErrorMessage("Point with no longitude or latitude"));
|
||||
}
|
||||
|
||||
// create new location and set attributes
|
||||
Location loc = new Location(LocationManager.GPS_PROVIDER);
|
||||
try {
|
||||
loc.setLatitude(Double.parseDouble(latitude));
|
||||
loc.setLongitude(Double.parseDouble(longitude));
|
||||
} catch (NumberFormatException e) {
|
||||
String msg = createErrorMessage(
|
||||
"Unable to parse lat/long: " + latitude + "/" + longitude);
|
||||
throw new SAXException(msg, e);
|
||||
}
|
||||
return loc;
|
||||
}
|
||||
|
||||
/**
|
||||
* Track point finished, write in database.
|
||||
*
|
||||
* @throws SAXException - thrown if track point is invalid
|
||||
*/
|
||||
private void onTrackPointElementEnd() throws SAXException {
|
||||
if (MyTracksUtils.isValidLocation(location)) {
|
||||
statsBuilder.addLocation(location, location.getTime());
|
||||
|
||||
// insert in db
|
||||
insertTrackPoint(location);
|
||||
|
||||
// first track point?
|
||||
if (lastLocation == null && numberOfSegments == 1) {
|
||||
track.setStartId(getLastPointId());
|
||||
}
|
||||
|
||||
lastLocation = location;
|
||||
lastSegmentLocation = location;
|
||||
location = null;
|
||||
} else {
|
||||
// invalid location - abort import
|
||||
String msg = createErrorMessage("Invalid location detected: " + location);
|
||||
throw new SAXException(msg);
|
||||
}
|
||||
}
|
||||
|
||||
private void insertTrackPoint(Location loc) {
|
||||
bufferedPointInserts[numBufferedPointInserts] = loc;
|
||||
numBufferedPointInserts++;
|
||||
numberOfLocations++;
|
||||
|
||||
if (numBufferedPointInserts >= MAX_BUFFERED_LOCATIONS) {
|
||||
flushPointInserts();
|
||||
}
|
||||
}
|
||||
|
||||
private void flushPointInserts() {
|
||||
if (numBufferedPointInserts <= 0) { return; }
|
||||
|
||||
providerUtils.bulkInsertTrackPoints(bufferedPointInserts, numBufferedPointInserts, track.getId());
|
||||
numBufferedPointInserts = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Track segment finished.
|
||||
*/
|
||||
private void onTrackSegmentElementEnd() {
|
||||
// Nothing to be done
|
||||
}
|
||||
|
||||
/**
|
||||
* Track finished - update in database.
|
||||
*/
|
||||
private void onTrackElementEnd() {
|
||||
if (lastLocation != null) {
|
||||
flushPointInserts();
|
||||
|
||||
// Calculate statistics for the imported track and update
|
||||
statsBuilder.pauseAt(lastLocation.getTime());
|
||||
track.setStopId(getLastPointId());
|
||||
track.setNumberOfPoints(numberOfLocations);
|
||||
track.setStatistics(statsBuilder.getStatistics());
|
||||
providerUtils.updateTrack(track);
|
||||
tracksWritten.add(track.getId());
|
||||
isCurrentTrackRollbackable = false;
|
||||
lastSegmentLocation = null;
|
||||
lastLocation = null;
|
||||
statsBuilder = null;
|
||||
} else {
|
||||
// track contains no track points makes no real
|
||||
// sense to import it as we have no location
|
||||
// information -> roll back
|
||||
rollbackUnfinishedTracks();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Setting time and doing additional calculations as this is the last value
|
||||
* required. Also sets the start time for track and statistics as there is no
|
||||
* start time in the track root element.
|
||||
*
|
||||
* @throws SAXException on parsing errors
|
||||
*/
|
||||
private void onTimeElementEnd() throws SAXException {
|
||||
if (location == null) { return; }
|
||||
|
||||
// Parse the time
|
||||
long time;
|
||||
try {
|
||||
time = StringUtils.parseXmlDateTime(content.trim());
|
||||
} catch (IllegalArgumentException e) {
|
||||
String msg = createErrorMessage("Unable to parse time: " + content);
|
||||
throw new SAXException(msg, e);
|
||||
}
|
||||
|
||||
// Calculate derived attributes from previous point
|
||||
if (lastSegmentLocation != null) {
|
||||
long timeDifference = time - lastSegmentLocation.getTime();
|
||||
|
||||
// check for negative time change
|
||||
if (timeDifference < 0) {
|
||||
String msg = createErrorMessage("Found negative time change.");
|
||||
throw new SAXException(msg);
|
||||
}
|
||||
|
||||
// We don't have a speed and bearing in GPX, make something up from
|
||||
// the last two points.
|
||||
// TODO GPS points tend to have some inherent imprecision,
|
||||
// speed and bearing will likely be off, so the statistics for things like
|
||||
// max speed will also be off.
|
||||
float speed = location.distanceTo(lastLocation) * 1000.0f / timeDifference;
|
||||
location.setSpeed(speed);
|
||||
location.setBearing(lastSegmentLocation.bearingTo(location));
|
||||
}
|
||||
|
||||
// Fill in the time
|
||||
location.setTime(time);
|
||||
|
||||
// initialize start time with time of first track point
|
||||
if (statsBuilder == null) {
|
||||
statsBuilder = new TripStatisticsBuilder(time);
|
||||
}
|
||||
}
|
||||
|
||||
private void onAltitudeElementEnd() throws SAXException {
|
||||
if (location != null) {
|
||||
try {
|
||||
location.setAltitude(Double.parseDouble(content));
|
||||
} catch (NumberFormatException e) {
|
||||
String msg = createErrorMessage("Unable to parse altitude: " + content);
|
||||
throw new SAXException(msg, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the last track if it was not completely imported.
|
||||
*/
|
||||
public void rollbackUnfinishedTracks() {
|
||||
if (isCurrentTrackRollbackable) {
|
||||
providerUtils.deleteTrack(track.getId());
|
||||
isCurrentTrackRollbackable = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all track ids of the tracks created by this importer run.
|
||||
*
|
||||
* @return array of track ids
|
||||
*/
|
||||
private long[] getImportedTrackIds() {
|
||||
// Convert from java.lang.Long for convenience
|
||||
long[] result = new long[tracksWritten.size()];
|
||||
for (int i = 0; i < result.length; i++) {
|
||||
result[i] = tracksWritten.get(i);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the ID of the last point inserted into the database.
|
||||
*/
|
||||
private long getLastPointId() {
|
||||
flushPointInserts();
|
||||
|
||||
return providerUtils.getLastLocationId(track.getId());
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a parsing error message with current line information.
|
||||
*
|
||||
* @param details details about the error, will be appended
|
||||
* @return error message string with current line information
|
||||
*/
|
||||
private String createErrorMessage(String details) {
|
||||
StringBuffer msg = new StringBuffer();
|
||||
msg.append("Parsing error at line: ");
|
||||
msg.append(locator.getLineNumber());
|
||||
msg.append(" column: ");
|
||||
msg.append(locator.getColumnNumber());
|
||||
msg.append(". ");
|
||||
msg.append(details);
|
||||
return msg.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,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.io;
|
||||
|
||||
import com.google.android.apps.mytracks.content.Track;
|
||||
import com.google.android.apps.mytracks.content.Waypoint;
|
||||
import com.google.android.apps.mytracks.io.TrackWriterFactory.TrackFileFormat;
|
||||
import com.google.android.apps.mytracks.util.StringUtils;
|
||||
|
||||
import android.location.Location;
|
||||
|
||||
import java.io.OutputStream;
|
||||
import java.io.PrintWriter;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.TimeZone;
|
||||
|
||||
/**
|
||||
* Log of one track.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public class GpxTrackWriter implements TrackFormatWriter {
|
||||
|
||||
static final SimpleDateFormat TIMESTAMP_FORMAT =
|
||||
new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
|
||||
static {
|
||||
TIMESTAMP_FORMAT.setTimeZone(TimeZone.getTimeZone("UTC"));
|
||||
}
|
||||
|
||||
private PrintWriter pw = null;
|
||||
private Track track;
|
||||
|
||||
@Override
|
||||
public void prepare(Track track, OutputStream out) {
|
||||
this.track = track;
|
||||
this.pw = new PrintWriter(out);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getExtension() {
|
||||
return TrackFileFormat.GPX.getExtension();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeHeader() {
|
||||
if (pw != null) {
|
||||
pw.println("<?xml version=\"1.0\" encoding=\"ISO-8859-1\" ");
|
||||
pw.println("standalone=\"yes\"?>");
|
||||
pw.println("<?xml-stylesheet type=\"text/xsl\" href=\"details.xsl\"?>");
|
||||
pw.println("<gpx");
|
||||
pw.println(" version=\"1.0\"");
|
||||
pw.println(" creator=\"My Tracks for the G1 running Android\"");
|
||||
pw.println(" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"");
|
||||
pw.println(" xmlns=\"http://www.topografix.com/GPX/1/0\"");
|
||||
pw.print(" xmlns:topografix=\"http://www.topografix.com/GPX/Private/"
|
||||
+ "TopoGrafix/0/1\"");
|
||||
pw.print(" xsi:schemaLocation=\"http://www.topografix.com/GPX/1/0 ");
|
||||
pw.print("http://www.topografix.com/GPX/1/0/gpx.xsd ");
|
||||
pw.print("http://www.topografix.com/GPX/Private/TopoGrafix/0/1 ");
|
||||
pw.println("http://www.topografix.com/GPX/Private/TopoGrafix/0/1/"
|
||||
+ "topografix.xsd\">");
|
||||
// TODO: Author etc.
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeFooter() {
|
||||
if (pw != null) {
|
||||
pw.println("</gpx>");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeBeginTrack(Location firstPoint) {
|
||||
if (pw != null) {
|
||||
pw.println("<trk>");
|
||||
pw.println("<name>" + StringUtils.stringAsCData(track.getName())
|
||||
+ "</name>");
|
||||
pw.println("<desc>" + StringUtils.stringAsCData(track.getDescription())
|
||||
+ "</desc>");
|
||||
pw.println("<number>" + track.getId() + "</number>");
|
||||
pw.println("<topografix:color>c0c0c0</topografix:color>");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeEndTrack(Location lastPoint) {
|
||||
if (pw != null) {
|
||||
pw.println("</trk>");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeOpenSegment() {
|
||||
pw.println("<trkseg>");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeCloseSegment() {
|
||||
pw.println("</trkseg>");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeLocation(Location l) {
|
||||
if (pw != null) {
|
||||
pw.println("<trkpt lat=\"" + l.getLatitude() + "\" lon=\""
|
||||
+ l.getLongitude() + "\">");
|
||||
Date d = new Date(l.getTime());
|
||||
pw.println("<ele>" + l.getAltitude() + "</ele>");
|
||||
pw.println("<time>" + TIMESTAMP_FORMAT.format(d) + "</time>");
|
||||
pw.println("</trkpt>");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (pw != null) {
|
||||
pw.close();
|
||||
pw = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeWaypoint(Waypoint waypoint) {
|
||||
if (pw != null) {
|
||||
// TODO: The gpx spec says waypoints should come *before* tracks
|
||||
Location l = waypoint.getLocation();
|
||||
if (l != null) {
|
||||
pw.println("<wpt lat=\"" + l.getLatitude() + "\" lon=\""
|
||||
+ l.getLongitude() + "\">");
|
||||
pw.println("<name>" + StringUtils.stringAsCData(waypoint.getName())
|
||||
+ "</name>");
|
||||
pw.println("<desc>"
|
||||
+ StringUtils.stringAsCData(waypoint.getDescription()) + "</desc>");
|
||||
pw.println("<time>" + TIMESTAMP_FORMAT.format(l.getTime()) + "</time>");
|
||||
pw.println("<ele>" + l.getAltitude() + "</ele>");
|
||||
pw.println("</wpt>");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
/*
|
||||
* Copyright 2008 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package com.google.android.apps.mytracks.io;
|
||||
|
||||
import com.google.android.apps.mytracks.content.Track;
|
||||
import com.google.android.apps.mytracks.content.Waypoint;
|
||||
import com.google.android.apps.mytracks.io.TrackWriterFactory.TrackFileFormat;
|
||||
import com.google.android.apps.mytracks.util.StringUtils;
|
||||
|
||||
import android.content.Context;
|
||||
import android.location.Location;
|
||||
|
||||
import java.io.OutputStream;
|
||||
import java.io.PrintWriter;
|
||||
import java.util.Vector;
|
||||
|
||||
/**
|
||||
* Write track as KML to a file.
|
||||
*
|
||||
* @author Leif Hendrik Wilden
|
||||
*/
|
||||
public class KmlTrackWriter implements TrackFormatWriter {
|
||||
|
||||
private final Vector<Double> distances = new Vector<Double>();
|
||||
private final Vector<Double> elevations = new Vector<Double>();
|
||||
private final StringUtils stringUtils;
|
||||
private PrintWriter pw = null;
|
||||
private Track track;
|
||||
|
||||
public KmlTrackWriter(Context context) {
|
||||
stringUtils = new StringUtils(context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Testing constructor.
|
||||
*/
|
||||
KmlTrackWriter(StringUtils stringUtils) {
|
||||
this.stringUtils = stringUtils;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void prepare(Track track, OutputStream out) {
|
||||
this.track = track;
|
||||
this.pw = new PrintWriter(out);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getExtension() {
|
||||
return TrackFileFormat.KML.getExtension();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeHeader() {
|
||||
if (pw != null) {
|
||||
pw.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
|
||||
pw.print("<kml");
|
||||
pw.print(" xmlns=\"http://earth.google.com/kml/2.0\"");
|
||||
pw.println(" xmlns:atom=\"http://www.w3.org/2005/Atom\">");
|
||||
pw.println("<Document>");
|
||||
pw.println("<atom:author><atom:name>My Tracks running on Android"
|
||||
+ "</atom:name></atom:author>");
|
||||
pw.println("<name>" + StringUtils.stringAsCData(track.getName())
|
||||
+ "</name>");
|
||||
pw.println("<description>"
|
||||
+ StringUtils.stringAsCData(track.getDescription())
|
||||
+ "</description>");
|
||||
writeStyles();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeFooter() {
|
||||
if (pw != null) {
|
||||
pw.println("</Document>");
|
||||
pw.println("</kml>");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeBeginTrack(Location firstPoint) {
|
||||
if (pw != null) {
|
||||
writePlacemark("(Start)", track.getDescription(), "#sh_green-circle",
|
||||
firstPoint);
|
||||
pw.println("<Placemark>");
|
||||
pw.println("<name>" + StringUtils.stringAsCData(track.getName())
|
||||
+ "</name>");
|
||||
pw.println("<description>"
|
||||
+ StringUtils.stringAsCData(track.getDescription())
|
||||
+ "</description>");
|
||||
pw.println("<styleUrl>#track</styleUrl>");
|
||||
pw.println("<MultiGeometry>");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeEndTrack(Location lastPoint) {
|
||||
if (pw != null) {
|
||||
pw.println("</MultiGeometry>");
|
||||
pw.println("</Placemark>");
|
||||
String description = stringUtils.generateTrackDescription(
|
||||
track, distances, elevations);
|
||||
writePlacemark("(End)", description, "#sh_red-circle", lastPoint);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeOpenSegment() {
|
||||
if (pw != null) {
|
||||
pw.print("<LineString><coordinates>");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeCloseSegment() {
|
||||
if (pw != null) {
|
||||
pw.println("</coordinates></LineString>");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeLocation(Location l) {
|
||||
if (pw != null) {
|
||||
pw.print(l.getLongitude() + "," + l.getLatitude() + ","
|
||||
+ l.getAltitude() + " ");
|
||||
}
|
||||
}
|
||||
|
||||
private String getPinStyle(Waypoint waypoint) {
|
||||
if (waypoint.getType() == Waypoint.TYPE_STATISTICS) {
|
||||
return "#sh_ylw-pushpin";
|
||||
}
|
||||
// Try to find the icon color.
|
||||
// The string should be of the form:
|
||||
// "http://maps.google.com/mapfiles/ms/micons/XXX.png"
|
||||
int slash = waypoint.getIcon().lastIndexOf('/');
|
||||
int png = waypoint.getIcon().lastIndexOf('.');
|
||||
if ((slash != -1) && (slash < png)) {
|
||||
String color = waypoint.getIcon().substring(slash + 1, png);
|
||||
return "#sh_" + color + "-pushpin";
|
||||
}
|
||||
return "#sh_blue-pushpin";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeWaypoint(Waypoint waypoint) {
|
||||
if (pw != null) {
|
||||
writePlacemark(
|
||||
waypoint.getName(),
|
||||
waypoint.getDescription(),
|
||||
getPinStyle(waypoint),
|
||||
waypoint.getLocation());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (pw != null) {
|
||||
pw.close();
|
||||
pw = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void writeStyles() {
|
||||
pw.println("<Style id=\"track\"><LineStyle><color>7f0000ff</color>"
|
||||
+ "<width>4</width></LineStyle></Style>");
|
||||
|
||||
pw.print("<Style id=\"sh_green-circle\"><IconStyle><scale>1.3</scale>");
|
||||
pw.print("<Icon><href>http://maps.google.com/mapfiles/kml/paddle/"
|
||||
+ "grn-circle.png</href></Icon>");
|
||||
pw.println("<hotSpot x=\"32\" y=\"1\" xunits=\"pixels\" yunits=\"pixels\"/>"
|
||||
+ "</IconStyle></Style>");
|
||||
|
||||
pw.print("<Style id=\"sh_red-circle\"><IconStyle><scale>1.3</scale>");
|
||||
pw.print("<Icon><href>http://maps.google.com/mapfiles/kml/paddle/"
|
||||
+ "red-circle.png</href></Icon>");
|
||||
pw.println("<hotSpot x=\"32\" y=\"1\" xunits=\"pixels\" yunits=\"pixels\"/>"
|
||||
+ "</IconStyle></Style>");
|
||||
|
||||
pw.print("<Style id=\"sh_ylw-pushpin\"><IconStyle><scale>1.3</scale>");
|
||||
pw.print("<Icon><href>http://maps.google.com/mapfiles/kml/pushpin/"
|
||||
+ "ylw-pushpin.png</href></Icon>");
|
||||
pw.println("<hotSpot x=\"20\" y=\"2\" xunits=\"pixels\" yunits=\"pixels\"/>"
|
||||
+ "</IconStyle></Style>");
|
||||
|
||||
pw.print("<Style id=\"sh_blue-pushpin\"><IconStyle><scale>1.3</scale>");
|
||||
pw.print("<Icon><href>http://maps.google.com/mapfiles/kml/pushpin/"
|
||||
+ "blue-pushpin.png</href></Icon>");
|
||||
pw.println("<hotSpot x=\"20\" y=\"2\" xunits=\"pixels\" yunits=\"pixels\"/>"
|
||||
+ "</IconStyle></Style>");
|
||||
|
||||
pw.print("<Style id=\"sh_green-pushpin\"><IconStyle><scale>1.3</scale>");
|
||||
pw.print("<Icon><href>http://maps.google.com/mapfiles/kml/pushpin/"
|
||||
+ "grn-pushpin.png</href></Icon>");
|
||||
pw.println("<hotSpot x=\"20\" y=\"2\" xunits=\"pixels\" yunits=\"pixels\"/>"
|
||||
+ "</IconStyle></Style>");
|
||||
|
||||
pw.print("<Style id=\"sh_red-pushpin\"><IconStyle><scale>1.3</scale>");
|
||||
pw.print("<Icon><href>http://maps.google.com/mapfiles/kml/pushpin/"
|
||||
+ "red-pushpin.png</href></Icon>");
|
||||
pw.println("<hotSpot x=\"20\" y=\"2\" xunits=\"pixels\" yunits=\"pixels\"/>"
|
||||
+ "</IconStyle></Style>");
|
||||
}
|
||||
|
||||
private void writePlacemark(String name, String description, String style,
|
||||
Location location) {
|
||||
if (location != null) {
|
||||
pw.println("<Placemark>");
|
||||
pw.println(" <name>" + StringUtils.stringAsCData(name) + "</name>");
|
||||
pw.println(" <description>" + StringUtils.stringAsCData(description)
|
||||
+ "</description>");
|
||||
pw.println(" <styleUrl>" + style + "</styleUrl>");
|
||||
pw.println(" <Point>");
|
||||
pw.println(" <coordinates>" + location.getLongitude() + ","
|
||||
+ location.getLatitude() + "</coordinates>");
|
||||
pw.println(" </Point>");
|
||||
pw.println("</Placemark>");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
/*
|
||||
* Copyright 2010 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package com.google.android.apps.mytracks.io;
|
||||
|
||||
import com.google.android.accounts.Account;
|
||||
import com.google.android.accounts.AccountManager;
|
||||
import com.google.android.accounts.AccountManagerCallback;
|
||||
import com.google.android.accounts.AccountManagerFuture;
|
||||
import com.google.android.accounts.AuthenticatorException;
|
||||
import com.google.android.accounts.OperationCanceledException;
|
||||
import com.google.android.apps.mytracks.AccountChooser;
|
||||
import com.google.android.apps.mytracks.MyTracks;
|
||||
import com.google.android.apps.mytracks.MyTracksConstants;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* AuthManager keeps track of the current auth token for a user. The advantage
|
||||
* over just passing around a String is that this class can renew the auth
|
||||
* token if necessary, and it will change for all classes using this
|
||||
* AuthManager.
|
||||
*/
|
||||
public class ModernAuthManager implements AuthManager {
|
||||
/** The activity that will handle auth result callbacks. */
|
||||
private final Activity activity;
|
||||
|
||||
/** The name of the service to authorize for. */
|
||||
private final String service;
|
||||
|
||||
/** The most recently fetched auth token or null if none is available. */
|
||||
private String authToken;
|
||||
|
||||
private final AccountManager accountManager;
|
||||
|
||||
private Runnable whenFinished;
|
||||
|
||||
/**
|
||||
* AuthManager requires many of the same parameters as
|
||||
* {@link com.google.android.googlelogindist.GoogleLoginServiceHelper
|
||||
* #getCredentials(Activity, int, Bundle, boolean, String, boolean)}.
|
||||
* The activity must have a handler in {@link Activity#onActivityResult} that
|
||||
* calls {@link #authResult(int, Intent)} if the request code is the code
|
||||
* given here.
|
||||
*
|
||||
* @param activity An activity with a handler in
|
||||
* {@link Activity#onActivityResult} that calls
|
||||
* {@link #authResult(int, Intent)} when {@literal code} is the request
|
||||
* code
|
||||
* @param code The request code to pass to
|
||||
* {@link Activity#onActivityResult} when
|
||||
* {@link #authResult(int, Intent)} should be called
|
||||
* @param extras A {@link Bundle} of extras for
|
||||
* {@link com.google.android.googlelogindist.GoogleLoginServiceHelper}
|
||||
* @param requireGoogle True if the account must be a Google account
|
||||
* @param service The name of the service to authenticate as
|
||||
*/
|
||||
public ModernAuthManager(Activity activity, int code, Bundle extras,
|
||||
boolean requireGoogle, String service) {
|
||||
this.activity = activity;
|
||||
this.service = service;
|
||||
this.accountManager = AccountManager.get(activity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Call this to do the initial login. The user will be asked to login if
|
||||
* they haven't already. The {@link Runnable} provided will be executed
|
||||
* when the auth token is successfully fetched.
|
||||
*
|
||||
* @param runnable A {@link Runnable} to execute when the auth token
|
||||
* has been successfully fetched and is available via
|
||||
* {@link #getAuthToken()}
|
||||
*/
|
||||
public void doLogin(final Runnable runnable, Object o) {
|
||||
this.whenFinished = runnable;
|
||||
if (!(o instanceof Account)) {
|
||||
throw new IllegalArgumentException("FroyoAuthManager requires an account.");
|
||||
}
|
||||
Account account = (Account) o;
|
||||
accountManager.getAuthToken(account, service, true,
|
||||
new AccountManagerCallback<Bundle>() {
|
||||
public void run(AccountManagerFuture<Bundle> future) {
|
||||
try {
|
||||
Bundle result = future.getResult();
|
||||
|
||||
// AccountManager needs user to grant permission
|
||||
if (result.containsKey(AccountManager.KEY_INTENT)) {
|
||||
Intent intent = (Intent) result.get(AccountManager.KEY_INTENT);
|
||||
clearNewTaskFlag(intent);
|
||||
activity.startActivityForResult(intent, MyTracksConstants.GET_LOGIN);
|
||||
return;
|
||||
}
|
||||
|
||||
authToken = result.getString(
|
||||
AccountManager.KEY_AUTHTOKEN);
|
||||
Log.e(MyTracksConstants.TAG, "Got auth token.");
|
||||
runWhenFinished();
|
||||
} catch (OperationCanceledException e) {
|
||||
Log.e(MyTracksConstants.TAG, "Operation Canceled", e);
|
||||
} catch (IOException e) {
|
||||
Log.e(MyTracksConstants.TAG, "IOException", e);
|
||||
} catch (AuthenticatorException e) {
|
||||
Log.e(MyTracksConstants.TAG, "Authentication Failed", e);
|
||||
}
|
||||
}
|
||||
}, null /* handler */);
|
||||
}
|
||||
|
||||
private static void clearNewTaskFlag(Intent intent) {
|
||||
int flags = intent.getFlags();
|
||||
flags &= ~Intent.FLAG_ACTIVITY_NEW_TASK;
|
||||
intent.setFlags(flags);
|
||||
}
|
||||
|
||||
/**
|
||||
* The {@link Activity} passed into the constructor should call this
|
||||
* function when it gets {@link Activity#onActivityResult} with the request
|
||||
* code passed into the constructor. The resultCode and results should
|
||||
* come directly from the {@link Activity#onActivityResult} function. This
|
||||
* function will return true if an auth token was successfully fetched or
|
||||
* the process is not finished.
|
||||
*
|
||||
* @param resultCode The result code passed in to the {@link Activity}'s
|
||||
* {@link Activity#onActivityResult} function
|
||||
* @param results The data passed in to the {@link Activity}'s
|
||||
* {@link Activity#onActivityResult} function
|
||||
* @return True if the auth token was fetched or we aren't done fetching
|
||||
* the auth token, or False if there was an error or the request was
|
||||
* canceled
|
||||
*/
|
||||
public boolean authResult(int resultCode, Intent results) {
|
||||
if (results != null) {
|
||||
authToken = results.getStringExtra(
|
||||
AccountManager.KEY_AUTHTOKEN);
|
||||
Log.w(MyTracksConstants.TAG, "authResult: " + authToken);
|
||||
} else {
|
||||
Log.e(MyTracksConstants.TAG, "No auth result results!!");
|
||||
}
|
||||
runWhenFinished();
|
||||
return authToken != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current auth token. Response may be null if no valid auth
|
||||
* token has been fetched.
|
||||
*
|
||||
* @return The current auth token or null if no auth token has been
|
||||
* fetched
|
||||
*/
|
||||
public String getAuthToken() {
|
||||
return authToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidates the existing auth token and request a new one. The
|
||||
* {@link Runnable} provided will be executed when the new auth token is
|
||||
* successfully fetched.
|
||||
*
|
||||
* @param runnable A {@link Runnable} to execute when a new auth token
|
||||
* is successfully fetched
|
||||
*/
|
||||
public void invalidateAndRefresh(final Runnable runnable) {
|
||||
this.whenFinished = runnable;
|
||||
|
||||
activity.runOnUiThread(new Runnable() {
|
||||
public void run() {
|
||||
accountManager.invalidateAuthToken(MyTracksConstants.ACCOUNT_TYPE,
|
||||
authToken);
|
||||
MyTracks.getInstance().getAccountChooser().chooseAccount(activity,
|
||||
new AccountChooser.AccountHandler() {
|
||||
@Override
|
||||
public void handleAccountSelected(Account account) {
|
||||
if (account != null) {
|
||||
doLogin(whenFinished, account);
|
||||
} else {
|
||||
runWhenFinished();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void runWhenFinished() {
|
||||
if (whenFinished != null) {
|
||||
(new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
whenFinished.run();
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,540 @@
|
||||
/*
|
||||
* Copyright 2008 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package com.google.android.apps.mytracks.io;
|
||||
|
||||
import com.google.android.apps.mytracks.MyTracks;
|
||||
import com.google.android.apps.mytracks.MyTracksConstants;
|
||||
import com.google.android.apps.mytracks.MyTracksSettings;
|
||||
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.GDataWrapper;
|
||||
import com.google.android.apps.mytracks.io.gdata.GDataWrapper.QueryFunction;
|
||||
import com.google.android.apps.mytracks.stats.TripStatistics;
|
||||
import com.google.android.apps.mytracks.util.ResourceUtils;
|
||||
import com.google.android.apps.mytracks.util.StringUtils;
|
||||
import com.google.android.apps.mytracks.util.UnitConversions;
|
||||
import com.google.android.common.gdata.AndroidXmlParserFactory;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
import com.google.wireless.gdata.client.GDataClient;
|
||||
import com.google.wireless.gdata.client.GDataServiceClient;
|
||||
import com.google.wireless.gdata.client.HttpException;
|
||||
import com.google.wireless.gdata.data.Entry;
|
||||
import com.google.wireless.gdata.docs.DocumentsClient;
|
||||
import com.google.wireless.gdata.docs.SpreadsheetsClient;
|
||||
import com.google.wireless.gdata.docs.SpreadsheetsClient.WorksheetEntry;
|
||||
import com.google.wireless.gdata.maps.xml.XmlMapsGDataParserFactory;
|
||||
import com.google.wireless.gdata.parser.GDataParser;
|
||||
import com.google.wireless.gdata.parser.ParseException;
|
||||
import com.google.wireless.gdata2.client.AuthenticationException;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Handler;
|
||||
import android.os.HandlerThread;
|
||||
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.text.DecimalFormat;
|
||||
import java.text.NumberFormat;
|
||||
|
||||
/**
|
||||
* A helper class used to transmit tracks statistics to Google Docs/Trix.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public class SendToDocs {
|
||||
private static final NumberFormat LARGE_UNIT_FORMAT =
|
||||
new DecimalFormat("#,###,###.00");
|
||||
private static final NumberFormat SMALL_UNIT_FORMAT =
|
||||
new DecimalFormat("###,###");
|
||||
private static final String DOCS_FEED_URL =
|
||||
"http://docs.google.com/feeds/documents/private/full";
|
||||
private static final String DOCS_MY_SPREADSHEETS_FEED_URL =
|
||||
"http://docs.google.com/feeds/documents/private/full?"
|
||||
+ "category=mine,spreadsheet";
|
||||
private static final String DOCS_SPREADSHEET_URL =
|
||||
"http://docs.google.com/feeds/documents/private/full/spreadsheet%3A";
|
||||
private static final String DOCS_SPREADSHEET_URL_FORMAT =
|
||||
"http://spreadsheets.google.com/feeds/list/%s/%s/private/full";
|
||||
protected static final String DOCS_WORKSHEETS_URL_FORMAT =
|
||||
"http://spreadsheets.google.com/feeds/worksheets/%s/private/full";
|
||||
private static final String CONTENT_TYPE_PARAM = "Content-Type";
|
||||
private static final String ATOM_FEED_MIME_TYPE = "application/atom+xml";
|
||||
private static final String OPENDOCUMENT_SPREADSHEET_MIME_TYPE =
|
||||
"application/x-vnd.oasis.opendocument.spreadsheet";
|
||||
|
||||
private final Activity activity;
|
||||
private final AuthManager wiseAuth;
|
||||
private final AuthManager writelyAuth;
|
||||
private final long trackId;
|
||||
private final boolean metricUnits;
|
||||
private final HandlerThread handlerThread;
|
||||
private final Handler handler;
|
||||
|
||||
private boolean createdNewSpreadSheet = false;
|
||||
|
||||
private String spreadSheetId = null;
|
||||
private String workSheetId = null;
|
||||
|
||||
private boolean success = true;
|
||||
private String statusMessage = "";
|
||||
private Runnable onCompletion = null;
|
||||
|
||||
public SendToDocs(Activity activity, AuthManager wiseAuth,
|
||||
AuthManager writelyAuth, long trackId) {
|
||||
this.activity = activity;
|
||||
this.wiseAuth = wiseAuth;
|
||||
this.writelyAuth = writelyAuth;
|
||||
this.trackId = trackId;
|
||||
|
||||
SharedPreferences preferences = activity.getSharedPreferences(
|
||||
MyTracksSettings.SETTINGS_NAME, 0);
|
||||
if (preferences != null) {
|
||||
metricUnits =
|
||||
preferences.getBoolean(activity.getString(R.string.metric_units_key),
|
||||
true);
|
||||
} else {
|
||||
metricUnits = true;
|
||||
}
|
||||
|
||||
Log.d(MyTracksConstants.TAG,
|
||||
"Sending to Google Docs: trackId = " + trackId);
|
||||
handlerThread = new HandlerThread("SendToGoogleDocs");
|
||||
handlerThread.start();
|
||||
handler = new Handler(handlerThread.getLooper());
|
||||
}
|
||||
|
||||
public void run() {
|
||||
handler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
doUpload();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void doUpload() {
|
||||
statusMessage = activity.getString(R.string.error_sending_to_mymap);
|
||||
success = false;
|
||||
|
||||
try {
|
||||
if (trackId == -1) {
|
||||
Log.w(MyTracksConstants.TAG, "Cannot get track id.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the track from the provider:
|
||||
Track track =
|
||||
MyTracksProviderUtils.Factory.get(activity).getTrack(trackId);
|
||||
if (track == null) {
|
||||
Log.w(MyTracksConstants.TAG, "Cannot get track.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Transmit track stats via GData feed:
|
||||
// -------------------------------
|
||||
|
||||
Log.d(MyTracksConstants.TAG, "SendToDocs: Uploading to spreadsheet");
|
||||
success = uploadToDocs(track);
|
||||
if (success) {
|
||||
if (createdNewSpreadSheet) {
|
||||
statusMessage = activity.getString(
|
||||
R.string.status_tracks_have_been_uploaded_to_new_doc);
|
||||
} else {
|
||||
statusMessage = activity.getString(
|
||||
R.string.status_tracks_have_been_uploaded_to_docs);
|
||||
}
|
||||
} else {
|
||||
statusMessage = activity.getString(R.string.error_sending_to_docs);
|
||||
}
|
||||
Log.d(MyTracksConstants.TAG, "SendToDocs: Done.");
|
||||
} finally {
|
||||
if (onCompletion != null) {
|
||||
activity.runOnUiThread(onCompletion);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean wasSuccess() {
|
||||
return success;
|
||||
}
|
||||
|
||||
public String getStatusMessage() {
|
||||
return statusMessage;
|
||||
}
|
||||
|
||||
public void setOnCompletion(Runnable onCompletion) {
|
||||
this.onCompletion = onCompletion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploads the statistics about a track to Google Docs using the docs GData
|
||||
* feed.
|
||||
*
|
||||
* @param track the track
|
||||
*/
|
||||
private boolean uploadToDocs(Track track) {
|
||||
GDataWrapper<GDataServiceClient> wiseWrapper = new GDataWrapper<GDataServiceClient>();
|
||||
wiseWrapper.setAuthManager(wiseAuth);
|
||||
wiseWrapper.setRetryOnAuthFailure(true);
|
||||
|
||||
GDataWrapper<GDataServiceClient> writelyWrapper = new GDataWrapper<GDataServiceClient>();
|
||||
writelyWrapper.setAuthManager(writelyAuth);
|
||||
writelyWrapper.setRetryOnAuthFailure(true);
|
||||
|
||||
GDataClient androidClient = null;
|
||||
try {
|
||||
androidClient = GDataClientFactory.getGDataClient(activity);
|
||||
SpreadsheetsClient gdataClient = new SpreadsheetsClient(androidClient,
|
||||
new XmlMapsGDataParserFactory(new AndroidXmlParserFactory()));
|
||||
wiseWrapper.setClient(gdataClient);
|
||||
Log.d(MyTracksConstants.TAG,
|
||||
"GData connection prepared: " + this.writelyAuth);
|
||||
String sheetTitle = "My Tracks";
|
||||
|
||||
if (track.getCategory() != null && !track.getCategory().equals("")) {
|
||||
sheetTitle += "-" + track.getCategory();
|
||||
}
|
||||
|
||||
DocumentsClient docsGdataClient = new DocumentsClient(androidClient,
|
||||
new XmlMapsGDataParserFactory(new AndroidXmlParserFactory()));
|
||||
writelyWrapper.setClient(docsGdataClient);
|
||||
|
||||
// First try to find the spreadsheet:
|
||||
if (!getSpreadsheetId(writelyWrapper, sheetTitle)) {
|
||||
Log.i(MyTracksConstants.TAG, "Spreadsheet lookup failed.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (spreadSheetId == null) {
|
||||
MyTracks.getInstance().setProgressValue(65);
|
||||
// Waiting a few seconds and trying again. Maybe the server just had a
|
||||
// hickup (unfortunately that happens quite a lot...).
|
||||
try {
|
||||
Thread.sleep(5000);
|
||||
} catch (InterruptedException e) {
|
||||
Log.e(MyTracksConstants.TAG, "Sleep interrupted", e);
|
||||
}
|
||||
if (!getSpreadsheetId(writelyWrapper, sheetTitle)) {
|
||||
Log.i(MyTracksConstants.TAG, "2nd spreadsheet lookup failed.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
MyTracks.getInstance().setProgressValue(70);
|
||||
if (spreadSheetId == null) {
|
||||
Log.i(MyTracksConstants.TAG, "Creating new spreadsheet: " + sheetTitle);
|
||||
|
||||
if (!createSpreadSheet(writelyWrapper, sheetTitle)) {
|
||||
return false;
|
||||
}
|
||||
MyTracks.getInstance().setProgressValue(80);
|
||||
|
||||
if (spreadSheetId == null) {
|
||||
MyTracks.getInstance().setProgressValue(85);
|
||||
// The previous creation might have succeeded even though GData
|
||||
// reported an error. Seems to be a know bug,
|
||||
// see http://code.google.com/p/gdata-issues/issues/detail?id=929
|
||||
// Try to find the created spreadsheet:
|
||||
Log.w(MyTracksConstants.TAG,
|
||||
"Create might have failed. Trying to find created document.");
|
||||
try {
|
||||
Thread.sleep(5000);
|
||||
} catch (InterruptedException e) {
|
||||
Log.e(MyTracksConstants.TAG, "Sleep interrupted", e);
|
||||
}
|
||||
if (!getSpreadsheetId(writelyWrapper, sheetTitle)) {
|
||||
return false;
|
||||
}
|
||||
if (spreadSheetId == null) {
|
||||
MyTracks.getInstance().setProgressValue(87);
|
||||
// Re-try
|
||||
try {
|
||||
Thread.sleep(5000);
|
||||
} catch (InterruptedException e) {
|
||||
Log.e(MyTracksConstants.TAG, "Sleep interrupted", e);
|
||||
}
|
||||
if (!getSpreadsheetId(writelyWrapper, sheetTitle)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (spreadSheetId == null) {
|
||||
Log.i(MyTracksConstants.TAG,
|
||||
"Creating new spreadsheet really failed.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!getWorkSheetId(wiseWrapper)) {
|
||||
Log.i(MyTracksConstants.TAG, "Looking up worksheet id failed.");
|
||||
return false;
|
||||
}
|
||||
|
||||
MyTracks.getInstance().setProgressValue(90);
|
||||
|
||||
insertRowNet(track, spreadSheetId, workSheetId);
|
||||
Log.i(MyTracksConstants.TAG, "Done uploading to docs.");
|
||||
} catch (IOException e) {
|
||||
Log.e(MyTracksConstants.TAG, "Unable to upload docs.", e);
|
||||
return false;
|
||||
} finally {
|
||||
if (androidClient != null) {
|
||||
androidClient.close();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean getSpreadsheetId(GDataWrapper<GDataServiceClient> wrapper, final String title) {
|
||||
spreadSheetId = null;
|
||||
return wrapper.runQuery(new QueryFunction<GDataServiceClient>() {
|
||||
@Override
|
||||
public void query(GDataServiceClient client)
|
||||
throws IOException, GDataWrapper.ParseException, GDataWrapper.HttpException {
|
||||
GDataParser listParser;
|
||||
try {
|
||||
listParser = client.getParserForFeed(Entry.class,
|
||||
DOCS_MY_SPREADSHEETS_FEED_URL, writelyAuth.getAuthToken());
|
||||
listParser.init();
|
||||
while (listParser.hasMoreData()) {
|
||||
Entry entry = listParser.readNextEntry(null);
|
||||
String entryTitle = entry.getTitle();
|
||||
Log.i(MyTracksConstants.TAG, "Found docs entry: " + entryTitle);
|
||||
if (entryTitle.equals(title)) {
|
||||
String entryId = entry.getId();
|
||||
int lastSlash = entryId.lastIndexOf('/');
|
||||
spreadSheetId = entryId.substring(lastSlash + 15);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (ParseException e) {
|
||||
throw new GDataWrapper.ParseException();
|
||||
} catch (HttpException e) {
|
||||
throw new GDataWrapper.HttpException(e.getStatusCode(), e.getMessage());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private boolean getWorkSheetId(GDataWrapper<GDataServiceClient> wrapper) {
|
||||
workSheetId = null;
|
||||
return wrapper.runQuery(new QueryFunction<GDataServiceClient>() {
|
||||
@Override
|
||||
public void query(GDataServiceClient client)
|
||||
throws GDataWrapper.AuthenticationException, IOException, GDataWrapper.ParseException {
|
||||
String uri = String.format(DOCS_WORKSHEETS_URL_FORMAT, spreadSheetId);
|
||||
GDataParser sheetParser;
|
||||
try {
|
||||
sheetParser = ((SpreadsheetsClient) client).getParserForWorksheetsFeed(uri,
|
||||
wiseAuth.getAuthToken());
|
||||
sheetParser.init();
|
||||
if (!sheetParser.hasMoreData()) {
|
||||
Log.i(MyTracksConstants.TAG, "Found no worksheets");
|
||||
return; // failure
|
||||
}
|
||||
|
||||
// just grab the first.
|
||||
WorksheetEntry worksheetEntry =
|
||||
(WorksheetEntry) sheetParser.readNextEntry(new WorksheetEntry());
|
||||
|
||||
int lastSlash = worksheetEntry.getId().lastIndexOf('/');
|
||||
workSheetId = worksheetEntry.getId().substring(lastSlash + 1);
|
||||
|
||||
} catch (AuthenticationException e) {
|
||||
throw new GDataWrapper.AuthenticationException();
|
||||
} catch (ParseException e) {
|
||||
throw new GDataWrapper.ParseException();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new MyTracks spreadsheet with the given name.
|
||||
*/
|
||||
private boolean createSpreadSheet(GDataWrapper<GDataServiceClient> writelyWrapper,
|
||||
final String name) {
|
||||
spreadSheetId = null;
|
||||
|
||||
return writelyWrapper.runQuery(new QueryFunction<GDataServiceClient>() {
|
||||
@Override
|
||||
public void query(GDataServiceClient client) throws IOException {
|
||||
// Construct data
|
||||
// Send data
|
||||
URL url = new URL(DOCS_FEED_URL);
|
||||
URLConnection conn = url.openConnection();
|
||||
conn.addRequestProperty(CONTENT_TYPE_PARAM,
|
||||
OPENDOCUMENT_SPREADSHEET_MIME_TYPE);
|
||||
conn.addRequestProperty("Slug", name);
|
||||
conn.addRequestProperty("Authorization",
|
||||
"GoogleLogin auth=" + writelyAuth.getAuthToken());
|
||||
conn.setDoOutput(true);
|
||||
OutputStream os = conn.getOutputStream();
|
||||
ResourceUtils.readBinaryFileToOutputStream(
|
||||
activity, R.raw.mytracks_empty_spreadsheet, os);
|
||||
|
||||
// Get the response
|
||||
// TODO: The following is a horrible ugly hack.
|
||||
// Hopefully we can retire it when there is a proper gdata api.
|
||||
BufferedReader rd = null;
|
||||
String line;
|
||||
StringBuilder resultBuilder = new StringBuilder();
|
||||
try {
|
||||
rd = new BufferedReader(new InputStreamReader(conn.getInputStream()));
|
||||
while ((line = rd.readLine()) != null) {
|
||||
resultBuilder.append(line);
|
||||
}
|
||||
os.close();
|
||||
rd.close();
|
||||
} catch (FileNotFoundException e) {
|
||||
// The GData API sometimes throws an error, even though creation of
|
||||
// the document succeeded. In that case let's just return. The caller
|
||||
// then needs to check if the doc actually exists.
|
||||
return;
|
||||
} finally {
|
||||
os.close();
|
||||
if (rd != null) {
|
||||
rd.close();
|
||||
}
|
||||
}
|
||||
|
||||
String result = resultBuilder.toString();
|
||||
// Try to find the id.
|
||||
int idTagIndex = result.indexOf("<id>");
|
||||
if (idTagIndex == -1) {
|
||||
return;
|
||||
}
|
||||
int idTagCloseIndex = result.indexOf("</id>", idTagIndex);
|
||||
if (idTagCloseIndex == -1) {
|
||||
return;
|
||||
}
|
||||
int idStringStart = result.indexOf(DOCS_SPREADSHEET_URL, idTagIndex);
|
||||
if (idStringStart == -1) {
|
||||
return;
|
||||
}
|
||||
spreadSheetId = result.substring(
|
||||
idStringStart + DOCS_SPREADSHEET_URL.length(), idTagCloseIndex);
|
||||
Log.i(MyTracksConstants.TAG, "Created new spreadsheet: " + spreadSheetId);
|
||||
}});
|
||||
}
|
||||
|
||||
private void insertRowNet(Track track, String spreadsheetId,
|
||||
String worksheetId) throws IOException {
|
||||
String worksheetUri = String.format(DOCS_SPREADSHEET_URL_FORMAT,
|
||||
spreadsheetId, worksheetId);
|
||||
TripStatistics stats = track.getStatistics();
|
||||
|
||||
/* Prepare the Post-Text we are going to send. */
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("<entry xmlns='http://www.w3.org/2005/Atom' ");
|
||||
sb.append("xmlns:gsx='http://schemas.google.com/spreadsheets/"
|
||||
+ "2006/extended'>");
|
||||
appendTag("name", track.getName(), sb);
|
||||
appendTag("description", track.getDescription(), sb);
|
||||
appendTag("date", String.format("%tc", stats.getStartTime()), sb);
|
||||
appendTag("totaltime", StringUtils.formatTimeAlwaysShowingHours(
|
||||
stats.getTotalTime()), sb);
|
||||
appendTag("movingtime", StringUtils.formatTimeAlwaysShowingHours(
|
||||
stats.getMovingTime()), sb);
|
||||
appendLargeUnitsTag("distance", stats.getTotalDistance() / 1000, sb);
|
||||
appendTag("distanceunit",
|
||||
metricUnits
|
||||
? activity.getString(R.string.kilometer)
|
||||
: activity.getString(R.string.mile),
|
||||
sb);
|
||||
appendLargeUnitsTag("averagespeed", stats.getAverageSpeed() * 3.6, sb);
|
||||
appendLargeUnitsTag("averagemovingspeed",
|
||||
stats.getAverageMovingSpeed() * 3.6, sb);
|
||||
appendLargeUnitsTag("maxspeed", stats.getMaxSpeed() * 3.6, sb);
|
||||
appendTag("speedunit",
|
||||
metricUnits
|
||||
? activity.getString(R.string.kilometer_per_hour)
|
||||
: activity.getString(R.string.mile_per_hour),
|
||||
sb);
|
||||
appendSmallUnitsTag("elevationgain", stats.getTotalElevationGain(), sb);
|
||||
appendSmallUnitsTag("minelevation", stats.getMinElevation(), sb);
|
||||
appendSmallUnitsTag("maxelevation", stats.getMaxElevation(), sb);
|
||||
appendTag("elevationunit",
|
||||
metricUnits
|
||||
? activity.getString(R.string.meter)
|
||||
: activity.getString(R.string.feet),
|
||||
sb);
|
||||
if (track.getMapId().length() > 0) {
|
||||
appendTag("map", SendToFusionTables.getMapVisualizationUrl(track), sb);
|
||||
}
|
||||
sb.append("</entry>");
|
||||
Log.i(MyTracksConstants.TAG,
|
||||
"Inserting at: " + spreadsheetId + " => " + worksheetUri);
|
||||
|
||||
String postText = sb.toString();
|
||||
Log.i(MyTracksConstants.TAG, postText);
|
||||
|
||||
// Send data
|
||||
// No need for a wrapper because we know that the authorization was good
|
||||
// enough to get this far
|
||||
URL url = new URL(worksheetUri);
|
||||
URLConnection conn = url.openConnection();
|
||||
conn.addRequestProperty(CONTENT_TYPE_PARAM, ATOM_FEED_MIME_TYPE);
|
||||
conn.addRequestProperty("Authorization",
|
||||
"GoogleLogin auth=" + wiseAuth.getAuthToken());
|
||||
conn.setDoOutput(true);
|
||||
OutputStreamWriter wr = new OutputStreamWriter(conn.getOutputStream());
|
||||
wr.write(postText);
|
||||
wr.flush();
|
||||
|
||||
// Get the response
|
||||
BufferedReader rd =
|
||||
new BufferedReader(new InputStreamReader(conn.getInputStream()));
|
||||
String line;
|
||||
while ((line = rd.readLine()) != null) {
|
||||
// Process line...
|
||||
Log.i(MyTracksConstants.TAG, "r: " + line);
|
||||
}
|
||||
wr.close();
|
||||
rd.close();
|
||||
|
||||
Log.i(MyTracksConstants.TAG, "Post finished.");
|
||||
}
|
||||
|
||||
private void appendTag(String name, String value, StringBuilder sb) {
|
||||
sb.append("<gsx:");
|
||||
sb.append(name);
|
||||
sb.append('>');
|
||||
sb.append(StringUtils.stringAsCData(value));
|
||||
sb.append("</gsx:");
|
||||
sb.append(name);
|
||||
sb.append('>');
|
||||
}
|
||||
|
||||
private void appendLargeUnitsTag(String name, double d, StringBuilder sb) {
|
||||
double value = metricUnits ? d : (d * UnitConversions.KM_TO_MI);
|
||||
appendTag(name, LARGE_UNIT_FORMAT.format(value), sb);
|
||||
}
|
||||
|
||||
private void appendSmallUnitsTag(String name, double d, StringBuilder sb) {
|
||||
double value = metricUnits ? d : (d * UnitConversions.M_TO_FT);
|
||||
appendTag(name, SMALL_UNIT_FORMAT.format(value), sb);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,633 @@
|
||||
/*
|
||||
* Copyright 2010 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package com.google.android.apps.mytracks.io;
|
||||
|
||||
import com.google.android.apps.mytracks.MyTracks;
|
||||
import com.google.android.apps.mytracks.MyTracksConstants;
|
||||
import com.google.android.apps.mytracks.MyTracksSettings;
|
||||
import com.google.android.apps.mytracks.ProgressIndicator;
|
||||
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
|
||||
import com.google.android.apps.mytracks.content.Track;
|
||||
import com.google.android.apps.mytracks.content.Waypoint;
|
||||
import com.google.android.apps.mytracks.io.gdata.GDataWrapper;
|
||||
import com.google.android.apps.mytracks.io.gdata.GDataWrapper.QueryFunction;
|
||||
import com.google.android.apps.mytracks.stats.DoubleBuffer;
|
||||
import com.google.android.apps.mytracks.stats.TripStatistics;
|
||||
import com.google.android.apps.mytracks.util.MyTracksUtils;
|
||||
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.api.client.googleapis.GoogleHeaders;
|
||||
import com.google.api.client.googleapis.GoogleTransport;
|
||||
import com.google.api.client.http.GenericUrl;
|
||||
import com.google.api.client.http.HttpRequest;
|
||||
import com.google.api.client.http.HttpResponse;
|
||||
import com.google.api.client.http.HttpTransport;
|
||||
import com.google.api.client.util.Strings;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.SharedPreferences;
|
||||
import android.database.Cursor;
|
||||
import android.location.Location;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URLEncoder;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Vector;
|
||||
|
||||
/**
|
||||
* A helper class used to transmit tracks to Google Fusion Tables.
|
||||
* A new instance should be used for each upload.
|
||||
*
|
||||
* @author Leif Hendrik Wilden
|
||||
*/
|
||||
public class SendToFusionTables implements Runnable {
|
||||
|
||||
public interface OnSendCompletedListener {
|
||||
void onSendCompleted(String tableId, boolean success, int statusMessage);
|
||||
}
|
||||
|
||||
/** The GData service id for Fusion Tables. */
|
||||
public static final String SERVICE_ID = "fusiontables";
|
||||
|
||||
/** The base url for viewing a fusion table. */
|
||||
public static final String FUSIONTABLES_TABLE_URL = "http://www.google.com/fusiontables";
|
||||
|
||||
/** The path for viewing a map visualization of a table. */
|
||||
private static final String FUSIONTABLES_MAP =
|
||||
"http://www.google.com/fusiontables/embedviz?" +
|
||||
"viz=MAP&q=select+col0,+col1,+col2,+col3+from+%s+&h=false&" +
|
||||
"lat=%f&lng=%f&z=%d&t=1&l=col2";
|
||||
|
||||
/** Standard base feed url for Fusion Tables. */
|
||||
private static final String FUSIONTABLES_BASE_FEED_URL =
|
||||
"http://www.google.com/fusiontables/api/query";
|
||||
|
||||
private static final int MAX_POINTS_PER_UPLOAD = 2048;
|
||||
|
||||
private final Activity context;
|
||||
private final AuthManager auth;
|
||||
private final long trackId;
|
||||
private final ProgressIndicator progressIndicator;
|
||||
private final OnSendCompletedListener onCompletion;
|
||||
private final StringUtils stringUtils;
|
||||
private final MyTracksProviderUtils providerUtils;
|
||||
|
||||
// Progress status
|
||||
private int totalLocationsRead;
|
||||
private int totalLocationsPrepared;
|
||||
private int totalLocationsUploaded;
|
||||
private int totalLocations;
|
||||
private int totalSegmentsUploaded;
|
||||
|
||||
private HttpTransport transport;
|
||||
private String tableId;
|
||||
|
||||
public SendToFusionTables(Activity context, AuthManager auth,
|
||||
long trackId, ProgressIndicator progressIndicator,
|
||||
OnSendCompletedListener onCompletion) {
|
||||
this.context = context;
|
||||
this.auth = auth;
|
||||
this.trackId = trackId;
|
||||
this.progressIndicator = progressIndicator;
|
||||
this.onCompletion = onCompletion;
|
||||
this.stringUtils = new StringUtils(context);
|
||||
this.providerUtils = MyTracksProviderUtils.Factory.get(context);
|
||||
transport = GoogleTransport.create();
|
||||
GoogleHeaders headers = (GoogleHeaders) transport.defaultHeaders;
|
||||
headers.setApplicationName("Google-MyTracks-" + MyTracks.getMyTracksVersion(context));
|
||||
headers.gdataVersion = "2";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
Log.d(MyTracksConstants.TAG, "Sending to Fusion tables: trackId = " + trackId);
|
||||
doUpload();
|
||||
}
|
||||
|
||||
public static String getMapVisualizationUrl(Track track) {
|
||||
// TODO(leifhendrik): Determine correct bounding box and zoom level.
|
||||
Location start = track.getLocations().get(0);
|
||||
double lat = start != null ? start.getLatitude() : 37.0;
|
||||
double lng = start != null ? start.getLongitude() : -121.0;
|
||||
int z = 15;
|
||||
return String.format(FUSIONTABLES_MAP, track.getTableId(), lat, lng, z);
|
||||
}
|
||||
|
||||
private void doUpload() {
|
||||
((GoogleHeaders) transport.defaultHeaders).setGoogleLogin(auth.getAuthToken());
|
||||
int statusMessageId = R.string.error_sending_to_fusiontables;
|
||||
boolean success = true;
|
||||
try {
|
||||
progressIndicator.setProgressValue(1);
|
||||
progressIndicator.setProgressMessage(R.string.progress_message_reading_track);
|
||||
|
||||
// Get the track meta-data
|
||||
Track track = providerUtils.getTrack(trackId);
|
||||
String originalDescription = track.getDescription();
|
||||
|
||||
// Create a new table:
|
||||
progressIndicator.setProgressMessage(R.string.progress_message_creating_fusiontable);
|
||||
success = createNewTable(track);
|
||||
|
||||
// Upload all of the segments of the track plus start/end markers
|
||||
if (success) {
|
||||
success = uploadAllTrackPoints(track, originalDescription);
|
||||
}
|
||||
|
||||
// Upload all the waypoints.
|
||||
if (success) {
|
||||
success = uploadWaypoints(track);
|
||||
statusMessageId = R.string.status_new_fusiontable_has_been_created;
|
||||
}
|
||||
Log.d(MyTracksConstants.TAG, "SendToFusionTables: Done: " + success);
|
||||
progressIndicator.setProgressValue(100);
|
||||
} finally {
|
||||
|
||||
final boolean finalSuccess = success;
|
||||
final int finalStatusMessageId = statusMessageId;
|
||||
context.runOnUiThread(new Runnable() {
|
||||
public void run() {
|
||||
if (onCompletion != null) {
|
||||
onCompletion.onSendCompleted(
|
||||
tableId, finalSuccess, finalStatusMessageId);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new table.
|
||||
* If successful sets {@link #tableId}.
|
||||
*
|
||||
* @return true in case of success.
|
||||
*/
|
||||
private boolean createNewTable(Track track) {
|
||||
Log.d(MyTracksConstants.TAG, "Creating a new fusion table.");
|
||||
String query = "CREATE TABLE '" + track.getName() +
|
||||
"' (name:STRING,description:STRING,geometry:LOCATION,marker:STRING)";
|
||||
return runUpdate(query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats given values SQL style. Escapes single quotes with a backslash.
|
||||
*
|
||||
* @param values the values to format
|
||||
* @return the values formatted as: ('value1','value2',...,'value_n').
|
||||
*/
|
||||
private static String values(String... values) {
|
||||
StringBuilder builder = new StringBuilder("(");
|
||||
for (int i = 0; i < values.length; i++) {
|
||||
builder.append('\'');
|
||||
builder.append(values[i].replaceAll("'", "xxx"));
|
||||
builder.append('\'');
|
||||
if (i < values.length - 1) {
|
||||
builder.append(',');
|
||||
}
|
||||
}
|
||||
builder.append(')');
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new row representing a marker.
|
||||
*
|
||||
* @param name the marker name
|
||||
* @param description the marker description
|
||||
* @param the marker location
|
||||
* @return true in case of success.
|
||||
*/
|
||||
private boolean createNewPoint(String name, String description, Location location,
|
||||
String marker) {
|
||||
Log.d(MyTracksConstants.TAG, "Creating a new row with a point.");
|
||||
String query = "INSERT INTO " + tableId + " (name,description,geometry,marker) VALUES "
|
||||
+ values(name, description, getKmlPoint(location), marker);
|
||||
return runUpdate(query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new row representing a line segment.
|
||||
*
|
||||
* @param track the track/segment to draw
|
||||
* @return true in case of success.
|
||||
*/
|
||||
private boolean createNewLineString(Track track) {
|
||||
Log.d(MyTracksConstants.TAG, "Creating a new row with a point.");
|
||||
String query = "INSERT INTO " + tableId
|
||||
+ " (name,description,geometry) VALUES "
|
||||
+ values(track.getName(), track.getDescription(), getKmlLineString(track));
|
||||
return runUpdate(query);
|
||||
}
|
||||
|
||||
private boolean uploadAllTrackPoints(final Track track, String originalDescription) {
|
||||
|
||||
SharedPreferences preferences = context.getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0);
|
||||
boolean metricUnits = true;
|
||||
if (preferences != null) {
|
||||
metricUnits = preferences.getBoolean(context.getString(R.string.metric_units_key), true);
|
||||
}
|
||||
|
||||
Cursor locationsCursor = providerUtils.getLocationsCursor(track.getId(), 0, -1, false);
|
||||
try {
|
||||
if (!locationsCursor.moveToFirst()) {
|
||||
Log.w(MyTracksConstants.TAG, "Unable to get any points to upload");
|
||||
return false;
|
||||
}
|
||||
|
||||
totalLocationsRead = 0;
|
||||
totalLocationsPrepared = 0;
|
||||
totalLocationsUploaded = 0;
|
||||
totalLocations = locationsCursor.getCount();
|
||||
totalSegmentsUploaded = 0;
|
||||
|
||||
// Limit the number of elevation readings. Ideally we would want around 250.
|
||||
int elevationSamplingFrequency =
|
||||
Math.max(1, (int) (totalLocations / 250.0));
|
||||
Log.d(MyTracksConstants.TAG,
|
||||
"Using elevation sampling factor: " + elevationSamplingFrequency
|
||||
+ " on " + totalLocations);
|
||||
double totalDistance = 0;
|
||||
|
||||
Vector<Double> distances = new Vector<Double>();
|
||||
Vector<Double> elevations = new Vector<Double>();
|
||||
DoubleBuffer elevationBuffer = new DoubleBuffer(MyTracksConstants.ELEVATION_SMOOTHING_FACTOR);
|
||||
|
||||
List<Location> locations = new ArrayList<Location>(MAX_POINTS_PER_UPLOAD);
|
||||
progressIndicator.setProgressMessage(R.string.progress_message_reading_track);
|
||||
Location lastLocation = null;
|
||||
do {
|
||||
if (totalLocationsRead % 100 == 0) {
|
||||
updateProgress();
|
||||
}
|
||||
|
||||
Location loc = providerUtils.createLocation(locationsCursor);
|
||||
locations.add(loc);
|
||||
|
||||
if (totalLocationsRead == 0) {
|
||||
// Put a marker at the first point of the first valid segment:
|
||||
createNewPoint(track.getName(), track.getDescription(), loc, "large_green");
|
||||
}
|
||||
|
||||
// Add to the elevation profile.
|
||||
if (loc != null && MyTracksUtils.isValidLocation(loc)) {
|
||||
// All points go into the smoothing buffer...
|
||||
elevationBuffer.setNext(metricUnits ? loc.getAltitude()
|
||||
: loc.getAltitude() * UnitConversions.M_TO_FT);
|
||||
if (lastLocation != null) {
|
||||
double dist = lastLocation.distanceTo(loc);
|
||||
totalDistance += dist;
|
||||
}
|
||||
|
||||
// ...but only a few points are really used to keep the url short.
|
||||
if (totalLocationsRead % elevationSamplingFrequency == 0) {
|
||||
distances.add(totalDistance);
|
||||
elevations.add(elevationBuffer.getAverage());
|
||||
}
|
||||
}
|
||||
|
||||
// If the location was not valid, it's a segment split, so make sure the
|
||||
// distance between the previous segment and the new one is not accounted
|
||||
// for in the next iteration.
|
||||
lastLocation = loc;
|
||||
|
||||
// Every now and then, upload the accumulated points
|
||||
if (totalLocationsRead % MAX_POINTS_PER_UPLOAD
|
||||
== MAX_POINTS_PER_UPLOAD - 1) {
|
||||
if (!prepareAndUploadPoints(track, locations)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
totalLocationsRead++;
|
||||
} while (locationsCursor.moveToNext());
|
||||
|
||||
// Do a final upload with what's left
|
||||
if (!prepareAndUploadPoints(track, locations)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Put an end marker at the last point of the last valid segment:
|
||||
if (lastLocation != null) {
|
||||
track.setDescription("<p>" + originalDescription + "</p><p>"
|
||||
+ stringUtils.generateTrackDescription(
|
||||
track, distances, elevations)
|
||||
+ "</p>");
|
||||
return createNewPoint(track.getName(), track.getDescription(), lastLocation, "large_red");
|
||||
}
|
||||
|
||||
return true;
|
||||
} finally {
|
||||
locationsCursor.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends the given location to the string in the format:
|
||||
* longitude,latitude[,altitude]
|
||||
*
|
||||
* @param location the location to be added
|
||||
* @param builder the string builder to use
|
||||
*/
|
||||
private void appendCoordinate(Location location, StringBuilder builder) {
|
||||
builder
|
||||
.append(location.getLongitude())
|
||||
.append(",")
|
||||
.append(location.getLatitude());
|
||||
if (location.hasAltitude()) {
|
||||
builder.append(",");
|
||||
builder.append(location.getAltitude());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a KML Point tag for the given location.
|
||||
*
|
||||
* @param location The location.
|
||||
* @return the kml.
|
||||
*/
|
||||
private String getKmlPoint(Location location) {
|
||||
StringBuilder builder = new StringBuilder("<Point><coordinates>");
|
||||
appendCoordinate(location, builder);
|
||||
builder.append("</coordinates></Point>");
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a KML Point tag for the given location.
|
||||
*
|
||||
* @param location The location.
|
||||
* @return the kml.
|
||||
*/
|
||||
private String getKmlLineString(Track track) {
|
||||
StringBuilder builder = new StringBuilder("<LineString><coordinates>");
|
||||
for (Location location : track.getLocations()) {
|
||||
appendCoordinate(location, builder);
|
||||
builder.append(' ');
|
||||
}
|
||||
builder.append("</coordinates></LineString>");
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private boolean prepareAndUploadPoints(Track track, List<Location> locations) {
|
||||
progressIndicator.setProgressMessage(R.string.progress_message_preparing_track);
|
||||
updateProgress();
|
||||
|
||||
int numLocations = locations.size();
|
||||
if (numLocations < 2) {
|
||||
Log.d(MyTracksConstants.TAG, "Not preparing/uploading too few points");
|
||||
totalLocationsUploaded += numLocations;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Prepare/pre-process the points
|
||||
ArrayList<Track> splitTracks = prepareLocations(track, locations);
|
||||
|
||||
// Start uploading them
|
||||
progressIndicator.setProgressMessage(R.string.progress_message_sending_fusiontables);
|
||||
for (Track splitTrack : splitTracks) {
|
||||
if (totalSegmentsUploaded > 1) {
|
||||
splitTrack.setName(splitTrack.getName() + " "
|
||||
+ String.format(
|
||||
context.getString(R.string.part), totalSegmentsUploaded));
|
||||
}
|
||||
totalSegmentsUploaded++;
|
||||
Log.d(MyTracksConstants.TAG,
|
||||
"SendToFusionTables: Prepared feature for upload w/ "
|
||||
+ splitTrack.getLocations().size() + " points.");
|
||||
|
||||
// Transmit tracks via GData feed:
|
||||
// -------------------------------
|
||||
Log.d(MyTracksConstants.TAG,
|
||||
"SendToFusionTables: Uploading to table " + tableId + " w/ auth " + auth);
|
||||
if (!uploadTrackPoints(splitTrack)) {
|
||||
Log.e(MyTracksConstants.TAG, "Uploading failed");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
locations.clear();
|
||||
totalLocationsUploaded += numLocations;
|
||||
updateProgress();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares a buffer of locations for transmission to google fusion tables.
|
||||
*
|
||||
* @param track the original track with meta data
|
||||
* @param buffer a buffer of locations on the track
|
||||
* @return an array of tracks each with a sub section of the points in the
|
||||
* original buffer
|
||||
*/
|
||||
private ArrayList<Track> prepareLocations(
|
||||
Track track, Iterable<Location> locations) {
|
||||
ArrayList<Track> splitTracks = new ArrayList<Track>();
|
||||
|
||||
// Create segments from each full track:
|
||||
Track segment = new Track();
|
||||
TripStatistics segmentStats = segment.getStatistics();
|
||||
TripStatistics trackStats = track.getStatistics();
|
||||
segment.setId(track.getId());
|
||||
segment.setName(track.getName());
|
||||
segment.setDescription(/* track.getDescription() */ "");
|
||||
segment.setCategory(track.getCategory());
|
||||
segmentStats.setStartTime(trackStats.getStartTime());
|
||||
segmentStats.setStopTime(trackStats.getStopTime());
|
||||
boolean startNewTrackSegment = false;
|
||||
for (Location loc : locations) {
|
||||
if (totalLocationsPrepared % 100 == 0) {
|
||||
updateProgress();
|
||||
}
|
||||
if (loc.getLatitude() > 90) {
|
||||
startNewTrackSegment = true;
|
||||
}
|
||||
|
||||
if (startNewTrackSegment) {
|
||||
// Close up the last segment.
|
||||
prepareTrackSegment(segment, splitTracks);
|
||||
|
||||
Log.d(MyTracksConstants.TAG,
|
||||
"MyTracksSendToFusionTables: Starting new track segment...");
|
||||
startNewTrackSegment = false;
|
||||
segment = new Track();
|
||||
segment.setId(track.getId());
|
||||
segment.setName(track.getName());
|
||||
segment.setDescription(/* track.getDescription() */ "");
|
||||
segment.setCategory(track.getCategory());
|
||||
}
|
||||
|
||||
if (loc.getLatitude() <= 90) {
|
||||
segment.addLocation(loc);
|
||||
if (segmentStats.getStartTime() < 0) {
|
||||
segmentStats.setStartTime(loc.getTime());
|
||||
}
|
||||
}
|
||||
totalLocationsPrepared++;
|
||||
}
|
||||
|
||||
prepareTrackSegment(segment, splitTracks);
|
||||
|
||||
return splitTracks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares a track segment for sending to google fusion tables.
|
||||
* The main steps are:
|
||||
* - correcting end time
|
||||
* - decimating locations
|
||||
* - splitting into smaller tracks.
|
||||
*
|
||||
* The final track pieces will be put in the array list splitTracks.
|
||||
*
|
||||
* @param segment the original segment of the track
|
||||
* @param splitTracks an array of smaller track segments
|
||||
*/
|
||||
private void prepareTrackSegment(
|
||||
Track segment, ArrayList<Track> splitTracks) {
|
||||
TripStatistics segmentStats = segment.getStatistics();
|
||||
if (segmentStats.getStopTime() < 0
|
||||
&& segment.getLocations().size() > 0) {
|
||||
segmentStats.setStopTime(segment.getLocations().size() - 1);
|
||||
}
|
||||
|
||||
/*
|
||||
* Decimate to 2 meter precision. Fusion tables doesn't like too many
|
||||
* points:
|
||||
*/
|
||||
MyTracksUtils.decimate(segment, 2.0);
|
||||
|
||||
/* If the track still has > 2500 points, split it in pieces: */
|
||||
final int maxPoints = 2500;
|
||||
if (segment.getLocations().size() > maxPoints) {
|
||||
splitTracks.addAll(MyTracksUtils.split(segment, maxPoints));
|
||||
} else if (segment.getLocations().size() >= 2) {
|
||||
splitTracks.add(segment);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean uploadTrackPoints(Track splitTrack) {
|
||||
int numLocations = splitTrack.getLocations().size();
|
||||
if (numLocations < 2) {
|
||||
// Need at least two points for a polyline:
|
||||
Log.w(MyTracksConstants.TAG, "Not uploading too few points");
|
||||
return true;
|
||||
}
|
||||
return createNewLineString(splitTrack);
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploads all of the waypoints associated with this track to a table.
|
||||
*
|
||||
* @param track The track to upload waypoints for.
|
||||
*
|
||||
* @return True on success.
|
||||
*/
|
||||
private boolean uploadWaypoints(final Track track) {
|
||||
// TODO: Stream through the waypoints in chunks.
|
||||
// I am leaving the number of waypoints very high which should not be a
|
||||
// problem because we don't try to load them into objects all at the
|
||||
// same time.
|
||||
boolean success = true;
|
||||
Cursor c = null;
|
||||
c = providerUtils.getWaypointsCursor(
|
||||
track.getId(), 0,
|
||||
MyTracksConstants.MAX_LOADED_WAYPOINTS_POINTS);
|
||||
if (c != null) {
|
||||
if (c.moveToFirst()) {
|
||||
// This will skip the 1st waypoint (it carries the stats for the
|
||||
// last segment).
|
||||
while (c.moveToNext()) {
|
||||
Waypoint wpt = providerUtils.createWaypoint(c);
|
||||
Log.d(MyTracksConstants.TAG, "SendToFusionTables: Creating waypoint.");
|
||||
success = createNewPoint(wpt.getName(), wpt.getDescription(), wpt.getLocation(),
|
||||
"large_yellow");
|
||||
if (!success) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!success) {
|
||||
Log.w(MyTracksConstants.TAG, "SendToFusionTables: upload waypoints failed.");
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current upload progress.
|
||||
*/
|
||||
private void updateProgress() {
|
||||
// The percent of the total that represents the completed part of this
|
||||
// segment.
|
||||
int totalPercentage =
|
||||
(totalLocationsRead + totalLocationsPrepared + totalLocationsUploaded)
|
||||
/ (totalLocations * 3);
|
||||
totalPercentage = Math.min(99, totalPercentage);
|
||||
progressIndicator.setProgressValue(totalPercentage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs an update query. Handles authentication.
|
||||
*
|
||||
* @param query The given SQL like query
|
||||
* @return true in case of success
|
||||
*/
|
||||
private boolean runUpdate(final String query) {
|
||||
GDataWrapper<HttpTransport> wrapper = new GDataWrapper<HttpTransport>();
|
||||
wrapper.setAuthManager(auth);
|
||||
wrapper.setRetryOnAuthFailure(true);
|
||||
wrapper.setClient(transport);
|
||||
Log.d(MyTracksConstants.TAG, "GData connection prepared: " + this.auth);
|
||||
wrapper.runQuery(new QueryFunction<HttpTransport>() {
|
||||
@Override
|
||||
public void query(HttpTransport client)
|
||||
throws IOException, GDataWrapper.ParseException, GDataWrapper.HttpException {
|
||||
HttpRequest request = transport.buildPostRequest();
|
||||
request.headers.contentType = "application/x-www-form-urlencoded";
|
||||
String sql = "sql=" + URLEncoder.encode(query, "UTF-8");
|
||||
GenericUrl url = new GenericUrl(FUSIONTABLES_BASE_FEED_URL + "?" + sql);
|
||||
request.url = url;
|
||||
//OutputStream outputStream = new ByteArrayOutputStream();
|
||||
//outputStream.write(Strings.toBytesUtf8(sql));
|
||||
//outputStream.flush();
|
||||
//request.content.writeTo(outputStream);
|
||||
Log.d(MyTracksConstants.TAG, "Running update query: " + url.toString());
|
||||
HttpResponse response = request.execute();
|
||||
boolean success = response.isSuccessStatusCode;
|
||||
if (success) {
|
||||
byte[] result = new byte[1024];
|
||||
response.getContent().read(result);
|
||||
String s = Strings.fromBytesUtf8(result);
|
||||
String[] lines = s.split(Strings.LINE_SEPARATOR);
|
||||
if (lines[0].equals("tableid")) {
|
||||
tableId = lines[1];
|
||||
Log.d(MyTracksConstants.TAG, "tableId = " + tableId);
|
||||
} else {
|
||||
Log.w(MyTracksConstants.TAG, "Unrecognized response: " + lines[0]);
|
||||
}
|
||||
} else {
|
||||
Log.d(MyTracksConstants.TAG, "Query failed: " + response.statusMessage + " (" +
|
||||
response.statusCode + ")");
|
||||
throw new GDataWrapper.HttpException(response.statusCode, response.statusMessage);
|
||||
}
|
||||
}
|
||||
});
|
||||
return wrapper.getErrorType() == GDataWrapper.ERROR_NO_ERROR;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
/*
|
||||
* Copyright 2010 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package com.google.android.apps.mytracks.io;
|
||||
|
||||
import com.google.android.apps.mytracks.content.MyTracksLocation;
|
||||
import com.google.android.apps.mytracks.content.Sensor;
|
||||
import com.google.android.apps.mytracks.content.Track;
|
||||
import com.google.android.apps.mytracks.content.Waypoint;
|
||||
import com.google.android.apps.mytracks.content.Sensor.SensorDataSet;
|
||||
import com.google.android.apps.mytracks.io.TrackWriterFactory.TrackFileFormat;
|
||||
|
||||
import android.location.Location;
|
||||
import android.os.Build;
|
||||
|
||||
import java.io.OutputStream;
|
||||
import java.io.PrintWriter;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.TimeZone;
|
||||
|
||||
/**
|
||||
* Write out a a track in the Garmin training center database, tcx format.
|
||||
* As defined by:
|
||||
* http://www.garmin.com/xmlschemas/TrainingCenterDatabase/v2
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public class TcxTrackWriter implements TrackFormatWriter {
|
||||
|
||||
private PrintWriter pw = null;
|
||||
private Track track;
|
||||
|
||||
static final SimpleDateFormat TIMESTAMP_FORMAT =
|
||||
new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
|
||||
static {
|
||||
TIMESTAMP_FORMAT.setTimeZone(TimeZone.getTimeZone("UTC"));
|
||||
}
|
||||
|
||||
public TcxTrackWriter() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void prepare(Track track, OutputStream out) {
|
||||
this.track = track;
|
||||
this.pw = new PrintWriter(out);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (pw != null) {
|
||||
pw.close();
|
||||
pw = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getExtension() {
|
||||
return TrackFileFormat.TCX.getExtension();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeHeader() {
|
||||
if (pw == null) {
|
||||
return;
|
||||
}
|
||||
pw.println("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\" ?>");
|
||||
pw.print("<TrainingCenterDatabase ");
|
||||
pw.print("xmlns=\"http://www.garmin.com/xmlschemas/TrainingCenterDatabase/v2\" ");
|
||||
pw.print("xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" ");
|
||||
pw.print("xsi:schemaLocation=\"http://www.garmin.com/xmlschemas/TrainingCenterDatabase/v2 ");
|
||||
pw.println("http://www.garmin.com/xmlschemas/TrainingCenterDatabasev2.xsd\">");
|
||||
pw.println();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeBeginTrack(Location firstPoint) {
|
||||
if (pw == null) {
|
||||
return;
|
||||
}
|
||||
pw.println(" <Activities>");
|
||||
pw.print(" <Activity Sport=\"");
|
||||
if (track.getCategory() != null) {
|
||||
pw.print(track.getCategory());
|
||||
}
|
||||
pw.println("\">");
|
||||
pw.print(" <Id>");
|
||||
pw.print(TIMESTAMP_FORMAT.format(track.getStatistics().getStartTime()));
|
||||
pw.println("</Id>");
|
||||
pw.println(" <Lap>");
|
||||
pw.print(" <TotalTimeSeconds>");
|
||||
pw.print(track.getStatistics().getTotalTime() / 1000);
|
||||
pw.println("</TotalTimeSeconds>");
|
||||
pw.print(" <DistanceMeters>");
|
||||
pw.print(track.getStatistics().getTotalDistance());
|
||||
pw.println("</DistanceMeters>");
|
||||
// TODO max speed etc.
|
||||
// Calories are a required element just put in 0.
|
||||
pw.print("<Calories>0</Calories>");
|
||||
pw.println("<Intensity>Active</Intensity>");
|
||||
pw.println("<TriggerMethod>Manual</TriggerMethod>)");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeOpenSegment() {
|
||||
if (pw != null) {
|
||||
pw.println(" <Track>");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeLocation(Location location) {
|
||||
if (pw == null) {
|
||||
return;
|
||||
}
|
||||
pw.println(" <Trackpoint>");
|
||||
Date d = new Date(location.getTime());
|
||||
pw.println(" <Time>" + TIMESTAMP_FORMAT.format(d) + "</Time>");
|
||||
pw.println(" <Position>");
|
||||
|
||||
pw.print(" <LatitudeDegrees>");
|
||||
pw.print(location.getLatitude());
|
||||
pw.println("</LatitudeDegrees>");
|
||||
|
||||
pw.print(" <LongitudeDegrees>");
|
||||
pw.print(location.getLongitude());
|
||||
pw.println("</LongitudeDegrees>");
|
||||
|
||||
pw.println(" </Position>");
|
||||
pw.print(" <AltitudeMeters>");
|
||||
pw.print(location.getAltitude());
|
||||
pw.println("</AltitudeMeters>");
|
||||
|
||||
if (location instanceof MyTracksLocation) {
|
||||
SensorDataSet sensorData = ((MyTracksLocation) location).getSensorDataSet();
|
||||
if (sensorData != null) {
|
||||
if (sensorData.hasHeartRate()
|
||||
&& sensorData.getHeartRate().getState() == Sensor.SensorState.SENDING
|
||||
&& sensorData.getHeartRate().hasValue()) {
|
||||
pw.print(" <HeartRateBpm>");
|
||||
pw.print("<Value>");
|
||||
pw.print(sensorData.getHeartRate().getValue());
|
||||
pw.print("</Value>");
|
||||
pw.println("</HeartRateBpm>");
|
||||
}
|
||||
if (sensorData.hasPower()
|
||||
&& sensorData.getPower().getState() == Sensor.SensorState.SENDING
|
||||
&& sensorData.getPower().hasValue()) {
|
||||
pw.print(" <Extensions>");
|
||||
pw.print("<TPX xmlns=\"http://www.garmin.com/xmlschemas/ActivityExtension/v2\">");
|
||||
pw.print("<Watts>");
|
||||
pw.print(sensorData.getPower().getValue());
|
||||
pw.print("</Watts>");
|
||||
pw.println("</TPX></Extensions>");
|
||||
}
|
||||
if (sensorData.hasCadence()
|
||||
&& sensorData.getCadence().getState() == Sensor.SensorState.SENDING
|
||||
&& sensorData.getCadence().hasValue()) {
|
||||
pw.print(" <Cadence>");
|
||||
pw.print(sensorData.getCadence().getValue());
|
||||
pw.println("</Cadence>");
|
||||
}
|
||||
}
|
||||
}
|
||||
pw.println(" </Trackpoint>");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeCloseSegment() {
|
||||
if (pw != null) {
|
||||
pw.println(" </Track>");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeEndTrack(Location lastPoint) {
|
||||
if (pw == null) {
|
||||
return;
|
||||
}
|
||||
pw.println(" </Lap>");
|
||||
pw.print(" <Creator xsi:type=\"Device_t\">");
|
||||
pw.print("<Name>");
|
||||
pw.print(Build.MODEL);
|
||||
pw.print("</Name>");
|
||||
pw.println("</Creator>)");
|
||||
pw.println(" </Activity>");
|
||||
pw.println(" </Activities>");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeFooter() {
|
||||
if (pw == null) {
|
||||
return;
|
||||
}
|
||||
pw.print(" <Author xsi:type=\"Application_t\">");
|
||||
pw.print("<Name>My Tracks by Google</Name>");
|
||||
pw.println("</Author>");
|
||||
pw.println("</TrainingCenterDatabase>");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeWaypoint(Waypoint waypoint) {
|
||||
// TODO Write out the waypoints somewhere.
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Copyright 2010 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package com.google.android.apps.mytracks.io;
|
||||
|
||||
import android.os.Environment;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
/**
|
||||
* A class to clean up old temporary files.
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public class TempFileCleaner {
|
||||
|
||||
private long currentTimeMillis;
|
||||
|
||||
public static void clean() {
|
||||
(new TempFileCleaner(System.currentTimeMillis())).cleanImpl();
|
||||
}
|
||||
|
||||
// @VisibleForTesting
|
||||
TempFileCleaner(long time) {
|
||||
currentTimeMillis = time;
|
||||
}
|
||||
|
||||
private void cleanImpl() {
|
||||
if (!Environment.getExternalStorageState().equals(
|
||||
Environment.MEDIA_MOUNTED)) {
|
||||
return; // Can't do anything now.
|
||||
}
|
||||
cleanTmpDirectory("csv");
|
||||
cleanTmpDirectory("gpx");
|
||||
cleanTmpDirectory("kml");
|
||||
cleanTmpDirectory("tcx");
|
||||
}
|
||||
|
||||
private void cleanTmpDirectory(String name) {
|
||||
String sep = System.getProperty("file.separator");
|
||||
cleanTmpDirectory(
|
||||
new File(
|
||||
Environment.getExternalStorageDirectory() + sep + name + sep
|
||||
+ "tmp"));
|
||||
}
|
||||
|
||||
// @VisibleForTesting
|
||||
int cleanTmpDirectory(File dir) {
|
||||
if (!dir.exists()) {
|
||||
return 0;
|
||||
}
|
||||
int count = 0;
|
||||
long oldest = currentTimeMillis - 1000 * 3600;
|
||||
for (File f : dir.listFiles()) {
|
||||
if (f.lastModified() < oldest) {
|
||||
f.delete();
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
* Copyright 2010 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package com.google.android.apps.mytracks.io;
|
||||
|
||||
import com.google.android.apps.mytracks.content.Track;
|
||||
import com.google.android.apps.mytracks.content.Waypoint;
|
||||
|
||||
import android.location.Location;
|
||||
|
||||
import java.io.OutputStream;
|
||||
|
||||
/**
|
||||
* Interface for writing data to a specific track file format.
|
||||
*
|
||||
* The expected sequence of calls is:
|
||||
* <ol>
|
||||
* <li>{@link #prepare}
|
||||
* <li>{@link #writeHeader}
|
||||
* <li>{@link #writeBeginTrack}
|
||||
* <li>For each segment:
|
||||
* <ol>
|
||||
* <li>{@link #writeOpenSegment}
|
||||
* <li>For each location in the segment: {@link #writeLocation}
|
||||
* <li>{@link #writeCloseSegment}
|
||||
* </ol>
|
||||
* <li>{@link #writeEndTrack}
|
||||
* <li>For each waypoint: {@link #writeWaypoint}
|
||||
* <li>{@link #writeFooter}
|
||||
* </ol>
|
||||
*
|
||||
* @author Rodrigo Damazio
|
||||
*/
|
||||
public interface TrackFormatWriter {
|
||||
|
||||
/**
|
||||
* Sets up the writer to write the given track to the given output.
|
||||
*
|
||||
* @param track the track to write
|
||||
* @param out the stream to write the track contents to
|
||||
*/
|
||||
void prepare(Track track, OutputStream out);
|
||||
|
||||
/**
|
||||
* @return The file extentsion (i.e. gpx, kml, ...)
|
||||
*/
|
||||
String getExtension();
|
||||
|
||||
/**
|
||||
* Writes the header.
|
||||
* This is chance for classes to write out opening information.
|
||||
*/
|
||||
void writeHeader();
|
||||
|
||||
/**
|
||||
* Writes the footer.
|
||||
* This is chance for classes to write out closing information.
|
||||
*/
|
||||
void writeFooter();
|
||||
|
||||
/**
|
||||
* Write the given location object.
|
||||
*
|
||||
* TODO Add some flexible handling of other sensor data.
|
||||
*
|
||||
* @param location the location to write
|
||||
*/
|
||||
void writeLocation(Location location);
|
||||
|
||||
/**
|
||||
* Write a way point.
|
||||
*
|
||||
* @param waypoint
|
||||
*/
|
||||
void writeWaypoint(Waypoint waypoint);
|
||||
|
||||
/**
|
||||
* Write the beginning of a track.
|
||||
*/
|
||||
void writeBeginTrack(Location firstPoint);
|
||||
|
||||
/**
|
||||
* Write the end of a track.
|
||||
*/
|
||||
void writeEndTrack(Location lastPoint);
|
||||
|
||||
/**
|
||||
* Write the statements necessary to open a new segment.
|
||||
*/
|
||||
void writeOpenSegment();
|
||||
|
||||
/**
|
||||
* Write the statements necessary to close a segment.
|
||||
*/
|
||||
void writeCloseSegment();
|
||||
|
||||
/**
|
||||
* Close the underlying file handle.
|
||||
*/
|
||||
void close();
|
||||
}
|
||||
@@ -0,0 +1,327 @@
|
||||
/*
|
||||
* Copyright 2008 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package com.google.android.apps.mytracks.io;
|
||||
|
||||
import com.google.android.apps.mytracks.MyTracksConstants;
|
||||
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.util.FileUtils;
|
||||
import com.google.android.apps.mytracks.util.MyTracksUtils;
|
||||
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;
|
||||
|
||||
/**
|
||||
* The class which exports tracks to the SD card.
|
||||
* This writer is 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 class TrackWriter {
|
||||
|
||||
private final Context context;
|
||||
private final MyTracksProviderUtils providerUtils;
|
||||
private final Track track;
|
||||
private final TrackFormatWriter writer;
|
||||
private final FileUtils fileUtils;
|
||||
private Runnable onCompletion = null;
|
||||
private boolean success = false;
|
||||
private int errorMessage = -1;
|
||||
private File directory = null;
|
||||
private File file = null;
|
||||
|
||||
public TrackWriter(Context context, MyTracksProviderUtils providerUtils,
|
||||
Track track, TrackFormatWriter writer) {
|
||||
this.context = context;
|
||||
this.providerUtils = providerUtils;
|
||||
this.track = track;
|
||||
this.writer = writer;
|
||||
this.fileUtils = new FileUtils();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a completion callback.
|
||||
*
|
||||
* @param onCompletion Runnable that will be executed when finished
|
||||
*/
|
||||
public void setOnCompletion(Runnable onCompletion) {
|
||||
this.onCompletion = onCompletion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a custom directory where the file will be written.
|
||||
*/
|
||||
public void setDirectory(File directory) {
|
||||
this.directory = directory;
|
||||
}
|
||||
|
||||
public String getAbsolutePath() {
|
||||
return file.getAbsolutePath();
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the given track id to the SD card.
|
||||
* This is non-blocking.
|
||||
*/
|
||||
public void writeTrackAsync() {
|
||||
Thread t = new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
writeTrack();
|
||||
}
|
||||
};
|
||||
t.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the given track id to the SD card.
|
||||
* This is blocking.
|
||||
*/
|
||||
public void writeTrack() {
|
||||
// Open the input and output
|
||||
success = false;
|
||||
errorMessage = R.string.error_track_does_not_exist;
|
||||
if (track != null) {
|
||||
if (openFile()) {
|
||||
writeDocument();
|
||||
}
|
||||
}
|
||||
finished();
|
||||
}
|
||||
|
||||
public int getErrorMessage() {
|
||||
return errorMessage;
|
||||
}
|
||||
|
||||
public boolean wasSuccess() {
|
||||
return success;
|
||||
}
|
||||
|
||||
/*
|
||||
* Helper methods:
|
||||
* ===============
|
||||
*/
|
||||
|
||||
private void finished() {
|
||||
if (onCompletion != null) {
|
||||
runOnUiThread(onCompletion);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the given runnable in the UI thread.
|
||||
*/
|
||||
protected void runOnUiThread(Runnable runnable) {
|
||||
if (context instanceof Activity) {
|
||||
((Activity) context).runOnUiThread(runnable);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the file and prepares the format writer for it.
|
||||
*
|
||||
* @return true on success, false otherwise (and errorMessage is set)
|
||||
*/
|
||||
protected boolean openFile() {
|
||||
if (!canWriteFile()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Make sure the file doesn't exist yet (possibly by changing the filename)
|
||||
String fileName = fileUtils.buildUniqueFileName(
|
||||
directory, track.getName(), writer.getExtension());
|
||||
if (fileName == null) {
|
||||
Log.e(MyTracksConstants.TAG,
|
||||
"Unable to get a unique filename for " + fileName);
|
||||
return false;
|
||||
}
|
||||
|
||||
Log.i(MyTracksConstants.TAG, "Writing track to: " + fileName);
|
||||
try {
|
||||
writer.prepare(track, newOutputStream(fileName));
|
||||
} catch (FileNotFoundException e) {
|
||||
Log.e(MyTracksConstants.TAG, "Failed to open output file.", e);
|
||||
errorMessage = R.string.io_write_failed;
|
||||
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(MyTracksConstants.TAG, "Could not find SD card.");
|
||||
errorMessage = R.string.io_no_external_storage_found;
|
||||
return false;
|
||||
}
|
||||
if (!fileUtils.ensureDirectoryExists(directory)) {
|
||||
Log.i(MyTracksConstants.TAG, "Could not create export directory.");
|
||||
errorMessage = R.string.io_create_dir_failed;
|
||||
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,
|
||||
MyTracksConstants.MAX_LOADED_WAYPOINTS_POINTS);
|
||||
if (cursor != null) {
|
||||
try {
|
||||
if (cursor.moveToFirst()) {
|
||||
// Yes, this will skip the 1st way point and that is intentional
|
||||
// as the 1st points holds the stats for the current/last segment.
|
||||
while (cursor.moveToNext()) {
|
||||
Waypoint wpt = providerUtils.createWaypoint(cursor);
|
||||
writer.writeWaypoint(wpt);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Does the actual work of writing the track to the now open file.
|
||||
*/
|
||||
void writeDocument() {
|
||||
Log.d(MyTracksConstants.TAG, "Started writing track.");
|
||||
writer.writeHeader();
|
||||
// TODO: Fix ordering (in GPX waypoints should come first)
|
||||
writeLocations();
|
||||
writeWaypoints(track.getId());
|
||||
writer.writeFooter();
|
||||
writer.close();
|
||||
success = true;
|
||||
Log.d(MyTracksConstants.TAG, "Done writing track.");
|
||||
errorMessage = R.string.io_write_finished;
|
||||
}
|
||||
|
||||
private void writeLocations() {
|
||||
boolean wroteFirst = false;
|
||||
boolean segmentOpen = false;
|
||||
Location lastLoc = null, loc = null;
|
||||
boolean isLastValid = false;
|
||||
Cursor locationsCursor =
|
||||
providerUtils.getLocationsCursor(track.getId(), 0, -1, false);
|
||||
|
||||
if (locationsCursor == null || !locationsCursor.moveToFirst()) {
|
||||
Log.w(MyTracksConstants.TAG, "Unable to get any points to write");
|
||||
return;
|
||||
}
|
||||
|
||||
do {
|
||||
if (loc == null) loc = new Location("");
|
||||
providerUtils.fillLocation(locationsCursor, loc);
|
||||
|
||||
boolean isValid = MyTracksUtils.isValidLocation(loc);
|
||||
boolean validSegment = isValid && isLastValid;
|
||||
if (!wroteFirst && validSegment) {
|
||||
// Found the first two consecutive points which are valid
|
||||
writer.writeBeginTrack(lastLoc);
|
||||
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(lastLoc);
|
||||
}
|
||||
|
||||
// Write the current point
|
||||
writer.writeLocation(loc);
|
||||
} else {
|
||||
if (segmentOpen) {
|
||||
writer.writeCloseSegment();
|
||||
segmentOpen = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Swap loc and lastLoc (so lastLoc is reused)
|
||||
Location tmp = lastLoc;
|
||||
lastLoc = loc;
|
||||
loc = tmp;
|
||||
if (loc != null) loc.reset();
|
||||
|
||||
isLastValid = isValid;
|
||||
} while (locationsCursor.moveToNext());
|
||||
|
||||
if (segmentOpen) {
|
||||
writer.writeCloseSegment();
|
||||
segmentOpen = false;
|
||||
}
|
||||
if (wroteFirst) {
|
||||
writer.writeEndTrack(lastLoc);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
* Copyright 2010 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
|
||||
package com.google.android.apps.mytracks.io;
|
||||
|
||||
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
|
||||
import com.google.android.apps.mytracks.content.Track;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
/**
|
||||
* A factory to produce track writers for any format.
|
||||
*
|
||||
* @author Rodrigo Damazio
|
||||
*/
|
||||
public class TrackWriterFactory {
|
||||
|
||||
/**
|
||||
* Definition of all possible track formats.
|
||||
*/
|
||||
public enum TrackFileFormat {
|
||||
GPX {
|
||||
@Override
|
||||
TrackFormatWriter newFormatWriter(Context context) {
|
||||
return new GpxTrackWriter();
|
||||
}
|
||||
},
|
||||
KML {
|
||||
@Override
|
||||
TrackFormatWriter newFormatWriter(Context context) {
|
||||
return new KmlTrackWriter(context);
|
||||
}
|
||||
},
|
||||
CSV {
|
||||
@Override
|
||||
public TrackFormatWriter newFormatWriter(Context context) {
|
||||
return new CsvTrackWriter();
|
||||
}
|
||||
},
|
||||
TCX {
|
||||
@Override
|
||||
public TrackFormatWriter newFormatWriter(Context context) {
|
||||
return new TcxTrackWriter();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 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);
|
||||
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 TrackWriter(context, providerUtils, track, writer);
|
||||
}
|
||||
|
||||
private TrackWriterFactory() { }
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
/*
|
||||
* 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.MyTracks;
|
||||
import com.google.android.apps.mytracks.MyTracksConstants;
|
||||
import com.google.android.apps.mytracks.util.FileUtils;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.app.AlertDialog.Builder;
|
||||
import android.app.Dialog;
|
||||
import android.app.ProgressDialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.DialogInterface.OnClickListener;
|
||||
import android.content.Intent;
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* Helper which shows a UI for writing or restoring a backup,
|
||||
* and calls the appropriate handler for actually executing those
|
||||
* operations.
|
||||
*
|
||||
* @author Rodrigo Damazio
|
||||
*/
|
||||
public class BackupActivityHelper {
|
||||
|
||||
// Since the user sees this format, we use the local timezone
|
||||
private static final SimpleDateFormat DISPLAY_BACKUP_FORMAT =
|
||||
new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
private static final Comparator<Date> REVERSE_DATE_ORDER =
|
||||
new Comparator<Date>() {
|
||||
@Override
|
||||
public int compare(Date s1, Date s2) {
|
||||
return s2.compareTo(s1);
|
||||
}
|
||||
};
|
||||
|
||||
private final FileUtils fileUtils;
|
||||
private final ExternalFileBackup backup;
|
||||
private final Activity activity;
|
||||
|
||||
public BackupActivityHelper(Activity activity) {
|
||||
this.activity = activity;
|
||||
this.fileUtils = new FileUtils();
|
||||
this.backup = new ExternalFileBackup(activity, fileUtils);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a full backup to the default file.
|
||||
* This shows the results to the user.
|
||||
*/
|
||||
public void writeBackup() {
|
||||
if (!fileUtils.isSdCardAvailable()) {
|
||||
showToast(R.string.io_no_external_storage_found);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!backup.isBackupsDirectoryAvailable(true)) {
|
||||
showToast(R.string.io_create_dir_failed);
|
||||
return;
|
||||
}
|
||||
|
||||
final ProgressDialog progressDialog = ProgressDialog.show(
|
||||
activity,
|
||||
activity.getString(R.string.progress_title),
|
||||
activity.getString(R.string.backup_write_progress_message),
|
||||
true);
|
||||
|
||||
// Do the writing in another thread
|
||||
new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
backup.writeToDefaultFile();
|
||||
showToast(R.string.io_write_finished);
|
||||
} catch (IOException e) {
|
||||
Log.e(MyTracksConstants.TAG, "Failed to write backup", e);
|
||||
showToast(R.string.io_write_failed);
|
||||
} finally {
|
||||
dismissDialog(progressDialog);
|
||||
}
|
||||
}
|
||||
}.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores a full backup from the SD card.
|
||||
* The user will be given a choice of which backup to restore as well as a
|
||||
* confirmation dialog.
|
||||
*/
|
||||
public void restoreBackup() {
|
||||
// Get the list of existing backups
|
||||
if (!fileUtils.isSdCardAvailable()) {
|
||||
showToast(R.string.io_no_external_storage_found);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!backup.isBackupsDirectoryAvailable(false)) {
|
||||
showToast(R.string.no_backups);
|
||||
return;
|
||||
}
|
||||
|
||||
final Date[] backupDates = backup.getAvailableBackups();
|
||||
if (backupDates == null || backupDates.length == 0) {
|
||||
showToast(R.string.no_backups);
|
||||
return;
|
||||
}
|
||||
Arrays.sort(backupDates, REVERSE_DATE_ORDER);
|
||||
|
||||
// Show a confirmation dialog
|
||||
Builder confirmationDialogBuilder = new AlertDialog.Builder(activity);
|
||||
confirmationDialogBuilder.setMessage(R.string.restore_overwrites_warning);
|
||||
confirmationDialogBuilder.setCancelable(true);
|
||||
confirmationDialogBuilder.setPositiveButton(android.R.string.yes,
|
||||
new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
pickBackupForRestore(backupDates);
|
||||
}
|
||||
});
|
||||
confirmationDialogBuilder.setNegativeButton(android.R.string.no, null);
|
||||
confirmationDialogBuilder.create().show();
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a backup list for the user to pick, then restores it.
|
||||
*
|
||||
* @param backupDir the backup directory
|
||||
* @param backupDates the list of available backup files
|
||||
*/
|
||||
private void pickBackupForRestore(final Date[] backupDates) {
|
||||
if (backupDates.length == 1) {
|
||||
// Only one choice, don't bother showing the list
|
||||
restoreFromDateAsync(backupDates[0]);
|
||||
return;
|
||||
}
|
||||
|
||||
// Make a user-visible version of the backup filenames
|
||||
final String backupDateStrs[] = new String[backupDates.length];
|
||||
for (int i = 0; i < backupDates.length; i++) {
|
||||
backupDateStrs[i] = DISPLAY_BACKUP_FORMAT.format(backupDates[i]);
|
||||
}
|
||||
|
||||
// Show a dialog for the user to pick which backup to restore
|
||||
Builder dialogBuilder = new AlertDialog.Builder(activity);
|
||||
dialogBuilder.setCancelable(true);
|
||||
dialogBuilder.setTitle(R.string.select_backup_to_restore);
|
||||
dialogBuilder.setItems(backupDateStrs, new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
// User picked to restore this one
|
||||
restoreFromDateAsync(backupDates[which]);
|
||||
}
|
||||
});
|
||||
dialogBuilder.create().show();
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a progress dialog, then starts restoring the backup osynchronously.
|
||||
*
|
||||
* @param inputFile the file to restore from
|
||||
*/
|
||||
private void restoreFromDateAsync(final Date date) {
|
||||
// Show a progress dialog
|
||||
final ProgressDialog progressDialog = ProgressDialog.show(
|
||||
activity,
|
||||
activity.getString(R.string.progress_title),
|
||||
activity.getString(R.string.backup_import_progress_message),
|
||||
true);
|
||||
|
||||
// Do the actual importing in another thread (don't block the UI)
|
||||
new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
backup.restoreFromDate(date);
|
||||
showToast(R.string.io_read_finished);
|
||||
} catch (IOException e) {
|
||||
Log.e(MyTracksConstants.TAG, "Failed to restore backup", e);
|
||||
showToast(R.string.io_read_failed);
|
||||
} finally {
|
||||
dismissDialog(progressDialog);
|
||||
|
||||
// Data may have been restored, "reboot" the app to catch it
|
||||
restartApplication();
|
||||
}
|
||||
}
|
||||
}.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Restarts My Tracks completely.
|
||||
* This forces any modified data to be re-read.
|
||||
*/
|
||||
private void restartApplication() {
|
||||
Intent intent = new Intent(activity, MyTracks.class);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||
activity.startActivity(intent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a toast with the given contents.
|
||||
*/
|
||||
private void showToast(final int resId) {
|
||||
activity.runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Toast.makeText(activity, resId, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely dismisses the given dialog.
|
||||
*/
|
||||
private void dismissDialog(final Dialog dialog) {
|
||||
activity.runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
dialog.dismiss();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* Copyright 2010 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package com.google.android.apps.mytracks.io.backup;
|
||||
|
||||
import static com.google.android.apps.mytracks.content.ContentTypeIds.*;
|
||||
import com.google.android.apps.mytracks.content.TrackPointsColumns;
|
||||
import com.google.android.apps.mytracks.content.TracksColumns;
|
||||
import com.google.android.apps.mytracks.content.WaypointsColumns;
|
||||
|
||||
public class BackupColumns {
|
||||
/** Columns that go into the backup. */
|
||||
public static final String[] POINTS_BACKUP_COLUMNS =
|
||||
{ TrackPointsColumns._ID, TrackPointsColumns.TRACKID, TrackPointsColumns.LATITUDE,
|
||||
TrackPointsColumns.LONGITUDE, TrackPointsColumns.ALTITUDE, TrackPointsColumns.BEARING,
|
||||
TrackPointsColumns.TIME, TrackPointsColumns.ACCURACY, TrackPointsColumns.SPEED };
|
||||
public static final byte[] POINTS_BACKUP_COLUMN_TYPES =
|
||||
{ LONG_TYPE_ID, LONG_TYPE_ID, INT_TYPE_ID, INT_TYPE_ID, FLOAT_TYPE_ID,
|
||||
FLOAT_TYPE_ID, LONG_TYPE_ID, FLOAT_TYPE_ID, FLOAT_TYPE_ID };
|
||||
|
||||
public static final String[] TRACKS_BACKUP_COLUMNS = {
|
||||
TracksColumns._ID, TracksColumns.NAME, TracksColumns.DESCRIPTION, TracksColumns.CATEGORY,
|
||||
TracksColumns.STARTID, TracksColumns.STOPID, TracksColumns.STARTTIME, TracksColumns.STOPTIME,
|
||||
TracksColumns.NUMPOINTS, TracksColumns.TOTALDISTANCE, TracksColumns.TOTALTIME,
|
||||
TracksColumns.MOVINGTIME, TracksColumns.AVGSPEED, TracksColumns.AVGMOVINGSPEED,
|
||||
TracksColumns.MAXSPEED, TracksColumns.MINELEVATION, TracksColumns.MAXELEVATION,
|
||||
TracksColumns.ELEVATIONGAIN, TracksColumns.MINGRADE, TracksColumns.MAXGRADE,
|
||||
TracksColumns.MINLAT, TracksColumns.MAXLAT, TracksColumns.MINLON, TracksColumns.MAXLON };
|
||||
public static final byte[] TRACKS_BACKUP_COLUMN_TYPES = {
|
||||
LONG_TYPE_ID, STRING_TYPE_ID, STRING_TYPE_ID, STRING_TYPE_ID,
|
||||
LONG_TYPE_ID, LONG_TYPE_ID, LONG_TYPE_ID, LONG_TYPE_ID, INT_TYPE_ID,
|
||||
FLOAT_TYPE_ID, LONG_TYPE_ID, LONG_TYPE_ID, FLOAT_TYPE_ID, FLOAT_TYPE_ID,
|
||||
FLOAT_TYPE_ID, FLOAT_TYPE_ID, FLOAT_TYPE_ID, FLOAT_TYPE_ID, FLOAT_TYPE_ID,
|
||||
FLOAT_TYPE_ID, INT_TYPE_ID, INT_TYPE_ID, INT_TYPE_ID, INT_TYPE_ID };
|
||||
|
||||
public static final String[] WAYPOINTS_BACKUP_COLUMNS = {
|
||||
WaypointsColumns._ID, WaypointsColumns.TRACKID, WaypointsColumns.NAME,
|
||||
WaypointsColumns.DESCRIPTION, WaypointsColumns.CATEGORY, WaypointsColumns.ICON,
|
||||
WaypointsColumns.TYPE, WaypointsColumns.LENGTH, WaypointsColumns.DURATION,
|
||||
WaypointsColumns.STARTTIME, WaypointsColumns.STARTID, WaypointsColumns.STOPID,
|
||||
WaypointsColumns.LATITUDE, WaypointsColumns.LONGITUDE, WaypointsColumns.ALTITUDE,
|
||||
WaypointsColumns.BEARING, WaypointsColumns.TIME, WaypointsColumns.ACCURACY,
|
||||
WaypointsColumns.SPEED, WaypointsColumns.TOTALDISTANCE, WaypointsColumns.TOTALTIME,
|
||||
WaypointsColumns.MOVINGTIME, WaypointsColumns.AVGSPEED, WaypointsColumns.AVGMOVINGSPEED,
|
||||
WaypointsColumns.MAXSPEED, WaypointsColumns.MINELEVATION, WaypointsColumns.MAXELEVATION,
|
||||
WaypointsColumns.ELEVATIONGAIN, WaypointsColumns.MINGRADE, WaypointsColumns.MAXGRADE };
|
||||
public static final byte[] WAYPOINTS_BACKUP_COLUMN_TYPES = {
|
||||
LONG_TYPE_ID, LONG_TYPE_ID, STRING_TYPE_ID, STRING_TYPE_ID,
|
||||
STRING_TYPE_ID, STRING_TYPE_ID, INT_TYPE_ID, FLOAT_TYPE_ID, LONG_TYPE_ID,
|
||||
LONG_TYPE_ID, LONG_TYPE_ID, LONG_TYPE_ID, INT_TYPE_ID, INT_TYPE_ID,
|
||||
FLOAT_TYPE_ID, FLOAT_TYPE_ID, LONG_TYPE_ID, FLOAT_TYPE_ID, FLOAT_TYPE_ID,
|
||||
FLOAT_TYPE_ID, LONG_TYPE_ID, LONG_TYPE_ID, FLOAT_TYPE_ID, FLOAT_TYPE_ID,
|
||||
FLOAT_TYPE_ID, FLOAT_TYPE_ID, FLOAT_TYPE_ID, FLOAT_TYPE_ID, FLOAT_TYPE_ID,
|
||||
FLOAT_TYPE_ID };
|
||||
}
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* 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.util.ApiFeatures;
|
||||
|
||||
import android.app.backup.BackupManager;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
|
||||
|
||||
/**
|
||||
* Shared preferences listener which notifies the backup system about new data
|
||||
* being available for backup.
|
||||
* This class is API-version-safe and will provide a dummy implementation if
|
||||
* the device doesn't support backup services.
|
||||
*
|
||||
* @author Rodrigo Damazio
|
||||
*/
|
||||
public abstract class BackupPreferencesListener
|
||||
implements OnSharedPreferenceChangeListener {
|
||||
|
||||
/**
|
||||
* Real implementation of the listener, which calls the {@link BackupManager}.
|
||||
*/
|
||||
private static class BackupPreferencesListenerImpl
|
||||
extends BackupPreferencesListener {
|
||||
private final BackupManager backupManager;
|
||||
|
||||
public BackupPreferencesListenerImpl(Context context) {
|
||||
this.backupManager = new BackupManager(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(
|
||||
SharedPreferences sharedPreferences, String key) {
|
||||
backupManager.dataChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dummy implementation of the listener which does nothing.
|
||||
*/
|
||||
private static class DummyBackupPreferencesListener
|
||||
extends BackupPreferencesListener {
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(
|
||||
SharedPreferences sharedPreferences, String key) {
|
||||
// Do nothing
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and returns a proper instance of the listener for this device.
|
||||
*/
|
||||
public static BackupPreferencesListener create(
|
||||
Context context, ApiFeatures apiFeatures) {
|
||||
if (apiFeatures.hasBackup()) {
|
||||
return new BackupPreferencesListenerImpl(context);
|
||||
} else {
|
||||
return new DummyBackupPreferencesListener();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
/*
|
||||
* 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
|
||||
* @throws IOException if there are errors while writing
|
||||
*/
|
||||
public void writeHeaders(Cursor cursor, int numRows, DataOutputStream writer)
|
||||
throws IOException {
|
||||
initializeCachedValues(cursor);
|
||||
writeQueryMetadata(cursor, 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
|
||||
* @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 cursor the cursor that the data will come from
|
||||
* @param numRows the number of rows that will be dumped
|
||||
* @throws IOException if there are any errors while writing
|
||||
*/
|
||||
private void writeQueryMetadata(
|
||||
Cursor cursor, 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 columnType the type of the column to be read
|
||||
* @param cursor the cursor to read from
|
||||
* @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;
|
||||
default:
|
||||
throw new IllegalArgumentException(
|
||||
"Type " + columnTypeId + " not supported");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a dummy cell value to the output.
|
||||
*
|
||||
* @param columnType 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;
|
||||
default:
|
||||
throw new IllegalArgumentException(
|
||||
"Type " + columnTypeId + " not supported");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
/*
|
||||
* Copyright 2010 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package com.google.android.apps.mytracks.io.backup;
|
||||
|
||||
import com.google.android.apps.mytracks.content.ContentTypeIds;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.content.ContentValues;
|
||||
import android.net.Uri;
|
||||
|
||||
import java.io.DataInputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Database importer which reads values written by {@link DatabaseDumper}.
|
||||
*
|
||||
* @author Rodrigo Damazio
|
||||
*/
|
||||
public class DatabaseImporter {
|
||||
|
||||
/** Maximum number of entries in a bulk insertion */
|
||||
private static final int DEFAULT_BULK_SIZE = 1024;
|
||||
|
||||
private final Uri destinationUri;
|
||||
private final ContentResolver resolver;
|
||||
private final boolean readNullFields;
|
||||
private final int bulkSize;
|
||||
|
||||
// Metadata read from the reader
|
||||
private String[] columnNames;
|
||||
private byte[] columnTypes;
|
||||
|
||||
public DatabaseImporter(Uri destinationUri, ContentResolver resolver,
|
||||
boolean readNullFields) {
|
||||
this(destinationUri, resolver, readNullFields, DEFAULT_BULK_SIZE);
|
||||
}
|
||||
|
||||
protected DatabaseImporter(Uri destinationUri, ContentResolver resolver,
|
||||
boolean readNullFields, int bulkSize) {
|
||||
this.destinationUri = destinationUri;
|
||||
this.resolver = resolver;
|
||||
this.readNullFields = readNullFields;
|
||||
this.bulkSize = bulkSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the header which includes metadata about the table being imported.
|
||||
*
|
||||
* @throws IOException if there are any problems while reading
|
||||
*/
|
||||
private void readHeaders(DataInputStream reader) throws IOException {
|
||||
int numColumns = reader.readInt();
|
||||
columnNames = new String[numColumns];
|
||||
columnTypes = new byte[numColumns];
|
||||
for (int i = 0; i < numColumns; i++) {
|
||||
columnNames[i] = reader.readUTF();
|
||||
columnTypes[i] = reader.readByte();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports all rows from the reader into the database.
|
||||
* Insertion is done in bulks for efficiency.
|
||||
*
|
||||
* @throws IOException if there are any errors while reading
|
||||
*/
|
||||
public void importAllRows(DataInputStream reader) throws IOException {
|
||||
readHeaders(reader);
|
||||
|
||||
ContentValues[] valueBulk = new ContentValues[bulkSize];
|
||||
int numValues = 0;
|
||||
|
||||
int numRows = reader.readInt();
|
||||
int numColumns = columnNames.length;
|
||||
|
||||
// For each row
|
||||
for (int r = 0; r < numRows; r++) {
|
||||
if (valueBulk[numValues] == null) {
|
||||
valueBulk[numValues] = new ContentValues(numColumns);
|
||||
} else {
|
||||
// Reuse values objects
|
||||
valueBulk[numValues].clear();
|
||||
}
|
||||
|
||||
// Read the fields bitmap
|
||||
long fields = reader.readLong();
|
||||
for (int c = 0; c < numColumns; c++) {
|
||||
if ((fields & 1) == 1) {
|
||||
// Field is present, read into values
|
||||
readOneCell(columnNames[c], columnTypes[c], valueBulk[numValues],
|
||||
reader);
|
||||
} else if (readNullFields) {
|
||||
// Field not present but still written, read and discard
|
||||
readOneCell(columnNames[c], columnTypes[c], null, reader);
|
||||
}
|
||||
|
||||
fields >>= 1;
|
||||
}
|
||||
|
||||
numValues++;
|
||||
|
||||
// If we have enough values, flush them as a bulk insertion
|
||||
if (numValues >= bulkSize) {
|
||||
doBulkInsert(valueBulk);
|
||||
numValues = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Do a final bulk insert with the leftovers
|
||||
if (numValues > 0) {
|
||||
ContentValues[] leftovers = new ContentValues[numValues];
|
||||
System.arraycopy(valueBulk, 0, leftovers, 0, numValues);
|
||||
doBulkInsert(leftovers);
|
||||
}
|
||||
}
|
||||
|
||||
protected void doBulkInsert(ContentValues[] values) {
|
||||
resolver.bulkInsert(destinationUri, values);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a single cell from the reader.
|
||||
*
|
||||
* @param name the name of the column to be read
|
||||
* @param typeId the type ID of the column to be read
|
||||
* @param values the {@link ContentValues} object to put the read cell value
|
||||
* in - if null, the value is just discarded
|
||||
* @throws IOException if there are any problems while reading
|
||||
*/
|
||||
private void readOneCell(String name, byte typeId, ContentValues values,
|
||||
DataInputStream reader) throws IOException {
|
||||
switch (typeId) {
|
||||
case ContentTypeIds.BOOLEAN_TYPE_ID: {
|
||||
boolean value = reader.readBoolean();
|
||||
if (values != null) { values.put(name, value); }
|
||||
return;
|
||||
}
|
||||
case ContentTypeIds.LONG_TYPE_ID: {
|
||||
long value = reader.readLong();
|
||||
if (values != null) { values.put(name, value); }
|
||||
return;
|
||||
}
|
||||
case ContentTypeIds.DOUBLE_TYPE_ID: {
|
||||
double value = reader.readDouble();
|
||||
if (values != null) { values.put(name, value); }
|
||||
return;
|
||||
}
|
||||
case ContentTypeIds.FLOAT_TYPE_ID: {
|
||||
Float value = reader.readFloat();
|
||||
if (values != null) { values.put(name, value); }
|
||||
return;
|
||||
}
|
||||
case ContentTypeIds.INT_TYPE_ID: {
|
||||
int value = reader.readInt();
|
||||
if (values != null) { values.put(name, value); }
|
||||
return;
|
||||
}
|
||||
case ContentTypeIds.STRING_TYPE_ID: {
|
||||
String value = reader.readUTF();
|
||||
if (values != null) { values.put(name, value); }
|
||||
return;
|
||||
}
|
||||
default:
|
||||
throw new IOException("Read unknown type " + typeId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
/*
|
||||
* 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.MyTracksConstants;
|
||||
import com.google.android.apps.mytracks.MyTracksSettings;
|
||||
import com.google.android.apps.mytracks.content.TrackPointsColumns;
|
||||
import com.google.android.apps.mytracks.content.TracksColumns;
|
||||
import com.google.android.apps.mytracks.content.WaypointsColumns;
|
||||
import com.google.android.apps.mytracks.util.FileUtils;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.database.Cursor;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.DataInputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.TimeZone;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipFile;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
/**
|
||||
* Handler for writing or reading single-file backups.
|
||||
*
|
||||
* @author Rodrigo Damazio
|
||||
*/
|
||||
class ExternalFileBackup {
|
||||
// Filename format - in UTC
|
||||
private static final SimpleDateFormat BACKUP_FILENAME_FORMAT =
|
||||
new SimpleDateFormat("'backup-'yyyy-MM-dd_HH-mm-ss'.zip'");
|
||||
static {
|
||||
BACKUP_FILENAME_FORMAT.setTimeZone(TimeZone.getTimeZone("UTC"));
|
||||
}
|
||||
|
||||
private static final String BACKUPS_SUBDIR = "backups";
|
||||
private static final int BACKUP_FORMAT_VERSION = 1;
|
||||
private static final String ZIP_ENTRY_NAME =
|
||||
"backup.mytracks.v" + BACKUP_FORMAT_VERSION;
|
||||
private static final int COMPRESSION_LEVEL = 8;
|
||||
|
||||
private final Context context;
|
||||
private final FileUtils fileUtils;
|
||||
|
||||
public ExternalFileBackup(Context context, FileUtils fileUtils) {
|
||||
this.context = context;
|
||||
this.fileUtils = fileUtils;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the backups directory is (or can be made) available.
|
||||
*
|
||||
* @param create whether to try creating the directory if it doesn't exist
|
||||
*/
|
||||
public boolean isBackupsDirectoryAvailable(boolean create) {
|
||||
return getBackupsDirectory(create) != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the backup directory, or null if not available.
|
||||
*
|
||||
* @param create whether to try creating the directory if it doesn't exist
|
||||
*/
|
||||
private File getBackupsDirectory(boolean create) {
|
||||
String dirName = fileUtils.buildExternalDirectoryPath(BACKUPS_SUBDIR);
|
||||
final File dir = new File(dirName);
|
||||
Log.d(MyTracksConstants.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(MyTracksConstants.TAG,
|
||||
"Writing backup to file " + outputFile.getAbsolutePath());
|
||||
|
||||
// Create all the auxiliary classes that will do the writing
|
||||
PreferenceBackupHelper preferencesHelper = new PreferenceBackupHelper();
|
||||
DatabaseDumper trackDumper = new DatabaseDumper(
|
||||
BackupColumns.TRACKS_BACKUP_COLUMNS,
|
||||
BackupColumns.TRACKS_BACKUP_COLUMN_TYPES,
|
||||
false);
|
||||
DatabaseDumper waypointDumper = new DatabaseDumper(
|
||||
BackupColumns.WAYPOINTS_BACKUP_COLUMNS,
|
||||
BackupColumns.WAYPOINTS_BACKUP_COLUMN_TYPES,
|
||||
false);
|
||||
DatabaseDumper pointDumper = new DatabaseDumper(
|
||||
BackupColumns.POINTS_BACKUP_COLUMNS,
|
||||
BackupColumns.POINTS_BACKUP_COLUMN_TYPES,
|
||||
false);
|
||||
|
||||
// Open the target for writing
|
||||
FileOutputStream outputStream = new FileOutputStream(outputFile);
|
||||
ZipOutputStream compressedStream = new ZipOutputStream(outputStream);
|
||||
compressedStream.setLevel(COMPRESSION_LEVEL);
|
||||
compressedStream.putNextEntry(new ZipEntry(ZIP_ENTRY_NAME));
|
||||
DataOutputStream outWriter = new DataOutputStream(compressedStream);
|
||||
|
||||
try {
|
||||
// Dump the entire contents of each table
|
||||
ContentResolver contentResolver = context.getContentResolver();
|
||||
Cursor tracksCursor = contentResolver.query(
|
||||
TracksColumns.CONTENT_URI, null, null, null, null);
|
||||
try {
|
||||
trackDumper.writeAllRows(tracksCursor, outWriter);
|
||||
} finally {
|
||||
tracksCursor.close();
|
||||
}
|
||||
|
||||
Cursor waypointsCursor = contentResolver.query(
|
||||
WaypointsColumns.CONTENT_URI, null, null, null, null);
|
||||
try {
|
||||
waypointDumper.writeAllRows(waypointsCursor, outWriter);
|
||||
} finally {
|
||||
waypointsCursor.close();
|
||||
}
|
||||
|
||||
Cursor pointsCursor = contentResolver.query(
|
||||
TrackPointsColumns.CONTENT_URI, null, null, null, null);
|
||||
try {
|
||||
pointDumper.writeAllRows(pointsCursor, outWriter);
|
||||
} finally {
|
||||
pointsCursor.close();
|
||||
}
|
||||
|
||||
// Dump preferences
|
||||
SharedPreferences preferences =
|
||||
context.getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0);
|
||||
preferencesHelper.exportPreferences(preferences, outWriter);
|
||||
} catch (IOException e) {
|
||||
// We tried to delete the partially created file, but do nothing
|
||||
// if that also fails.
|
||||
outputFile.delete();
|
||||
|
||||
throw e;
|
||||
} finally {
|
||||
compressedStream.closeEntry();
|
||||
compressedStream.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronously restores the backup from the given file.
|
||||
*/
|
||||
private void restoreFromFile(File inputFile) throws IOException {
|
||||
Log.d(MyTracksConstants.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(MyTracksSettings.SETTINGS_NAME, 0);
|
||||
preferencesHelper.importPreferences(reader, preferences);
|
||||
} finally {
|
||||
compressedStream.close();
|
||||
zipFile.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
* Copyright 2010 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package com.google.android.apps.mytracks.io.backup;
|
||||
|
||||
import com.google.android.apps.mytracks.MyTracksConstants;
|
||||
import com.google.android.apps.mytracks.MyTracksSettings;
|
||||
|
||||
import android.app.backup.BackupAgent;
|
||||
import android.app.backup.BackupDataInput;
|
||||
import android.app.backup.BackupDataOutput;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Backup agent used to backup and restore all preferences.
|
||||
* We use a regular {@link BackupAgent} instead of the convenient helpers in
|
||||
* order to be future-proof (assuming we'll want to back up tracks later).
|
||||
*
|
||||
* @author Rodrigo Damazio
|
||||
*/
|
||||
public class MyTracksBackupAgent extends BackupAgent {
|
||||
private static final String PREFERENCES_ENTITY = "prefs";
|
||||
|
||||
@Override
|
||||
public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data,
|
||||
ParcelFileDescriptor newState) throws IOException {
|
||||
Log.i(MyTracksConstants.TAG, "Performing backup");
|
||||
SharedPreferences preferences = this.getSharedPreferences(
|
||||
MyTracksSettings.SETTINGS_NAME, 0);
|
||||
|
||||
backupPreferences(data, preferences);
|
||||
Log.i(MyTracksConstants.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(MyTracksConstants.TAG, "Restoring from backup");
|
||||
while (data.readNextHeader()) {
|
||||
String key = data.getKey();
|
||||
Log.d(MyTracksConstants.TAG, "Restoring entity " + key);
|
||||
if (key.equals(PREFERENCES_ENTITY)) {
|
||||
restorePreferences(data);
|
||||
} else {
|
||||
Log.e(MyTracksConstants.TAG, "Found unknown backup entity: " + key);
|
||||
data.skipEntityData();
|
||||
}
|
||||
}
|
||||
Log.i(MyTracksConstants.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(
|
||||
MyTracksSettings.SETTINGS_NAME, 0);
|
||||
PreferenceBackupHelper importer = createPreferenceBackupHelper();
|
||||
importer.importPreferences(dataBuffer, preferences);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
/*
|
||||
* 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.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
|
||||
* @return whether the preference change was successful
|
||||
* @throws IOException if there are any errors while reading
|
||||
*/
|
||||
public boolean importPreferences(byte[] data, SharedPreferences preferences)
|
||||
throws IOException {
|
||||
ByteArrayInputStream bufStream = new ByteArrayInputStream(data);
|
||||
DataInputStream reader = new DataInputStream(bufStream);
|
||||
|
||||
return importPreferences(reader, preferences);
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports all preferences from the given stream.
|
||||
*
|
||||
* @param reader the stream to read from
|
||||
* @param preferences the shared preferences to edit
|
||||
* @return whether the preference change was successful
|
||||
* @throws IOException if there are any errors while reading
|
||||
*/
|
||||
public boolean 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);
|
||||
}
|
||||
|
||||
return editor.commit();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,460 @@
|
||||
/*
|
||||
* 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.Config;
|
||||
import android.util.Log;
|
||||
|
||||
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;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* 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 boolean LOCAL_LOGV = DEBUG ? Config.LOGD : Config.LOGV;
|
||||
|
||||
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("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 (LOCAL_LOGV) {
|
||||
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 (response != null && 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,50 @@
|
||||
/*
|
||||
* 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.MyTracksConstants;
|
||||
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) {
|
||||
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(MyTracksConstants.TAG, "Using mytracks AndroidGDataClient.", e);
|
||||
return new com.google.android.apps.mytracks.io.gdata.AndroidGDataClient();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
/*
|
||||
* Copyright 2008 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package com.google.android.apps.mytracks.io.gdata;
|
||||
|
||||
import com.google.android.apps.mytracks.MyTracksConstants;
|
||||
import com.google.android.apps.mytracks.io.AuthManager;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.FutureTask;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
|
||||
/**
|
||||
* GDataWrapper provides a wrapper around GData operations that maintains the
|
||||
* GData client, and provides a method to run GData queries with proper error
|
||||
* handling. After a query is run, the wrapper can be queried about the error
|
||||
* that occurred.
|
||||
*
|
||||
* @param C the GData service client
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public class GDataWrapper<C> {
|
||||
|
||||
public static class AuthenticationException extends Exception {
|
||||
private static final long serialVersionUID = 1L;
|
||||
};
|
||||
|
||||
public static class ParseException extends Exception {
|
||||
private static final long serialVersionUID = 1L;
|
||||
};
|
||||
|
||||
public static class ConflictDetectedException extends Exception {
|
||||
private static final long serialVersionUID = 1L;
|
||||
};
|
||||
|
||||
public static class HttpException extends Exception {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private int statusCode;
|
||||
private String statusMessage;
|
||||
public HttpException(int statusCode, String statusMessage) {
|
||||
super();
|
||||
this.statusCode = statusCode;
|
||||
this.statusMessage = statusMessage;
|
||||
}
|
||||
public int getStatusCode() {
|
||||
return statusCode;
|
||||
}
|
||||
public String getStatusMessage() {
|
||||
return statusMessage;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* A QueryFunction is passed in when executing a query. The query function of
|
||||
* the class is called with the GData client as a parameter. The function
|
||||
* should execute whatever operations it desires on the client without concern
|
||||
* for whether the client will throw an error.
|
||||
*/
|
||||
public interface QueryFunction<C> {
|
||||
public abstract void query(C client)
|
||||
throws AuthenticationException, IOException, ParseException,
|
||||
ConflictDetectedException, HttpException;
|
||||
}
|
||||
|
||||
/**
|
||||
* A AuthenticatedFunction is passed in when executing the google
|
||||
* authenticated service. The authenticated function of the class is called
|
||||
* with the current authentication token for the service. The function should
|
||||
* execute whatever operations with the google service without concern for
|
||||
* whether the client will throw an error.
|
||||
*/
|
||||
public interface AuthenticatedFunction {
|
||||
public abstract void run(String authenticationToken)
|
||||
throws AuthenticationException, IOException;
|
||||
}
|
||||
|
||||
// The types of error that may be encountered
|
||||
// No error occurred.
|
||||
public static final int ERROR_NO_ERROR = 0;
|
||||
// There was an authentication error, the auth token may be invalid.
|
||||
public static final int ERROR_AUTH = 1;
|
||||
// There was an internal error on the server side.
|
||||
public static final int ERROR_INTERNAL = 2;
|
||||
// There was an error connecting to the server.
|
||||
public static final int ERROR_CONNECTION = 3;
|
||||
// The item queried did not exit.
|
||||
public static final int ERROR_NOT_FOUND = 4;
|
||||
// There was an error parsing or serializing locally.
|
||||
public static final int ERROR_LOCAL = 5;
|
||||
// There was a conflict, update the entry and try again.
|
||||
public static final int ERROR_CONFLICT = 6;
|
||||
// A query was run after cleaning up the wrapper, so the client was invalid.
|
||||
public static final int ERROR_CLEANED_UP = 7;
|
||||
// An unknown error occurred.
|
||||
public static final int ERROR_UNKNOWN = 100;
|
||||
|
||||
private static final int AUTH_TOKEN_INVALIDATE_REFRESH_NUM_RETRIES = 1;
|
||||
private static final int AUTH_TOKEN_INVALIDATE_REFRESH_TIMEOUT = 5000;
|
||||
|
||||
private String errorMessage;
|
||||
private int errorType;
|
||||
private C gdataServiceClient;
|
||||
private AuthManager auth;
|
||||
private boolean retryOnAuthFailure;
|
||||
|
||||
public GDataWrapper() {
|
||||
errorType = ERROR_NO_ERROR;
|
||||
errorMessage = null;
|
||||
auth = null;
|
||||
retryOnAuthFailure = false;
|
||||
}
|
||||
|
||||
public void setClient(C gdataServiceClient) {
|
||||
this.gdataServiceClient = gdataServiceClient;
|
||||
}
|
||||
|
||||
public boolean runAuthenticatedFunction(
|
||||
final AuthenticatedFunction function) {
|
||||
return runCommon(function, null);
|
||||
}
|
||||
|
||||
public boolean runQuery(final QueryFunction<C> query) {
|
||||
return runCommon(null, query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs an arbitrary piece of code.
|
||||
*/
|
||||
private boolean runCommon(final AuthenticatedFunction function,
|
||||
final QueryFunction<C> query) {
|
||||
for (int i = 0; i <= AUTH_TOKEN_INVALIDATE_REFRESH_NUM_RETRIES; i++) {
|
||||
runOne(function, query);
|
||||
if (errorType == ERROR_NO_ERROR) {
|
||||
return true;
|
||||
}
|
||||
|
||||
Log.d(MyTracksConstants.TAG, "GData error encountered: " + errorMessage);
|
||||
if (errorType == ERROR_AUTH && auth != null) {
|
||||
if (!retryOnAuthFailure || !invalidateAndRefreshAuthToken()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Log.d(MyTracksConstants.TAG, "retrying function/query");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a given function or query. If one is executed, errorType and
|
||||
* errorMessage will contain the result/status of the function/query.
|
||||
*/
|
||||
private void runOne(final AuthenticatedFunction function,
|
||||
final QueryFunction<C> query) {
|
||||
try {
|
||||
if (function != null) {
|
||||
function.run(this.auth.getAuthToken());
|
||||
} else if (query != null) {
|
||||
query.query(gdataServiceClient);
|
||||
} else {
|
||||
throw new IllegalArgumentException(
|
||||
"invalid invocation of runOne; one of function/query " +
|
||||
"must be non-null");
|
||||
}
|
||||
|
||||
errorType = ERROR_NO_ERROR;
|
||||
errorMessage = null;
|
||||
|
||||
} catch (AuthenticationException e) {
|
||||
Log.e(MyTracksConstants.TAG, "Exception", e);
|
||||
errorType = ERROR_AUTH;
|
||||
errorMessage = e.getMessage();
|
||||
} catch (HttpException e) {
|
||||
Log.e(MyTracksConstants.TAG, "HttpException", e);
|
||||
errorMessage = e.getMessage();
|
||||
if (errorMessage.contains("401")) {
|
||||
errorType = ERROR_AUTH;
|
||||
} else {
|
||||
errorType = ERROR_CONNECTION;
|
||||
}
|
||||
} catch (FileNotFoundException e) {
|
||||
Log.e(MyTracksConstants.TAG, "Exception", e);
|
||||
errorType = ERROR_AUTH;
|
||||
errorMessage = e.getMessage();
|
||||
} catch (IOException e) {
|
||||
Log.e(MyTracksConstants.TAG, "Exception", e);
|
||||
errorMessage = e.getMessage();
|
||||
if (errorMessage.contains("503")) {
|
||||
errorType = ERROR_INTERNAL;
|
||||
} else {
|
||||
errorType = ERROR_CONNECTION;
|
||||
}
|
||||
} catch (ParseException e) {
|
||||
Log.e(MyTracksConstants.TAG, "Exception", e);
|
||||
errorType = ERROR_LOCAL;
|
||||
errorMessage = e.getMessage();
|
||||
} catch (ConflictDetectedException e) {
|
||||
Log.e(MyTracksConstants.TAG, "Exception", e);
|
||||
errorType = ERROR_CONFLICT;
|
||||
errorMessage = e.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidates and refreshes the auth token. Blocks until the refresh has
|
||||
* completed or until we deem the refresh as having timed out.
|
||||
*
|
||||
* @return true If the invalidate/refresh succeeds, false if it fails or
|
||||
* times out.
|
||||
*/
|
||||
private boolean invalidateAndRefreshAuthToken() {
|
||||
Log.d(MyTracksConstants.TAG, "Retrying due to auth failure");
|
||||
// This FutureTask doesn't do anything -- it exists simply to be
|
||||
// blocked upon using get().
|
||||
FutureTask<?> whenFinishedFuture = new FutureTask<Object>(new Runnable() {
|
||||
public void run() {}
|
||||
}, null);
|
||||
|
||||
auth.invalidateAndRefresh(whenFinishedFuture);
|
||||
|
||||
try {
|
||||
Log.d(MyTracksConstants.TAG, "waiting for invalidate");
|
||||
whenFinishedFuture.get(AUTH_TOKEN_INVALIDATE_REFRESH_TIMEOUT,
|
||||
TimeUnit.MILLISECONDS);
|
||||
Log.d(MyTracksConstants.TAG, "invalidate finished");
|
||||
return true;
|
||||
|
||||
} catch (InterruptedException e) {
|
||||
Log.e(MyTracksConstants.TAG, "Failed to invalidate", e);
|
||||
} catch (ExecutionException e) {
|
||||
Log.e(MyTracksConstants.TAG, "Failed to invalidate", e);
|
||||
} catch (TimeoutException e) {
|
||||
Log.e(MyTracksConstants.TAG, "Invalidate didn't complete in time", e);
|
||||
} finally {
|
||||
whenFinishedFuture.cancel(false);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public int getErrorType() {
|
||||
return errorType;
|
||||
}
|
||||
|
||||
public String getErrorMessage() {
|
||||
return errorMessage;
|
||||
}
|
||||
|
||||
public void setAuthManager(AuthManager auth) {
|
||||
this.auth = auth;
|
||||
}
|
||||
|
||||
public void setRetryOnAuthFailure(boolean retry) {
|
||||
retryOnAuthFailure = retry;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* 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.services;
|
||||
|
||||
/**
|
||||
* This is a simple location listener policy that will always dictate the same
|
||||
* polling interval.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public class AbsoluteLocationListenerPolicy implements LocationListenerPolicy {
|
||||
|
||||
/**
|
||||
* The interval to request for gps signal.
|
||||
*/
|
||||
private final long interval;
|
||||
|
||||
/**
|
||||
* @param interval The interval to request for gps signal
|
||||
*/
|
||||
public AbsoluteLocationListenerPolicy(final long interval) {
|
||||
this.interval = interval;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The interval given in the constructor
|
||||
*/
|
||||
public long getDesiredPollingInterval() {
|
||||
return interval;
|
||||
}
|
||||
|
||||
/**
|
||||
* Discards the idle time.
|
||||
*/
|
||||
public void updateIdleTime(long idleTime) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the minimum distance between updates.
|
||||
* Get all updates to properly measure moving time.
|
||||
*/
|
||||
public int getMinDistance() {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* 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.services;
|
||||
|
||||
/**
|
||||
* A LocationListenerPolicy that will change based on how long the user has been
|
||||
* stationary.
|
||||
*
|
||||
* This policy will dictate a policy based on a min, max and idle time.
|
||||
* The policy will dictate an interval bounded by min and max whic is half of
|
||||
* the idle time.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public class AdaptiveLocationListenerPolicy implements LocationListenerPolicy {
|
||||
|
||||
/**
|
||||
* Smallest interval this policy will dictate, in milliseconds.
|
||||
*/
|
||||
private final long minInterval;
|
||||
|
||||
/**
|
||||
* Largest interval this policy will dictate, in milliseconds.
|
||||
*/
|
||||
private final long maxInterval;
|
||||
|
||||
private final int minDistance;
|
||||
|
||||
/**
|
||||
* The time the user has been at the current location, in milliseconds.
|
||||
*/
|
||||
private long idleTime;
|
||||
|
||||
/**
|
||||
* Creates a policy that will be bounded by the given min and max.
|
||||
*
|
||||
* @param min Smallest interval this policy will dictate, in milliseconds
|
||||
* @param max Largest interval this policy will dictate, in milliseconds
|
||||
*/
|
||||
public AdaptiveLocationListenerPolicy(long min, long max, int minDistance) {
|
||||
this.minInterval = min;
|
||||
this.maxInterval = max;
|
||||
this.minDistance = minDistance;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return An interval bounded by min and max which is half of the idle time
|
||||
*/
|
||||
public long getDesiredPollingInterval() {
|
||||
long desiredInterval = idleTime / 2;
|
||||
// Round to avoid setting the interval too often.
|
||||
desiredInterval = (desiredInterval / 1000) * 1000;
|
||||
return Math.max(Math.min(maxInterval, desiredInterval),
|
||||
minInterval);
|
||||
}
|
||||
|
||||
public void updateIdleTime(long newIdleTime) {
|
||||
this.idleTime = newIdleTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the minimum distance between updates.
|
||||
*/
|
||||
public int getMinDistance() {
|
||||
return minDistance;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* 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.services;
|
||||
|
||||
import com.google.android.apps.mytracks.MyTracksSettings;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* Creates a default track name based on the current default track name policy.
|
||||
*
|
||||
* @author Matthew Simmons
|
||||
*/
|
||||
class DefaultTrackNameFactory {
|
||||
private static final String TIMESTAMP_DATE_FORMAT = "yyyy-MM-dd HH:mm";
|
||||
|
||||
private final Context context;
|
||||
|
||||
DefaultTrackNameFactory(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new track name.
|
||||
*
|
||||
* @param trackId The ID for the current track.
|
||||
* @param startTime The start time, in milliseconds since the epoch, of the
|
||||
* current track.
|
||||
* @return The new track name.
|
||||
*/
|
||||
String newTrackName(long trackId, long startTime) {
|
||||
if (useTimestampTrackName()) {
|
||||
SimpleDateFormat formatter = new SimpleDateFormat(TIMESTAMP_DATE_FORMAT);
|
||||
return formatter.format(new Date(startTime));
|
||||
} else {
|
||||
return String.format(context.getString(R.string.new_track), trackId);
|
||||
}
|
||||
}
|
||||
|
||||
/** Determines whether the preferences allow a timestamp-based track name */
|
||||
protected boolean useTimestampTrackName() {
|
||||
SharedPreferences prefs =
|
||||
context.getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0);
|
||||
return prefs.getBoolean(
|
||||
context.getString(R.string.timestamp_track_name_key), true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* 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.services;
|
||||
|
||||
/**
|
||||
* This is an interface for classes that will manage the location listener policy.
|
||||
* Different policy options are:
|
||||
* Absolute
|
||||
* Addaptive
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public interface LocationListenerPolicy {
|
||||
|
||||
/**
|
||||
* Returns the polling time this policy would like at this time.
|
||||
*
|
||||
* @return The polling that this policy dictates
|
||||
*/
|
||||
public long getDesiredPollingInterval();
|
||||
|
||||
/**
|
||||
* Returns the minimum distance between updates.
|
||||
*/
|
||||
public int getMinDistance();
|
||||
|
||||
/**
|
||||
* Notifies the amount of time the user has been idle at their current
|
||||
* location.
|
||||
*
|
||||
* @param idleTime The time that the user has been idle at this spot
|
||||
*/
|
||||
public void updateIdleTime(long idleTime);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* 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.services;
|
||||
|
||||
/**
|
||||
* This is interface for a task that will be executed on some schedule.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public interface PeriodicTask {
|
||||
/**
|
||||
* Sets up this task for subsequent calls to the run method.
|
||||
*/
|
||||
public void start();
|
||||
|
||||
/**
|
||||
* This method will be called periodically.
|
||||
*/
|
||||
public void run(TrackRecordingService service);
|
||||
|
||||
/**
|
||||
* Shuts down this task and clean up resources.
|
||||
*/
|
||||
public void shutdown();
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
* 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.services;
|
||||
|
||||
import com.google.android.apps.mytracks.MyTracksConstants;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
|
||||
/**
|
||||
* This class will periodically announce the user's trip statistics.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public class PeriodicTaskExecuter {
|
||||
|
||||
private final PeriodicTask task;
|
||||
private final TrackRecordingService service;
|
||||
|
||||
/**
|
||||
* A timer to schedule the announcements.
|
||||
* This is non-null if the task is in started (scheduled) state.
|
||||
*/
|
||||
private Timer timer;
|
||||
|
||||
public PeriodicTaskExecuter(PeriodicTask task,
|
||||
TrackRecordingService service) {
|
||||
this.task = task;
|
||||
this.service = service;
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedules the task at the given interval.
|
||||
*
|
||||
* @param interval The interval in milliseconds
|
||||
*/
|
||||
public void scheduleTask(long interval) {
|
||||
// TODO: Decouple service from this class once and forever.
|
||||
if (!service.isRecording()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (timer != null) {
|
||||
timer.cancel();
|
||||
timer.purge();
|
||||
} else {
|
||||
// First start, or we were previously shut down.
|
||||
task.start();
|
||||
}
|
||||
|
||||
timer = new Timer();
|
||||
if (interval <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
long now = System.currentTimeMillis();
|
||||
long next = service.getTripStatistics().getStartTime();
|
||||
if (next < now) {
|
||||
next = now + interval - ((now - next) % interval);
|
||||
}
|
||||
|
||||
Date start = new Date(next);
|
||||
Log.i(MyTracksConstants.TAG,
|
||||
task.getClass().getSimpleName() + " scheduled to start at " + start
|
||||
+ " every " + interval + " milliseconds.");
|
||||
timer.scheduleAtFixedRate(new PeriodicTimerTask(), start, interval);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up this object.
|
||||
*/
|
||||
public void shutdown() {
|
||||
Log.i(MyTracksConstants.TAG,
|
||||
task.getClass().getSimpleName() + " shutting down.");
|
||||
if (timer != null) {
|
||||
timer.cancel();
|
||||
timer.purge();
|
||||
timer = null;
|
||||
task.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The timer task to announce the trip status.
|
||||
*/
|
||||
private class PeriodicTimerTask extends TimerTask {
|
||||
@Override
|
||||
public void run() {
|
||||
task.run(service);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
/*
|
||||
* 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.services;
|
||||
|
||||
import com.google.android.apps.mytracks.MyTracksConstants;
|
||||
import com.google.android.apps.mytracks.MyTracksSettings;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
|
||||
import android.util.Log;
|
||||
|
||||
/**
|
||||
* A class that manages reading the shared preferences for the service.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public class PreferenceManager implements OnSharedPreferenceChangeListener {
|
||||
private TrackRecordingService service;
|
||||
private SharedPreferences sharedPreferences;
|
||||
private final String announcementFrequencyKey;
|
||||
private final String autoResumeTrackCurrentRetryKey;
|
||||
private final String autoResumeTrackTimeoutKey;
|
||||
private final String maxRecordingDistanceKey;
|
||||
private final String metricUnitsKey;
|
||||
private final String minRecordingDistanceKey;
|
||||
private final String minRecordingIntervalKey;
|
||||
private final String minRequiredAccuracyKey;
|
||||
private final String recordingTrackKey;
|
||||
private final String splitFrequencyKey;
|
||||
|
||||
public PreferenceManager(TrackRecordingService service) {
|
||||
this.service = service;
|
||||
this.sharedPreferences = service.getSharedPreferences(
|
||||
MyTracksSettings.SETTINGS_NAME, 0);
|
||||
if (sharedPreferences == null) {
|
||||
Log.w(MyTracksConstants.TAG,
|
||||
"TrackRecordingService: Couldn't get shared preferences.");
|
||||
throw new IllegalStateException("Couldn't get shared preferences");
|
||||
}
|
||||
sharedPreferences.registerOnSharedPreferenceChangeListener(this);
|
||||
|
||||
announcementFrequencyKey =
|
||||
service.getString(R.string.announcement_frequency_key);
|
||||
autoResumeTrackCurrentRetryKey =
|
||||
service.getString(R.string.auto_resume_track_current_retry_key);
|
||||
autoResumeTrackTimeoutKey =
|
||||
service.getString(R.string.auto_resume_track_timeout_key);
|
||||
maxRecordingDistanceKey =
|
||||
service.getString(R.string.max_recording_distance_key);
|
||||
metricUnitsKey =
|
||||
service.getString(R.string.metric_units_key);
|
||||
minRecordingDistanceKey =
|
||||
service.getString(R.string.min_recording_distance_key);
|
||||
minRecordingIntervalKey =
|
||||
service.getString(R.string.min_recording_interval_key);
|
||||
minRequiredAccuracyKey =
|
||||
service.getString(R.string.min_required_accuracy_key);
|
||||
recordingTrackKey =
|
||||
service.getString(R.string.recording_track_key);
|
||||
splitFrequencyKey =
|
||||
service.getString(R.string.split_frequency_key);
|
||||
|
||||
// Refresh all properties.
|
||||
onSharedPreferenceChanged(sharedPreferences, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies that preferences have changed.
|
||||
* Call this with key == null to update all preferences in one call.
|
||||
*
|
||||
* @param key the key that changed (may be null to update all preferences)
|
||||
*/
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences,
|
||||
String key) {
|
||||
if (service == null) {
|
||||
Log.w(MyTracksConstants.TAG,
|
||||
"onSharedPreferenceChanged: a preference change (key = " + key
|
||||
+ ") after a call to shutdown()");
|
||||
return;
|
||||
}
|
||||
if (key == null || key.equals(minRecordingDistanceKey)) {
|
||||
service.setMinRecordingDistance(
|
||||
sharedPreferences.getInt(
|
||||
minRecordingDistanceKey,
|
||||
MyTracksSettings.DEFAULT_MIN_RECORDING_DISTANCE));
|
||||
Log.d(MyTracksConstants.TAG,
|
||||
"TrackRecordingService: minRecordingDistance = "
|
||||
+ service.getMinRecordingDistance());
|
||||
}
|
||||
if (key == null || key.equals(maxRecordingDistanceKey)) {
|
||||
service.setMaxRecordingDistance(sharedPreferences.getInt(
|
||||
maxRecordingDistanceKey,
|
||||
MyTracksSettings.DEFAULT_MAX_RECORDING_DISTANCE));
|
||||
}
|
||||
if (key == null || key.equals(minRecordingIntervalKey)) {
|
||||
int minRecordingInterval = sharedPreferences.getInt(
|
||||
minRecordingIntervalKey,
|
||||
MyTracksSettings.DEFAULT_MIN_RECORDING_INTERVAL);
|
||||
switch (minRecordingInterval) {
|
||||
case -2:
|
||||
// Battery Miser
|
||||
// min: 30 seconds
|
||||
// max: 5 minutes
|
||||
// minDist: 5 meters Choose battery life over moving time accuracy.
|
||||
service.setLocationListenerPolicy(
|
||||
new AdaptiveLocationListenerPolicy(30000, 300000, 5));
|
||||
break;
|
||||
case -1:
|
||||
// High Accuracy
|
||||
// min: 1 second
|
||||
// max: 30 seconds
|
||||
// minDist: 0 meters get all updates to properly measure moving time.
|
||||
service.setLocationListenerPolicy(
|
||||
new AdaptiveLocationListenerPolicy(1000, 30000, 0));
|
||||
break;
|
||||
default:
|
||||
service.setLocationListenerPolicy(
|
||||
new AbsoluteLocationListenerPolicy(minRecordingInterval * 1000));
|
||||
}
|
||||
}
|
||||
if (key == null || key.equals(minRequiredAccuracyKey)) {
|
||||
service.setMinRequiredAccuracy(sharedPreferences.getInt(
|
||||
minRequiredAccuracyKey,
|
||||
MyTracksSettings.DEFAULT_MIN_REQUIRED_ACCURACY));
|
||||
}
|
||||
if (key == null || key.equals(announcementFrequencyKey)) {
|
||||
service.setAnnouncementFrequency(
|
||||
sharedPreferences.getInt(announcementFrequencyKey, -1));
|
||||
}
|
||||
if (key == null || key.equals(autoResumeTrackTimeoutKey)) {
|
||||
service.setAutoResumeTrackTimeout(sharedPreferences.getInt(
|
||||
autoResumeTrackTimeoutKey,
|
||||
MyTracksSettings.DEFAULT_AUTO_RESUME_TRACK_TIMEOUT));
|
||||
}
|
||||
if (key == null || key.equals(recordingTrackKey)) {
|
||||
long recordingTrackId = sharedPreferences.getLong(recordingTrackKey, -1);
|
||||
// Only read the id if it is valid.
|
||||
// Setting it to -1 should only happen in
|
||||
// TrackRecordingService.endCurrentTrack()
|
||||
if (recordingTrackId > 0) {
|
||||
service.setRecordingTrackId(recordingTrackId);
|
||||
}
|
||||
}
|
||||
if (key == null || key.equals(splitFrequencyKey)) {
|
||||
service.getSplitManager().setSplitFrequency(
|
||||
sharedPreferences.getInt(splitFrequencyKey, 0));
|
||||
}
|
||||
if (key == null || key.equals(metricUnitsKey)) {
|
||||
service.getSplitManager().setMetricUnits(
|
||||
sharedPreferences.getBoolean(metricUnitsKey, true));
|
||||
}
|
||||
}
|
||||
|
||||
public void setAutoResumeTrackCurrentRetry(int retryAttempts) {
|
||||
sharedPreferences
|
||||
.edit()
|
||||
.putInt(autoResumeTrackCurrentRetryKey, retryAttempts)
|
||||
.commit();
|
||||
}
|
||||
|
||||
public void setRecordingTrack(long id) {
|
||||
sharedPreferences
|
||||
.edit()
|
||||
.putLong(recordingTrackKey, id)
|
||||
.commit();
|
||||
}
|
||||
|
||||
public void shutdown() {
|
||||
sharedPreferences.unregisterOnSharedPreferenceChangeListener(this);
|
||||
service = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
/*
|
||||
* 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.services;
|
||||
|
||||
import com.google.android.apps.mytracks.MyTracksConstants;
|
||||
import com.google.android.apps.mytracks.util.UnitConversions;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
/**
|
||||
* This class manages inserting time or distance splits.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public class SplitManager {
|
||||
|
||||
/**
|
||||
* The frequency of splits.
|
||||
*/
|
||||
private int splitFrequency = 0;
|
||||
|
||||
/**
|
||||
* The next distance when a split should be taken.
|
||||
*/
|
||||
private double nextSplitDistance = 0;
|
||||
|
||||
/**
|
||||
* Splits executer.
|
||||
*/
|
||||
private PeriodicTaskExecuter splitExecuter = null;
|
||||
|
||||
private boolean metricUnits;
|
||||
|
||||
private final TrackRecordingService service;
|
||||
|
||||
public SplitManager(TrackRecordingService service) {
|
||||
this.service = service;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores the manager.
|
||||
*/
|
||||
public void restore() {
|
||||
if ((splitFrequency > 0) && (splitExecuter != null)) {
|
||||
splitExecuter.scheduleTask(splitFrequency * 60000);
|
||||
}
|
||||
calculateNextSplit();
|
||||
}
|
||||
|
||||
/**
|
||||
* Shuts down the manager.
|
||||
*/
|
||||
public void shutdown() {
|
||||
if (splitExecuter != null) {
|
||||
splitExecuter.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the next distance that a split should be inserted at.
|
||||
*/
|
||||
public void calculateNextSplit() {
|
||||
// TODO: Decouple service from this class once and forever.
|
||||
if (!service.isRecording()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (splitFrequency >= 0) {
|
||||
nextSplitDistance = Double.MAX_VALUE;
|
||||
Log.d(MyTracksConstants.TAG,
|
||||
"SplitManager: Distance splits disabled.");
|
||||
return;
|
||||
}
|
||||
|
||||
double dist = service.getTripStatistics().getTotalDistance() / 1000;
|
||||
if (!metricUnits) {
|
||||
dist *= UnitConversions.KM_TO_MI;
|
||||
}
|
||||
// The index will be negative since the frequency is negative.
|
||||
int index = (int) (dist / splitFrequency);
|
||||
index -= 1;
|
||||
nextSplitDistance = splitFrequency * index;
|
||||
Log.d(MyTracksConstants.TAG,
|
||||
"SplitManager: Next split distance: " + nextSplitDistance);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates split manager with new trip statistics.
|
||||
*/
|
||||
public void updateSplits() {
|
||||
if (this.splitFrequency >= 0) {
|
||||
return;
|
||||
}
|
||||
// Convert the distance in meters to km or mi.
|
||||
double distance = service.getTripStatistics().getTotalDistance() / 1000.0;
|
||||
if (!metricUnits) {
|
||||
distance *= UnitConversions.KM_TO_MI;
|
||||
}
|
||||
|
||||
if (distance > this.nextSplitDistance) {
|
||||
service.insertStatisticsMarker(service.getLastLocation());
|
||||
calculateNextSplit();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the split frequency.
|
||||
* < 0 Use the absolute value as a distance in the current measurement km
|
||||
* or mi
|
||||
* 0 Turn off splits
|
||||
* > 0 Use the value as a time in minutes
|
||||
* @param splitFrequency The frequency in time or distance
|
||||
*/
|
||||
public void setSplitFrequency(int splitFrequency) {
|
||||
Log.d(MyTracksConstants.TAG,
|
||||
"setSplitFrequency: splitFrequency = " + splitFrequency);
|
||||
this.splitFrequency = splitFrequency;
|
||||
|
||||
// TODO: Decouple service from this class once and forever.
|
||||
if (!service.isRecording()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (splitFrequency < 1) {
|
||||
if (splitExecuter != null) {
|
||||
splitExecuter.shutdown();
|
||||
splitExecuter = null;
|
||||
}
|
||||
}
|
||||
if (splitFrequency > 0) {
|
||||
if (splitExecuter == null) {
|
||||
TimeSplitTask splitter = new TimeSplitTask();
|
||||
splitExecuter = new PeriodicTaskExecuter(splitter, service);
|
||||
}
|
||||
splitExecuter.scheduleTask(splitFrequency * 60000);
|
||||
} else {
|
||||
// For distance based splits.
|
||||
calculateNextSplit();
|
||||
}
|
||||
}
|
||||
|
||||
public void setMetricUnits(boolean metricUnits) {
|
||||
this.metricUnits = metricUnits;
|
||||
calculateNextSplit();
|
||||
}
|
||||
}
|
||||
@@ -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.services;
|
||||
|
||||
import com.google.android.apps.mytracks.util.ApiFeatures;
|
||||
|
||||
import android.content.Context;
|
||||
import android.media.AudioManager;
|
||||
|
||||
/**
|
||||
* Factory which wraps construction and setup of text-to-speech announcements in
|
||||
* an API-level-safe way.
|
||||
*
|
||||
* @author Rodrigo Damazio
|
||||
*/
|
||||
public class StatusAnnouncerFactory {
|
||||
|
||||
private final boolean hasTts;
|
||||
|
||||
public StatusAnnouncerFactory(ApiFeatures apiFeatures) {
|
||||
this.hasTts = apiFeatures.hasTextToSpeech();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a periodic task which does voice announcements.
|
||||
*
|
||||
* @return the task, or null if announcements are not supported
|
||||
*/
|
||||
public PeriodicTask create(Context context) {
|
||||
if (hasTts) {
|
||||
return new StatusAnnouncerTask(context);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the appropriate volume stream for controlling announcement
|
||||
* volume.
|
||||
*/
|
||||
public int getVolumeStream() {
|
||||
if (hasTts) {
|
||||
return StatusAnnouncerTask.getVolumeStream();
|
||||
} else {
|
||||
return AudioManager.USE_DEFAULT_STREAM_TYPE;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,279 @@
|
||||
/*
|
||||
* 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.services;
|
||||
|
||||
import com.google.android.apps.mytracks.MyTracksConstants;
|
||||
import com.google.android.apps.mytracks.MyTracksSettings;
|
||||
import com.google.android.apps.mytracks.stats.TripStatistics;
|
||||
import com.google.android.apps.mytracks.util.StringUtils;
|
||||
import com.google.android.apps.mytracks.util.UnitConversions;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.speech.tts.TextToSpeech;
|
||||
import android.speech.tts.TextToSpeech.OnInitListener;
|
||||
import android.telephony.PhoneStateListener;
|
||||
import android.telephony.TelephonyManager;
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* This class will periodically announce the user's trip statistics.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public class StatusAnnouncerTask implements PeriodicTask {
|
||||
|
||||
/**
|
||||
* The rate at which announcements are spoken.
|
||||
*/
|
||||
// @VisibleForTesting
|
||||
static final float TTS_SPEECH_RATE = 0.9f;
|
||||
|
||||
/**
|
||||
* A pointer to the service context.
|
||||
*/
|
||||
private final Context context;
|
||||
|
||||
/**
|
||||
* String utilities.
|
||||
*/
|
||||
private final StringUtils stringUtils;
|
||||
|
||||
/**
|
||||
* The interface to the text to speech engine.
|
||||
*/
|
||||
private TextToSpeech tts;
|
||||
|
||||
/**
|
||||
* The response received from the TTS engine after initialization.
|
||||
*/
|
||||
private boolean ready = false;
|
||||
|
||||
/**
|
||||
* Whether we're allowed to speak right now.
|
||||
*/
|
||||
private boolean speechAllowed;
|
||||
|
||||
/**
|
||||
* Listener which updates {@link #speechAllowed} when the phone state changes.
|
||||
*/
|
||||
private final PhoneStateListener phoneListener = new PhoneStateListener() {
|
||||
@Override
|
||||
public void onCallStateChanged(int state, String incomingNumber) {
|
||||
speechAllowed = state == TelephonyManager.CALL_STATE_IDLE;
|
||||
|
||||
if (!speechAllowed && tts.isSpeaking()) {
|
||||
// If we're already speaking, stop it.
|
||||
tts.stop();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public StatusAnnouncerTask(Context context) {
|
||||
this(context, new StringUtils(context));
|
||||
}
|
||||
|
||||
public StatusAnnouncerTask(Context context, StringUtils stringUtils) {
|
||||
this.context = context;
|
||||
this.stringUtils = stringUtils;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the TTS engine is initialized.
|
||||
*/
|
||||
private void onTtsInit(int status) {
|
||||
Log.i(MyTracksConstants.TAG, "TrackRecordingService.TTS init: " + status);
|
||||
this.ready = status == TextToSpeech.SUCCESS;
|
||||
|
||||
if (ready) {
|
||||
// Force the language to be the same as the string we will be speaking,
|
||||
// if that's available.
|
||||
Locale speechLanguage = Locale.getDefault();
|
||||
int languageAvailability = tts.isLanguageAvailable(speechLanguage);
|
||||
if (languageAvailability == TextToSpeech.LANG_MISSING_DATA ||
|
||||
languageAvailability == TextToSpeech.LANG_NOT_SUPPORTED) {
|
||||
// English is probably supported.
|
||||
// TODO: Somehow use announcement strings from English too.
|
||||
Log.w(MyTracksConstants.TAG, "Default language not available, using English.");
|
||||
speechLanguage = Locale.ENGLISH;
|
||||
}
|
||||
tts.setLanguage(speechLanguage);
|
||||
|
||||
// Slow down the speed just a bit as it is hard to hear when exercising.
|
||||
tts.setSpeechRate(TTS_SPEECH_RATE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* Announces the trip status.
|
||||
*/
|
||||
@Override
|
||||
public void run(TrackRecordingService service) {
|
||||
if (!ready || tts == null) {
|
||||
Log.e(MyTracksConstants.TAG, "StatusAnnouncer Tts not ready.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!speechAllowed) {
|
||||
Log.i(MyTracksConstants.TAG,
|
||||
"Not making announcement - not allowed at this time");
|
||||
return;
|
||||
}
|
||||
|
||||
if (service == null || service.getTripStatistics() == null) {
|
||||
Log.e(MyTracksConstants.TAG, "StatusAnnouncer stats not initialized.");
|
||||
return;
|
||||
}
|
||||
|
||||
String announcement = getAnnouncement(service.getTripStatistics());
|
||||
Log.d(MyTracksConstants.TAG, "Announcement: " + announcement);
|
||||
tts.speak(announcement, TextToSpeech.QUEUE_FLUSH, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the announcement string.
|
||||
*
|
||||
* @return The string that will be read to the user
|
||||
*/
|
||||
// @VisibleForTesting
|
||||
protected String getAnnouncement(TripStatistics stats) {
|
||||
SharedPreferences preferences =
|
||||
context.getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0);
|
||||
boolean metricUnits = true;
|
||||
boolean reportSpeed = true;
|
||||
if (preferences != null) {
|
||||
metricUnits =
|
||||
preferences.getBoolean(context.getString(R.string.metric_units_key),
|
||||
true);
|
||||
reportSpeed =
|
||||
preferences.getBoolean(context.getString(R.string.report_speed_key),
|
||||
true);
|
||||
}
|
||||
|
||||
double d = stats.getTotalDistance() / 1000;
|
||||
double s = stats.getAverageMovingSpeed() * 3.6;
|
||||
if (d == 0) {
|
||||
return context.getString(R.string.announce_no_distance);
|
||||
}
|
||||
|
||||
int speedLabel;
|
||||
if (metricUnits) {
|
||||
if (reportSpeed) {
|
||||
speedLabel = R.string.kilometer_per_hour_long;
|
||||
} else {
|
||||
speedLabel = R.string.per_kilometer;
|
||||
}
|
||||
} else {
|
||||
s *= UnitConversions.KMH_TO_MPH;
|
||||
d *= UnitConversions.KM_TO_MI;
|
||||
if (reportSpeed) {
|
||||
speedLabel = R.string.mile_per_hour_long;
|
||||
} else {
|
||||
speedLabel = R.string.per_mile;
|
||||
}
|
||||
}
|
||||
|
||||
String speed = null;
|
||||
if ((s == 0) || Double.isNaN(s)) {
|
||||
speed = context.getString(R.string.unknown);
|
||||
} else {
|
||||
if (reportSpeed) {
|
||||
speed = String.format("%.1f", s);
|
||||
} else {
|
||||
double pace = 3600000.0 / s;
|
||||
Log.w(MyTracksConstants.TAG,
|
||||
"Converted speed: " + s + " to pace: " + pace);
|
||||
speed = stringUtils.formatTimeLong((long) pace);
|
||||
}
|
||||
}
|
||||
|
||||
return context.getString(R.string.announce_template,
|
||||
context.getString(R.string.total_distance_label),
|
||||
d,
|
||||
context.getString(metricUnits
|
||||
? R.string.kilometers_long
|
||||
: R.string.miles_long),
|
||||
stringUtils.formatTimeLong(stats.getMovingTime()),
|
||||
speed,
|
||||
context.getString(speedLabel));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
Log.i(MyTracksConstants.TAG, "Starting TTS");
|
||||
if (tts == null) {
|
||||
// We can't have this class also be the listener, otherwise it's unsafe to
|
||||
// reference it in Cupcake (even if we don't instantiate it).
|
||||
tts = newTextToSpeech(context, new OnInitListener() {
|
||||
@Override
|
||||
public void onInit(int status) {
|
||||
onTtsInit(status);
|
||||
}
|
||||
});
|
||||
}
|
||||
speechAllowed = true;
|
||||
|
||||
// Register ourselves as a listener so we won't speak during a call.
|
||||
listenToPhoneState(phoneListener, PhoneStateListener.LISTEN_CALL_STATE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void shutdown() {
|
||||
// Stop listening to phone state.
|
||||
listenToPhoneState(phoneListener, PhoneStateListener.LISTEN_NONE);
|
||||
|
||||
if (tts != null) {
|
||||
tts.shutdown();
|
||||
tts = null;
|
||||
}
|
||||
|
||||
Log.i(MyTracksConstants.TAG, "TTS shut down");
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper for instantiating a {@link TextToSpeech} object, which causes
|
||||
* several issues during testing.
|
||||
*/
|
||||
// @VisibleForTesting
|
||||
protected TextToSpeech newTextToSpeech(Context ctx, OnInitListener onInitListener) {
|
||||
return new TextToSpeech(ctx, onInitListener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper for calls to the 100%-unmockable {@link TelephonyManager#listen}.
|
||||
*/
|
||||
// @VisibleForTesting
|
||||
protected void listenToPhoneState(PhoneStateListener listener, int events) {
|
||||
TelephonyManager telephony =
|
||||
(TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
|
||||
if (telephony != null) {
|
||||
telephony.listen(listener, events);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the volume stream to use for controlling announcement volume.
|
||||
*/
|
||||
public static int getVolumeStream() {
|
||||
return TextToSpeech.Engine.DEFAULT_STREAM;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* 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.services;
|
||||
|
||||
import com.google.android.apps.mytracks.MyTracksConstants;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
/**
|
||||
* This class manages a period task executer.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public class TaskExecuterManager {
|
||||
|
||||
private int frequency;
|
||||
private final PeriodicTask task;
|
||||
private PeriodicTaskExecuter executer;
|
||||
|
||||
public TaskExecuterManager(int frequency, PeriodicTask task,
|
||||
TrackRecordingService service) {
|
||||
this.task = task;
|
||||
setFrequency(frequency, service);
|
||||
}
|
||||
|
||||
public int getFrequency() {
|
||||
return frequency;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the frequency that the task should be run at.
|
||||
* If needed the task will be scheduled.
|
||||
*
|
||||
* @param frequency The frequency in minutes for the task to run
|
||||
* @param service The service to run the task on
|
||||
*/
|
||||
public void setFrequency(int frequency, TrackRecordingService service) {
|
||||
this.frequency = frequency;
|
||||
Log.i(MyTracksConstants.TAG, "Frequency set to " + frequency
|
||||
+ " for task " + task.getClass().getSimpleName());
|
||||
|
||||
if (frequency == -1) {
|
||||
if (executer != null) {
|
||||
executer.shutdown();
|
||||
executer = null;
|
||||
Log.i(MyTracksConstants.TAG,
|
||||
"Shut down service: " + task.getClass().getSimpleName());
|
||||
}
|
||||
} else {
|
||||
if (executer == null) {
|
||||
executer = new PeriodicTaskExecuter(task, service);
|
||||
}
|
||||
executer.scheduleTask(frequency * 60000);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores the task at the current frequency.
|
||||
*/
|
||||
public void restore() {
|
||||
if (frequency > 0) {
|
||||
executer.scheduleTask(frequency * 60000);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shuts down this executer.
|
||||
*/
|
||||
public void shutdown() {
|
||||
if (executer != null) {
|
||||
executer.shutdown();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* 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.services;
|
||||
|
||||
/**
|
||||
* A simple task to insert statistics markers every n minutes.
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public class TimeSplitTask implements PeriodicTask {
|
||||
|
||||
@Override
|
||||
public void run(TrackRecordingService service) {
|
||||
service.insertStatisticsMarker(service.getLastLocation());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void shutdown() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
+432
@@ -0,0 +1,432 @@
|
||||
/*
|
||||
* Copyright (C) 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.services.sensors;
|
||||
|
||||
import com.google.android.apps.mytracks.MyTracksConstants;
|
||||
import com.google.android.apps.mytracks.content.Sensor;
|
||||
|
||||
import android.bluetooth.BluetoothAdapter;
|
||||
import android.bluetooth.BluetoothDevice;
|
||||
import android.bluetooth.BluetoothSocket;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Message;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* This class does all the work for setting up and managing Bluetooth
|
||||
* connections with other devices. It has a thread that listens for incoming
|
||||
* connections, a thread for connecting with a device, and a thread for
|
||||
* performing data transmissions when connected.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public class BluetoothConnectionManager {
|
||||
// Unique UUID for this application
|
||||
private static final UUID SPP_UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB");
|
||||
|
||||
private MessageParser parser;
|
||||
|
||||
// Member fields
|
||||
private final BluetoothAdapter adapter;
|
||||
private final Handler handler;
|
||||
private ConnectThread connectThread;
|
||||
private ConnectedThread connectedThread;
|
||||
|
||||
private Sensor.SensorState state;
|
||||
|
||||
// Message types sent from the BluetoothSenorService Handler
|
||||
public static final int MESSAGE_STATE_CHANGE = 1;
|
||||
public static final int MESSAGE_READ = 2;
|
||||
public static final int MESSAGE_WRITE = 3;
|
||||
public static final int MESSAGE_DEVICE_NAME = 4;
|
||||
|
||||
// Key names received from the BluetoothSenorService Handler
|
||||
public static final String DEVICE_NAME = "device_name";
|
||||
|
||||
/**
|
||||
* Constructor. Prepares a new BluetoothSensor session.
|
||||
*
|
||||
* @param context The UI Activity Context
|
||||
* @param handler A Handler to send messages back to the UI Activity
|
||||
*/
|
||||
public BluetoothConnectionManager(Handler handler, MessageParser parser) {
|
||||
this.adapter = BluetoothAdapter.getDefaultAdapter();
|
||||
this.state = Sensor.SensorState.NONE;
|
||||
this.handler = handler;
|
||||
this.parser = parser;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current state of the sensor connection
|
||||
*
|
||||
* @param state An integer defining the current connection state
|
||||
*/
|
||||
private synchronized void setState(Sensor.SensorState state) {
|
||||
// TODO pretty print this.
|
||||
Log.d(MyTracksConstants.TAG, "setState(" + state + ")");
|
||||
this.state = state;
|
||||
|
||||
// Give the new state to the Handler so the UI Activity can update
|
||||
handler.obtainMessage(MESSAGE_STATE_CHANGE, state.getNumber(), -1).sendToTarget();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the current connection state.
|
||||
*/
|
||||
public synchronized Sensor.SensorState getState() {
|
||||
return state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the sensor service. Specifically start AcceptThread to begin a session
|
||||
* in listening (server) mode. Called by the Activity onResume()
|
||||
*/
|
||||
public synchronized void start() {
|
||||
Log.d(MyTracksConstants.TAG, "BluetoothConnectionManager.start()");
|
||||
|
||||
// Cancel any thread attempting to make a connection
|
||||
if (connectThread != null) {
|
||||
connectThread.cancel();
|
||||
connectThread = null;
|
||||
}
|
||||
|
||||
// Cancel any thread currently running a connection
|
||||
if (connectedThread != null) {
|
||||
connectedThread.cancel();
|
||||
connectedThread = null;
|
||||
}
|
||||
|
||||
setState(Sensor.SensorState.NONE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the ConnectThread to initiate a connection to a remote device.
|
||||
*
|
||||
* @param device The BluetoothDevice to connect
|
||||
*/
|
||||
public synchronized void connect(BluetoothDevice device) {
|
||||
Log.d(MyTracksConstants.TAG, "connect to: " + device);
|
||||
|
||||
// Cancel any thread attempting to make a connection
|
||||
if (state == Sensor.SensorState.CONNECTING) {
|
||||
if (connectThread != null) {
|
||||
connectThread.cancel();
|
||||
connectThread = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Cancel any thread currently running a connection
|
||||
if (connectedThread != null) {
|
||||
connectedThread.cancel();
|
||||
connectedThread = null;
|
||||
}
|
||||
|
||||
// Start the thread to connect with the given device
|
||||
connectThread = new ConnectThread(device);
|
||||
connectThread.start();
|
||||
setState(Sensor.SensorState.CONNECTING);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the ConnectedThread to begin managing a Bluetooth connection
|
||||
*
|
||||
* @param socket The BluetoothSocket on which the connection was made
|
||||
* @param device The BluetoothDevice that has been connected
|
||||
*/
|
||||
public synchronized void connected(BluetoothSocket socket,
|
||||
BluetoothDevice device) {
|
||||
Log.d(MyTracksConstants.TAG, "connected");
|
||||
|
||||
// Cancel the thread that completed the connection
|
||||
if (connectThread != null) {
|
||||
connectThread.cancel();
|
||||
connectThread = null;
|
||||
}
|
||||
|
||||
// Cancel any thread currently running a connection
|
||||
if (connectedThread != null) {
|
||||
connectedThread.cancel();
|
||||
connectedThread = null;
|
||||
}
|
||||
|
||||
// Start the thread to manage the connection and perform transmissions
|
||||
connectedThread = new ConnectedThread(socket);
|
||||
connectedThread.start();
|
||||
|
||||
// Send the name of the connected device back to the UI Activity
|
||||
Message msg = handler.obtainMessage(MESSAGE_DEVICE_NAME);
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putString(DEVICE_NAME, device.getName());
|
||||
msg.setData(bundle);
|
||||
handler.sendMessage(msg);
|
||||
|
||||
setState(Sensor.SensorState.CONNECTED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop all threads
|
||||
*/
|
||||
public synchronized void stop() {
|
||||
Log.d(MyTracksConstants.TAG, "stop()");
|
||||
if (connectThread != null) {
|
||||
connectThread.cancel();
|
||||
connectThread = null;
|
||||
}
|
||||
if (connectedThread != null) {
|
||||
connectedThread.cancel();
|
||||
connectedThread = null;
|
||||
}
|
||||
setState(Sensor.SensorState.NONE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Write to the ConnectedThread in an unsynchronized manner
|
||||
*
|
||||
* @param out The bytes to write
|
||||
* @see ConnectedThread#write(byte[])
|
||||
*/
|
||||
public void write(byte[] out) {
|
||||
// Create temporary object
|
||||
ConnectedThread r;
|
||||
// Synchronize a copy of the ConnectedThread
|
||||
synchronized (this) {
|
||||
if (state != Sensor.SensorState.CONNECTED) {
|
||||
return;
|
||||
}
|
||||
r = connectedThread;
|
||||
}
|
||||
// Perform the write unsynchronized
|
||||
r.write(out);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the connection attempt failed and notify the UI Activity.
|
||||
*/
|
||||
private void connectionFailed() {
|
||||
setState(Sensor.SensorState.DISCONNECTED);
|
||||
Log.i(MyTracksConstants.TAG, "Bluetooth connection failed.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the connection was lost and notify the UI Activity.
|
||||
*/
|
||||
private void connectionLost() {
|
||||
setState(Sensor.SensorState.DISCONNECTED);
|
||||
Log.i(MyTracksConstants.TAG, "Bluetooth connection lost.");
|
||||
}
|
||||
|
||||
/**
|
||||
* This thread runs while attempting to make an outgoing connection with a
|
||||
* device. It runs straight through; the connection either succeeds or fails.
|
||||
*/
|
||||
private class ConnectThread extends Thread {
|
||||
private final BluetoothSocket socket;
|
||||
private final BluetoothDevice device;
|
||||
|
||||
public ConnectThread(BluetoothDevice device) {
|
||||
setName("ConnectThread-" + device.getName());
|
||||
this.device = device;
|
||||
BluetoothSocket tmp = null;
|
||||
|
||||
// Get a BluetoothSocket for a connection with the
|
||||
// given BluetoothDevice
|
||||
try {
|
||||
tmp = getSocket();
|
||||
} catch (IOException e) {
|
||||
Log.e(MyTracksConstants.TAG, "create() failed", e);
|
||||
}
|
||||
socket = tmp;
|
||||
}
|
||||
|
||||
private BluetoothSocket getSocket() throws IOException {
|
||||
try {
|
||||
Class<? extends BluetoothDevice> c = device.getClass();
|
||||
Method insecure = c.getMethod("createInsecureRfcommSocket",
|
||||
Integer.class);
|
||||
insecure.setAccessible(true);
|
||||
return (BluetoothSocket) insecure.invoke(device, 1);
|
||||
} catch (SecurityException e) {
|
||||
Log.e(MyTracksConstants.TAG, "Unable to get insecure connect.", e);
|
||||
} catch (NoSuchMethodException e) {
|
||||
Log.e(MyTracksConstants.TAG, "Unable to get insecure connect.", e);
|
||||
} catch (IllegalArgumentException e) {
|
||||
Log.e(MyTracksConstants.TAG, "Unable to get insecure connect.", e);
|
||||
} catch (IllegalAccessException e) {
|
||||
Log.e(MyTracksConstants.TAG, "Unable to get insecure connect.", e);
|
||||
} catch (InvocationTargetException e) {
|
||||
Log.e(MyTracksConstants.TAG, "Unable to get insecure connect.", e);
|
||||
}
|
||||
return device.createRfcommSocketToServiceRecord(SPP_UUID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
Log.d(MyTracksConstants.TAG, "BEGIN mConnectThread");
|
||||
|
||||
// Always cancel discovery because it will slow down a connection
|
||||
adapter.cancelDiscovery();
|
||||
|
||||
// Make a connection to the BluetoothSocket
|
||||
try {
|
||||
// This is a blocking call and will only return on a
|
||||
// successful connection or an exception
|
||||
socket.connect();
|
||||
} catch (IOException e) {
|
||||
connectionFailed();
|
||||
// Close the socket
|
||||
try {
|
||||
socket.close();
|
||||
} catch (IOException e2) {
|
||||
Log.e(MyTracksConstants.TAG,
|
||||
"unable to close() socket during connection failure", e2);
|
||||
}
|
||||
// Start the service over to restart listening mode
|
||||
BluetoothConnectionManager.this.start();
|
||||
return;
|
||||
}
|
||||
|
||||
// Reset the ConnectThread because we're done
|
||||
synchronized (BluetoothConnectionManager.this) {
|
||||
connectThread = null;
|
||||
}
|
||||
|
||||
// Start the connected thread
|
||||
connected(socket, device);
|
||||
}
|
||||
|
||||
public void cancel() {
|
||||
try {
|
||||
socket.close();
|
||||
} catch (IOException e) {
|
||||
Log.e(MyTracksConstants.TAG, "close() of connect socket failed", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This thread runs during a connection with a remote device. It handles all
|
||||
* incoming and outgoing transmissions.
|
||||
*/
|
||||
private class ConnectedThread extends Thread {
|
||||
private final BluetoothSocket btSocket;
|
||||
private final InputStream mmInStream;
|
||||
private final OutputStream mmOutStream;
|
||||
|
||||
public ConnectedThread(BluetoothSocket socket) {
|
||||
Log.d(MyTracksConstants.TAG, "create ConnectedThread");
|
||||
btSocket = socket;
|
||||
InputStream tmpIn = null;
|
||||
OutputStream tmpOut = null;
|
||||
|
||||
// Get the BluetoothSocket input and output streams
|
||||
try {
|
||||
tmpIn = socket.getInputStream();
|
||||
tmpOut = socket.getOutputStream();
|
||||
} catch (IOException e) {
|
||||
Log.e(MyTracksConstants.TAG, "temp sockets not created", e);
|
||||
}
|
||||
|
||||
mmInStream = tmpIn;
|
||||
mmOutStream = tmpOut;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
Log.i(MyTracksConstants.TAG, "BEGIN mConnectedThread");
|
||||
byte[] buffer = new byte[parser.getFrameSize()];
|
||||
int bytes;
|
||||
int offset = 0;
|
||||
|
||||
// Keep listening to the InputStream while connected
|
||||
while (true) {
|
||||
try {
|
||||
// Read from the InputStream
|
||||
bytes = mmInStream.read(buffer, offset, parser.getFrameSize() - offset);
|
||||
|
||||
if (bytes < 0) {
|
||||
throw new IOException("EOF reached");
|
||||
}
|
||||
|
||||
offset += bytes;
|
||||
|
||||
if (offset != parser.getFrameSize()) {
|
||||
// partial frame received, call read() again to receive the rest
|
||||
continue;
|
||||
}
|
||||
|
||||
// check if its a valid frame
|
||||
if (!parser.isValid(buffer)) {
|
||||
int index = parser.findNextAlignment(buffer);
|
||||
if (index > 0) {
|
||||
// re-align
|
||||
offset = parser.getFrameSize() - index;
|
||||
System.arraycopy(buffer, index, buffer, 0, offset);
|
||||
Log.w(MyTracksConstants.TAG, "Misaligned data, found new message at " +
|
||||
index + " recovering...");
|
||||
continue;
|
||||
}
|
||||
Log.w(MyTracksConstants.TAG, "Could not find valid data, dropping data");
|
||||
offset = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
offset = 0;
|
||||
|
||||
// Send the obtained bytes to the UI Activity
|
||||
handler.obtainMessage(MESSAGE_READ, bytes, -1, buffer)
|
||||
.sendToTarget();
|
||||
} catch (IOException e) {
|
||||
Log.e(MyTracksConstants.TAG, "disconnected", e);
|
||||
connectionLost();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write to the connected OutStream.
|
||||
*
|
||||
* @param buffer The bytes to write
|
||||
*/
|
||||
public void write(byte[] buffer) {
|
||||
try {
|
||||
mmOutStream.write(buffer);
|
||||
|
||||
// Share the sent message back to the UI Activity
|
||||
handler.obtainMessage(MESSAGE_WRITE, -1, -1, buffer).sendToTarget();
|
||||
} catch (IOException e) {
|
||||
Log.e(MyTracksConstants.TAG, "Exception during write", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void cancel() {
|
||||
try {
|
||||
btSocket.close();
|
||||
} catch (IOException e) {
|
||||
Log.e(MyTracksConstants.TAG, "close() of connect socket failed", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+166
@@ -0,0 +1,166 @@
|
||||
/*
|
||||
* 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.services.sensors;
|
||||
|
||||
import com.google.android.apps.mytracks.MyTracksConstants;
|
||||
import com.google.android.apps.mytracks.MyTracksSettings;
|
||||
import com.google.android.apps.mytracks.content.Sensor;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.bluetooth.BluetoothAdapter;
|
||||
import android.bluetooth.BluetoothDevice;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Handler;
|
||||
import android.os.Message;
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
|
||||
/**
|
||||
* Manage the connection to a bluetooth sensor.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public class BluetoothSensorManager extends SensorManager {
|
||||
|
||||
// Local Bluetooth adapter
|
||||
private BluetoothAdapter bluetoothAdapter = null;
|
||||
|
||||
// Member object for the sensor threads and connections.
|
||||
private BluetoothConnectionManager connectionManager = null;
|
||||
|
||||
// Name of the connected device
|
||||
private String connectedDeviceName = null;
|
||||
|
||||
private Context context = null;
|
||||
|
||||
private Sensor.SensorDataSet sensorDataSet = null;
|
||||
|
||||
private MessageParser parser;
|
||||
|
||||
public BluetoothSensorManager(
|
||||
Context context, MessageParser parser) {
|
||||
this.context = context;
|
||||
this.parser = parser;
|
||||
// Get local Bluetooth adapter
|
||||
bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
|
||||
// If BT is not available or not enabled quit.
|
||||
if (!isEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
setupSensor();
|
||||
}
|
||||
|
||||
private void setupSensor() {
|
||||
Log.d(MyTracksConstants.TAG, "setupSensor()");
|
||||
|
||||
// Initialize the BluetoothSensorAdapter to perform bluetooth connections.
|
||||
connectionManager = new BluetoothConnectionManager(messageHandler, parser);
|
||||
}
|
||||
|
||||
public boolean isEnabled() {
|
||||
return bluetoothAdapter != null && bluetoothAdapter.isEnabled();
|
||||
}
|
||||
|
||||
public void setupChannel() {
|
||||
if (!isEnabled() || connectionManager == null) {
|
||||
Log.w(MyTracksConstants.TAG, "Disabled manager onStartTrack");
|
||||
return;
|
||||
}
|
||||
SharedPreferences prefs =
|
||||
context.getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0);
|
||||
String address =
|
||||
prefs.getString(context.getString(R.string.bluetooth_sensor_key), null);
|
||||
if (address == null) {
|
||||
return;
|
||||
}
|
||||
Log.w(MyTracksConstants.TAG, "Connecting to bluetooth sensor: " + address);
|
||||
// Get the BluetoothDevice object
|
||||
BluetoothDevice device = bluetoothAdapter.getRemoteDevice(address);
|
||||
// Attempt to connect to the device
|
||||
connectionManager.connect(device);
|
||||
|
||||
// Performing this check in onResume() covers the case in which BT was
|
||||
// not enabled during onStart(), so we were paused to enable it...
|
||||
// onResume() will be called when ACTION_REQUEST_ENABLE activity returns.
|
||||
if (connectionManager != null) {
|
||||
// Only if the state is STATE_NONE, do we know that we haven't started
|
||||
// already
|
||||
if (connectionManager.getState() == Sensor.SensorState.NONE) {
|
||||
// Start the Bluetooth sensor services
|
||||
Log.w(MyTracksConstants.TAG, "Disabled manager onStartTrack");
|
||||
connectionManager.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void onDestroy() {
|
||||
// Stop the Bluetooth sensor services
|
||||
if (connectionManager != null) {
|
||||
connectionManager.stop();
|
||||
}
|
||||
}
|
||||
|
||||
public Sensor.SensorDataSet getSensorDataSet() {
|
||||
return sensorDataSet;
|
||||
}
|
||||
|
||||
public Sensor.SensorState getSensorState() {
|
||||
return (connectionManager == null)
|
||||
? Sensor.SensorState.NONE
|
||||
: connectionManager.getState();
|
||||
}
|
||||
|
||||
// The Handler that gets information back from the BluetoothSensorService
|
||||
private final Handler messageHandler = new Handler() {
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
switch (msg.what) {
|
||||
case BluetoothConnectionManager.MESSAGE_STATE_CHANGE:
|
||||
// TODO should we update the SensorManager state var?
|
||||
Log.i(MyTracksConstants.TAG, "MESSAGE_STATE_CHANGE: " + msg.arg1);
|
||||
break;
|
||||
case BluetoothConnectionManager.MESSAGE_WRITE:
|
||||
break;
|
||||
case BluetoothConnectionManager.MESSAGE_READ:
|
||||
byte[] readBuf = null;
|
||||
try {
|
||||
readBuf = (byte[]) msg.obj;
|
||||
sensorDataSet = parser.parseBuffer(readBuf);
|
||||
Log.d(MyTracksConstants.TAG, "MESSAGE_READ: " + sensorDataSet.toString());
|
||||
} catch (IllegalArgumentException iae) {
|
||||
sensorDataSet = null;
|
||||
Log.i(MyTracksConstants.TAG,
|
||||
"Got bad sensor data: " + new String(readBuf, 0, readBuf.length),
|
||||
iae);
|
||||
} catch (RuntimeException re) {
|
||||
sensorDataSet = null;
|
||||
Log.i(MyTracksConstants.TAG, "Unexpected exception on read.", re);
|
||||
}
|
||||
break;
|
||||
case BluetoothConnectionManager.MESSAGE_DEVICE_NAME:
|
||||
// save the connected device's name
|
||||
connectedDeviceName =
|
||||
msg.getData().getString(BluetoothConnectionManager.DEVICE_NAME);
|
||||
Toast.makeText(context.getApplicationContext(),
|
||||
"Connected to " + connectedDeviceName, Toast.LENGTH_SHORT)
|
||||
.show();
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright (C) 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.services.sensors;
|
||||
|
||||
import com.google.android.apps.mytracks.content.Sensor;
|
||||
|
||||
/**
|
||||
* An interface for parsing a byte array to a SensorData object.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public interface MessageParser {
|
||||
|
||||
public int getFrameSize();
|
||||
|
||||
public Sensor.SensorDataSet parseBuffer(byte[] readBuff);
|
||||
|
||||
public boolean isValid(byte[] buffer);
|
||||
|
||||
public int findNextAlignment(byte[] buffer);
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
/*
|
||||
* 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.services.sensors;
|
||||
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import com.google.android.apps.mytracks.MyTracksConstants;
|
||||
import com.google.android.apps.mytracks.content.Sensor;
|
||||
|
||||
/**
|
||||
* Manage the connection to a sensor.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public abstract class SensorManager {
|
||||
|
||||
/**
|
||||
* The maximum age where the data is considered valid.
|
||||
*/
|
||||
public static final long MAX_AGE = 5000;
|
||||
|
||||
/**
|
||||
* Time to wait after a time out to retry.
|
||||
*/
|
||||
public static final int RETRY_PERIOD = 30000;
|
||||
|
||||
private Sensor.SensorState sensorState = Sensor.SensorState.NONE;
|
||||
|
||||
private long sensorStateTimestamp = 0;
|
||||
|
||||
/**
|
||||
* A task to run periodically to check to see if connection was lost.
|
||||
*/
|
||||
private TimerTask checkSensorManager = new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
Log.i(MyTracksConstants.TAG,
|
||||
"SensorManager state: " + getSensorState());
|
||||
switch (getSensorState()) {
|
||||
case CONNECTING:
|
||||
long age = System.currentTimeMillis() - getSensorStateTimestamp();
|
||||
if (age > 2 * RETRY_PERIOD) {
|
||||
Log.i(MyTracksConstants.TAG, "Retrying connecting SensorManager.");
|
||||
setupChannel();
|
||||
}
|
||||
break;
|
||||
case DISCONNECTED:
|
||||
Log.i(MyTracksConstants.TAG,
|
||||
"Re-registering disconnected SensoManager.");
|
||||
setupChannel();
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* This timer invokes periodically the checkLocationListener timer task.
|
||||
*/
|
||||
private final Timer timer = new Timer();
|
||||
|
||||
/**
|
||||
* Is the sensor that this manages enabled.
|
||||
* @return true if the sensor is enabled
|
||||
*/
|
||||
public abstract boolean isEnabled();
|
||||
|
||||
/**
|
||||
* This is called when my tracks starts recording a new track.
|
||||
* This is the place to open connections to the sensor.
|
||||
*/
|
||||
public void onStartTrack() {
|
||||
setupChannel();
|
||||
timer.schedule(checkSensorManager, RETRY_PERIOD, RETRY_PERIOD);
|
||||
}
|
||||
|
||||
public abstract void setupChannel();
|
||||
|
||||
public void shutdown() {
|
||||
timer.cancel();
|
||||
onDestroy();
|
||||
}
|
||||
|
||||
/**
|
||||
* This is called when my tracks stops recording.
|
||||
* This is the place to shutdown any open connections.
|
||||
*/
|
||||
public abstract void onDestroy();
|
||||
|
||||
/**
|
||||
* Return the last sensor reading.
|
||||
* @return The last reading from the sensor.
|
||||
*/
|
||||
public abstract Sensor.SensorDataSet getSensorDataSet();
|
||||
|
||||
public void setSensorState(Sensor.SensorState sensorState) {
|
||||
this.sensorState = sensorState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the current sensor state.
|
||||
* @return The current sensor state.
|
||||
*/
|
||||
public Sensor.SensorState getSensorState() {
|
||||
return sensorState;
|
||||
}
|
||||
|
||||
public long getSensorStateTimestamp() {
|
||||
return sensorStateTimestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return True if the data is recent enough to be considered valid.
|
||||
*/
|
||||
public boolean isDataValid() {
|
||||
return (System.currentTimeMillis() - getSensorDataSet().getCreationTime()) < MAX_AGE;
|
||||
}
|
||||
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* 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.services.sensors;
|
||||
|
||||
import com.google.android.apps.mytracks.MyTracksConstants;
|
||||
import com.google.android.apps.mytracks.MyTracksSettings;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.util.Log;
|
||||
|
||||
/**
|
||||
* A factory of SensorManagers.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public class SensorManagerFactory {
|
||||
|
||||
private SensorManagerFactory() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a new sensor manager.
|
||||
* @param context Context to fetch system preferences.
|
||||
* @return The sensor manager that corresponds to the sensor type setting.
|
||||
*/
|
||||
public static SensorManager getSensorManager(Context context) {
|
||||
SharedPreferences prefs =
|
||||
context.getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0);
|
||||
if (prefs == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String sensor = prefs.getString(context.getString(R.string.sensor_type_key),
|
||||
null);
|
||||
Log.i(MyTracksConstants.TAG, "Creating sensor of type: " + sensor);
|
||||
|
||||
if (sensor == null) {
|
||||
return null;
|
||||
} else if (sensor.equals(context.getString(R.string.zephyr_sensor_type))) {
|
||||
return new ZephyrSensorManager(context);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.services.sensors;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.google.android.apps.mytracks.content.Sensor;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
/**
|
||||
* A collection of methods for message parsers.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public class SensorUtils {
|
||||
|
||||
private SensorUtils() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract one unsigned short from a big endian byte array.
|
||||
* @param buffer the buffer to extract the short from
|
||||
* @param index the first byte to be interpreted as part of the short
|
||||
* @return The unsigned short at the given index in the buffer
|
||||
*/
|
||||
public static int unsignedShortToInt(byte[] buffer, int index) {
|
||||
int r = (buffer[index] & 0xFF) << 8;
|
||||
r |= buffer[index + 1] & 0xFF;
|
||||
return r;
|
||||
}
|
||||
|
||||
public static String getStateAsString(Sensor.SensorState state, Context c) {
|
||||
switch (state) {
|
||||
case NONE:
|
||||
return c.getString(R.string.none);
|
||||
case CONNECTING:
|
||||
return c.getString(R.string.connecting);
|
||||
case CONNECTED:
|
||||
return c.getString(R.string.connected);
|
||||
case DISCONNECTED:
|
||||
return c.getString(R.string.disconnected);
|
||||
case SENDING:
|
||||
return c.getString(R.string.sending);
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* 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.services.sensors;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import com.google.android.apps.mytracks.MyTracksConstants;
|
||||
import com.google.android.apps.mytracks.content.Sensor;
|
||||
|
||||
/**
|
||||
* An implementation of a SensorData parser for Zephyr HRM.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public class ZephyrMessageParser implements MessageParser {
|
||||
|
||||
@Override
|
||||
public Sensor.SensorDataSet parseBuffer(byte[] buffer) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < buffer.length; i++) {
|
||||
sb.append(String.format("%02X", buffer[i]));
|
||||
}
|
||||
Log.w(MyTracksConstants.TAG, "Got zephyr data: " + sb);
|
||||
// The provided units are 1/16 strides per minute.
|
||||
int cadence = SensorUtils.unsignedShortToInt(buffer, 56);
|
||||
// Heart Rate
|
||||
Sensor.SensorData.Builder b = Sensor.SensorData.newBuilder()
|
||||
.setValue(buffer[12] & 0xFF)
|
||||
.setState(Sensor.SensorState.SENDING);
|
||||
// Cadence
|
||||
//.setCadence(cadence / 16)
|
||||
//.build();
|
||||
Sensor.SensorDataSet sds =
|
||||
Sensor.SensorDataSet.newBuilder()
|
||||
.setCreationTime(System.currentTimeMillis())
|
||||
.setHeartRate(b)
|
||||
.build();
|
||||
|
||||
return sds;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValid(byte[] buffer) {
|
||||
// TODO crc etc.
|
||||
return buffer[0] == 0x02 && buffer[59] == 0x03;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getFrameSize() {
|
||||
return 60;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int findNextAlignment(byte[] buffer) {
|
||||
// TODO test or understand this code.
|
||||
for (int i = 0; i < buffer.length - 1; i++) {
|
||||
if (buffer[i] == 0x03 && buffer[i+1] == 0x02) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* 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.services.sensors;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
public class ZephyrSensorManager extends BluetoothSensorManager {
|
||||
|
||||
public ZephyrSensorManager(Context context) {
|
||||
super(context, new ZephyrMessageParser());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
/*
|
||||
* 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.stats;
|
||||
|
||||
/**
|
||||
* This class maintains a buffer of doubles. This buffer is a convenient class
|
||||
* for storing a series of doubles and calculating information about them. This
|
||||
* is a FIFO buffer.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public class DoubleBuffer {
|
||||
|
||||
/**
|
||||
* The location that the next write will occur at.
|
||||
*/
|
||||
private int index;
|
||||
|
||||
/**
|
||||
* The sliding buffer of doubles.
|
||||
*/
|
||||
private final double[] buffer;
|
||||
|
||||
/**
|
||||
* Have all of the slots in the buffer been filled?
|
||||
*/
|
||||
private boolean isFull;
|
||||
|
||||
/**
|
||||
* Creates a buffer with size elements.
|
||||
*
|
||||
* @param size the number of elements in the buffer
|
||||
* @throws IllegalArgumentException if the size is not a positive value
|
||||
*/
|
||||
public DoubleBuffer(int size) {
|
||||
if (size < 1) {
|
||||
throw new IllegalArgumentException("The buffer size must be positive.");
|
||||
}
|
||||
buffer = new double[size];
|
||||
reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a double to the buffer. If the buffer is full the oldest element is
|
||||
* overwritten.
|
||||
*
|
||||
* @param d the double to add
|
||||
*/
|
||||
public void setNext(double d) {
|
||||
if (index == buffer.length) {
|
||||
index = 0;
|
||||
}
|
||||
buffer[index] = d;
|
||||
index++;
|
||||
if (index == buffer.length) {
|
||||
isFull = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Are all of the entries in the buffer used?
|
||||
*/
|
||||
public boolean isFull() {
|
||||
return isFull;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the buffer to the initial state.
|
||||
*/
|
||||
public void reset() {
|
||||
index = 0;
|
||||
isFull = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the average of values from the buffer.
|
||||
*
|
||||
* @return The average of the buffer
|
||||
*/
|
||||
public double getAverage() {
|
||||
int numberOfEntries = isFull ? buffer.length : index;
|
||||
if (numberOfEntries == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
double sum = 0;
|
||||
for (int i = 0; i < numberOfEntries; i++) {
|
||||
sum += buffer[i];
|
||||
}
|
||||
return sum / numberOfEntries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the average and standard deviation of the buffer.
|
||||
*
|
||||
* @return An array of two elements - the first is the average, and the second
|
||||
* is the variance
|
||||
*/
|
||||
public double[] getAverageAndVariance() {
|
||||
int numberOfEntries = isFull ? buffer.length : index;
|
||||
if (numberOfEntries == 0) {
|
||||
return new double[]{0, 0};
|
||||
}
|
||||
|
||||
double sum = 0;
|
||||
double sumSquares = 0;
|
||||
for (int i = 0; i < numberOfEntries; i++) {
|
||||
sum += buffer[i];
|
||||
sumSquares += Math.pow(buffer[i], 2);
|
||||
}
|
||||
|
||||
double average = sum / numberOfEntries;
|
||||
return new double[]{average,
|
||||
sumSquares / numberOfEntries - Math.pow(average, 2)};
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuffer stringBuffer = new StringBuffer("Full: ");
|
||||
stringBuffer.append(isFull);
|
||||
stringBuffer.append("\n");
|
||||
for (int i = 0; i < buffer.length; i++) {
|
||||
stringBuffer.append((i == index) ? "<<" : "[");
|
||||
stringBuffer.append(buffer[i]);
|
||||
stringBuffer.append((i == index) ? ">> " : "] ");
|
||||
}
|
||||
return stringBuffer.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,362 @@
|
||||
/*
|
||||
* 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.stats;
|
||||
|
||||
import com.google.android.apps.mytracks.MyTracksConstants;
|
||||
|
||||
import android.location.Location;
|
||||
import android.util.Log;
|
||||
|
||||
/**
|
||||
* Statistics keeper for a trip.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
* @author Rodrigo Damazio
|
||||
*/
|
||||
public class TripStatisticsBuilder {
|
||||
/**
|
||||
* Statistical data about the trip, which can be displayed to the user.
|
||||
*/
|
||||
private final TripStatistics data;
|
||||
|
||||
/**
|
||||
* The last location that the gps reported.
|
||||
*/
|
||||
private Location lastLocation;
|
||||
|
||||
/**
|
||||
* The last location that contributed to the stats. It is also the last
|
||||
* location the user was found to be moving.
|
||||
*/
|
||||
private Location lastMovingLocation;
|
||||
|
||||
/**
|
||||
* The current speed in meters/second as reported by the gps.
|
||||
*/
|
||||
private double currentSpeed;
|
||||
|
||||
/**
|
||||
* The current grade. This value is very noisy and not reported to the user.
|
||||
*/
|
||||
private double currentGrade;
|
||||
|
||||
/**
|
||||
* Is the trip currently paused?
|
||||
* All trips start paused.
|
||||
*/
|
||||
private boolean paused = true;
|
||||
|
||||
/**
|
||||
* A buffer of the last speed readings in meters/second.
|
||||
*/
|
||||
private final DoubleBuffer speedBuffer =
|
||||
new DoubleBuffer(MyTracksConstants.SPEED_SMOOTHING_FACTOR);
|
||||
|
||||
/**
|
||||
* A buffer of the recent elevation readings in meters.
|
||||
*/
|
||||
private final DoubleBuffer elevationBuffer =
|
||||
new DoubleBuffer(MyTracksConstants.ELEVATION_SMOOTHING_FACTOR);
|
||||
|
||||
/**
|
||||
* A buffer of the distance between recent gps readings in meters.
|
||||
*/
|
||||
private final DoubleBuffer distanceBuffer =
|
||||
new DoubleBuffer(MyTracksConstants.DISTANCE_SMOOTHING_FACTOR);
|
||||
|
||||
/**
|
||||
* A buffer of the recent grade calculations.
|
||||
*/
|
||||
private final DoubleBuffer gradeBuffer =
|
||||
new DoubleBuffer(MyTracksConstants.GRADE_SMOOTHING_FACTOR);
|
||||
|
||||
/**
|
||||
* The total number of locations in this trip.
|
||||
*/
|
||||
private long totalLocations = 0;
|
||||
|
||||
/**
|
||||
* Creates a new trip starting at the given time.
|
||||
*
|
||||
* @param startTime the start time.
|
||||
*/
|
||||
public TripStatisticsBuilder(long startTime) {
|
||||
data = new TripStatistics();
|
||||
resumeAt(startTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new trip, starting with existing statistics data.
|
||||
*
|
||||
* @param statsData the statistics data to copy and start from
|
||||
*/
|
||||
public TripStatisticsBuilder(TripStatistics statsData) {
|
||||
data = new TripStatistics(statsData);
|
||||
if (data.getStartTime() > 0) {
|
||||
resumeAt(data.getStartTime());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a location to the current trip. This will update all of the internal
|
||||
* variables with this new location.
|
||||
*
|
||||
* @param currentLocation the current gps location
|
||||
* @param systemTime the time used for calculation of totalTime. This should
|
||||
* be the phone's time (not GPS time)
|
||||
* @return true if the person is moving
|
||||
*/
|
||||
public boolean addLocation(Location currentLocation, long systemTime) {
|
||||
if (paused) {
|
||||
Log.w(MyTracksConstants.TAG,
|
||||
"Tried to account for location while track is paused");
|
||||
return false;
|
||||
}
|
||||
|
||||
totalLocations++;
|
||||
|
||||
double elevationDifference = updateElevation(currentLocation.getAltitude());
|
||||
|
||||
// Update the "instant" values:
|
||||
data.setTotalTime(systemTime - data.getStartTime());
|
||||
currentSpeed = currentLocation.getSpeed();
|
||||
|
||||
// This was the 1st location added, remember it and do nothing else:
|
||||
if (lastLocation == null) {
|
||||
lastLocation = currentLocation;
|
||||
lastMovingLocation = currentLocation;
|
||||
return false;
|
||||
}
|
||||
|
||||
updateBounds(currentLocation);
|
||||
|
||||
// Don't do anything if we didn't move since last fix:
|
||||
double distance = lastLocation.distanceTo(currentLocation);
|
||||
if (distance < MyTracksConstants.MAX_NO_MOVEMENT_DISTANCE &&
|
||||
currentSpeed < MyTracksConstants.MAX_NO_MOVEMENT_SPEED) {
|
||||
lastLocation = currentLocation;
|
||||
return false;
|
||||
}
|
||||
|
||||
data.addTotalDistance(lastMovingLocation.distanceTo(currentLocation));
|
||||
updateSpeed(currentLocation.getTime(), currentSpeed,
|
||||
lastLocation.getTime(), lastLocation.getSpeed());
|
||||
|
||||
updateGrade(distance, elevationDifference);
|
||||
lastLocation = currentLocation;
|
||||
lastMovingLocation = currentLocation;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the track's bounding box to include the given location.
|
||||
*/
|
||||
private void updateBounds(Location location) {
|
||||
data.updateLatitudeExtremities(location.getLatitude());
|
||||
data.updateLongitudeExtremities(location.getLongitude());
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the elevation measurements.
|
||||
*
|
||||
* @param elevation the current elevation
|
||||
*/
|
||||
// @VisibleForTesting
|
||||
double updateElevation(double elevation) {
|
||||
double oldSmoothedElevation = getSmoothedElevation();
|
||||
elevationBuffer.setNext(elevation);
|
||||
double smoothedElevation = getSmoothedElevation();
|
||||
data.updateElevationExtremities(smoothedElevation);
|
||||
double elevationDifference = elevationBuffer.isFull()
|
||||
? smoothedElevation - oldSmoothedElevation
|
||||
: 0.0;
|
||||
if (elevationDifference > 0) {
|
||||
data.addTotalElevationGain(elevationDifference);
|
||||
}
|
||||
return elevationDifference;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the speed measurements.
|
||||
*
|
||||
* @param updateTime the time of the speed update
|
||||
* @param speed the current speed
|
||||
* @param lastLocationTime the time of the last speed update
|
||||
* @param lastLocationSpeed the speed of the last update
|
||||
*/
|
||||
// @VisibleForTesting
|
||||
void updateSpeed(long updateTime, double speed, long lastLocationTime,
|
||||
double lastLocationSpeed) {
|
||||
// We are now sure the user is moving.
|
||||
long timeDifference = updateTime - lastLocationTime;
|
||||
if (timeDifference < 0) {
|
||||
Log.e(MyTracksConstants.TAG,
|
||||
"Found negative time change: " + timeDifference);
|
||||
}
|
||||
data.addMovingTime(timeDifference);
|
||||
|
||||
if (isValidSpeed(updateTime, speed, lastLocationTime, lastLocationSpeed,
|
||||
speedBuffer)) {
|
||||
speedBuffer.setNext(speed);
|
||||
if (speed > data.getMaxSpeed()) {
|
||||
data.setMaxSpeed(speed);
|
||||
}
|
||||
double movingSpeed = data.getAverageMovingSpeed();
|
||||
if (speedBuffer.isFull() && (movingSpeed > data.getMaxSpeed())) {
|
||||
data.setMaxSpeed(movingSpeed);
|
||||
}
|
||||
} else {
|
||||
Log.d(MyTracksConstants.TAG,
|
||||
"TripStatistics ignoring big change: Raw Speed: " + speed
|
||||
+ " old: " + lastLocationSpeed + " [" + toString() + "]");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks to see if this is a valid speed.
|
||||
*
|
||||
* @param updateTime The time at the current reading
|
||||
* @param speed The current speed
|
||||
* @param lastLocationTime The time at the last location
|
||||
* @param lastLocationSpeed Speed at the last location
|
||||
* @param speedBuffer A buffer of recent readings
|
||||
* @return True if this is likely a valid speed
|
||||
*/
|
||||
public static boolean isValidSpeed(long updateTime, double speed,
|
||||
long lastLocationTime, double lastLocationSpeed,
|
||||
DoubleBuffer speedBuffer) {
|
||||
|
||||
// We don't want to count 0 towards the speed.
|
||||
if (speed == 0) {
|
||||
return false;
|
||||
}
|
||||
// We are now sure the user is moving.
|
||||
long timeDifference = updateTime - lastLocationTime;
|
||||
|
||||
// There are a lot of noisy speed readings.
|
||||
// Do the cheapest checks first, most expensive last.
|
||||
// The following code will ignore unlikely to be real readings.
|
||||
// - 128 m/s seems to be an internal android error code.
|
||||
if (Math.abs(speed - 128) < 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Another check for a spurious reading. See if the path seems physically
|
||||
// likely. Ignore any speeds that imply accelaration greater than 2g's
|
||||
// Really who can accelerate faster?
|
||||
double speedDifference = Math.abs(lastLocationSpeed - speed);
|
||||
if (speedDifference > MyTracksConstants.MAX_ACCELERATION * timeDifference) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// There are three additional checks if the reading gets this far:
|
||||
// - Only use the speed if the buffer is full
|
||||
// - Check that the current speed is less than 10x the recent smoothed speed
|
||||
// - Double check that the current speed does not imply crazy acceleration
|
||||
double smoothedSpeed = speedBuffer.getAverage();
|
||||
double smoothedDiff = Math.abs(smoothedSpeed - speed);
|
||||
return !speedBuffer.isFull() ||
|
||||
(speed < smoothedSpeed * 10
|
||||
&& smoothedDiff < MyTracksConstants.MAX_ACCELERATION * timeDifference);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the grade measurements.
|
||||
*
|
||||
* @param distance the distance the user just traveled
|
||||
* @param elevationDifference the elevation difference between the current
|
||||
* reading and the previous reading
|
||||
*/
|
||||
// @VisibleForTesting
|
||||
void updateGrade(double distance, double elevationDifference) {
|
||||
distanceBuffer.setNext(distance);
|
||||
double smoothedDistance = distanceBuffer.getAverage();
|
||||
|
||||
// With the error in the altitude measurement it is dangerous to divide
|
||||
// by anything less than 5.
|
||||
if (!elevationBuffer.isFull() || !distanceBuffer.isFull()
|
||||
|| smoothedDistance < 5.0) {
|
||||
return;
|
||||
}
|
||||
currentGrade = elevationDifference / smoothedDistance;
|
||||
gradeBuffer.setNext(currentGrade);
|
||||
data.updateGradeExtremities(gradeBuffer.getAverage());
|
||||
}
|
||||
|
||||
/**
|
||||
* Pauses the track at the given time.
|
||||
*
|
||||
* @param time the time to pause at
|
||||
*/
|
||||
public void pauseAt(long time) {
|
||||
if (paused) { return; }
|
||||
|
||||
data.setStopTime(time);
|
||||
data.setTotalTime(time - data.getStartTime());
|
||||
lastLocation = null; // Make sure the counter restarts.
|
||||
paused = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resumes the current track at the given time.
|
||||
*
|
||||
* @param time the time to resume at
|
||||
*/
|
||||
public void resumeAt(long time) {
|
||||
if (!paused) { return; }
|
||||
|
||||
// TODO: The times are bogus if the track is paused then resumed again
|
||||
data.setStartTime(time);
|
||||
data.setStopTime(-1);
|
||||
paused = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "TripStatistics { Data: " + data.toString()
|
||||
+ "; Total Locations: " + totalLocations
|
||||
+ "; Paused: " + paused
|
||||
+ "; Current speed: " + currentSpeed
|
||||
+ "; Current grade: " + currentGrade
|
||||
+ "}";
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the amount of time the user has been idle or 0 if they are moving.
|
||||
*/
|
||||
public long getIdleTime() {
|
||||
if (lastLocation == null || lastMovingLocation == null)
|
||||
return 0;
|
||||
return lastLocation.getTime() - lastMovingLocation.getTime();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current elevation smoothed over several readings. The elevation
|
||||
* data is very noisy so it is better to use the smoothed elevation than the
|
||||
* raw elevation for many tasks.
|
||||
*
|
||||
* @return The elevation smoothed over several readings
|
||||
*/
|
||||
public double getSmoothedElevation() {
|
||||
return elevationBuffer.getAverage();
|
||||
}
|
||||
|
||||
public TripStatistics getStatistics() {
|
||||
// Take a snapshot - we don't want anyone messing with our internals
|
||||
return new TripStatistics(data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
* 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.util;
|
||||
|
||||
import com.google.android.apps.mytracks.MyTracksConstants;
|
||||
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
|
||||
/**
|
||||
* Utility class for determining if newer-API features are available on the
|
||||
* current device.
|
||||
*
|
||||
* @author Rodrigo Damazio
|
||||
*/
|
||||
public class ApiFeatures {
|
||||
|
||||
/**
|
||||
* The API level of the Android version we're being run under.
|
||||
*/
|
||||
public static final int ANDROID_API_LEVEL = Integer.parseInt(
|
||||
Build.VERSION.SDK);
|
||||
|
||||
private static ApiFeatures instance;
|
||||
|
||||
/**
|
||||
* The API platform adapter supported by this system.
|
||||
*/
|
||||
private ApiPlatformAdapter apiPlatformAdapter;
|
||||
|
||||
/**
|
||||
* Returns the singleton instance of this class.
|
||||
*/
|
||||
public static ApiFeatures getInstance() {
|
||||
if (instance == null) {
|
||||
instance = new ApiFeatures();
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injects a specific singleton instance, to be used for unit tests.
|
||||
*/
|
||||
public static void injectInstance(ApiFeatures instance) {
|
||||
ApiFeatures.instance = instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow subclasses for mocking, but no direct instantiation.
|
||||
*/
|
||||
protected ApiFeatures() {
|
||||
if (getApiLevel() >= 5) {
|
||||
try {
|
||||
Class<?> clazz = Class.forName(
|
||||
"com.google.android.apps.mytracks.util.EclairPlatformAdapter");
|
||||
apiPlatformAdapter = (ApiPlatformAdapter) clazz.newInstance();
|
||||
} catch (Exception e) {
|
||||
Log.i(MyTracksConstants.TAG, "ApiFeatures: Unable to instantiate Eclair"
|
||||
+ " platform adapter", e);
|
||||
}
|
||||
}
|
||||
if (apiPlatformAdapter == null) {
|
||||
Log.i(MyTracksConstants.TAG,
|
||||
"ApiFeatures: Using default platform adapter");
|
||||
// Cupcake adapter is always supported, so it's safe to do static linkage.
|
||||
apiPlatformAdapter = new CupcakePlatformAdapter();
|
||||
}
|
||||
}
|
||||
|
||||
public ApiPlatformAdapter getApiPlatformAdapter() {
|
||||
return apiPlatformAdapter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether cloud backup (a.k.a. Froyo backup) is available.
|
||||
*/
|
||||
public boolean hasBackup() {
|
||||
return getApiLevel() >= 8;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether text-to-speech is available.
|
||||
*/
|
||||
public boolean hasTextToSpeech() {
|
||||
if (getApiLevel() < 4) return false;
|
||||
|
||||
try {
|
||||
Class.forName("android.speech.tts.TextToSpeech");
|
||||
} catch (ClassNotFoundException ex) {
|
||||
return false;
|
||||
} catch (LinkageError er) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Visible for testing.
|
||||
protected int getApiLevel() {
|
||||
return ANDROID_API_LEVEL;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* 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.util;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.Service;
|
||||
import android.os.HandlerThread;
|
||||
|
||||
/**
|
||||
* A set of methods that may be implemented in a platform specific way.
|
||||
*
|
||||
* @author Bartlomiej Niechwiej
|
||||
*/
|
||||
public interface ApiPlatformAdapter {
|
||||
|
||||
/**
|
||||
* Puts the specified service into foreground.
|
||||
*
|
||||
* @param service the service to be put in foreground.
|
||||
* @param notificationManager the notification manager used to post the given
|
||||
* notification.
|
||||
* @param id the ID of the notification, unique within the application.
|
||||
* @param notification the notification to post.
|
||||
*/
|
||||
void startForeground(Service service, NotificationManager notificationManager,
|
||||
int id, Notification notification);
|
||||
|
||||
/**
|
||||
* Puts the given service into background.
|
||||
* @param service the service to put into background.
|
||||
* @param notificationManager the notification manager to user when removing
|
||||
* notifications.
|
||||
* @param id the ID of the notification to be remove, or -1 if the
|
||||
* notification shouldn't be removed.
|
||||
*/
|
||||
void stopForeground(Service service, NotificationManager notificationManager,
|
||||
int id);
|
||||
|
||||
/**
|
||||
* Terminates the given handler thread.
|
||||
*
|
||||
* @param handlerThread the thread to be terminated.
|
||||
* @return true whether the handler has been stopped or not.
|
||||
*/
|
||||
boolean stopHandlerThread(HandlerThread handlerThread);
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
/*
|
||||
* 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.util;
|
||||
|
||||
import com.google.android.apps.mytracks.MyTracksConstants;
|
||||
|
||||
import android.bluetooth.BluetoothAdapter;
|
||||
import android.bluetooth.BluetoothClass;
|
||||
import android.bluetooth.BluetoothDevice;
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Utilities for dealing with bluetooth devices.
|
||||
* This can be used safely even in systems that don't support bluetooth,
|
||||
* in which case a dummy implementation will be used.
|
||||
*
|
||||
* @author Rodrigo Damazio
|
||||
*/
|
||||
public abstract class BluetoothDeviceUtils {
|
||||
public static final String ANY_DEVICE = "any";
|
||||
private static BluetoothDeviceUtils instance;
|
||||
|
||||
/**
|
||||
* Dummy implementation, for systems that don't support bluetooth.
|
||||
*/
|
||||
private static class DummyImpl extends BluetoothDeviceUtils {
|
||||
@Override
|
||||
public void populateDeviceLists(List<String> deviceNames, List<String> deviceAddresses) {
|
||||
// Do nothing - no devices to add
|
||||
}
|
||||
|
||||
@Override
|
||||
public BluetoothDevice findDeviceMatching(String targetDeviceAddress) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Real implementation, for systems that DO support bluetooth.
|
||||
*/
|
||||
private static class RealImpl extends BluetoothDeviceUtils {
|
||||
private final BluetoothAdapter bluetoothAdapter;
|
||||
|
||||
public RealImpl() {
|
||||
bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
|
||||
|
||||
if (bluetoothAdapter == null) {
|
||||
throw new IllegalStateException("Unable to get bluetooth adapter");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void populateDeviceLists(List<String> deviceNames, List<String> deviceAddresses) {
|
||||
ensureNotDiscovering();
|
||||
|
||||
Set<BluetoothDevice> pairedDevices = bluetoothAdapter.getBondedDevices();
|
||||
for (BluetoothDevice device : pairedDevices) {
|
||||
BluetoothClass bluetoothClass = device.getBluetoothClass();
|
||||
if (bluetoothClass != null) {
|
||||
// Not really sure what we want, but I know what we don't want.
|
||||
switch(bluetoothClass.getMajorDeviceClass()) {
|
||||
case BluetoothClass.Device.Major.COMPUTER:
|
||||
case BluetoothClass.Device.Major.PHONE:
|
||||
break;
|
||||
default:
|
||||
deviceAddresses.add(device.getAddress());
|
||||
deviceNames.add(device.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public BluetoothDevice findDeviceMatching(String targetDeviceAddress) {
|
||||
if (targetDeviceAddress.equals(ANY_DEVICE)) {
|
||||
return findAnyDevice();
|
||||
} else {
|
||||
return findDeviceByAddress(targetDeviceAddress);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds and returns the first suitable bluetooth sensor.
|
||||
*/
|
||||
private BluetoothDevice findAnyDevice() {
|
||||
ensureNotDiscovering();
|
||||
|
||||
Set<BluetoothDevice> pairedDevices = bluetoothAdapter.getBondedDevices();
|
||||
for (BluetoothDevice device : pairedDevices) {
|
||||
// Look for the first paired computer device
|
||||
if (isSuitableDevice(device)) {
|
||||
return device;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds and returns a device with the given address, or null if it's not
|
||||
* a suitable sensor.
|
||||
*/
|
||||
private BluetoothDevice findDeviceByAddress(String targetDeviceAddress) {
|
||||
ensureNotDiscovering();
|
||||
|
||||
BluetoothDevice device = bluetoothAdapter.getRemoteDevice(targetDeviceAddress);
|
||||
if (isSuitableDevice(device)) {
|
||||
return device;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures the bluetooth adapter is not in discovery mode.
|
||||
*/
|
||||
private void ensureNotDiscovering() {
|
||||
// If it's in discovery mode, cancel that for now.
|
||||
bluetoothAdapter.cancelDiscovery();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the given device is a suitable sensor.
|
||||
*
|
||||
* @param device the device to check
|
||||
* @return true if it's suitable, false otherwise
|
||||
*/
|
||||
private boolean isSuitableDevice(BluetoothDevice device) {
|
||||
// Check that the device is bonded
|
||||
if (device.getBondState() != BluetoothDevice.BOND_BONDED) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the proper (singleton) instance of this class.
|
||||
*/
|
||||
public static BluetoothDeviceUtils getInstance() {
|
||||
if (instance == null) {
|
||||
if (!isBluetoothMethodSupported()) {
|
||||
Log.d(MyTracksConstants.TAG, "Using dummy bluetooth utils");
|
||||
instance = new DummyImpl();
|
||||
} else {
|
||||
Log.d(MyTracksConstants.TAG, "Using real bluetooth utils");
|
||||
try {
|
||||
instance = new RealImpl();
|
||||
} catch (IllegalStateException ise) {
|
||||
Log.w(MyTracksConstants.TAG, "Oops, I mean, using dummy bluetooth utils", ise);
|
||||
instance = new DummyImpl();
|
||||
}
|
||||
}
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Populates the given lists with the names and addresses of all suitable
|
||||
* bluetooth devices.
|
||||
*
|
||||
* @param deviceNames the list to populate with user-visible names
|
||||
* @param deviceAddresses the list to populate with device addresses
|
||||
*/
|
||||
public abstract void populateDeviceLists(List<String> deviceNames, List<String> deviceAddresses);
|
||||
|
||||
/**
|
||||
* Finds the bluetooth device with the given address.
|
||||
*
|
||||
* @param targetDeviceAddress the address of the device, or
|
||||
* {@link #ANY_DEVICE} for using the first suitable device
|
||||
* @return the device's descriptor, or null if not found
|
||||
*/
|
||||
public abstract BluetoothDevice findDeviceMatching(String targetDeviceAddress);
|
||||
|
||||
/**
|
||||
* @return whether the bluetooth method is supported on this device
|
||||
*/
|
||||
public static boolean isBluetoothMethodSupported() {
|
||||
return Integer.parseInt(Build.VERSION.SDK) >= 5;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
/*
|
||||
* 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.util;
|
||||
|
||||
import com.google.android.apps.mytracks.MyTracksSettings;
|
||||
import com.google.android.apps.mytracks.content.Track;
|
||||
import com.google.android.apps.mytracks.stats.TripStatistics;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
import java.util.Vector;
|
||||
|
||||
/**
|
||||
* This class will generate google chart server url's.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public class ChartURLGenerator {
|
||||
|
||||
private static final String CHARTS_BASE_URL =
|
||||
"http://chart.apis.google.com/chart?";
|
||||
|
||||
private ChartURLGenerator() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a chart of a track.
|
||||
*
|
||||
* @param distances An array of distance measurements
|
||||
* @param elevations A matching array of elevation measurements
|
||||
* @param track The track for this chart
|
||||
* @param context The current appplication context
|
||||
*/
|
||||
public static String getChartUrl(Vector<Double> distances,
|
||||
Vector<Double> elevations, Track track, Context context) {
|
||||
SharedPreferences preferences =
|
||||
context.getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0);
|
||||
boolean metricUnits = true;
|
||||
if (preferences != null) {
|
||||
metricUnits = preferences.getBoolean(
|
||||
context.getString(R.string.metric_units_key), true);
|
||||
}
|
||||
|
||||
return getChartUrl(distances, elevations, track,
|
||||
context.getString(R.string.elevation_label), metricUnits, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a chart of a track.
|
||||
* This form is for testing without contexts.
|
||||
*
|
||||
* @param distances An array of distance measurements
|
||||
* @param elevations A matching array of elevation measurements
|
||||
* @param track The track for this chart
|
||||
* @param title The title for the chart
|
||||
* @param metricUnits Should the data be displayed in metric units
|
||||
* @param escapePipe Should the pipe character be escaped
|
||||
*/
|
||||
public static String getChartUrl(
|
||||
Vector<Double> distances, Vector<Double> elevations,
|
||||
Track track, String title, boolean metricUnits, boolean escapePipe) {
|
||||
if (distances == null || elevations == null || track == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (distances.size() != elevations.size()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Round it up.
|
||||
TripStatistics stats = track.getStatistics();
|
||||
double effectiveMaxY = metricUnits
|
||||
? stats.getMaxElevation()
|
||||
: stats.getMaxElevation() * UnitConversions.M_TO_FT;
|
||||
effectiveMaxY = ((int) (effectiveMaxY / 100)) * 100 + 100;
|
||||
// Round it down.
|
||||
double effectiveMinY = 0;
|
||||
double minElevation = metricUnits
|
||||
? stats.getMinElevation()
|
||||
: stats.getMinElevation() * UnitConversions.M_TO_FT;
|
||||
|
||||
effectiveMinY = ((int) (minElevation / 100)) * 100;
|
||||
if (stats.getMinElevation() < 0) {
|
||||
effectiveMinY -= 100;
|
||||
}
|
||||
double ySpread = effectiveMaxY - effectiveMinY;
|
||||
|
||||
StringBuilder sb = new StringBuilder(CHARTS_BASE_URL);
|
||||
sb.append("&chs=600x350");
|
||||
sb.append("&cht=lxy");
|
||||
|
||||
// Title
|
||||
sb.append("&chtt=");
|
||||
sb.append(title);
|
||||
|
||||
// Labels
|
||||
sb.append("&chxt=x,y");
|
||||
double distKM = stats.getTotalDistance() / 1000.0;
|
||||
double distDisplay =
|
||||
metricUnits ? distKM : (distKM * UnitConversions.KM_TO_MI);
|
||||
int xInterval = ((int) (distDisplay / 6));
|
||||
int yInterval = ((int) (ySpread / 600)) * 100;
|
||||
if (yInterval < 100) {
|
||||
yInterval = 25;
|
||||
}
|
||||
// Range
|
||||
sb.append("&chxr=0,0,");
|
||||
sb.append((int) distDisplay);
|
||||
sb.append(',');
|
||||
sb.append(xInterval);
|
||||
|
||||
// This is an ugly hack. We really want a pipe but we must double escape it.
|
||||
sb.append(escapePipe ? "%257C" : "|");
|
||||
sb.append("1,");
|
||||
sb.append(effectiveMinY);
|
||||
sb.append(',');
|
||||
sb.append(effectiveMaxY);
|
||||
sb.append(',');
|
||||
sb.append(yInterval);
|
||||
|
||||
// Line color
|
||||
sb.append("&chco=009A00");
|
||||
|
||||
// Fill
|
||||
sb.append("&chm=B,00AA00,0,0,0");
|
||||
|
||||
// Grid lines
|
||||
double desiredGrids = ySpread / yInterval;
|
||||
sb.append("&chg=100000,");
|
||||
sb.append(100.0 / desiredGrids);
|
||||
sb.append(",1,0");
|
||||
|
||||
// Data
|
||||
sb.append("&chd=e:");
|
||||
for (int i = 0; i < distances.size(); i++) {
|
||||
int normalized =
|
||||
(int) (getNormalizedDistance(distances.elementAt(i), track) * 4095);
|
||||
sb.append(ChartsExtendedEncoder.getEncodedValue(normalized));
|
||||
}
|
||||
|
||||
sb.append(ChartsExtendedEncoder.getSeparator());
|
||||
for (int i = 0; i < elevations.size(); i++) {
|
||||
int normalized =
|
||||
(int) (getNormalizedElevation(
|
||||
elevations.elementAt(i), effectiveMinY, ySpread) * 4095);
|
||||
sb.append(ChartsExtendedEncoder.getEncodedValue(normalized));
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private static double getNormalizedDistance(double d, Track track) {
|
||||
return d / track.getStatistics().getTotalDistance();
|
||||
}
|
||||
|
||||
private static double getNormalizedElevation(
|
||||
double d, double effectiveMinY, double ySpread) {
|
||||
return (d - effectiveMinY) / ySpread;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* 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.util;
|
||||
|
||||
/**
|
||||
* The singleton class representing the extended encoding of chart data.
|
||||
*/
|
||||
public class ChartsExtendedEncoder {
|
||||
|
||||
// ChartServer data encoding in extended mode
|
||||
private static final String CHARTSERVER_EXTENDED_ENCODING_SEPARATOR = ",";
|
||||
private static final String CHARTSERVER_EXTENDED_ENCODING =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-.";
|
||||
private static final int CHARTSERVER_EXTENDED_SINGLE_CHARACTER_VALUES =
|
||||
CHARTSERVER_EXTENDED_ENCODING.length();
|
||||
private static final String MISSING_POINT_EXTENDED_ENCODING = "__";
|
||||
|
||||
private ChartsExtendedEncoder() { }
|
||||
|
||||
public static String getEncodedValue(int scaled) {
|
||||
int index1 = scaled / CHARTSERVER_EXTENDED_SINGLE_CHARACTER_VALUES;
|
||||
if (index1 < 0 || index1 >= CHARTSERVER_EXTENDED_SINGLE_CHARACTER_VALUES) {
|
||||
return MISSING_POINT_EXTENDED_ENCODING;
|
||||
}
|
||||
|
||||
int index2 = scaled % CHARTSERVER_EXTENDED_SINGLE_CHARACTER_VALUES;
|
||||
if (index2 < 0 || index2 >= CHARTSERVER_EXTENDED_SINGLE_CHARACTER_VALUES) {
|
||||
return MISSING_POINT_EXTENDED_ENCODING;
|
||||
}
|
||||
|
||||
return String.valueOf(CHARTSERVER_EXTENDED_ENCODING.charAt(index1))
|
||||
+ String.valueOf(CHARTSERVER_EXTENDED_ENCODING.charAt(index2));
|
||||
}
|
||||
|
||||
public static String getSeparator() {
|
||||
return CHARTSERVER_EXTENDED_ENCODING_SEPARATOR;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* 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.util;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.Service;
|
||||
import android.os.HandlerThread;
|
||||
|
||||
/**
|
||||
* The Cupcake (API level 3) specific implementation of the
|
||||
* {@link ApiPlatformAdapter}.
|
||||
*
|
||||
* @author Bartlomiej Niechwiej
|
||||
*/
|
||||
public class CupcakePlatformAdapter implements ApiPlatformAdapter {
|
||||
|
||||
@Override
|
||||
public void startForeground(Service service,
|
||||
NotificationManager notificationManager, int id,
|
||||
Notification notification) {
|
||||
service.setForeground(true);
|
||||
notificationManager.notify(id, notification);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stopForeground(Service service,
|
||||
NotificationManager notificationManager, int id) {
|
||||
service.setForeground(false);
|
||||
if (id != -1) {
|
||||
notificationManager.cancel(id);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean stopHandlerThread(HandlerThread handlerThread) {
|
||||
// Do nothing, as Cupcake doesn't provide quit().
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* 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.util;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.Service;
|
||||
import android.os.HandlerThread;
|
||||
|
||||
/**
|
||||
* The Eclair (API level 5) specific implementation of the
|
||||
* {@link ApiPlatformAdapter}.
|
||||
*
|
||||
* @author Bartlomiej Niechwiej
|
||||
*/
|
||||
public class EclairPlatformAdapter implements ApiPlatformAdapter {
|
||||
|
||||
@Override
|
||||
public void startForeground(Service service,
|
||||
NotificationManager notificationManager, int id,
|
||||
Notification notification) {
|
||||
service.startForeground(id, notification);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stopForeground(Service service,
|
||||
NotificationManager notificationManager, int id) {
|
||||
service.stopForeground(id != -1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean stopHandlerThread(HandlerThread handlerThread) {
|
||||
return handlerThread.quit();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
/*
|
||||
* 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.util;
|
||||
|
||||
import com.google.android.apps.mytracks.MyTracksConstants;
|
||||
|
||||
import android.os.Environment;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Utilities for dealing with files.
|
||||
*
|
||||
* @author Rodrigo Damazio
|
||||
*/
|
||||
public class FileUtils {
|
||||
/**
|
||||
* The maximum length of a filename, as per the FAT32 specification.
|
||||
*/
|
||||
private static final int MAX_FILENAME_LENGTH = 260;
|
||||
|
||||
/**
|
||||
* A set of characters that are prohibited from being in file names.
|
||||
*/
|
||||
private static final Pattern PROHIBITED_CHAR_PATTERN =
|
||||
Pattern.compile("[^ A-Za-z0-9_.()-]+");
|
||||
|
||||
/**
|
||||
* Builds a path inside the My Tracks directory in the SD card.
|
||||
*
|
||||
* @param components the path components inside the mytracks directory
|
||||
* @return the full path to the destination
|
||||
*/
|
||||
public String buildExternalDirectoryPath(String... components) {
|
||||
StringBuilder dirNameBuilder = new StringBuilder();
|
||||
dirNameBuilder.append(Environment.getExternalStorageDirectory());
|
||||
dirNameBuilder.append(File.separatorChar);
|
||||
dirNameBuilder.append(MyTracksConstants.SDCARD_TOP_DIR);
|
||||
for (String component : components) {
|
||||
dirNameBuilder.append(File.separatorChar);
|
||||
dirNameBuilder.append(component);
|
||||
}
|
||||
return dirNameBuilder.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the SD card is available.
|
||||
*/
|
||||
public boolean isSdCardAvailable() {
|
||||
return Environment.MEDIA_MOUNTED.equals(
|
||||
Environment.getExternalStorageState());
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes the input string and make sure it is a valid fat32 file name.
|
||||
*
|
||||
* @param name the name to normalize
|
||||
* @return the sanitized name
|
||||
*/
|
||||
String sanitizeName(String name) {
|
||||
String cleaned = PROHIBITED_CHAR_PATTERN.matcher(name).replaceAll("");
|
||||
|
||||
return (cleaned.length() > MAX_FILENAME_LENGTH)
|
||||
? cleaned.substring(0, MAX_FILENAME_LENGTH)
|
||||
: cleaned.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures the given directory exists by creating it and its parents if
|
||||
* necessary.
|
||||
*
|
||||
* @return whether the directory exists (either already existed or was
|
||||
* successfully created)
|
||||
*/
|
||||
public boolean ensureDirectoryExists(File dir) {
|
||||
if (dir.exists() && dir.isDirectory()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (dir.mkdirs()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a filename with the given base name (prefix) and the given
|
||||
* extension, possibly adding a suffix to ensure the file doesn't exist.
|
||||
*
|
||||
* @param directory the directory the file will live in
|
||||
* @param fileBaseName the prefix for the file name
|
||||
* @param extension the file's extension
|
||||
* @return the complete file name, without the directory
|
||||
*/
|
||||
public synchronized String buildUniqueFileName(File directory,
|
||||
String fileBaseName, String extension) {
|
||||
return buildUniqueFileName(directory, fileBaseName, extension, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a filename with the given base name (prefix) and the given
|
||||
* extension, possibly adding a suffix to ensure the file doesn't exist.
|
||||
*
|
||||
* @param directory the directory the file will live in
|
||||
* @param fileBaseName the prefix for the file name
|
||||
* @param extension the file's extension
|
||||
* @param suffix the first numeric suffix to try to use, or 0 for none
|
||||
* @return the complete file name, without the directory
|
||||
*/
|
||||
private String buildUniqueFileName(File directory, String fileBaseName,
|
||||
String extension, int suffix) {
|
||||
String suffixedBaseName = fileBaseName;
|
||||
if (suffix > 0) {
|
||||
suffixedBaseName += " (" + Integer.toString(suffix) + ")";
|
||||
}
|
||||
|
||||
String fullName = suffixedBaseName + "." + extension;
|
||||
String sanitizedName = sanitizeName(fullName);
|
||||
if (!fileExists(directory, sanitizedName)) {
|
||||
return sanitizedName;
|
||||
}
|
||||
|
||||
return buildUniqueFileName(directory, fileBaseName, extension, suffix + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a file with the given name exists in the given directory.
|
||||
* This is isolated so it can be overridden in tests.
|
||||
*/
|
||||
protected boolean fileExists(File directory, String fullName) {
|
||||
File file = new File(directory, fullName);
|
||||
return file.exists();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* 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.util;
|
||||
|
||||
import com.google.android.maps.GeoPoint;
|
||||
|
||||
/**
|
||||
* A rectangle in geographical space.
|
||||
*/
|
||||
public class GeoRect {
|
||||
public int top;
|
||||
public int left;
|
||||
public int bottom;
|
||||
public int right;
|
||||
|
||||
public GeoRect() {
|
||||
top = 0;
|
||||
left = 0;
|
||||
bottom = 0;
|
||||
right = 0;
|
||||
}
|
||||
|
||||
public GeoRect(GeoPoint center, int latSpan, int longSpan) {
|
||||
top = center.getLatitudeE6() - latSpan / 2;
|
||||
left = center.getLongitudeE6() - longSpan / 2;
|
||||
bottom = center.getLatitudeE6() + latSpan / 2;
|
||||
right = center.getLongitudeE6() + longSpan / 2;
|
||||
}
|
||||
|
||||
public GeoPoint getCenter() {
|
||||
return new GeoPoint(top / 2 + bottom / 2, left / 2 + right / 2);
|
||||
}
|
||||
|
||||
public int getLatSpan() {
|
||||
return bottom - top;
|
||||
}
|
||||
|
||||
public int getLongSpan() {
|
||||
return right - left;
|
||||
}
|
||||
|
||||
public boolean contains(GeoPoint geoPoint) {
|
||||
if (geoPoint.getLatitudeE6() >= top
|
||||
&& geoPoint.getLatitudeE6() <= bottom
|
||||
&& geoPoint.getLongitudeE6() >= left
|
||||
&& geoPoint.getLongitudeE6() <= right) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
/*
|
||||
* 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.util;
|
||||
|
||||
import com.google.android.apps.mytracks.MyTracksConstants;
|
||||
import com.google.android.apps.mytracks.content.Track;
|
||||
import com.google.android.apps.mytracks.stats.TripStatistics;
|
||||
|
||||
import com.google.android.maps.GeoPoint;
|
||||
|
||||
import android.location.Location;
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Stack;
|
||||
|
||||
/**
|
||||
* Utility class for decimating tracks at a given level of precision.
|
||||
*
|
||||
* @author Leif Hendrik Wilden
|
||||
*/
|
||||
public class MyTracksUtils {
|
||||
|
||||
/**
|
||||
* Computes the distance on the two sphere between the point c0 and the line
|
||||
* segment c1 to c2.
|
||||
*
|
||||
* @param c0 the first coordinate
|
||||
* @param c1 the beginning of the line segment
|
||||
* @param c2 the end of the lone segment
|
||||
* @return the distance in m (assuming spherical earth)
|
||||
*/
|
||||
public static double distance(
|
||||
final Location c0, final Location c1, final Location c2) {
|
||||
if (c1.equals(c2)) {
|
||||
return c2.distanceTo(c0);
|
||||
}
|
||||
|
||||
final double s0lat = c0.getLatitude() * UnitConversions.TO_RADIANS;
|
||||
final double s0lng = c0.getLongitude() * UnitConversions.TO_RADIANS;
|
||||
final double s1lat = c1.getLatitude() * UnitConversions.TO_RADIANS;
|
||||
final double s1lng = c1.getLongitude() * UnitConversions.TO_RADIANS;
|
||||
final double s2lat = c2.getLatitude() * UnitConversions.TO_RADIANS;
|
||||
final double s2lng = c2.getLongitude() * UnitConversions.TO_RADIANS;
|
||||
|
||||
double s2s1lat = s2lat - s1lat;
|
||||
double s2s1lng = s2lng - s1lng;
|
||||
final double u =
|
||||
((s0lat - s1lat) * s2s1lat + (s0lng - s1lng) * s2s1lng)
|
||||
/ (s2s1lat * s2s1lat + s2s1lng * s2s1lng);
|
||||
if (u <= 0) {
|
||||
return c0.distanceTo(c1);
|
||||
}
|
||||
if (u >= 1) {
|
||||
return c0.distanceTo(c2);
|
||||
}
|
||||
Location sa = new Location("");
|
||||
sa.setLatitude(c0.getLatitude() - c1.getLatitude());
|
||||
sa.setLongitude(c0.getLongitude() - c1.getLongitude());
|
||||
Location sb = new Location("");
|
||||
sb.setLatitude(u * (c2.getLatitude() - c1.getLatitude()));
|
||||
sb.setLongitude(u * (c2.getLongitude() - c1.getLongitude()));
|
||||
return sa.distanceTo(sb);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decimates the given locations for a given zoom level. This uses a
|
||||
* Douglas-Peucker decimation algorithm.
|
||||
*
|
||||
* @param tolerance in meters
|
||||
* @param locations input
|
||||
* @param decimated output
|
||||
*/
|
||||
public static void decimate(double tolerance, ArrayList<Location> locations,
|
||||
ArrayList<Location> decimated) {
|
||||
final int n = locations.size();
|
||||
if (n < 1) {
|
||||
return;
|
||||
}
|
||||
int idx;
|
||||
int maxIdx = 0;
|
||||
Stack<int[]> stack = new Stack<int[]>();
|
||||
double[] dists = new double[n];
|
||||
dists[0] = 1;
|
||||
dists[n - 1] = 1;
|
||||
double maxDist;
|
||||
double dist = 0.0;
|
||||
int[] current;
|
||||
|
||||
if (n > 2) {
|
||||
int[] stackVal = new int[] {0, (n - 1)};
|
||||
stack.push(stackVal);
|
||||
while (stack.size() > 0) {
|
||||
current = stack.pop();
|
||||
maxDist = 0;
|
||||
for (idx = current[0] + 1; idx < current[1]; ++idx) {
|
||||
dist = MyTracksUtils.distance(
|
||||
locations.get(idx),
|
||||
locations.get(current[0]),
|
||||
locations.get(current[1]));
|
||||
if (dist > maxDist) {
|
||||
maxDist = dist;
|
||||
maxIdx = idx;
|
||||
}
|
||||
}
|
||||
if (maxDist > tolerance) {
|
||||
dists[maxIdx] = maxDist;
|
||||
int[] stackValCurMax = {current[0], maxIdx};
|
||||
stack.push(stackValCurMax);
|
||||
int[] stackValMaxCur = {maxIdx, current[1]};
|
||||
stack.push(stackValMaxCur);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int i = 0;
|
||||
idx = 0;
|
||||
decimated.clear();
|
||||
for (Location l : locations) {
|
||||
if (dists[idx] != 0) {
|
||||
decimated.add(l);
|
||||
i++;
|
||||
}
|
||||
idx++;
|
||||
}
|
||||
Log.d(MyTracksConstants.TAG, "Decimating " + n + " points to " + i
|
||||
+ " w/ tolerance = " + tolerance);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decimates the given track for the given precision.
|
||||
*
|
||||
* @param track a track
|
||||
* @param precision desired precision in meters
|
||||
*/
|
||||
public static void decimate(Track track, double precision) {
|
||||
ArrayList<Location> decimated = new ArrayList<Location>();
|
||||
decimate(precision, track.getLocations(), decimated);
|
||||
track.setLocations(decimated);
|
||||
}
|
||||
|
||||
/**
|
||||
* Limits number of points by dropping any points beyond the given number of
|
||||
* points. Note: That'll actually discard points.
|
||||
*
|
||||
* @param track a track
|
||||
* @param numberOfPoints maximum number of points
|
||||
*/
|
||||
public static void cut(Track track, int numberOfPoints) {
|
||||
ArrayList<Location> locations = track.getLocations();
|
||||
while (locations.size() > numberOfPoints) {
|
||||
locations.remove(locations.size() - 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits a track in multiple tracks where each piece has less or equal than
|
||||
* maxPoints.
|
||||
*
|
||||
* @param track the track to split
|
||||
* @param maxPoints maximum number of points for each piece
|
||||
* @return a list of one or more track pieces
|
||||
*/
|
||||
public static ArrayList<Track> split(Track track, int maxPoints) {
|
||||
ArrayList<Track> result = new ArrayList<Track>();
|
||||
final int nTotal = track.getLocations().size();
|
||||
int n = 0;
|
||||
Track piece = null;
|
||||
do {
|
||||
piece = new Track();
|
||||
TripStatistics pieceStats = piece.getStatistics();
|
||||
piece.setId(track.getId());
|
||||
piece.setName(track.getName());
|
||||
piece.setDescription(track.getDescription());
|
||||
piece.setCategory(track.getCategory());
|
||||
List<Location> pieceLocations = piece.getLocations();
|
||||
for (int i = n; i < nTotal && pieceLocations.size() < maxPoints; i++) {
|
||||
piece.addLocation(track.getLocations().get(i));
|
||||
}
|
||||
int nPointsPiece = pieceLocations.size();
|
||||
if (nPointsPiece >= 2) {
|
||||
pieceStats.setStartTime(pieceLocations.get(0).getTime());
|
||||
pieceStats.setStopTime(pieceLocations.get(nPointsPiece - 1).getTime());
|
||||
result.add(piece);
|
||||
}
|
||||
n += (pieceLocations.size() - 1);
|
||||
} while (n < nTotal && piece != null && piece.getLocations().size() > 1);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if a given GeoPoint is valid, i.e. within physical bounds.
|
||||
*
|
||||
* @param geoPoint the point to be tested
|
||||
* @return true, if it is a physical location on earth.
|
||||
*/
|
||||
public static boolean isValidGeoPoint(GeoPoint geoPoint) {
|
||||
return Math.abs(geoPoint.getLatitudeE6()) < 90E6
|
||||
&& Math.abs(geoPoint.getLongitudeE6()) <= 180E6;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a given location is a valid (i.e. physically possible) location
|
||||
* on Earth. Note: The special separator locations (which have latitude =
|
||||
* 100) will not qualify as valid. Neither will locations with lat=0 and lng=0
|
||||
* as these are most likely "bad" measurements which often cause trouble.
|
||||
*
|
||||
* @param location the location to test
|
||||
* @return true if the location is a valid location.
|
||||
*/
|
||||
public static boolean isValidLocation(Location location) {
|
||||
return location != null && Math.abs(location.getLatitude()) <= 90
|
||||
&& Math.abs(location.getLongitude()) <= 180;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a location from a GeoPoint.
|
||||
*
|
||||
* @param p a GeoPoint
|
||||
* @return the corresponding location
|
||||
*/
|
||||
public static Location getLocation(GeoPoint p) {
|
||||
Location result = new Location("");
|
||||
result.setLatitude(p.getLatitudeE6() / 1.0E6);
|
||||
result.setLongitude(p.getLongitudeE6() / 1.0E6);
|
||||
return result;
|
||||
}
|
||||
|
||||
public static GeoPoint getGeoPoint(Location location) {
|
||||
return new GeoPoint((int) (location.getLatitude() * 1E6),
|
||||
(int) (location.getLongitude() * 1E6));
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a utility class w/ only static memebers.
|
||||
*/
|
||||
protected MyTracksUtils() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* 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.util;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStream;
|
||||
|
||||
/**
|
||||
* Utility functions for android resources.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public class ResourceUtils {
|
||||
public static CharSequence readFile(Context activity, int id) {
|
||||
BufferedReader in = null;
|
||||
try {
|
||||
in = new BufferedReader(new InputStreamReader(
|
||||
activity.getResources().openRawResource(id)));
|
||||
String line;
|
||||
StringBuilder buffer = new StringBuilder();
|
||||
while ((line = in.readLine()) != null) {
|
||||
buffer.append(line).append('\n');
|
||||
}
|
||||
return buffer;
|
||||
} catch (IOException e) {
|
||||
return "";
|
||||
} finally {
|
||||
if (in != null) {
|
||||
try {
|
||||
in.close();
|
||||
} catch (IOException e) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void readBinaryFileToOutputStream(
|
||||
Context activity, int id, OutputStream os) {
|
||||
BufferedInputStream in = null;
|
||||
try {
|
||||
in = new BufferedInputStream(
|
||||
activity.getResources().openRawResource(id));
|
||||
BufferedOutputStream out = new BufferedOutputStream(os);
|
||||
int b;
|
||||
while ((b = in.read()) != -1) {
|
||||
out.write(b);
|
||||
}
|
||||
out.flush();
|
||||
} catch (IOException e) {
|
||||
return;
|
||||
} finally {
|
||||
if (in != null) {
|
||||
try {
|
||||
in.close();
|
||||
} catch (IOException e) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private ResourceUtils() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,480 @@
|
||||
/*
|
||||
* 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.util;
|
||||
|
||||
import com.google.android.apps.mytracks.MyTracksSettings;
|
||||
import com.google.android.apps.mytracks.content.DescriptionGenerator;
|
||||
import com.google.android.apps.mytracks.content.Track;
|
||||
import com.google.android.apps.mytracks.content.Waypoint;
|
||||
import com.google.android.apps.mytracks.stats.TripStatistics;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
import java.text.ParsePosition;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.SimpleTimeZone;
|
||||
import java.util.Vector;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Various string manipulation methods.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
* @author Rodrigo Damazio
|
||||
*/
|
||||
public class StringUtils implements DescriptionGenerator {
|
||||
|
||||
private final Context context;
|
||||
|
||||
/**
|
||||
* Formats a number of milliseconds as a string.
|
||||
*
|
||||
* @param time - A period of time in milliseconds.
|
||||
* @return A string of the format M:SS, MM:SS or HH:MM:SS
|
||||
*/
|
||||
public static String formatTime(long time) {
|
||||
return formatTimeInternal(time, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a number of milliseconds as a string. To be used when we need the
|
||||
* hours to be shown even when it is zero, e.g. exporting data to a
|
||||
* spreadsheet.
|
||||
*
|
||||
* @param time - A period of time in milliseconds
|
||||
* @return A string of the format HH:MM:SS even if time is less than 1 hour
|
||||
*/
|
||||
public static String formatTimeAlwaysShowingHours(long time) {
|
||||
return formatTimeInternal(time, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the given text as a CDATA element to be used in a XML file. This
|
||||
* includes adding the starting and ending CDATA tags. Please notice that this
|
||||
* may result in multiple consecutive CDATA tags.
|
||||
*
|
||||
* @param unescaped the unescaped text to be formatted
|
||||
* @return the formatted text, inside one or more CDATA tags
|
||||
*/
|
||||
public static String stringAsCData(String unescaped) {
|
||||
// "]]>" needs to be broken into multiple CDATA segments, like:
|
||||
// "Foo]]>Bar" becomes "<![CDATA[Foo]]]]><![CDATA[>Bar]]>"
|
||||
// (the end of the first CDATA has the "]]", the other has ">")
|
||||
String escaped = unescaped.replaceAll("]]>", "]]]]><![CDATA[>");
|
||||
return "<![CDATA[" + escaped + "]]>";
|
||||
}
|
||||
|
||||
private static final SimpleDateFormat BASE_XML_DATE_FORMAT =
|
||||
new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
|
||||
static {
|
||||
BASE_XML_DATE_FORMAT.setTimeZone(new SimpleTimeZone(0, "UTC"));
|
||||
}
|
||||
private static final Pattern XML_DATE_EXTRAS_PATTERN =
|
||||
Pattern.compile("^(\\.\\d+)?(?:Z|([+-])(\\d{2}):(\\d{2}))?$");
|
||||
|
||||
/**
|
||||
* Parses an XML dateTime element as defined by the XML standard.
|
||||
*
|
||||
* @see <a href="http://www.w3.org/TR/xmlschema-2/#dateTime">dateTime</a>
|
||||
*/
|
||||
public static long parseXmlDateTime(String xmlTime) {
|
||||
// Parse the base date (fixed format)
|
||||
ParsePosition position = new ParsePosition(0);
|
||||
Date date = BASE_XML_DATE_FORMAT.parse(xmlTime, position);
|
||||
if (date == null) {
|
||||
throw new IllegalArgumentException("Invalid XML dateTime value: '" + xmlTime
|
||||
+ "' (at position " + position.getErrorIndex() + ")");
|
||||
}
|
||||
|
||||
// Parse the extras
|
||||
Matcher matcher =
|
||||
XML_DATE_EXTRAS_PATTERN.matcher(xmlTime.substring(position.getIndex()));
|
||||
if (!matcher.matches()) {
|
||||
// This will match even an empty string as all groups are optional,
|
||||
// so a non-match means some other garbage was there
|
||||
throw new IllegalArgumentException("Invalid XML dateTime value: " + xmlTime);
|
||||
}
|
||||
|
||||
long time = date.getTime();
|
||||
|
||||
// Account for fractional seconds
|
||||
String fractional = matcher.group(1);
|
||||
if (fractional != null) {
|
||||
// Regex ensures fractional part is in (0,1(
|
||||
float fractionalSeconds = Float.parseFloat(fractional);
|
||||
long fractionalMillis = (long) (fractionalSeconds * 1000.0f);
|
||||
time += fractionalMillis;
|
||||
}
|
||||
|
||||
// Account for timezones
|
||||
String sign = matcher.group(2);
|
||||
String offsetHoursStr = matcher.group(3);
|
||||
String offsetMinsStr = matcher.group(4);
|
||||
if (sign != null && offsetHoursStr != null && offsetMinsStr != null) {
|
||||
// Regex ensures sign is + or -
|
||||
boolean plusSign = sign.equals("+");
|
||||
int offsetHours = Integer.parseInt(offsetHoursStr);
|
||||
int offsetMins = Integer.parseInt(offsetMinsStr);
|
||||
|
||||
// Regex ensures values are >= 0
|
||||
if (offsetHours > 14 || offsetMins > 59) {
|
||||
throw new IllegalArgumentException("Bad timezone in " + xmlTime);
|
||||
}
|
||||
|
||||
long totalOffsetMillis = (offsetMins + offsetHours * 60L) * 60000L;
|
||||
|
||||
// Make time go back to UTC
|
||||
if (plusSign) {
|
||||
time -= totalOffsetMillis;
|
||||
} else {
|
||||
time += totalOffsetMillis;
|
||||
}
|
||||
}
|
||||
|
||||
return time;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a number of milliseconds as a string.
|
||||
*
|
||||
* @param time - A period of time in milliseconds
|
||||
* @param alwaysShowHours - Whether to display 00 hours if time is less than 1
|
||||
* hour
|
||||
* @return A string of the format HH:MM:SS
|
||||
*/
|
||||
private static String formatTimeInternal(long time, boolean alwaysShowHours) {
|
||||
int[] parts = getTimeParts(time);
|
||||
StringBuilder builder = new StringBuilder();
|
||||
if (parts[2] > 0 || alwaysShowHours) {
|
||||
builder.append(parts[2]);
|
||||
builder.append(':');
|
||||
if (parts[1] <= 9) {
|
||||
builder.append("0");
|
||||
}
|
||||
}
|
||||
|
||||
builder.append(parts[1]);
|
||||
builder.append(':');
|
||||
if (parts[0] <= 9) {
|
||||
builder.append("0");
|
||||
}
|
||||
builder.append(parts[0]);
|
||||
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the time as an array of parts.
|
||||
*/
|
||||
public static int[] getTimeParts(long time) {
|
||||
if (time < 0) {
|
||||
int[] parts = getTimeParts(time * -1);
|
||||
parts[0] *= -1;
|
||||
parts[1] *= -1;
|
||||
parts[2] *= -1;
|
||||
return parts;
|
||||
}
|
||||
int[] parts = new int[3];
|
||||
|
||||
long seconds = time / 1000;
|
||||
parts[0] = (int) (seconds % 60);
|
||||
int tmp = (int) (seconds / 60);
|
||||
parts[1] = tmp % 60;
|
||||
parts[2] = tmp / 60;
|
||||
|
||||
return parts;
|
||||
}
|
||||
|
||||
public StringUtils(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
public String formatTimeLong(long time) {
|
||||
int[] parts = getTimeParts(time);
|
||||
String secLabel =
|
||||
context.getString(parts[0] == 1 ? R.string.second : R.string.seconds);
|
||||
String minLabel =
|
||||
context.getString(parts[1] == 1 ? R.string.minute : R.string.minutes);
|
||||
String hourLabel =
|
||||
context.getString(parts[2] == 1 ? R.string.hour : R.string.hours);
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
if (parts[2] != 0) {
|
||||
sb.append(parts[2]);
|
||||
sb.append(" ");
|
||||
sb.append(hourLabel);
|
||||
sb.append(" ");
|
||||
sb.append(parts[1]);
|
||||
sb.append(minLabel);
|
||||
} else {
|
||||
sb.append(parts[1]);
|
||||
sb.append(" ");
|
||||
sb.append(minLabel);
|
||||
sb.append(" ");
|
||||
sb.append(parts[0]);
|
||||
sb.append(secLabel);
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a description for a track (with information about the
|
||||
* statistics).
|
||||
*
|
||||
* @param track the track
|
||||
* @return a track description
|
||||
*/
|
||||
public String generateTrackDescription(Track track, Vector<Double> distances,
|
||||
Vector<Double> elevations) {
|
||||
boolean displaySpeed = true;
|
||||
SharedPreferences preferences =
|
||||
context.getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0);
|
||||
if (preferences != null) {
|
||||
displaySpeed =
|
||||
preferences.getBoolean(context.getString(R.string.report_speed_key), true);
|
||||
}
|
||||
|
||||
TripStatistics trackStats = track.getStatistics();
|
||||
final double distanceInKm = trackStats.getTotalDistance() / 1000;
|
||||
final double distanceInMiles = distanceInKm * UnitConversions.KM_TO_MI;
|
||||
final long minElevationInMeters = Math.round(trackStats.getMinElevation());
|
||||
final long minElevationInFeet =
|
||||
Math.round(trackStats.getMinElevation() * UnitConversions.M_TO_FT);
|
||||
final long maxElevationInMeters = Math.round(trackStats.getMaxElevation());
|
||||
final long maxElevationInFeet =
|
||||
Math.round(trackStats.getMaxElevation() * UnitConversions.M_TO_FT);
|
||||
final long elevationGainInMeters =
|
||||
Math.round(trackStats.getTotalElevationGain());
|
||||
final long elevationGainInFeet = Math.round(
|
||||
trackStats.getTotalElevationGain() * UnitConversions.M_TO_FT);
|
||||
|
||||
long minGrade = 0;
|
||||
long maxGrade = 0;
|
||||
double trackMaxGrade = trackStats.getMaxGrade();
|
||||
double trackMinGrade = trackStats.getMinGrade();
|
||||
if (!Double.isNaN(trackMaxGrade)
|
||||
&& !Double.isInfinite(trackMaxGrade)) {
|
||||
maxGrade = Math.round(trackMaxGrade * 100);
|
||||
}
|
||||
if (!Double.isNaN(trackMinGrade) && !Double.isInfinite(trackMinGrade)) {
|
||||
minGrade = Math.round(trackMinGrade * 100);
|
||||
}
|
||||
|
||||
String category = context.getString(R.string.unknown);
|
||||
String trackCategory = track.getCategory();
|
||||
if (trackCategory != null && trackCategory.length() > 0) {
|
||||
category = trackCategory;
|
||||
}
|
||||
|
||||
String averageSpeed =
|
||||
getSpeedString(trackStats.getAverageSpeed(),
|
||||
R.string.average_speed_label,
|
||||
R.string.average_pace_label,
|
||||
displaySpeed);
|
||||
|
||||
String averageMovingSpeed =
|
||||
getSpeedString(trackStats.getAverageMovingSpeed(),
|
||||
R.string.average_moving_speed_label,
|
||||
R.string.average_moving_pace_label,
|
||||
displaySpeed);
|
||||
|
||||
String maxSpeed =
|
||||
getSpeedString(trackStats.getMaxSpeed(),
|
||||
R.string.max_speed_label,
|
||||
R.string.min_pace_label,
|
||||
displaySpeed);
|
||||
|
||||
return String.format("%s<p>"
|
||||
+ "%s: %.2f %s (%.1f %s)<br>"
|
||||
+ "%s: %s<br>"
|
||||
+ "%s: %s<br>"
|
||||
+ "%s %s %s"
|
||||
+ "%s: %d %s (%d %s)<br>"
|
||||
+ "%s: %d %s (%d %s)<br>"
|
||||
+ "%s: %d %s (%d %s)<br>"
|
||||
+ "%s: %d %%<br>"
|
||||
+ "%s: %d %%<br>"
|
||||
+ "%s: %tc<br>"
|
||||
+ "%s: %s<br>"
|
||||
+ "<img border=\"0\" src=\"%s\"/>",
|
||||
|
||||
// Line 1
|
||||
context.getString(R.string.fusiontable_link),
|
||||
|
||||
// Line 2
|
||||
context.getString(R.string.total_distance_label),
|
||||
distanceInKm, context.getString(R.string.kilometer),
|
||||
distanceInMiles, context.getString(R.string.mile),
|
||||
|
||||
// Line 3
|
||||
context.getString(R.string.total_time_label),
|
||||
StringUtils.formatTime(trackStats.getTotalTime()),
|
||||
|
||||
// Line 4
|
||||
context.getString(R.string.moving_time_label),
|
||||
StringUtils.formatTime(trackStats.getMovingTime()),
|
||||
|
||||
// Line 5
|
||||
averageSpeed, averageMovingSpeed, maxSpeed,
|
||||
|
||||
// Line 6
|
||||
context.getString(R.string.min_elevation_label),
|
||||
minElevationInMeters, context.getString(R.string.meter),
|
||||
minElevationInFeet, context.getString(R.string.feet),
|
||||
|
||||
// Line 7
|
||||
context.getString(R.string.max_elevation_label),
|
||||
maxElevationInMeters, context.getString(R.string.meter),
|
||||
maxElevationInFeet, context.getString(R.string.feet),
|
||||
|
||||
// Line 8
|
||||
context.getString(R.string.elevation_gain_label),
|
||||
elevationGainInMeters, context.getString(R.string.meter),
|
||||
elevationGainInFeet, context.getString(R.string.feet),
|
||||
|
||||
// Line 9
|
||||
context.getString(R.string.max_grade_label), maxGrade,
|
||||
|
||||
// Line 10
|
||||
context.getString(R.string.min_grade_label), minGrade,
|
||||
|
||||
// Line 11
|
||||
context.getString(R.string.recorded_date),
|
||||
new Date(trackStats.getStartTime()),
|
||||
|
||||
// Line 12
|
||||
context.getString(R.string.category), category,
|
||||
|
||||
// Line 13
|
||||
ChartURLGenerator.getChartUrl(distances, elevations, track, context));
|
||||
}
|
||||
|
||||
private String getSpeedString(double speed, int speedLabel, int paceLabel,
|
||||
boolean displaySpeed) {
|
||||
double speedInKph = speed * 3.6;
|
||||
double speedInMph = speedInKph * UnitConversions.KMH_TO_MPH;
|
||||
if (displaySpeed) {
|
||||
return String.format("%s: %.2f %s (%.1f %s)<br>",
|
||||
context.getString(speedLabel),
|
||||
speedInKph, context.getString(R.string.kilometer_per_hour),
|
||||
speedInMph, context.getString(R.string.mile_per_hour));
|
||||
} else {
|
||||
double paceInKm;
|
||||
double paceInMi;
|
||||
if (speed == 0) {
|
||||
paceInKm = 0.0;
|
||||
paceInMi = 0.0;
|
||||
} else {
|
||||
paceInKm = 60.0 / speedInKph;
|
||||
paceInMi = 60.0 / speedInMph;
|
||||
}
|
||||
return String.format("%s: %.2f %s (%.1f %s)<br>",
|
||||
context.getString(paceLabel),
|
||||
paceInKm, context.getString(R.string.min_per_kilometer),
|
||||
paceInMi, context.getString(R.string.min_per_mile));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a description for a waypoint (with information about the
|
||||
* statistics).
|
||||
*
|
||||
* @return a track description
|
||||
*/
|
||||
public String generateWaypointDescription(Waypoint waypoint) {
|
||||
TripStatistics stats = waypoint.getStatistics();
|
||||
|
||||
final double distanceInKm = stats.getTotalDistance() / 1000;
|
||||
final double distanceInMiles = distanceInKm * UnitConversions.KM_TO_MI;
|
||||
final double averageSpeedInKmh = stats.getAverageSpeed() * 3.6;
|
||||
final double averageSpeedInMph =
|
||||
averageSpeedInKmh * UnitConversions.KMH_TO_MPH;
|
||||
final double movingSpeedInKmh = stats.getAverageMovingSpeed() * 3.6;
|
||||
final double movingSpeedInMph =
|
||||
movingSpeedInKmh * UnitConversions.KMH_TO_MPH;
|
||||
final double maxSpeedInKmh = stats.getMaxSpeed() * 3.6;
|
||||
final double maxSpeedInMph = maxSpeedInKmh * UnitConversions.KMH_TO_MPH;
|
||||
final long minElevationInMeters = Math.round(stats.getMinElevation());
|
||||
final long minElevationInFeet =
|
||||
Math.round(stats.getMinElevation() * UnitConversions.M_TO_FT);
|
||||
final long maxElevationInMeters = Math.round(stats.getMaxElevation());
|
||||
final long maxElevationInFeet =
|
||||
Math.round(stats.getMaxElevation() * UnitConversions.M_TO_FT);
|
||||
final long elevationGainInMeters =
|
||||
Math.round(stats.getTotalElevationGain());
|
||||
final long elevationGainInFeet = Math.round(
|
||||
stats.getTotalElevationGain() * UnitConversions.M_TO_FT);
|
||||
long theMinGrade = 0;
|
||||
long theMaxGrade = 0;
|
||||
double maxGrade = stats.getMaxGrade();
|
||||
double minGrade = stats.getMinGrade();
|
||||
if (!Double.isNaN(maxGrade) &&
|
||||
!Double.isInfinite(maxGrade)) {
|
||||
theMaxGrade = Math.round(maxGrade * 100);
|
||||
}
|
||||
if (!Double.isNaN(minGrade) &&
|
||||
!Double.isInfinite(minGrade)) {
|
||||
theMinGrade = Math.round(minGrade * 100);
|
||||
}
|
||||
final String percent = "%";
|
||||
|
||||
return String.format(
|
||||
"%s: %.2f %s (%.1f %s)\n"
|
||||
+ "%s: %s\n"
|
||||
+ "%s: %s\n"
|
||||
+ "%s: %.2f %s (%.1f %s)\n"
|
||||
+ "%s: %.2f %s (%.1f %s)\n"
|
||||
+ "%s: %.2f %s (%.1f %s)\n"
|
||||
+ "%s: %d %s (%d %s)\n"
|
||||
+ "%s: %d %s (%d %s)\n"
|
||||
+ "%s: %d %s (%d %s)\n"
|
||||
+ "%s: %d %s\n"
|
||||
+ "%s: %d %s\n",
|
||||
context.getString(R.string.distance_label),
|
||||
distanceInKm, context.getString(R.string.kilometer),
|
||||
distanceInMiles, context.getString(R.string.mile),
|
||||
context.getString(R.string.time_label),
|
||||
StringUtils.formatTime(stats.getTotalTime()),
|
||||
context.getString(R.string.moving_time_label),
|
||||
StringUtils.formatTime(stats.getMovingTime()),
|
||||
context.getString(R.string.average_speed_label),
|
||||
averageSpeedInKmh, context.getString(R.string.kilometer_per_hour),
|
||||
averageSpeedInMph, context.getString(R.string.mile_per_hour),
|
||||
context.getString(R.string.average_moving_speed_label),
|
||||
movingSpeedInKmh, context.getString(R.string.kilometer_per_hour),
|
||||
movingSpeedInMph, context.getString(R.string.mile_per_hour),
|
||||
context.getString(R.string.max_speed_label),
|
||||
maxSpeedInKmh, context.getString(R.string.kilometer_per_hour),
|
||||
maxSpeedInMph, context.getString(R.string.mile_per_hour),
|
||||
context.getString(R.string.min_elevation_label),
|
||||
minElevationInMeters, context.getString(R.string.meter),
|
||||
minElevationInFeet, context.getString(R.string.feet),
|
||||
context.getString(R.string.max_elevation_label),
|
||||
maxElevationInMeters, context.getString(R.string.meter),
|
||||
maxElevationInFeet, context.getString(R.string.feet),
|
||||
context.getString(R.string.elevation_gain_label),
|
||||
elevationGainInMeters, context.getString(R.string.meter),
|
||||
elevationGainInFeet, context.getString(R.string.feet),
|
||||
context.getString(R.string.max_grade_label),
|
||||
theMaxGrade, percent,
|
||||
context.getString(R.string.min_grade_label),
|
||||
theMinGrade, percent);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* 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.util;
|
||||
|
||||
/**
|
||||
* Unit conversion constants.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public abstract class UnitConversions {
|
||||
|
||||
public static final double KM_TO_MI = 0.621371192;
|
||||
public static final double M_TO_FT = 3.2808399;
|
||||
public static final double MI_TO_M = 1609.344;
|
||||
public static final double MI_TO_FEET = 5280.0;
|
||||
public static final double KMH_TO_MPH = 1000 * M_TO_FT / MI_TO_FEET;
|
||||
public static final double TO_RADIANS = Math.PI / 180.0;
|
||||
|
||||
protected UnitConversions() {
|
||||
}
|
||||
}
|
||||
@@ -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.wireless.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. */
|
||||
private 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.wireless.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. */
|
||||
private static final String SERVICE = "wise";
|
||||
|
||||
/** Standard base feed url for spreadsheets. */
|
||||
public static final String SPREADSHEETS_BASE_FEED_URL =
|
||||
"http://spreadsheets.google.com/feeds/spreadsheets/private/full";
|
||||
|
||||
/**
|
||||
* Represents an entry in a GData Spreadsheets meta-feed.
|
||||
*/
|
||||
public static class SpreadsheetEntry extends Entry { }
|
||||
|
||||
/**
|
||||
* Represents an entry in a GData Worksheets meta-feed.
|
||||
*/
|
||||
public static class WorksheetEntry extends Entry { }
|
||||
|
||||
/**
|
||||
* Creates a new SpreadsheetsClient. Uses the standard base URL for
|
||||
* spreadsheets feeds.
|
||||
*
|
||||
* @param client The GDataClient that should be used to authenticate requests,
|
||||
* retrieve feeds, etc
|
||||
*/
|
||||
public SpreadsheetsClient(GDataClient client,
|
||||
GDataParserFactory spreadsheetFactory) {
|
||||
super(client, spreadsheetFactory);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getServiceName() {
|
||||
return SERVICE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a parser for the specified feed type.
|
||||
*
|
||||
* @param feedEntryClass the Class of entry type that will be parsed, which
|
||||
* lets this method figure out which parser to create
|
||||
* @param feedUri the URI of the feed to be fetched and parsed
|
||||
* @param authToken the current authToken to use for the request
|
||||
* @return a parser for the indicated feed
|
||||
* @throws AuthenticationException if the authToken is not valid
|
||||
* @throws ParseException if the response from the server could not be parsed
|
||||
*/
|
||||
private GDataParser getParserForTypedFeed(
|
||||
Class<? extends Entry> feedEntryClass, String feedUri, String authToken)
|
||||
throws AuthenticationException, ParseException, IOException {
|
||||
GDataClient gDataClient = getGDataClient();
|
||||
GDataParserFactory gDataParserFactory = getGDataParserFactory();
|
||||
|
||||
try {
|
||||
InputStream is = gDataClient.getFeedAsStream(feedUri, authToken);
|
||||
return gDataParserFactory.createParser(feedEntryClass, is);
|
||||
} catch (HttpException e) {
|
||||
convertHttpExceptionForReads("Could not fetch parser feed.", e);
|
||||
return null; // never reached
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an HTTP exception that happened while reading into the equivalent
|
||||
* local exception.
|
||||
*/
|
||||
public void convertHttpExceptionForReads(String message, HttpException cause)
|
||||
throws AuthenticationException, IOException {
|
||||
switch (cause.getStatusCode()) {
|
||||
case HttpException.SC_FORBIDDEN:
|
||||
case HttpException.SC_UNAUTHORIZED:
|
||||
throw new AuthenticationException(message, cause);
|
||||
case HttpException.SC_GONE:
|
||||
default:
|
||||
throw new IOException(message + ": " + cause.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Entry createEntry(String feedUri, String authToken, Entry entry)
|
||||
throws ParseException, IOException {
|
||||
|
||||
GDataParserFactory factory = getGDataParserFactory();
|
||||
GDataSerializer serializer = factory.createSerializer(entry);
|
||||
|
||||
InputStream is;
|
||||
try {
|
||||
is = getGDataClient().createEntry(feedUri, authToken, serializer);
|
||||
} catch (HttpException e) {
|
||||
convertHttpExceptionForWrites(entry.getClass(),
|
||||
"Could not update entry.", e);
|
||||
return null; // never reached.
|
||||
}
|
||||
|
||||
GDataParser parser = factory.createParser(entry.getClass(), is);
|
||||
try {
|
||||
return parser.parseStandaloneEntry();
|
||||
} finally {
|
||||
parser.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches a GDataParser for the indicated feed. The parser can be used to
|
||||
* access the contents of URI. WARNING: because we cannot reliably infer the
|
||||
* feed type from the URI alone, this method assumes the default feed type!
|
||||
* This is probably NOT what you want. Please use the getParserFor[Type]Feed
|
||||
* methods.
|
||||
*
|
||||
* @param feedEntryClass
|
||||
* @param feedUri the URI of the feed to be fetched and parsed
|
||||
* @param authToken the current authToken to use for the request
|
||||
* @return a parser for the indicated feed
|
||||
* @throws ParseException if the response from the server could not be parsed
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public GDataParser getParserForFeed(
|
||||
Class feedEntryClass, String feedUri, String authToken)
|
||||
throws ParseException, IOException {
|
||||
try {
|
||||
return getParserForTypedFeed(SpreadsheetEntry.class, feedUri, authToken);
|
||||
} catch (AuthenticationException e) {
|
||||
throw new IOException("Authentication Failure: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a parser for a Worksheets meta-feed.
|
||||
*
|
||||
* @param feedUri the URI of the feed to be fetched and parsed
|
||||
* @param authToken the current authToken to use for the request
|
||||
* @return a parser for the indicated feed
|
||||
* @throws AuthenticationException if the authToken is not valid
|
||||
* @throws ParseException if the response from the server could not be parsed
|
||||
*/
|
||||
public GDataParser getParserForWorksheetsFeed(
|
||||
String feedUri, String authToken)
|
||||
throws AuthenticationException, ParseException, IOException {
|
||||
return getParserForTypedFeed(WorksheetEntry.class, feedUri, authToken);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates an entry. The URI to be updated is taken from <code>entry</code>.
|
||||
* Note that only entries in List and Cells feeds can be updated, so
|
||||
* <code>entry</code> must be of the corresponding type; other types will
|
||||
* result in an exception.
|
||||
*
|
||||
* @param entry the entry to be updated; must include its URI
|
||||
* @param authToken the current authToken to be used for the operation
|
||||
* @return An Entry containing the re-parsed version of the entry returned by
|
||||
* the server in response to the update
|
||||
* @throws ParseException if the server returned an error, if the server's
|
||||
* response was unparseable (unlikely), or if <code>entry</code> is of
|
||||
* a read-only type
|
||||
* @throws IOException on network error
|
||||
*/
|
||||
@Override
|
||||
public Entry updateEntry(Entry entry, String authToken)
|
||||
throws ParseException, IOException {
|
||||
GDataParserFactory factory = getGDataParserFactory();
|
||||
GDataSerializer serializer = factory.createSerializer(entry);
|
||||
|
||||
String editUri = entry.getEditUri();
|
||||
if (StringUtils.isEmpty(editUri)) {
|
||||
throw new ParseException("No edit URI -- cannot update.");
|
||||
}
|
||||
|
||||
InputStream is;
|
||||
try {
|
||||
is = getGDataClient().updateEntry(editUri, authToken, serializer);
|
||||
} catch (HttpException e) {
|
||||
convertHttpExceptionForWrites(entry.getClass(),
|
||||
"Could not update entry.", e);
|
||||
return null; // never reached
|
||||
}
|
||||
|
||||
GDataParser parser = factory.createParser(entry.getClass(), is);
|
||||
try {
|
||||
return parser.parseStandaloneEntry();
|
||||
} finally {
|
||||
parser.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an HTTP exception which happened while writing to the equivalent
|
||||
* local exception.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private void convertHttpExceptionForWrites(
|
||||
Class entryClass, String message, HttpException cause)
|
||||
throws ParseException, IOException {
|
||||
switch (cause.getStatusCode()) {
|
||||
case HttpException.SC_CONFLICT:
|
||||
if (entryClass != null) {
|
||||
InputStream is = cause.getResponseStream();
|
||||
if (is != null) {
|
||||
parseEntry(entryClass, cause.getResponseStream());
|
||||
}
|
||||
}
|
||||
throw new IOException(message);
|
||||
case HttpException.SC_BAD_REQUEST:
|
||||
throw new ParseException(message + ": " + cause);
|
||||
case HttpException.SC_FORBIDDEN:
|
||||
case HttpException.SC_UNAUTHORIZED:
|
||||
throw new IOException(message);
|
||||
default:
|
||||
throw new IOException(message + ": " + cause.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses one entry from the input stream.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private Entry parseEntry(Class entryClass, InputStream is)
|
||||
throws ParseException, IOException {
|
||||
GDataParser parser = null;
|
||||
try {
|
||||
parser = getGDataParserFactory().createParser(entryClass, is);
|
||||
return parser.parseStandaloneEntry();
|
||||
} finally {
|
||||
if (parser != null) {
|
||||
parser.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* 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.wireless.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,113 @@
|
||||
/*
|
||||
* Copyright 2010 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package com.google.wireless.gdata.maps;
|
||||
|
||||
import com.google.wireless.gdata.client.GDataClient;
|
||||
import com.google.wireless.gdata.client.GDataParserFactory;
|
||||
import com.google.wireless.gdata.client.GDataServiceClient;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
/**
|
||||
* Client to talk to Google Maps via GData.
|
||||
*/
|
||||
public class MapsClient extends GDataServiceClient {
|
||||
public static final String SERVICE = "local";
|
||||
|
||||
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 SERVICE;
|
||||
}
|
||||
|
||||
public static String getMapsFeed() {
|
||||
if (DEBUG) {
|
||||
Log.d("Maps Client", "Requesting map feed:");
|
||||
}
|
||||
return MAPS_BASE_FEED_URL + MAPS_MAP_FEED_PATH + V2_ONLY_PARAM;
|
||||
}
|
||||
|
||||
public static String getFeaturesFeed(String mapid) {
|
||||
StringBuilder feed = new StringBuilder();
|
||||
feed.append(MAPS_BASE_FEED_URL);
|
||||
feed.append(MAPS_FEATURE_FEED_PATH_BEFORE_MAPID);
|
||||
feed.append(mapid);
|
||||
feed.append(MAPS_FEATURE_FEED_PATH_AFTER_MAPID);
|
||||
feed.append(V2_ONLY_PARAM);
|
||||
return feed.toString();
|
||||
}
|
||||
|
||||
public static String getMapIdFromMapEntryId(String entryId) {
|
||||
String userId = null;
|
||||
String mapId = null;
|
||||
if (DEBUG) {
|
||||
Log.d("Maps GData Client", "Getting mapid from entry id: " + entryId);
|
||||
}
|
||||
int userIdStart =
|
||||
entryId.indexOf(MAP_ENTRY_ID_BEFORE_USER_ID)
|
||||
+ MAP_ENTRY_ID_BEFORE_USER_ID.length();
|
||||
int userIdEnd =
|
||||
entryId.indexOf(MAP_ENTRY_ID_BETWEEN_USER_ID_AND_MAP_ID, userIdStart);
|
||||
if (userIdStart >= 0 && userIdEnd < entryId.length()
|
||||
&& userIdStart <= userIdEnd) {
|
||||
userId = entryId.substring(userIdStart, userIdEnd);
|
||||
}
|
||||
int mapIdStart =
|
||||
entryId.indexOf(MAP_ENTRY_ID_BETWEEN_USER_ID_AND_MAP_ID, userIdEnd)
|
||||
+ MAP_ENTRY_ID_BETWEEN_USER_ID_AND_MAP_ID.length();
|
||||
if (mapIdStart >= 0 && mapIdStart < entryId.length()) {
|
||||
mapId = entryId.substring(mapIdStart);
|
||||
}
|
||||
if (userId == null) {
|
||||
userId = "";
|
||||
}
|
||||
if (mapId == null) {
|
||||
mapId = "";
|
||||
}
|
||||
if (DEBUG) {
|
||||
Log.d("Maps GData Client", "Got user id: " + userId);
|
||||
Log.d("Maps GData Client", "Got map id: " + mapId);
|
||||
}
|
||||
return userId + "." + mapId;
|
||||
}
|
||||
|
||||
public static String getVersionFeed(String versionUserId,
|
||||
String versionClient, String currentVersion) {
|
||||
return String.format(MAPS_VERSION_FEED_PATH_FORMAT,
|
||||
MAPS_BASE_FEED_URL, versionUserId,
|
||||
versionClient, currentVersion);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
/*
|
||||
* 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.wireless.gdata.maps.xml;
|
||||
|
||||
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.maps.MapFeatureEntry;
|
||||
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.
|
||||
*/
|
||||
public 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 <entry>.
|
||||
*
|
||||
* @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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user