Merging gingerbread changes to head.

This commit is contained in:
Rodrigo Damazio
2010-12-10 14:30:29 -02:00
parent 93b95954c4
commit 19d5fe1ba6
111 changed files with 5041 additions and 837 deletions
@@ -16,11 +16,14 @@
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;
@@ -178,12 +181,12 @@ public class ChartActivity extends Activity implements
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 Location("");
Location location = new MyTracksLocation("");
do {
lastSeenLocationId = cursor.getLong(idColumnIdx);
providerUtils.fillLocation(cursor, location);
if (MyTracksUtils.isValidLocation(location)) {
double[] point = new double[3];
double[] point = new double[6];
location = getDataPoint(location, track, point);
data.add(point);
}
@@ -464,10 +467,14 @@ public class ChartActivity extends Activity implements
/**
* Given a location, creates a new data point for the chart. A data point is
* an array double[2], where:
* 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.
*
@@ -477,6 +484,35 @@ public class ChartActivity extends Activity implements
* @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;
@@ -529,7 +565,7 @@ public class ChartActivity extends Activity implements
if (oldLastLocation == null) {
// No previous location, but return a blank one for reuse
return new Location("");
return new MyTracksLocation("");
}
return oldLastLocation;
@@ -576,7 +612,7 @@ public class ChartActivity extends Activity implements
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 Location("");
Location location = new MyTracksLocation("");
while (lastLocationRead < track.getStopId()) {
cursor = providerUtils.getLocationsCursor(
selectedTrackId, lastLocationRead, bufferSize, false);
@@ -592,7 +628,8 @@ public class ChartActivity extends Activity implements
if (MyTracksUtils.isValidLocation(location)) {
lastLocationRead = lastSeenLocationId =
cursor.getLong(idColumnIdx);
double[] point = new double[3];
// 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);
@@ -35,8 +35,7 @@ import android.widget.RadioGroup;
*/
public class ChartSettingsDialog extends Dialog {
private RadioButton distance;
private CheckBox elevation;
private CheckBox speed;
private CheckBox[] series;
public ChartSettingsDialog(Context context) {
super(context);
@@ -61,8 +60,18 @@ public class ChartSettingsDialog extends Dialog {
});
distance = (RadioButton) findViewById(R.id.chart_settings_by_distance);
elevation = (CheckBox) findViewById(R.id.chart_settings_elevation);
speed = (CheckBox) findViewById(R.id.chart_settings_speed);
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) {
@@ -74,8 +83,9 @@ public class ChartSettingsDialog extends Dialog {
rd.check(chart.getMode() == Mode.BY_DISTANCE
? R.id.chart_settings_by_distance
: R.id.chart_settings_by_time);
elevation.setChecked(chart.isSeriesEnabled(ChartView.ELEVATION_SERIES));
speed.setChecked(chart.isSeriesEnabled(ChartView.SPEED_SERIES));
for (int i = 0; i < ChartView.NUM_SERIES; i++) {
series[i].setChecked(chart.isSeriesEnabled(i));
}
}
private void handleOk() {
@@ -83,8 +93,9 @@ public class ChartSettingsDialog extends Dialog {
chart.setMode(distance.isChecked() ? Mode.BY_DISTANCE : Mode.BY_TIME);
// TODO: check that something is visible.
chart.setSeriesEnabled(ChartView.ELEVATION_SERIES, elevation.isChecked());
chart.setSeriesEnabled(ChartView.SPEED_SERIES, speed.isChecked());
for (int i = 0; i < ChartView.NUM_SERIES; i++) {
chart.setSeriesEnabled(i, series[i].isChecked());
}
dismiss();
}
@@ -42,6 +42,7 @@ public class ChartValueSeries {
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;
@@ -131,7 +132,7 @@ public class ChartValueSeries {
if (min < 0) {
effectiveMin -= rounding;
}
spread = effectiveMax - effectiveMin;
spread = getMax() - effectiveMin;
}
/**
@@ -139,7 +140,7 @@ public class ChartValueSeries {
*/
public int getMaxLabelLength() {
String minS = format.format(effectiveMin);
String maxS = format.format(effectiveMax);
String maxS = format.format(getMax());
return Math.max(minS.length(), maxS.length());
}
@@ -151,10 +152,16 @@ public class ChartValueSeries {
}
/**
* @return The rounded up maximum value
* @return The minimum of the rounded up max value and the effective max.
*/
public int getMax() {
return effectiveMax;
// 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;
}
}
/**
@@ -211,4 +218,16 @@ public class ChartValueSeries {
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();
}
}
@@ -87,7 +87,10 @@ public class ChartView extends View {
*/
public static final int ELEVATION_SERIES = 0;
public static final int SPEED_SERIES = 1;
public static final int NUM_SERIES = 2;
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();
@@ -172,38 +175,44 @@ public class ChartView extends View {
series = new ChartValueSeries[NUM_SERIES];
// Create the paints
Paint fillPaint1 = new Paint();
fillPaint1.setStyle(Style.FILL);
fillPaint1.setColor(context.getResources().getColor(R.color.green));
fillPaint1.setAntiAlias(true);
Paint fillPaint2 = new Paint();
fillPaint2.setStyle(Style.FILL);
fillPaint2.setColor(
context.getResources().getColor(R.color.blue_transparent));
fillPaint2.setAntiAlias(true);
Paint strokePaint = new Paint();
strokePaint.setStyle(Style.STROKE);
strokePaint.setColor(context.getResources().getColor(R.color.blue));
strokePaint.setAntiAlias(true);
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,
"###,###",
fillPaint1,
fillPaint,
null,
100,
R.string.elevation);
series[SPEED_SERIES] =
new ChartValueSeries(context,
"###,###.0",
fillPaint2,
strokePaint,
5,
R.string.speed);
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() {
@@ -266,13 +275,25 @@ public class ChartView extends View {
*/
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++) {
series[i].update(theData[i + 1]);
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);
}
}
updateDimensions();
setupPath();
}
/**
@@ -284,11 +305,7 @@ public class ChartView extends View {
data.addAll(theData);
for (int i = 0; i < theData.size(); i++) {
double d[] = theData.get(i);
xMonitor.update(d[0]);
int min = Math.min(series.length, d.length - 1);
for (int j = 0; j < min; j++) {
series[j].update(d[j + 1]);
}
addDataPointInternal(d);
}
updateDimensions();
setupPath();
@@ -526,7 +543,7 @@ public class ChartView extends View {
// Draw the data series
for (ChartValueSeries cvs : series) {
if (cvs.isEnabled()) {
if (cvs.isEnabled() && cvs.hasData()) {
cvs.drawPath(c);
}
}
@@ -560,7 +577,7 @@ public class ChartView extends View {
c.translate(getScrollX(), 0);
drawYAxis(c);
for (ChartValueSeries cvs : series) {
if (cvs.isEnabled()) {
if (cvs.isEnabled() && cvs.hasData()) {
drawYLabels(cvs, c);
}
}
@@ -576,13 +593,13 @@ public class ChartView extends View {
private void drawSeriesTitles(Canvas c) {
int sections = 1;
for (ChartValueSeries cvs : series) {
if (cvs.isEnabled()) {
if (cvs.isEnabled() && cvs.hasData()) {
sections++;
}
}
int j = 0;
for (ChartValueSeries cvs : series) {
if (cvs.isEnabled()) {
if (cvs.isEnabled() && cvs.hasData()) {
int y = (int) (w * zoomLevel * ((double) (++j) / sections));
c.drawText(cvs.getTitle(), y, TOP_BORDER, cvs.getPaint());
}
@@ -601,11 +618,11 @@ public class ChartView extends View {
return;
}
int min = Math.min(series.length, data.get(0).length - 1);
// 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();
@@ -616,17 +633,36 @@ public class ChartView extends View {
// 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();
// 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(0)[j + 1]));
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.
@@ -17,6 +17,7 @@ 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;
@@ -63,8 +64,8 @@ class MenuManager {
return true;
}
case R.id.menu_list_tracks: {
Intent startIntent = new Intent(activity, MyTracksList.class);
activity.startActivityForResult(startIntent, MyTracksConstants.SHOW_TRACK);
activity.startActivityForResult(new Intent(activity, MyTracksList.class),
MyTracksConstants.SHOW_TRACK);
return true;
}
case R.id.menu_list_markers: {
@@ -73,20 +74,17 @@ class MenuManager {
activity.startActivityForResult(startIntent, MyTracksConstants.SHOW_WAYPOINT);
return true;
}
case R.id.menu_sensor_state: {
return startActivity(SensorStateActivity.class);
}
case R.id.menu_settings: {
Intent startIntent = new Intent(activity, MyTracksSettings.class);
activity.startActivity(startIntent);
return true;
return startActivity(MyTracksSettings.class);
}
case R.id.menu_aggregated_stats: {
Intent startIntent = new Intent(activity, AggregatedStatsActivity.class);
activity.startActivity(startIntent);
return true;
return startActivity(AggregatedStatsActivity.class);
}
case R.id.menu_help: {
Intent startIntent = new Intent(activity, WelcomeActivity.class);
activity.startActivity(startIntent);
return true;
return startActivity(WelcomeActivity.class);
}
case MyTracksConstants.MENU_CLEAR_MAP: {
activity.setSelectedTrackId(-1);
@@ -95,4 +93,9 @@ class MenuManager {
}
return false;
}
private boolean startActivity(Class<? extends Activity> activityClass) {
activity.startActivity(new Intent(activity, activityClass));
return true;
}
}
@@ -63,7 +63,6 @@ import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
@@ -229,7 +228,9 @@ public class MyTracks extends TabActivity implements OnTouchListener,
}
try {
return trackRecordingService.isRecording();
} catch (RemoteException e) {
// TODO: We catch Exception, because after eliminating the service process
// all exceptions it may throw are no longer wrapped in a RemoteException.
} catch (Exception e) {
Log.e(MyTracksConstants.TAG, "MyTracks: Remote exception.", e);
// Fall back to alternative check method.
@@ -411,6 +412,11 @@ public class MyTracks extends TabActivity implements OnTouchListener,
return dialogManager.onCreateDialog(id, args);
}
@Override
protected Dialog onCreateDialog(int id) {
return dialogManager.onCreateDialog(id, null);
}
@Override
protected void onPrepareDialog(int id, Dialog dialog) {
super.onPrepareDialog(id, dialog);
@@ -914,7 +920,9 @@ public class MyTracks extends TabActivity implements OnTouchListener,
Log.e(MyTracksConstants.TAG, "Cannot insert waypoint marker?");
return -4;
}
} catch (RemoteException e) {
// TODO: We catch Exception, because after eliminating the service process
// all exceptions it may throw are no longer wrapped in a RemoteException.
} catch (Exception e) {
Toast.makeText(this, R.string.error_unable_to_insert_marker,
Toast.LENGTH_LONG).show();
Log.e(MyTracksConstants.TAG, "Cannot insert waypoint marker.", e);
@@ -954,7 +962,9 @@ public class MyTracks extends TabActivity implements OnTouchListener,
Log.e(MyTracksConstants.TAG, "Cannot insert statistics marker?");
return -4;
}
} catch (RemoteException e) {
// TODO: We catch Exception, because after eliminating the service process
// all exceptions it may throw are no longer wrapped in a RemoteException.
} catch (Exception e) {
Toast.makeText(this, R.string.error_unable_to_insert_marker,
Toast.LENGTH_LONG).show();
Log.e(MyTracksConstants.TAG, "Cannot insert statistics marker?", e);
@@ -1022,7 +1032,9 @@ public class MyTracks extends TabActivity implements OnTouchListener,
setSelectedTrackId(recordingTrackId);
Toast.makeText(this, getString(R.string.status_now_recording),
Toast.LENGTH_SHORT).show();
} catch (RemoteException e) {
// TODO: We catch Exception, because after eliminating the service process
// all exceptions it may throw are no longer wrapped in a RemoteException.
} catch (Exception e) {
Toast.makeText(this,
getString(R.string.error_unable_to_start_recording),
Toast.LENGTH_SHORT).show();
@@ -1055,7 +1067,9 @@ public class MyTracks extends TabActivity implements OnTouchListener,
long currentTrackId = recordingTrackId;
try {
trackRecordingService.endCurrentTrack();
} catch (RemoteException e) {
// TODO: We catch Exception, because after eliminating the service process
// all exceptions it may throw are no longer wrapped in a RemoteException.
} catch (Exception e) {
Log.e(MyTracksConstants.TAG, "Unable to stop recording.", e);
}
Intent intent = new Intent(MyTracks.this, MyTracksDetails.class);
@@ -1120,7 +1134,7 @@ public class MyTracks extends TabActivity implements OnTouchListener,
long getSelectedTrackId() {
return selectedTrackId;
}
/**
* Binds to track recording service if it is running.
*/
@@ -15,12 +15,17 @@
*/
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;
@@ -30,6 +35,7 @@ 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.
@@ -53,6 +59,7 @@ public class MyTracksSettings extends PreferenceActivity {
public static final int DEFAULT_SPLIT_FREQUENCY = 0;
private BackupPreferencesListener backupListener;
private SharedPreferences preferences;
/** Called when the activity is first created. */
@@ -96,6 +103,19 @@ public class MyTracksSettings extends PreferenceActivity {
});
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 =
@@ -112,6 +132,7 @@ public class MyTracksSettings extends PreferenceActivity {
protected void onResume() {
super.onResume();
configureBluetoothPreferences();
Preference backupNowPreference =
findPreference(getString(R.string.backup_to_sd_key));
Preference restoreNowPreference =
@@ -158,7 +179,15 @@ public class MyTracksSettings extends PreferenceActivity {
getPreferenceManager().getSharedPreferences()
.unregisterOnSharedPreferenceChangeListener(backupListener);
super.onDestroy();
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);
}
/**
@@ -194,4 +223,44 @@ public class MyTracksSettings extends PreferenceActivity {
? 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,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));
}
}
}
@@ -1,41 +0,0 @@
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.content;
import java.util.Vector;
/**
* An interface for an object that can generate descriptions of waypoints.
*
* @author Sandor Dornbush
*/
public interface DescriptionGenerator {
/**
* Generate a description of the waypoint.
*/
public String generateWaypointDescription(Waypoint waypoint);
/**
* 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);
}
@@ -15,6 +15,8 @@
*/
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;
@@ -38,7 +40,7 @@ import android.util.Log;
public class MyTracksProvider extends ContentProvider {
private static final String DATABASE_NAME = "mytracks.db";
private static final int DATABASE_VERSION = 17;
private static final int DATABASE_VERSION = 18;
private static final int TRACKPOINTS = 1;
private static final int TRACKPOINTS_ID = 2;
private static final int TRACKS = 3;
@@ -70,7 +72,8 @@ public class MyTracksProvider extends ContentProvider {
+ TrackPointsColumns.ALTITUDE + " FLOAT, "
+ TrackPointsColumns.ACCURACY + " FLOAT, "
+ TrackPointsColumns.SPEED + " FLOAT, "
+ TrackPointsColumns.BEARING + " FLOAT);");
+ TrackPointsColumns.BEARING + " FLOAT, "
+ TrackPointsColumns.SENSOR + " BLOB);");
db.execSQL("CREATE TABLE " + TRACKS_TABLE + " ("
+ TracksColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, "
+ TracksColumns.NAME + " STRING, "
@@ -132,12 +135,19 @@ public class MyTracksProvider extends ContentProvider {
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
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);
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");
}
}
}
@@ -235,6 +245,7 @@ public class MyTracksProvider extends ContentProvider {
}
}
@Override
public int bulkInsert(Uri url, ContentValues[] valuesBulk) {
Log.d(MyTracksProvider.TAG, "MyTracksProvider.bulkInsert");
@@ -346,6 +357,8 @@ public class MyTracksProvider extends ContentProvider {
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);
@@ -1,351 +0,0 @@
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.content;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.location.Location;
import android.net.Uri;
import java.util.List;
/**
* Utility to access data from the mytracks content provider.
*
* @author Rodrigo Damazio
*/
public interface MyTracksProviderUtils {
/**
* Authority (first part of URI) for the MyTracks content provider:
*/
public static final String AUTHORITY = "com.google.android.maps.mytracks";
/**
* Deletes all tracks (including track points) from the provider.
*/
void deleteAllTracks();
/**
* Deletes a track with the given track id.
*
* @param trackId the unique track id
*/
void deleteTrack(long trackId);
/**
* Deletes a way point with the given way point id.
* This will also correct the next statistics way point after the deleted one
* to reflect the deletion.
* The generator is needed to stitch together statistics waypoints.
*
* @param waypointId the unique way point id
* @param descriptionGenerator the class to generate descriptions
*/
void deleteWaypoint(long waypointId,
DescriptionGenerator descriptionGenerator);
/**
* Finds the next statistics waypoint after the given waypoint.
*
* @param waypoint a given waypoint
* @return the next statistics waypoint, or null if none found.
*/
Waypoint getNextStatisticsWaypointAfter(Waypoint waypoint);
/**
* Updates the waypoint in the provider.
*
* @param waypoint
* @return true if successful
*/
boolean updateWaypoint(Waypoint waypoint);
/**
* Finds the last recorded location from the location provider.
*
* @return the last location, or null if no locations available
*/
Location getLastLocation();
/**
* Finds the first recorded waypoint for a given track from the location
* provider.
* This is a special waypoint that holds the stats for current segment.
*
* @param trackId the id of the track the waypoint belongs to
* @return the first waypoint, or null if no waypoints available
*/
Waypoint getFirstWaypoint(long trackId);
/**
* Finds the given waypoint from the location provider.
*
* @param waypointId
* @return the waypoint, or null if it does not exist
*/
Waypoint getWaypoint(long waypointId);
/**
* Finds the last recorded location id from the track points provider.
*
* @param trackId find last location on this track
* @return the location id, or -1 if no locations available
*/
long getLastLocationId(long trackId);
/**
* Finds the id of the 1st waypoint for a given track.
* The 1st waypoint is special as it contains the stats for the current
* segment.
*
* @param trackId find last location on this track
* @return the waypoint id, or -1 if no waypoints are available
*/
long getFirstWaypointId(long trackId);
/**
* Finds the id of the 1st waypoint for a given track.
* The 1st waypoint is special as it contains the stats for the current
* segment.
*
* @param trackId find last location on this track
* @return the waypoint id, or -1 if no waypoints are available
*/
long getLastWaypointId(long trackId);
/**
* Finds the last recorded track from the track provider.
*
* @return the last track, or null if no tracks available
*/
Track getLastTrack();
/**
* Finds the last recorded track id from the tracks provider.
*
* @return the track id, or -1 if no tracks available
*/
long getLastTrackId();
/**
* Finds a location by given unique id.
*
* @param id the desired id
* @return a Location object, or null if not found
*/
Location getLocation(long id);
/**
* Creates a cursor over the locations in the track points provider which
* iterates over a given range of unique ids.
* Caller gets to own the returned cursor. Don't forget to close it.
*
* @param trackId the id of the track for which to get the points
* @param minTrackPointId the minimum id for the track points
* @param maxLocations maximum number of locations retrieved
* @param descending if true the results will be returned in descending id
* order (latest location first)
* @return A cursor over the selected range of locations
*/
Cursor getLocationsCursor(long trackId, long minTrackPointId,
int maxLocations, boolean descending);
/**
* Creates a cursor over the waypoints of a track.
* Caller gets to own the returned cursor. Don't forget to close it.
*
* @param trackId the id of the track for which to get the points
* @param minWaypointId the minimum id for the track points
* @param maxWaypoints the maximum number of waypoints to return
* @return A cursor over the selected range of locations
*/
Cursor getWaypointsCursor(long trackId, long minWaypointId,
int maxWaypoints);
/**
* Finds a track by given unique track id.
* Note that the returned track object does not have any track points
* attached. Use {@link #getTrackPoints(Track, int)} to load the track points.
*
* @param id desired unique track id
* @return a Track object, or null if not found
*/
Track getTrack(long id);
/**
* Retrieves all tracks without track points. If no tracks exist an empty
* list will be returned. Use {@link #getTrackPoints(Track, int)} to load
* the track points.
*
* @return a list of all the recorded tracks
*/
List<Track> getAllTracks();
/**
* Loads the track points for a given track.
*
* @param track assumes that startId and stopId are filled in correctly
* @param maxPoints maximum number of points to load, or -1 for no limit
* (oldest ones will be discarded)
* @return the id of the last location in the track
*/
long getTrackPoints(Track track, int maxPoints);
/**
* Creates a cursor over the tracks provider with a given selection.
* Caller gets to own the returned cursor. Don't forget to close it.
*
* @param selection a given selection
* @return a cursor of the selected tracks
*/
Cursor getTracksCursor(String selection);
/**
* Inserts a track in the tracks provider.
* Note: This does not insert any track points.
* Use {@link #insertTrackPoint(Location, long)} to insert them.
*
* @param track the track to insert
* @return the content provider URI for the inserted track
*/
Uri insertTrack(Track track);
/**
* Inserts a track point in the tracks provider.
*
* @param location the location to insert
* @return the content provider URI for the inserted track point
*/
Uri insertTrackPoint(Location location, long trackId);
/**
* Inserts multiple track points in a single operation.
*
* @param locations an array of locations to insert
* @param length the number of locations (from the beginning of the array)
* to actually insert, or -1 for all of them
* @param trackId the ID of the track to insert the points into
* @return the number of points inserted
*/
int bulkInsertTrackPoints(Location[] locations, int length, long trackId);
/**
* Inserts a waypoint in the provider.
*
* @param waypoint the waypoint to insert
* @return the content provider URI for the inserted track
*/
Uri insertWaypoint(Waypoint waypoint);
/**
* Tests if a track with given id exists.
*
* @param id the unique id
* @return true if track exists
*/
boolean trackExists(long id);
/**
* Updates a track in the content provider.
* Note: This will not update any track points.
*
* @param track a given track
*/
void updateTrack(Track track);
/**
* Creates a Track object from a given cursor.
*
* @param cursor a cursor pointing at a db or provider with tracks
* @return a new Track object
*/
Track createTrack(Cursor cursor);
/**
* Creates the ContentValues for a given Track object.
*
* Note: If the track has an id<0 the id column will not be filled.
*
* @param track a given track object
* @return a filled in ContentValues object
*/
ContentValues createContentValues(Track track);
/**
* Creates a location object from a given cursor.
*
* @param cursor a cursor pointing at a db or provider with locations
* @return a new location object
*/
Location createLocation(Cursor cursor);
/**
* Fill a location object with values from a given cursor.
*
* @param cursor a cursor pointing at a db or provider with locations
* @param location a location object to be overwritten
*/
void fillLocation(Cursor cursor, Location location);
/**
* Creates a waypoint object from a given cursor.
*
* @param cursor a cursor pointing at a db or provider with waypoints.
* @return a new waypoint object
*/
Waypoint createWaypoint(Cursor cursor);
/**
* A factory which can produce instances of {@link MyTracksProviderUtils},
* and can be overridden in tests (a.k.a. poor man's guice).
*/
public static class Factory {
private static Factory instance = new Factory();
/**
* Creates and returns an instance of {@link MyTracksProviderUtils} which
* uses the given context to access its data.
*/
public static MyTracksProviderUtils get(Context context) {
return instance.newForContext(context);
}
/**
* Returns the global instance of this factory.
*/
public static Factory getInstance() {
return instance;
}
/**
* Overrides the global instance for this factory, to be used for testing.
* If used, don't forget to set it back to the original value after the
* test is run.
*/
public static void overrideInstance(Factory factory) {
instance = factory;
}
/**
* Creates an instance of {@link MyTracksProviderUtils}.
*/
protected MyTracksProviderUtils newForContext(Context context) {
return new MyTracksProviderUtilsImpl(context.getContentResolver());
}
}
}
@@ -1,914 +0,0 @@
/*
* Copyright 2008 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.content;
import com.google.android.apps.mytracks.stats.TripStatistics;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.database.Cursor;
import android.location.Location;
import android.net.Uri;
import android.util.Log;
import java.util.ArrayList;
import java.util.List;
/**
* Helper class providing easy access to locations and tracks in the
* MyTracksProvider. All static members.
*
* @author Leif Hendrik Wilden
*/
public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils {
private final ContentResolver contentResolver;
public MyTracksProviderUtilsImpl(ContentResolver contentResolver) {
this.contentResolver = contentResolver;
}
/**
* Creates the ContentValues for a given location object.
*
* @param location a given location
* @param trackId the id of the track it belongs to
* @return a filled in ContentValues object
*/
private static ContentValues createContentValues(
Location location, long trackId) {
ContentValues values = new ContentValues();
values.put(TrackPointsColumns.TRACKID, trackId);
values.put(TrackPointsColumns.LATITUDE,
(int) (location.getLatitude() * 1E6));
values.put(TrackPointsColumns.LONGITUDE,
(int) (location.getLongitude() * 1E6));
// This is an ugly hack for Samsung phones that don't properly populate the
// time field.
values.put(TrackPointsColumns.TIME,
(location.getTime() == 0)
? System.currentTimeMillis()
: location.getTime());
if (location.hasAltitude()) {
values.put(TrackPointsColumns.ALTITUDE, location.getAltitude());
}
if (location.hasBearing()) {
values.put(TrackPointsColumns.BEARING, location.getBearing());
}
if (location.hasAccuracy()) {
values.put(TrackPointsColumns.ACCURACY, location.getAccuracy());
}
if (location.hasSpeed()) {
values.put(TrackPointsColumns.SPEED, location.getSpeed());
}
return values;
}
@Override
public ContentValues createContentValues(Track track) {
ContentValues values = new ContentValues();
TripStatistics stats = track.getStatistics();
// Values id < 0 indicate no id is available:
if (track.getId() >= 0) {
values.put(TracksColumns._ID, track.getId());
}
values.put(TracksColumns.NAME, track.getName());
values.put(TracksColumns.DESCRIPTION, track.getDescription());
values.put(TracksColumns.MAPID, track.getMapId());
values.put(TracksColumns.CATEGORY, track.getCategory());
values.put(TracksColumns.NUMPOINTS, track.getNumberOfPoints());
values.put(TracksColumns.STARTID, track.getStartId());
values.put(TracksColumns.STARTTIME, stats.getStartTime());
values.put(TracksColumns.STOPTIME, stats.getStopTime());
values.put(TracksColumns.STOPID, track.getStopId());
values.put(TracksColumns.TOTALDISTANCE, stats.getTotalDistance());
values.put(TracksColumns.TOTALTIME, stats.getTotalTime());
values.put(TracksColumns.MOVINGTIME, stats.getMovingTime());
values.put(TracksColumns.MAXLAT, stats.getTop());
values.put(TracksColumns.MINLAT, stats.getBottom());
values.put(TracksColumns.MAXLON, stats.getRight());
values.put(TracksColumns.MINLON, stats.getLeft());
values.put(TracksColumns.AVGSPEED, stats.getAverageSpeed());
values.put(TracksColumns.AVGMOVINGSPEED, stats.getAverageMovingSpeed());
values.put(TracksColumns.MAXSPEED, stats.getMaxSpeed());
values.put(TracksColumns.MINELEVATION, stats.getMinElevation());
values.put(TracksColumns.MAXELEVATION, stats.getMaxElevation());
values.put(TracksColumns.ELEVATIONGAIN, stats.getTotalElevationGain());
values.put(TracksColumns.MINGRADE, stats.getMinGrade());
values.put(TracksColumns.MAXGRADE, stats.getMaxGrade());
return values;
}
private static ContentValues createContentValues(Waypoint waypoint) {
ContentValues values = new ContentValues();
TripStatistics stats = waypoint.getStatistics();
// Values id < 0 indicate no id is available:
if (waypoint.getId() >= 0) {
values.put(WaypointsColumns._ID, waypoint.getId());
}
values.put(WaypointsColumns.NAME, waypoint.getName());
values.put(WaypointsColumns.DESCRIPTION, waypoint.getDescription());
values.put(WaypointsColumns.CATEGORY, waypoint.getCategory());
values.put(WaypointsColumns.ICON, waypoint.getIcon());
values.put(WaypointsColumns.TRACKID, waypoint.getTrackId());
values.put(WaypointsColumns.TYPE, waypoint.getType());
values.put(WaypointsColumns.LENGTH, waypoint.getLength());
values.put(WaypointsColumns.DURATION, waypoint.getDuration());
values.put(WaypointsColumns.STARTTIME, stats.getStartTime());
values.put(WaypointsColumns.STARTID, waypoint.getStartId());
values.put(WaypointsColumns.STOPID, waypoint.getStopId());
values.put(WaypointsColumns.TOTALDISTANCE, stats.getTotalDistance());
values.put(WaypointsColumns.TOTALTIME, stats.getTotalTime());
values.put(WaypointsColumns.MOVINGTIME, stats.getMovingTime());
values.put(WaypointsColumns.AVGSPEED, stats.getAverageSpeed());
values.put(WaypointsColumns.AVGMOVINGSPEED, stats.getAverageMovingSpeed());
values.put(WaypointsColumns.MAXSPEED, stats.getMaxSpeed());
values.put(WaypointsColumns.MINELEVATION, stats.getMinElevation());
values.put(WaypointsColumns.MAXELEVATION, stats.getMaxElevation());
values.put(WaypointsColumns.ELEVATIONGAIN, stats.getTotalElevationGain());
values.put(WaypointsColumns.MINGRADE, stats.getMinGrade());
values.put(WaypointsColumns.MAXGRADE, stats.getMaxGrade());
Location location = waypoint.getLocation();
if (location != null) {
values.put(WaypointsColumns.LATITUDE,
(int) (location.getLatitude() * 1E6));
values.put(WaypointsColumns.LONGITUDE,
(int) (location.getLongitude() * 1E6));
values.put(WaypointsColumns.TIME, location.getTime());
if (location.hasAltitude()) {
values.put(WaypointsColumns.ALTITUDE, location.getAltitude());
}
if (location.hasBearing()) {
values.put(WaypointsColumns.BEARING, location.getBearing());
}
if (location.hasAccuracy()) {
values.put(WaypointsColumns.ACCURACY, location.getAccuracy());
}
if (location.hasSpeed()) {
values.put(WaypointsColumns.SPEED, location.getSpeed());
}
}
return values;
}
@Override
public Location createLocation(Cursor cursor) {
Location location = new Location("");
fillLocation(cursor, location);
return location;
}
@Override
public void fillLocation(Cursor cursor, Location location) {
location.reset();
int idxLatitude = cursor.getColumnIndexOrThrow(TrackPointsColumns.LATITUDE);
int idxLongitude =
cursor.getColumnIndexOrThrow(TrackPointsColumns.LONGITUDE);
int idxAltitude = cursor.getColumnIndexOrThrow(TrackPointsColumns.ALTITUDE);
int idxTime = cursor.getColumnIndexOrThrow(TrackPointsColumns.TIME);
int idxBearing = cursor.getColumnIndexOrThrow(TrackPointsColumns.BEARING);
int idxAccuracy = cursor.getColumnIndexOrThrow(TrackPointsColumns.ACCURACY);
int idxSpeed = cursor.getColumnIndexOrThrow(TrackPointsColumns.SPEED);
if (!cursor.isNull(idxLatitude)) {
location.setLatitude(1. * cursor.getInt(idxLatitude) / 1E6);
}
if (!cursor.isNull(idxLongitude)) {
location.setLongitude(1. * cursor.getInt(idxLongitude) / 1E6);
}
if (!cursor.isNull(idxAltitude)) {
location.setAltitude(cursor.getFloat(idxAltitude));
}
if (!cursor.isNull(idxTime)) {
location.setTime(cursor.getLong(idxTime));
}
if (!cursor.isNull(idxBearing)) {
location.setBearing(cursor.getFloat(idxBearing));
}
if (!cursor.isNull(idxSpeed)) {
location.setSpeed(cursor.getFloat(idxSpeed));
}
if (!cursor.isNull(idxAccuracy)) {
location.setAccuracy(cursor.getFloat(idxAccuracy));
}
}
@Override
public Track createTrack(Cursor cursor) {
int idxId = cursor.getColumnIndexOrThrow(TracksColumns._ID);
int idxName = cursor.getColumnIndexOrThrow(TracksColumns.NAME);
int idxDescription =
cursor.getColumnIndexOrThrow(TracksColumns.DESCRIPTION);
int idxMapId = cursor.getColumnIndexOrThrow(TracksColumns.MAPID);
int idxCategory = cursor.getColumnIndexOrThrow(TracksColumns.CATEGORY);
int idxStartId = cursor.getColumnIndexOrThrow(TracksColumns.STARTID);
int idxStartTime = cursor.getColumnIndexOrThrow(TracksColumns.STARTTIME);
int idxStopTime = cursor.getColumnIndexOrThrow(TracksColumns.STOPTIME);
int idxStopId = cursor.getColumnIndexOrThrow(TracksColumns.STOPID);
int idxNumPoints = cursor.getColumnIndexOrThrow(TracksColumns.NUMPOINTS);
int idxMaxlat = cursor.getColumnIndexOrThrow(TracksColumns.MAXLAT);
int idxMinlat = cursor.getColumnIndexOrThrow(TracksColumns.MINLAT);
int idxMaxlon = cursor.getColumnIndexOrThrow(TracksColumns.MAXLON);
int idxMinlon = cursor.getColumnIndexOrThrow(TracksColumns.MINLON);
int idxTotalDistance =
cursor.getColumnIndexOrThrow(TracksColumns.TOTALDISTANCE);
int idxTotalTime = cursor.getColumnIndexOrThrow(TracksColumns.TOTALTIME);
int idxMovingTime = cursor.getColumnIndexOrThrow(TracksColumns.MOVINGTIME);
int idxMaxSpeed = cursor.getColumnIndexOrThrow(TracksColumns.MAXSPEED);
int idxMinElevation =
cursor.getColumnIndexOrThrow(TracksColumns.MINELEVATION);
int idxMaxElevation =
cursor.getColumnIndexOrThrow(TracksColumns.MAXELEVATION);
int idxElevationGain =
cursor.getColumnIndexOrThrow(TracksColumns.ELEVATIONGAIN);
int idxMinGrade = cursor.getColumnIndexOrThrow(TracksColumns.MINGRADE);
int idxMaxGrade = cursor.getColumnIndexOrThrow(TracksColumns.MAXGRADE);
Track track = new Track();
TripStatistics stats = track.getStatistics();
if (!cursor.isNull(idxId)) {
track.setId(cursor.getLong(idxId));
}
if (!cursor.isNull(idxName)) {
track.setName(cursor.getString(idxName));
}
if (!cursor.isNull(idxDescription)) {
track.setDescription(cursor.getString(idxDescription));
}
if (!cursor.isNull(idxMapId)) {
track.setMapId(cursor.getString(idxMapId));
}
if (!cursor.isNull(idxCategory)) {
track.setCategory(cursor.getString(idxCategory));
}
if (!cursor.isNull(idxStartId)) {
track.setStartId(cursor.getInt(idxStartId));
}
if (!cursor.isNull(idxStartTime)) {
stats.setStartTime(cursor.getLong(idxStartTime));
}
if (!cursor.isNull(idxStopTime)) {
stats.setStopTime(cursor.getLong(idxStopTime));
}
if (!cursor.isNull(idxStopId)) {
track.setStopId(cursor.getInt(idxStopId));
}
if (!cursor.isNull(idxNumPoints)) {
track.setNumberOfPoints(cursor.getInt(idxNumPoints));
}
if (!cursor.isNull(idxTotalDistance)) {
stats.setTotalDistance(cursor.getFloat(idxTotalDistance));
}
if (!cursor.isNull(idxTotalTime)) {
stats.setTotalTime(cursor.getLong(idxTotalTime));
}
if (!cursor.isNull(idxMovingTime)) {
stats.setMovingTime(cursor.getLong(idxMovingTime));
}
if (!cursor.isNull(idxMaxlat)
&& !cursor.isNull(idxMinlat)
&& !cursor.isNull(idxMaxlon)
&& !cursor.isNull(idxMinlon)) {
int top = cursor.getInt(idxMaxlat);
int bottom = cursor.getInt(idxMinlat);
int right = cursor.getInt(idxMaxlon);
int left = cursor.getInt(idxMinlon);
stats.setBounds(left, top, right, bottom);
}
if (!cursor.isNull(idxMaxSpeed)) {
stats.setMaxSpeed(cursor.getFloat(idxMaxSpeed));
}
if (!cursor.isNull(idxMinElevation)) {
stats.setMinElevation(cursor.getFloat(idxMinElevation));
}
if (!cursor.isNull(idxMaxElevation)) {
stats.setMaxElevation(cursor.getFloat(idxMaxElevation));
}
if (!cursor.isNull(idxElevationGain)) {
stats.setTotalElevationGain(cursor.getFloat(idxElevationGain));
}
if (!cursor.isNull(idxMinGrade)) {
stats.setMinGrade(cursor.getFloat(idxMinGrade));
}
if (!cursor.isNull(idxMaxGrade)) {
stats.setMaxGrade(cursor.getFloat(idxMaxGrade));
}
return track;
}
@Override
public Waypoint createWaypoint(Cursor cursor) {
int idxId = cursor.getColumnIndexOrThrow(WaypointsColumns._ID);
int idxName = cursor.getColumnIndexOrThrow(WaypointsColumns.NAME);
int idxDescription =
cursor.getColumnIndexOrThrow(WaypointsColumns.DESCRIPTION);
int idxCategory = cursor.getColumnIndexOrThrow(WaypointsColumns.CATEGORY);
int idxIcon = cursor.getColumnIndexOrThrow(WaypointsColumns.ICON);
int idxTrackId = cursor.getColumnIndexOrThrow(WaypointsColumns.TRACKID);
int idxType = cursor.getColumnIndexOrThrow(WaypointsColumns.TYPE);
int idxLength = cursor.getColumnIndexOrThrow(WaypointsColumns.LENGTH);
int idxDuration = cursor.getColumnIndexOrThrow(WaypointsColumns.DURATION);
int idxStartTime = cursor.getColumnIndexOrThrow(WaypointsColumns.STARTTIME);
int idxStartId = cursor.getColumnIndexOrThrow(WaypointsColumns.STARTID);
int idxStopId = cursor.getColumnIndexOrThrow(WaypointsColumns.STOPID);
int idxTotalDistance =
cursor.getColumnIndexOrThrow(WaypointsColumns.TOTALDISTANCE);
int idxTotalTime = cursor.getColumnIndexOrThrow(WaypointsColumns.TOTALTIME);
int idxMovingTime =
cursor.getColumnIndexOrThrow(WaypointsColumns.MOVINGTIME);
int idxMaxSpeed = cursor.getColumnIndexOrThrow(WaypointsColumns.MAXSPEED);
int idxMinElevation =
cursor.getColumnIndexOrThrow(WaypointsColumns.MINELEVATION);
int idxMaxElevation =
cursor.getColumnIndexOrThrow(WaypointsColumns.MAXELEVATION);
int idxElevationGain =
cursor.getColumnIndexOrThrow(WaypointsColumns.ELEVATIONGAIN);
int idxMinGrade = cursor.getColumnIndexOrThrow(WaypointsColumns.MINGRADE);
int idxMaxGrade = cursor.getColumnIndexOrThrow(WaypointsColumns.MAXGRADE);
int idxLatitude = cursor.getColumnIndexOrThrow(WaypointsColumns.LATITUDE);
int idxLongitude = cursor.getColumnIndexOrThrow(WaypointsColumns.LONGITUDE);
int idxAltitude = cursor.getColumnIndexOrThrow(WaypointsColumns.ALTITUDE);
int idxTime = cursor.getColumnIndexOrThrow(WaypointsColumns.TIME);
int idxBearing = cursor.getColumnIndexOrThrow(WaypointsColumns.BEARING);
int idxAccuracy = cursor.getColumnIndexOrThrow(WaypointsColumns.ACCURACY);
int idxSpeed = cursor.getColumnIndexOrThrow(WaypointsColumns.SPEED);
Waypoint waypoint = new Waypoint();
TripStatistics stats = waypoint.getStatistics();
if (!cursor.isNull(idxId)) {
waypoint.setId(cursor.getLong(idxId));
}
if (!cursor.isNull(idxName)) {
waypoint.setName(cursor.getString(idxName));
}
if (!cursor.isNull(idxDescription)) {
waypoint.setDescription(cursor.getString(idxDescription));
}
if (!cursor.isNull(idxCategory)) {
waypoint.setCategory(cursor.getString(idxCategory));
}
if (!cursor.isNull(idxIcon)) {
waypoint.setIcon(cursor.getString(idxIcon));
}
if (!cursor.isNull(idxTrackId)) {
waypoint.setTrackId(cursor.getLong(idxTrackId));
}
if (!cursor.isNull(idxType)) {
waypoint.setType(cursor.getInt(idxType));
}
if (!cursor.isNull(idxLength)) {
waypoint.setLength(cursor.getDouble(idxLength));
}
if (!cursor.isNull(idxDuration)) {
waypoint.setDuration(cursor.getLong(idxDuration));
}
if (!cursor.isNull(idxStartTime)) {
stats.setStartTime(cursor.getLong(idxStartTime));
}
if (!cursor.isNull(idxStartId)) {
waypoint.setStartId(cursor.getLong(idxStartId));
}
if (!cursor.isNull(idxStopId)) {
waypoint.setStopId(cursor.getLong(idxStopId));
}
if (!cursor.isNull(idxTotalDistance)) {
stats.setTotalDistance(cursor.getFloat(idxTotalDistance));
}
if (!cursor.isNull(idxTotalTime)) {
stats.setTotalTime(cursor.getLong(idxTotalTime));
}
if (!cursor.isNull(idxMovingTime)) {
stats.setMovingTime(cursor.getLong(idxMovingTime));
}
if (!cursor.isNull(idxMaxSpeed)) {
stats.setMaxSpeed(cursor.getFloat(idxMaxSpeed));
}
if (!cursor.isNull(idxMinElevation)) {
stats.setMinElevation(cursor.getFloat(idxMinElevation));
}
if (!cursor.isNull(idxMaxElevation)) {
stats.setMaxElevation(cursor.getFloat(idxMaxElevation));
}
if (!cursor.isNull(idxElevationGain)) {
stats.setTotalElevationGain(cursor.getFloat(idxElevationGain));
}
if (!cursor.isNull(idxMinGrade)) {
stats.setMinGrade(cursor.getFloat(idxMinGrade));
}
if (!cursor.isNull(idxMaxGrade)) {
stats.setMaxGrade(cursor.getFloat(idxMaxGrade));
}
Location location = new Location("");
if (!cursor.isNull(idxLatitude) && !cursor.isNull(idxLongitude)) {
location.setLatitude(1. * cursor.getInt(idxLatitude) / 1E6);
location.setLongitude(1. * cursor.getInt(idxLongitude) / 1E6);
}
if (!cursor.isNull(idxAltitude)) {
location.setAltitude(cursor.getFloat(idxAltitude));
}
if (!cursor.isNull(idxTime)) {
location.setTime(cursor.getLong(idxTime));
}
if (!cursor.isNull(idxBearing)) {
location.setBearing(cursor.getFloat(idxBearing));
}
if (!cursor.isNull(idxSpeed)) {
location.setSpeed(cursor.getFloat(idxSpeed));
}
if (!cursor.isNull(idxAccuracy)) {
location.setAccuracy(cursor.getFloat(idxAccuracy));
}
waypoint.setLocation(location);
return waypoint;
}
@Override
public void deleteAllTracks() {
contentResolver.delete(TracksColumns.CONTENT_URI, null, null);
contentResolver.delete(TrackPointsColumns.CONTENT_URI, null, null);
contentResolver.delete(WaypointsColumns.CONTENT_URI, null, null);
}
@Override
public void deleteTrack(long trackId) {
Track track = getTrack(trackId);
if (track != null) {
contentResolver.delete(TrackPointsColumns.CONTENT_URI,
"_id>=" + track.getStartId() + " AND _id<=" + track.getStopId(),
null);
}
contentResolver.delete(WaypointsColumns.CONTENT_URI,
WaypointsColumns.TRACKID + "=" + trackId, null);
contentResolver.delete(TracksColumns.CONTENT_URI, "_id=" + trackId, null);
}
@Override
public void deleteWaypoint(long waypointId,
DescriptionGenerator descriptionGenerator) {
final Waypoint deletedWaypoint = getWaypoint(waypointId);
if (deletedWaypoint != null
&& deletedWaypoint.getType() == Waypoint.TYPE_STATISTICS) {
final Waypoint nextWaypoint =
getNextStatisticsWaypointAfter(deletedWaypoint);
if (nextWaypoint != null) {
Log.d(MyTracksProvider.TAG, "Correcting marker " + nextWaypoint.getId()
+ " after deleted marker " + deletedWaypoint.getId());
nextWaypoint.getStatistics().merge(deletedWaypoint.getStatistics());
nextWaypoint.setDescription(
descriptionGenerator.generateWaypointDescription(nextWaypoint));
if (!updateWaypoint(nextWaypoint)) {
Log.w(MyTracksProvider.TAG, "Update of marker was unsuccessful.");
}
} else {
Log.d(MyTracksProvider.TAG,
"No statistics marker after the deleted one was found.");
}
}
contentResolver.delete(
WaypointsColumns.CONTENT_URI, "_id=" + waypointId, null);
}
@Override
public Waypoint getNextStatisticsWaypointAfter(Waypoint waypoint) {
final String selection = WaypointsColumns._ID + ">" + waypoint.getId()
+ " AND " + WaypointsColumns.TRACKID + "=" + waypoint.getTrackId()
+ " AND " + WaypointsColumns.TYPE + "=" + Waypoint.TYPE_STATISTICS;
final String sortOrder = WaypointsColumns._ID + " LIMIT 1";
Cursor cursor = null;
try {
cursor = contentResolver.query(
WaypointsColumns.CONTENT_URI,
null /*projection*/,
selection,
null /*selectionArgs*/,
sortOrder);
if (cursor != null && cursor.moveToFirst()) {
return createWaypoint(cursor);
}
} catch (RuntimeException e) {
Log.w(MyTracksProvider.TAG, "Caught unexpected exception.", e);
} finally {
if (cursor != null) {
cursor.close();
}
}
return null;
}
@Override
public boolean updateWaypoint(Waypoint waypoint) {
try {
final int rows = contentResolver.update(
WaypointsColumns.CONTENT_URI,
createContentValues(waypoint),
"_id=" + waypoint.getId(),
null /*selectionArgs*/);
return rows == 1;
} catch (RuntimeException e) {
Log.e(MyTracksProvider.TAG, "Caught unexpected exception.", e);
}
return false;
}
/**
* Finds a locations from the provider by the given selection.
*
* @param select a selection argument that identifies a unique location
* @return the fist location matching, or null if not found
*/
private Location findLocationBy(String select) {
Cursor cursor = null;
try {
cursor = contentResolver.query(
TrackPointsColumns.CONTENT_URI, null, select, null, null);
if (cursor != null && cursor.moveToNext()) {
return createLocation(cursor);
}
} catch (RuntimeException e) {
Log.w(MyTracksProvider.TAG, "Caught an unexpeceted exception.", e);
} finally {
if (cursor != null) {
cursor.close();
}
}
return null;
}
/**
* Finds a track from the provider by the given selection.
*
* @param select a selection argument that identifies a unique track
* @return the first track matching, or null if not found
*/
private Track findTrackBy(String select) {
Cursor cursor = null;
try {
cursor = contentResolver.query(
TracksColumns.CONTENT_URI, null, select, null, null);
if (cursor != null && cursor.moveToNext()) {
return createTrack(cursor);
}
} catch (RuntimeException e) {
Log.w(MyTracksProvider.TAG, "Caught unexpected exception.", e);
} finally {
if (cursor != null) {
cursor.close();
}
}
return null;
}
@Override
public Location getLastLocation() {
return findLocationBy("_id=(select max(_id) from trackpoints)");
}
@Override
public Waypoint getFirstWaypoint(long trackId) {
Cursor cursor = contentResolver.query(
WaypointsColumns.CONTENT_URI,
null /*projection*/,
"trackid=" + trackId,
null /*selectionArgs*/,
"_id LIMIT 1");
if (cursor != null) {
try {
if (cursor.moveToFirst()) {
return createWaypoint(cursor);
}
} catch (RuntimeException e) {
Log.w(MyTracksProvider.TAG, "Caught an unexpected exception.", e);
} finally {
cursor.close();
}
}
return null;
}
@Override
public Waypoint getWaypoint(long waypointId) {
Cursor cursor = contentResolver.query(
WaypointsColumns.CONTENT_URI,
null /*projection*/,
"_id=" + waypointId,
null /*selectionArgs*/,
null /*sortOrder*/);
if (cursor != null) {
try {
if (cursor.moveToFirst()) {
return createWaypoint(cursor);
}
} catch (RuntimeException e) {
Log.w(MyTracksProvider.TAG, "Caught an unexpected exception.", e);
} finally {
cursor.close();
}
}
return null;
}
@Override
public long getLastLocationId(long trackId) {
final String[] projection = {"_id"};
Cursor cursor = contentResolver.query(
TrackPointsColumns.CONTENT_URI,
projection,
"_id=(select max(_id) from trackpoints WHERE trackid=" + trackId + ")",
null /*selectionArgs*/,
null /*sortOrder*/);
if (cursor != null) {
try {
if (cursor.moveToFirst()) {
return cursor.getLong(
cursor.getColumnIndexOrThrow(TrackPointsColumns._ID));
}
} catch (RuntimeException e) {
Log.w(MyTracksProvider.TAG, "Caught an unexpected exception.", e);
} finally {
cursor.close();
}
}
return -1;
}
@Override
public long getFirstWaypointId(long trackId) {
final String[] projection = {"_id"};
Cursor cursor = contentResolver.query(
WaypointsColumns.CONTENT_URI,
projection,
"trackid=" + trackId,
null /*selectionArgs*/,
"_id LIMIT 1" /*sortOrder*/);
if (cursor != null) {
try {
if (cursor.moveToFirst()) {
return cursor.getLong(
cursor.getColumnIndexOrThrow(WaypointsColumns._ID));
}
} catch (RuntimeException e) {
Log.w(MyTracksProvider.TAG, "Caught an unexpected exception.", e);
} finally {
cursor.close();
}
}
return -1;
}
@Override
public long getLastWaypointId(long trackId) {
final String[] projection = {"_id"};
Cursor cursor = contentResolver.query(
WaypointsColumns.CONTENT_URI,
projection,
WaypointsColumns.TRACKID + "=" + trackId,
null /*selectionArgs*/,
"_id DESC LIMIT 1" /*sortOrder*/);
if (cursor != null) {
try {
if (cursor.moveToFirst()) {
return cursor.getLong(
cursor.getColumnIndexOrThrow(WaypointsColumns._ID));
}
} catch (RuntimeException e) {
Log.w(MyTracksProvider.TAG, "Caught an unexpected exception.", e);
} finally {
cursor.close();
}
}
return -1;
}
@Override
public Track getLastTrack() {
Cursor cursor = null;
try {
cursor = contentResolver.query(
TracksColumns.CONTENT_URI, null, "_id=(select max(_id) from tracks)",
null, null);
if (cursor != null && cursor.moveToNext()) {
return createTrack(cursor);
}
} catch (RuntimeException e) {
Log.w(MyTracksProvider.TAG, "Caught an unexpected exception.", e);
} finally {
if (cursor != null) {
cursor.close();
}
}
return null;
}
@Override
public long getLastTrackId() {
String[] proj = { TracksColumns._ID };
Cursor cursor = contentResolver.query(
TracksColumns.CONTENT_URI, proj, "_id=(select max(_id) from tracks)",
null, null);
if (cursor != null) {
try {
if (cursor.moveToFirst()) {
return cursor.getLong(
cursor.getColumnIndexOrThrow(TracksColumns._ID));
}
} finally {
cursor.close();
}
}
return -1;
}
@Override
public Location getLocation(long id) {
String selection = TrackPointsColumns._ID + "=" + id;
return findLocationBy(selection);
}
@Override
public Cursor getLocationsCursor(long trackId, long minTrackPointId,
int maxLocations, boolean descending) {
String selection;
if (minTrackPointId > 0) {
selection = String.format("%s=%d AND %s>=%d",
TrackPointsColumns.TRACKID, trackId,
TrackPointsColumns._ID, minTrackPointId);
} else {
selection = String.format("%s=%d",
TrackPointsColumns.TRACKID, trackId);
}
String sortOrder = "_id " + (descending ? "DESC" : "ASC");
if (maxLocations > 0) {
sortOrder += " LIMIT " + maxLocations;
}
return contentResolver.query(
TrackPointsColumns.CONTENT_URI, null, selection, null, sortOrder);
}
@Override
public Cursor getWaypointsCursor(long trackId, long minWaypointId,
int maxWaypoints) {
String selection;
if (minWaypointId > 0) {
selection = String.format("%s=%d AND %s>=%d",
WaypointsColumns.TRACKID, trackId,
WaypointsColumns._ID, minWaypointId);
} else {
selection = String.format("%s=%d",
WaypointsColumns.TRACKID, trackId);
}
String sortOrder = "_id ASC";
if (maxWaypoints > 0) {
sortOrder += " LIMIT " + maxWaypoints;
}
return contentResolver.query(
WaypointsColumns.CONTENT_URI, null, selection, null, sortOrder);
}
@Override
public Track getTrack(long id) {
String select = TracksColumns._ID + "=" + id;
return findTrackBy(select);
}
@Override
public List<Track> getAllTracks() {
Cursor cursor = getTracksCursor(null);
if (cursor == null || !cursor.moveToFirst()) {
return new ArrayList<Track>();
}
List<Track> tracks = new ArrayList<Track>(cursor.getCount());
do {
tracks.add(createTrack(cursor));
} while(cursor.moveToNext());
return tracks;
}
@Override
public long getTrackPoints(Track track, int maxPoints) {
long lastId = -1;
Cursor cursor = getLocationsCursor(track.getId(), -1, maxPoints, true);
if (cursor == null) {
Log.w(MyTracksProvider.TAG, "Cannot get a locations cursor!");
return lastId;
}
try {
final int idColumnIdx =
cursor.getColumnIndexOrThrow(TrackPointsColumns._ID);
if (cursor.moveToLast()) {
do {
Location location = createLocation(cursor);
if (location == null) {
continue;
}
track.addLocation(location);
lastId = cursor.getLong(idColumnIdx);
} while (cursor.moveToPrevious());
}
} catch (RuntimeException e) {
Log.w(MyTracksProvider.TAG, "Caught unexpected exception.", e);
} finally {
cursor.close();
}
return lastId;
}
@Override
public Cursor getTracksCursor(String selection) {
Cursor cursor = contentResolver.query(
TracksColumns.CONTENT_URI, null, selection, null, "_id");
return cursor;
}
@Override
public Uri insertTrack(Track track) {
Log.d(MyTracksProvider.TAG, "MyTracksProviderUtilsImpl.insertTrack");
return contentResolver.insert(TracksColumns.CONTENT_URI,
createContentValues(track));
}
@Override
public Uri insertTrackPoint(Location location, long trackId) {
Log.d(MyTracksProvider.TAG, "MyTracksProviderUtilsImpl.insertTrackPoint");
return contentResolver.insert(TrackPointsColumns.CONTENT_URI,
createContentValues(location, trackId));
}
@Override
public int bulkInsertTrackPoints(Location[] locations, int length, long trackId) {
if (length == -1) { length = locations.length; }
ContentValues[] values = new ContentValues[length];
for (int i = 0; i < length; i++) {
values[i] = createContentValues(locations[i], trackId);
}
return contentResolver.bulkInsert(TrackPointsColumns.CONTENT_URI, values);
}
@Override
public Uri insertWaypoint(Waypoint waypoint) {
Log.d(MyTracksProvider.TAG, "MyTracksProviderUtilsImpl.insertWaypoint");
waypoint.setId(-1);
return contentResolver.insert(WaypointsColumns.CONTENT_URI,
createContentValues(waypoint));
}
@Override
public boolean trackExists(long id) {
Cursor cursor = null;
try {
final String[] projection = { TracksColumns._ID };
cursor = contentResolver.query(
TracksColumns.CONTENT_URI,
projection,
TracksColumns._ID + "=" + id/*selection*/,
null/*selectionArgs*/,
null/*sortOrder*/);
if (cursor != null && cursor.moveToNext()) {
return true;
}
} finally {
if (cursor != null) {
cursor.close();
}
}
return false;
}
@Override
public void updateTrack(Track track) {
Log.d(MyTracksProvider.TAG, "MyTracksProviderUtilsImpl.updateTrack");
contentResolver.update(TracksColumns.CONTENT_URI,
createContentValues(track), "_id=" + track.getId(), null);
}
}
@@ -1,199 +0,0 @@
/*
* Copyright 2008 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.content;
import com.google.android.apps.mytracks.stats.TripStatistics;
import android.location.Location;
import android.os.Parcel;
import android.os.Parcelable;
import java.util.ArrayList;
/**
* A class representing a (GPS) Track.
*
* TODO: hashCode and equals
*
* @author Leif Hendrik Wilden
* @author Rodrigo Damazio
*/
public class Track implements Parcelable {
/**
* Creator for a Track object
*/
public static class Creator implements Parcelable.Creator<Track> {
public Track createFromParcel(Parcel source) {
ClassLoader classLoader = getClass().getClassLoader();
Track track = new Track();
track.id = source.readLong();
track.name = source.readString();
track.description = source.readString();
track.mapId = source.readString();
track.category = source.readString();
track.startId = source.readLong();
track.stopId = source.readLong();
track.stats = source.readParcelable(classLoader);
track.numberOfPoints = source.readInt();
for (int i = 0; i < track.numberOfPoints; ++i) {
Location loc = source.readParcelable(classLoader);
track.locations.add(loc);
}
return track;
}
public Track[] newArray(int size) {
return new Track[size];
}
}
public static final Creator CREATOR = new Creator();
/**
* The track points (which may not have been loaded).
*/
private ArrayList<Location> locations = new ArrayList<Location>();
/**
* The number of location points (present even if the points themselves were
* not loaded).
*/
private int numberOfPoints = 0;
private long id = -1;
private String name = "";
private String description = "";
private String mapId = "";
private long startId = -1;
private long stopId = -1;
private String category = "";
private TripStatistics stats = new TripStatistics();
public Track() {
}
public void writeToParcel(Parcel dest, int flags) {
dest.writeLong(id);
dest.writeString(name);
dest.writeString(description);
dest.writeString(mapId);
dest.writeString(category);
dest.writeLong(startId);
dest.writeLong(stopId);
dest.writeParcelable(stats, 0);
dest.writeInt(numberOfPoints);
for (int i = 0; i < numberOfPoints; ++i) {
dest.writeParcelable(locations.get(i), 0);
}
}
// Getters and setters:
//---------------------
public int describeContents() {
return 0;
}
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public long getStartId() {
return startId;
}
public void setStartId(long startId) {
this.startId = startId;
}
public long getStopId() {
return stopId;
}
public void setStopId(long stopId) {
this.stopId = stopId;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public String getMapId() {
return mapId;
}
public void setMapId(String mapId) {
this.mapId = mapId;
}
public String getCategory() {
return category;
}
public void setCategory(String category) {
this.category = category;
}
public int getNumberOfPoints() {
return numberOfPoints;
}
public void setNumberOfPoints(int numberOfPoints) {
this.numberOfPoints = numberOfPoints;
}
public void addLocation(Location l) {
locations.add(l);
}
public ArrayList<Location> getLocations() {
return locations;
}
public void setLocations(ArrayList<Location> locations) {
this.locations = locations;
}
public TripStatistics getStatistics() {
return stats;
}
public void setStatistics(TripStatistics stats) {
this.stats = stats;
}
}
@@ -1,57 +0,0 @@
/*
* Copyright 2008 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.content;
import static com.google.android.apps.mytracks.content.ContentTypeIds.*;
import android.net.Uri;
import android.provider.BaseColumns;
/**
* Defines the URI for the track points provider and the available column names
* and content types.
*
* @author Leif Hendrik Wilden
*/
public interface TrackPointsColumns extends BaseColumns {
public static final Uri CONTENT_URI =
Uri.parse("content://com.google.android.maps.mytracks/trackpoints");
public static final String CONTENT_TYPE =
"vnd.android.cursor.dir/vnd.google.trackpoint";
public static final String CONTENT_ITEMTYPE =
"vnd.android.cursor.item/vnd.google.trackpoint";
public static final String DEFAULT_SORT_ORDER = "_id";
/* All columns */
public static final String TRACKID = "trackid";
public static final String LATITUDE = "latitude";
public static final String LONGITUDE = "longitude";
public static final String ALTITUDE = "elevation";
public static final String BEARING = "bearing";
public static final String TIME = "time";
public static final String ACCURACY = "accuracy";
public static final String SPEED = "speed";
/** Columns that go into the backup. */
public static final String[] BACKUP_COLUMNS =
{ _ID, TRACKID, LATITUDE, LONGITUDE, ALTITUDE, BEARING, TIME,
ACCURACY, SPEED };
/** Types of the columns that go into the backup. */
public static final byte[] 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 };
}
@@ -1,76 +0,0 @@
/*
* Copyright 2008 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.content;
import static com.google.android.apps.mytracks.content.ContentTypeIds.*;
import android.net.Uri;
import android.provider.BaseColumns;
/**
* Defines the URI for the tracks provider and the available column names
* and content types.
*
* @author Leif Hendrik Wilden
*/
public interface TracksColumns extends BaseColumns {
public static final Uri CONTENT_URI =
Uri.parse("content://com.google.android.maps.mytracks/tracks");
public static final String CONTENT_TYPE =
"vnd.android.cursor.dir/vnd.google.track";
public static final String CONTENT_ITEMTYPE =
"vnd.android.cursor.item/vnd.google.track";
public static final String DEFAULT_SORT_ORDER = "_id";
/* All columns */
public static final String NAME = "name";
public static final String DESCRIPTION = "description";
public static final String CATEGORY = "category";
public static final String STARTID = "startid";
public static final String STOPID = "stopid";
public static final String STARTTIME = "starttime";
public static final String STOPTIME = "stoptime";
public static final String NUMPOINTS = "numpoints";
public static final String TOTALDISTANCE = "totaldistance";
public static final String TOTALTIME = "totaltime";
public static final String MOVINGTIME = "movingtime";
public static final String AVGSPEED = "avgspeed";
public static final String AVGMOVINGSPEED = "avgmovingspeed";
public static final String MAXSPEED = "maxspeed";
public static final String MINELEVATION = "minelevation";
public static final String MAXELEVATION = "maxelevation";
public static final String ELEVATIONGAIN = "elevationgain";
public static final String MINGRADE = "mingrade";
public static final String MAXGRADE = "maxgrade";
public static final String MINLAT = "minlat";
public static final String MAXLAT = "maxlat";
public static final String MINLON = "minlon";
public static final String MAXLON = "maxlon";
public static final String MAPID = "mapid";
public static final String[] BACKUP_COLUMNS = {
_ID, NAME, DESCRIPTION, CATEGORY, STARTID, STOPID, STARTTIME, STOPTIME,
NUMPOINTS, TOTALDISTANCE, TOTALTIME, MOVINGTIME, AVGSPEED, AVGMOVINGSPEED,
MAXSPEED, MINELEVATION, MAXELEVATION, ELEVATIONGAIN, MINGRADE, MAXGRADE,
MINLAT, MAXLAT, MINLON, MAXLON };
public static final byte[] 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 };
}
@@ -1,2 +0,0 @@
package com.google.android.apps.mytracks.content;
parcelable Waypoint;
@@ -1,223 +0,0 @@
/*
* Copyright 2009 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.content;
import com.google.android.apps.mytracks.stats.TripStatistics;
import android.location.Location;
import android.os.Parcel;
import android.os.Parcelable;
/**
* A way point. It has a location, meta data such as name, description,
* category, and icon, plus it can store track statistics for a "sub-track".
*
* TODO: hashCode and equals
*
* @author Leif Hendrik Wilden
* @author Rodrigo Damazio
*/
public final class Waypoint implements Parcelable {
/**
* Creator for a Waypoint object
*/
public static class Creator implements Parcelable.Creator<Waypoint> {
public Waypoint createFromParcel(Parcel source) {
ClassLoader classLoader = getClass().getClassLoader();
Waypoint waypoint = new Waypoint();
waypoint.id = source.readLong();
waypoint.name = source.readString();
waypoint.description = source.readString();
waypoint.category = source.readString();
waypoint.icon = source.readString();
waypoint.trackId = source.readLong();
waypoint.type = source.readInt();
waypoint.startId = source.readLong();
waypoint.stopId = source.readLong();
waypoint.stats = source.readParcelable(classLoader);
byte hasLocation = source.readByte();
if (hasLocation > 0) {
waypoint.location = source.readParcelable(classLoader);
}
return waypoint;
}
public Waypoint[] newArray(int size) {
return new Waypoint[size];
}
}
public static final Creator CREATOR = new Creator();
public static final int TYPE_WAYPOINT = 0;
public static final int TYPE_STATISTICS = 1;
private long id = -1;
private String name = "";
private String description = "";
private String category = "";
private String icon = "";
private long trackId = -1;
private int type = 0;
private Location location;
/** Start track point id */
private long startId = -1;
/** Stop track point id */
private long stopId = -1;
private TripStatistics stats = new TripStatistics();
/** The length of the track, without smoothing. */
private double length;
/** The total duration of the track (not from the last waypoint) */
private long duration;
public void writeToParcel(Parcel dest, int flags) {
dest.writeLong(id);
dest.writeString(name);
dest.writeString(description);
dest.writeString(category);
dest.writeString(icon);
dest.writeLong(trackId);
dest.writeInt(type);
dest.writeLong(startId);
dest.writeLong(stopId);
dest.writeParcelable(stats, 0);
dest.writeByte(location == null ? (byte) 0 : (byte) 1);
if (location != null) {
dest.writeParcelable(location, 0);
}
}
// Getters and setters:
//---------------------
public String getIcon() {
return icon;
}
public void setIcon(String icon) {
this.icon = icon;
}
public Location getLocation() {
return location;
}
public void setTrackId(long trackId) {
this.trackId = trackId;
}
public int describeContents() {
return 0;
}
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public long getTrackId() {
return trackId;
}
public int getType() {
return type;
}
public void setType(int type) {
this.type = type;
}
public long getStartId() {
return startId;
}
public void setStartId(long startId) {
this.startId = startId;
}
public long getStopId() {
return stopId;
}
public void setStopId(long stopId) {
this.stopId = stopId;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public String getCategory() {
return category;
}
public void setCategory(String category) {
this.category = category;
}
public void setLocation(Location location) {
this.location = location;
}
public TripStatistics getStatistics() {
return stats;
}
public void setStatistics(TripStatistics stats) {
this.stats = stats;
}
// WARNING: These fields are used for internal state keeping. You probably
// want to look at getStatistics instead.
public double getLength() {
return length;
}
public void setLength(double length) {
this.length = length;
}
public long getDuration() {
return duration;
}
public void setDuration(long duration) {
this.duration = duration;
}
}
@@ -1,86 +0,0 @@
/*
* Copyright 2009 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.content;
import static com.google.android.apps.mytracks.content.ContentTypeIds.*;
import android.net.Uri;
import android.provider.BaseColumns;
/**
* Defines the URI for the tracks provider and the available column names
* and content types.
*
* @author Leif Hendrik Wilden
*/
public interface WaypointsColumns extends BaseColumns {
public static final Uri CONTENT_URI =
Uri.parse("content://com.google.android.maps.mytracks/waypoints");
public static final String CONTENT_TYPE =
"vnd.android.cursor.dir/vnd.google.waypoint";
public static final String CONTENT_ITEMTYPE =
"vnd.android.cursor.item/vnd.google.waypoint";
public static final String DEFAULT_SORT_ORDER = "_id";
/* All columns */
public static final String NAME = "name";
public static final String DESCRIPTION = "description";
public static final String CATEGORY = "category";
public static final String ICON = "icon";
public static final String TRACKID = "trackid";
public static final String TYPE = "type";
public static final String LENGTH = "length";
public static final String DURATION = "duration";
public static final String STARTTIME = "starttime";
public static final String STARTID = "startid";
public static final String STOPID = "stopid";
public static final String LATITUDE = "latitude";
public static final String LONGITUDE = "longitude";
public static final String ALTITUDE = "elevation";
public static final String BEARING = "bearing";
public static final String TIME = "time";
public static final String ACCURACY = "accuracy";
public static final String SPEED = "speed";
public static final String TOTALDISTANCE = "totaldistance";
public static final String TOTALTIME = "totaltime";
public static final String MOVINGTIME = "movingtime";
public static final String AVGSPEED = "avgspeed";
public static final String AVGMOVINGSPEED = "avgmovingspeed";
public static final String MAXSPEED = "maxspeed";
public static final String MINELEVATION = "minelevation";
public static final String MAXELEVATION = "maxelevation";
public static final String ELEVATIONGAIN = "elevationgain";
public static final String MINGRADE = "mingrade";
public static final String MAXGRADE = "maxgrade";
public static final String[] BACKUP_COLUMNS = {
_ID, TRACKID, NAME, DESCRIPTION, CATEGORY, ICON, TYPE, LENGTH, DURATION,
STARTTIME, STARTID, STOPID, LATITUDE, LONGITUDE, ALTITUDE, BEARING, TIME,
ACCURACY, SPEED, TOTALDISTANCE, TOTALTIME, MOVINGTIME, AVGSPEED,
AVGMOVINGSPEED, MAXSPEED, MINELEVATION, MAXELEVATION, ELEVATIONGAIN,
MINGRADE, MAXGRADE };
public static final byte[] 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 };
}
@@ -342,8 +342,14 @@ public class GpxImporter extends DefaultHandler {
// create new location and set attributes
Location loc = new Location(LocationManager.GPS_PROVIDER);
loc.setLatitude(Double.parseDouble(latitude));
loc.setLongitude(Double.parseDouble(longitude));
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;
}
@@ -472,9 +478,14 @@ public class GpxImporter extends DefaultHandler {
}
}
private void onAltitudeElementEnd() {
private void onAltitudeElementEnd() throws SAXException {
if (location != null) {
location.setAltitude(Double.parseDouble(content));
try {
location.setAltitude(Double.parseDouble(content));
} catch (NumberFormatException e) {
String msg = createErrorMessage("Unable to parse altitude: " + content);
throw new SAXException(msg, e);
}
}
}
@@ -15,8 +15,11 @@
*/
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;
@@ -141,6 +144,37 @@ public class TcxTrackWriter implements TrackFormatWriter {
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>");
}
@@ -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 };
}
@@ -153,16 +153,16 @@ class ExternalFileBackup {
// Create all the auxiliary classes that will do the writing
PreferenceBackupHelper preferencesHelper = new PreferenceBackupHelper();
DatabaseDumper trackDumper = new DatabaseDumper(
TracksColumns.BACKUP_COLUMNS,
TracksColumns.BACKUP_COLUMN_TYPES,
BackupColumns.TRACKS_BACKUP_COLUMNS,
BackupColumns.TRACKS_BACKUP_COLUMN_TYPES,
false);
DatabaseDumper waypointDumper = new DatabaseDumper(
WaypointsColumns.BACKUP_COLUMNS,
WaypointsColumns.BACKUP_COLUMN_TYPES,
BackupColumns.WAYPOINTS_BACKUP_COLUMNS,
BackupColumns.WAYPOINTS_BACKUP_COLUMN_TYPES,
false);
DatabaseDumper pointDumper = new DatabaseDumper(
TrackPointsColumns.BACKUP_COLUMNS,
TrackPointsColumns.BACKUP_COLUMN_TYPES,
BackupColumns.POINTS_BACKUP_COLUMNS,
BackupColumns.POINTS_BACKUP_COLUMN_TYPES,
false);
// Open the target for writing
@@ -1,87 +0,0 @@
/*
* Copyright 2008 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.services;
import com.google.android.apps.mytracks.content.Waypoint;
/**
* MyTracks service.
* This service is the process that actually records and manages tracks.
*/
interface ITrackRecordingService {
/**
* Starts recording a new track.
*
* @return the track ID of the new track
*/
long startNewTrack();
/**
* Checks and returns whether we're currently recording a track.
*/
boolean isRecording();
/**
* Returns the track ID of the track currently being recorded, or -1 if none
* is being recorded. This ID can then be used to read track data from the
* content source.
*/
long getRecordingTrackId();
/**
* Inserts a waypoint marker in the track being recorded.
*
* @param waypoint the waypoint to insert
* @return the unique ID of the inserted marker
*/
long insertWaypointMarker(in Waypoint waypoint);
/**
* Inserts a statistics marker in the track being recorded.
*
* @param location the location at which to insert the marker
* @return the unique ID of the inserted marker
*/
long insertStatisticsMarker(in Location location);
/**
* Inserts a location in the track being recorded.
*
* When recording, locations detected by the GPS are already automatically
* added to the track, so this should be used only for adding special points
* or for testing.
*
* @param loc the location to insert
*/
void recordLocation(in Location loc);
/**
* Stops recording the current track.
*/
void endCurrentTrack();
/**
* Returns whether at least one track has been or is being recorded (and was
* not deleted).
*/
boolean hasRecorded();
/**
* Deletes all the stored tracks.
*/
void deleteAllTracks();
}
@@ -40,7 +40,6 @@ public class PreferenceManager implements OnSharedPreferenceChangeListener {
private final String minRecordingIntervalKey;
private final String minRequiredAccuracyKey;
private final String recordingTrackKey;
private final String signalSamplingFrequencyKey;
private final String splitFrequencyKey;
public PreferenceManager(TrackRecordingService service) {
@@ -72,8 +71,6 @@ public class PreferenceManager implements OnSharedPreferenceChangeListener {
service.getString(R.string.min_required_accuracy_key);
recordingTrackKey =
service.getString(R.string.recording_track_key);
signalSamplingFrequencyKey =
service.getString(R.string.signal_sampling_frequency_key);
splitFrequencyKey =
service.getString(R.string.split_frequency_key);
@@ -163,10 +160,6 @@ public class PreferenceManager implements OnSharedPreferenceChangeListener {
service.getSplitManager().setSplitFrequency(
sharedPreferences.getInt(splitFrequencyKey, 0));
}
if (key == null || key.equals(signalSamplingFrequencyKey)) {
service.getSignalManager().setFrequency(
sharedPreferences.getInt(signalSamplingFrequencyKey, -1), service);
}
if (key == null || key.equals(metricUnitsKey)) {
service.getSplitManager().setMetricUnits(
sharedPreferences.getBoolean(metricUnitsKey, true));
@@ -1,195 +0,0 @@
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.services;
import com.google.android.apps.mytracks.MyTracksConstants;
import com.google.android.apps.mytracks.content.Waypoint;
import android.content.Context;
import android.location.Location;
import android.telephony.CellLocation;
import android.telephony.NeighboringCellInfo;
import android.telephony.PhoneStateListener;
import android.telephony.TelephonyManager;
import android.util.Log;
import java.util.List;
/**
* A class to monitor the network signal strength.
*
* @author Sandor Dornbush
*/
public class SignalStrengthTask extends PhoneStateListener
implements PeriodicTask {
private final Context context;
private TelephonyManager manager;
private int signalStrength = -1;
public SignalStrengthTask(Context c) {
context = c;
}
@Override
public void start() {
manager =
(TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
if (manager == null) {
Log.e(MyTracksConstants.TAG, "Cannot get telephony manager.");
} else {
manager.listen(this, getListenEvents());
}
}
protected int getListenEvents() {
return PhoneStateListener.LISTEN_SIGNAL_STRENGTHS;
}
@Override
public void onSignalStrengthChanged(int asu) {
this.signalStrength = asu;
Log.d(MyTracksConstants.TAG, "Signal Strength: " + signalStrength);
}
@Override
public void run(TrackRecordingService service) {
Log.d(MyTracksConstants.TAG, "Adding signal marker");
Location location = service.getLastLocation();
if (manager == null) {
Log.d(MyTracksConstants.TAG, "Adding signal marker: marker null");
return;
}
if (location == null) {
Log.d(MyTracksConstants.TAG, "Adding signal marker: location null");
return;
}
Waypoint wpt = new Waypoint();
wpt.setName("Signal Strength");
wpt.setType(Waypoint.TYPE_WAYPOINT);
wpt.setTrackId(service.getRecordingTrackId());
int networkType = manager.getNetworkType();
wpt.setIcon(getIcon(networkType));
wpt.setLocation(location);
wpt.setDescription(getDescription());
long waypointId = service.insertWaypointMarker(wpt);
if (waypointId >= 0) {
Log.d(MyTracksConstants.TAG, "Added signal marker");
} else {
Log.e(MyTracksConstants.TAG, "Cannot insert waypoint marker?");
}
}
/**
* Gets a human readable description for the network type.
*
* @param type The integer constant for the network type
* @return A human readable description of the network type
*/
protected String getTypeAsString(int type) {
switch (type) {
case TelephonyManager.NETWORK_TYPE_EDGE:
return "EDGE";
case TelephonyManager.NETWORK_TYPE_GPRS:
return "GPRS";
case TelephonyManager.NETWORK_TYPE_UMTS:
return "UMTS";
case TelephonyManager.NETWORK_TYPE_UNKNOWN:
default:
return "UNKNOWN";
}
}
/**
* Builds a description for the current signal strength.
*
* @return A human readable description of the network state
*/
private String getDescription() {
StringBuffer sb = new StringBuffer();
sb.append(getStrengthAsString());
sb.append("Network Type: ");
sb.append(getTypeAsString(manager.getNetworkType()));
sb.append('\n');
sb.append("Operator: ");
sb.append(manager.getNetworkOperatorName());
sb.append(" / ");
sb.append(manager.getNetworkOperator());
sb.append('\n');
sb.append("Roaming: ");
sb.append(manager.isNetworkRoaming());
sb.append('\n');
List<NeighboringCellInfo> infos = manager.getNeighboringCellInfo();
Log.i(MyTracksConstants.TAG, "Found " + infos.size() + " cells.");
if (infos.size() > 0) {
sb.append("Neighbors: ");
for (NeighboringCellInfo info : infos) {
sb.append(info.toString());
sb.append(' ');
}
sb.append('\n');
}
CellLocation cell = manager.getCellLocation();
if (cell != null) {
sb.append("Cell: ");
sb.append(cell.toString());
sb.append('\n');
}
return sb.toString();
}
/**
* Gets the url for the waypoint icon for the current network type.
*
* @param type The network type
* @return A url to a image to use as the waypoint icon
*/
protected String getIcon(int type) {
switch (type) {
case TelephonyManager.NETWORK_TYPE_GPRS:
case TelephonyManager.NETWORK_TYPE_EDGE:
return "http://maps.google.com/mapfiles/ms/micons/green.png";
case TelephonyManager.NETWORK_TYPE_UMTS:
return "http://maps.google.com/mapfiles/ms/micons/blue.png";
case TelephonyManager.NETWORK_TYPE_UNKNOWN:
default:
return "http://maps.google.com/mapfiles/ms/micons/red.png";
}
}
@Override
public void shutdown() {
if (manager != null) {
manager.listen(this, PhoneStateListener.LISTEN_NONE);
manager = null;
}
}
public String getStrengthAsString() {
return "Strength: " + signalStrength + "\n";
}
protected Context getContext() {
return context;
}
}
@@ -1,48 +0,0 @@
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.services;
import com.google.android.apps.mytracks.MyTracksConstants;
import com.google.android.apps.mytracks.util.ApiFeatures;
import android.content.Context;
import android.util.Log;
/**
* Factory for producing a proper {@link SignalStrengthTask} according to the
* current API level.
*
* @author Rodrigo Damazio
*/
public class SignalStrengthTaskFactory {
private final boolean hasModernSignalStrength;
SignalStrengthTaskFactory(ApiFeatures apiFeatures) {
this.hasModernSignalStrength = apiFeatures.hasModernSignalStrength();
}
public PeriodicTask create(Context context) {
if (hasModernSignalStrength) {
Log.d(MyTracksConstants.TAG,
"TrackRecordingService using modern signal strength api.");
return new SignalStrengthTaskModern(context);
} else {
Log.w(MyTracksConstants.TAG,
"TrackRecordingService using legacy signal strength api.");
return new SignalStrengthTask(context);
}
}
}
@@ -1,155 +0,0 @@
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.services;
import com.google.android.apps.mytracks.MyTracksConstants;
import com.google.android.maps.mytracks.R;
import android.content.Context;
import android.telephony.PhoneStateListener;
import android.telephony.SignalStrength;
import android.telephony.TelephonyManager;
import android.util.Log;
/**
* A class to monitor the network signal strength.
*
* @author Sandor Dornbush
*/
public class SignalStrengthTaskModern extends SignalStrengthTask {
private SignalStrength signalStrength = null;
public SignalStrengthTaskModern(Context c) {
super(c);
}
@Override
protected int getListenEvents() {
return PhoneStateListener.LISTEN_SIGNAL_STRENGTHS;
}
/**
* Gets a human readable description for the network type.
*
* @param type The integer constant for the network type
* @return A human readable description of the network type
*/
@Override
protected String getTypeAsString(int type) {
switch (type) {
case TelephonyManager.NETWORK_TYPE_1xRTT:
return "1xRTT";
case TelephonyManager.NETWORK_TYPE_CDMA:
return "CDMA";
case TelephonyManager.NETWORK_TYPE_EDGE:
return "EDGE";
case TelephonyManager.NETWORK_TYPE_EVDO_0:
return "EVDO 0";
case TelephonyManager.NETWORK_TYPE_EVDO_A:
return "EVDO A";
case TelephonyManager.NETWORK_TYPE_GPRS:
return "GPRS";
case TelephonyManager.NETWORK_TYPE_HSDPA:
return "HSDPA";
case TelephonyManager.NETWORK_TYPE_HSPA:
return "HSPA";
case TelephonyManager.NETWORK_TYPE_HSUPA:
return "HSUPA";
case TelephonyManager.NETWORK_TYPE_UMTS:
return "UMTS";
case TelephonyManager.NETWORK_TYPE_UNKNOWN:
default:
return "UNKNOWN";
}
}
/**
* Gets the url for the waypoint icon for the current network type.
*
* @param type The network type
* @return A url to a image to use as the waypoint icon
*/
@Override
protected String getIcon(int type) {
switch (type) {
case TelephonyManager.NETWORK_TYPE_1xRTT:
case TelephonyManager.NETWORK_TYPE_CDMA:
case TelephonyManager.NETWORK_TYPE_GPRS:
case TelephonyManager.NETWORK_TYPE_EDGE:
return "http://maps.google.com/mapfiles/ms/micons/green.png";
case TelephonyManager.NETWORK_TYPE_EVDO_0:
case TelephonyManager.NETWORK_TYPE_EVDO_A:
case TelephonyManager.NETWORK_TYPE_HSDPA:
case TelephonyManager.NETWORK_TYPE_HSPA:
case TelephonyManager.NETWORK_TYPE_HSUPA:
case TelephonyManager.NETWORK_TYPE_UMTS:
return "http://maps.google.com/mapfiles/ms/micons/blue.png";
case TelephonyManager.NETWORK_TYPE_UNKNOWN:
default:
return "http://maps.google.com/mapfiles/ms/micons/red.png";
}
}
@Override
public String getStrengthAsString() {
if (signalStrength == null) {
return "Strength: " + getContext().getString(R.string.unknown) + "\n";
}
StringBuffer sb = new StringBuffer();
if (signalStrength.isGsm()) {
appendSignal(signalStrength.getGsmSignalStrength(),
R.string.gsm_strength,
sb);
maybeAppendSignal(signalStrength.getGsmBitErrorRate(),
R.string.error_rate,
sb);
} else {
appendSignal(signalStrength.getCdmaDbm(), R.string.cdma_strength, sb);
appendSignal(signalStrength.getCdmaEcio() / 10.0, R.string.ecio, sb);
appendSignal(signalStrength.getEvdoDbm(), R.string.evdo_strength, sb);
appendSignal(signalStrength.getEvdoEcio() / 10.0, R.string.ecio, sb);
appendSignal(signalStrength.getEvdoSnr(),
R.string.signal_to_noise_ratio,
sb);
}
return sb.toString();
}
private void maybeAppendSignal(
int signal, int signalFormat, StringBuffer sb) {
if (signal > 0) {
sb.append(getContext().getString(signalFormat, signal));
}
}
private void appendSignal(int signal, int signalFormat, StringBuffer sb) {
sb.append(getContext().getString(signalFormat, signal));
sb.append("\n");
}
private void appendSignal(double signal, int signalFormat, StringBuffer sb) {
sb.append(getContext().getString(signalFormat, signal));
sb.append("\n");
}
@Override
public void onSignalStrengthsChanged(SignalStrength signalStrength) {
Log.d(MyTracksConstants.TAG, "Signal Strength Modern: " + signalStrength);
this.signalStrength = signalStrength;
}
}
@@ -20,11 +20,16 @@ import static com.google.android.apps.mytracks.MyTracksConstants.RESUME_TRACK_EX
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.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.TracksColumns;
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.sensors.SensorManager;
import com.google.android.apps.mytracks.services.sensors.SensorManagerFactory;
import com.google.android.apps.mytracks.stats.TripStatistics;
import com.google.android.apps.mytracks.stats.TripStatisticsBuilder;
import com.google.android.apps.mytracks.util.ApiFeatures;
@@ -69,7 +74,7 @@ public class TrackRecordingService extends Service implements LocationListener {
"http://maps.google.com/mapfiles/ms/micons/ylw-pushpin.png";
static final int MAX_AUTO_RESUME_TRACK_RETRY_ATTEMPTS = 3;
private NotificationManager notificationManager;
private LocationManager locationManager;
private WakeLock wakeLock;
@@ -109,9 +114,10 @@ public class TrackRecordingService extends Service implements LocationListener {
* Status announcer executer.
*/
private PeriodicTaskExecuter announcementExecuter;
private TaskExecuterManager signalManager;
private SplitManager splitManager;
private SensorManager sensorManager;
private PreferenceManager prefManager;
/**
@@ -215,7 +221,14 @@ public class TrackRecordingService extends Service implements LocationListener {
// Insert the new location:
try {
Uri pointUri = providerUtils.insertTrackPoint(location, trackId);
Location locationToInsert = location;
if (sensorManager != null && sensorManager.isEnabled()) {
SensorDataSet sd = sensorManager.getSensorDataSet();
if (sd != null && sensorManager.isDataValid()) {
locationToInsert = new MyTracksLocation(location, sd);
}
}
Uri pointUri = providerUtils.insertTrackPoint(locationToInsert, trackId);
int pointId = Integer.parseInt(pointUri.getLastPathSegment());
// Update the current track:
@@ -411,7 +424,6 @@ public class TrackRecordingService extends Service implements LocationListener {
statsBuilder = new TripStatisticsBuilder(stats.getStartTime());
setUpAnnouncer();
signalManager.restore();
splitManager.restore();
length = 0;
lastValidLocation = null;
@@ -525,11 +537,15 @@ public class TrackRecordingService extends Service implements LocationListener {
if (lastLocation != null) {
distanceToLast = location.distanceTo(lastLocation);
}
boolean hasSensorData = sensorManager != null
&& sensorManager.isEnabled()
&& sensorManager.getSensorDataSet() != null
&& sensorManager.isDataValid();
// If the user has been stationary for two recording just record the first
// two and ignore the rest. This code will only have an effect if the
// maxRecordingDistance = 0
if (distanceToLast == 0) {
if (distanceToLast == 0 && !hasSensorData) {
if (isMoving) {
Log.d(MyTracksConstants.TAG, "Found two identical locations.");
isMoving = false;
@@ -549,7 +565,8 @@ public class TrackRecordingService extends Service implements LocationListener {
Log.d(MyTracksConstants.TAG,
"Not recording. More than two identical locations.");
}
} else if (distanceToLastRecorded > minRecordingDistance) {
} else if (distanceToLastRecorded > minRecordingDistance
|| hasSensorData) {
if (lastLocation != null && !isMoving) {
// Last location was the last stationary location. Need to go back and
// add it.
@@ -629,14 +646,11 @@ public class TrackRecordingService extends Service implements LocationListener {
locationManager = (LocationManager) getSystemService(LOCATION_SERVICE);
splitManager = new SplitManager(this);
SignalStrengthTaskFactory strengthTaskFactory =
new SignalStrengthTaskFactory(ApiFeatures.getInstance());
signalManager =
new TaskExecuterManager(-1, strengthTaskFactory.create(this), this);
sensorManager = SensorManagerFactory.getSensorManager(this);
prefManager = new PreferenceManager(this);
registerLocationListener();
/**
/*
* After 5 min, check every minute that location listener still is
* registered and spit out additional debugging info to the logs:
*/
@@ -710,17 +724,27 @@ public class TrackRecordingService extends Service implements LocationListener {
showNotification();
prefManager.shutdown();
prefManager = null;
binder = null;
checkLocationListener.cancel();
checkLocationListener = null;
timer.cancel();
timer.purge();
unregisterLocationListener();
shutdownAnnouncer();
signalManager.shutdown();
signalManager = null;
splitManager.shutdown();
splitManager = null;
if (sensorManager != null) {
sensorManager.onDestroy();
sensorManager = null;
}
// Make sure we have no indirect references to this service.
locationManager = null;
notificationManager = null;
providerUtils = null;
binder.detachFromService();
binder = null;
// This should be the last operation.
releaseWakeLock();
super.onDestroy();
@@ -841,15 +865,25 @@ public class TrackRecordingService extends Service implements LocationListener {
throw new IllegalStateException(
"Unable to insert waypoint marker while not recording!");
}
if (waypoint.getLocation() != null) {
waypoint.setLength(length);
waypoint.setDuration(waypoint.getLocation().getTime()
- statsBuilder.getStatistics().getStartTime());
Uri uri = providerUtils.insertWaypoint(waypoint);
return Long.parseLong(uri.getLastPathSegment());
if (waypoint.getLocation() == null) {
if (lastValidLocation == null) {
Log.w(MyTracksConstants.TAG, "Cannot insert waypoint with no location");
return -1;
}
waypoint.setLocation(lastValidLocation);
}
return -1;
if (waypoint.getTrackId() < 0) {
waypoint.setTrackId(recordingTrackId);
}
waypoint.setLength(length);
waypoint.setDuration(waypoint.getLocation().getTime()
- statsBuilder.getStatistics().getStartTime());
Uri uri = providerUtils.insertWaypoint(waypoint);
return Long.parseLong(uri.getLastPathSegment());
}
/**
@@ -896,82 +930,153 @@ public class TrackRecordingService extends Service implements LocationListener {
updateCurrentWaypoint();
return Long.parseLong(uri.getLastPathSegment());
}
private ServiceBinder binder = new ServiceBinder(this);
/**
* The ITrackRecordingService is defined through IDL.
* TODO: There is a bug in Android that leaks Binder instances. This bug is
* especially visible if we have a non-static class, as there is no way to
* nullify reference to the outer class (the service).
* A workaround is to use a static class and explicitly clear service
* and detach it from the underlying Binder. With this approach, we minimize
* the leak to 24 bytes per each service instance.
*
* For more details, see the following bug:
* http://code.google.com/p/android/issues/detail?id=6426.
*/
private ITrackRecordingService.Stub binder =
new ITrackRecordingService.Stub() {
@Override
public boolean isRecording() {
return TrackRecordingService.this.isRecording();
}
private static class ServiceBinder extends ITrackRecordingService.Stub {
private TrackRecordingService service;
public ServiceBinder(TrackRecordingService service) {
this.service = service;
}
/**
* Clears the reference to the outer class to minimize the leak.
*/
public void detachFromService() {
this.service = null;
attachInterface(null, null);
}
@Override
public boolean isRecording() {
if (service == null) {
throw new IllegalStateException("The service has been already detached!");
}
return service.isRecording();
}
@Override
public long getRecordingTrackId() {
return recordingTrackId;
}
@Override
public long getRecordingTrackId() {
if (service == null) {
throw new IllegalStateException("The service has been already detached!");
}
return service.recordingTrackId;
}
@Override
public boolean hasRecorded() {
return providerUtils.getLastTrackId() >= 0;
}
@Override
public boolean hasRecorded() {
if (service == null) {
throw new IllegalStateException("The service has been already detached!");
}
return service.providerUtils.getLastTrackId() >= 0;
}
@Override
public long startNewTrack() {
return TrackRecordingService.this.startNewTrack();
}
@Override
public long startNewTrack() {
if (service == null) {
throw new IllegalStateException("The service has been already detached!");
}
return service.startNewTrack();
}
/**
* Insert the given waypoint marker. Users can insert waypoint markers
* to tag locations with a name, description, category etc.
*
* @param waypoint a waypoint
* @return the unique id of the inserted marker
*/
@Override
public long insertWaypointMarker(Waypoint waypoint) {
return TrackRecordingService.this.insertWaypointMarker(waypoint);
}
/**
* Insert the given waypoint marker. Users can insert waypoint markers
* to tag locations with a name, description, category etc.
*
* @param waypoint a waypoint
* @return the unique id of the inserted marker
*/
@Override
public long insertWaypointMarker(Waypoint waypoint) {
if (service == null) {
throw new IllegalStateException("The service has been already detached!");
}
return service.insertWaypointMarker(waypoint);
}
/**
* Insert a statistics marker. A statistics marker holds the stats for
* the last segment up to this marker.
*
* @param location the location where to insert
* @return the unique id of the inserted marker
*/
@Override
public long insertStatisticsMarker(Location location) {
return TrackRecordingService.this.insertStatisticsMarker(location);
}
/**
* Insert a statistics marker. A statistics marker holds the stats for
* the last segment up to this marker.
*
* @param location the location where to insert
* @return the unique id of the inserted marker
*/
@Override
public long insertStatisticsMarker(Location location) {
if (service == null) {
throw new IllegalStateException("The service has been already detached!");
}
return service.insertStatisticsMarker(location);
}
@Override
public void endCurrentTrack() {
TrackRecordingService.this.endCurrentTrack();
}
@Override
public void endCurrentTrack() {
if (service == null) {
throw new IllegalStateException("The service has been already detached!");
}
service.endCurrentTrack();
}
@Override
public void deleteAllTracks() {
if (isRecording()) {
throw new IllegalStateException(
"Cannot delete all tracks while recording!");
}
providerUtils.deleteAllTracks();
}
@Override
public void deleteAllTracks() {
if (service == null) {
throw new IllegalStateException("The service has been already detached!");
}
if (isRecording()) {
throw new IllegalStateException("Cannot delete all tracks while recording!");
}
service.providerUtils.deleteAllTracks();
}
@Override
public void recordLocation(Location loc) {
onLocationChanged(loc);
}
};
@Override
public void recordLocation(Location loc) {
if (service == null) {
throw new IllegalStateException("The service has been already detached!");
}
service.onLocationChanged(loc);
}
@Override
public byte[] getSensorData() {
if (service.sensorManager == null) {
Log.d(MyTracksConstants.TAG, "No sensor manager for data.");
return null;
}
if (service.sensorManager.getSensorDataSet() == null) {
Log.d(MyTracksConstants.TAG, "Sensor data set is null.");
return null;
}
return service.sensorManager.getSensorDataSet().toByteArray();
}
@Override
public int getSensorState() {
if (service.sensorManager == null) {
Log.d(MyTracksConstants.TAG, "No sensor manager for data.");
return Sensor.SensorState.NONE.getNumber();
}
return service.sensorManager.getSensorState().getNumber();
}
}
public long startNewTrack() {
Log.d(MyTracksConstants.TAG, "TrackRecordingService.startNewTrack");
if (recordingTrackId != -1 || isRecording) {
throw new IllegalStateException("A track is already in progress!");
}
long startTime = System.currentTimeMillis();
acquireWakeLock();
@@ -996,14 +1101,20 @@ public class TrackRecordingService extends Service implements LocationListener {
showNotification();
registerLocationListener();
splitManager.restore();
signalManager.restore();
if (sensorManager != null) {
sensorManager.onStartTrack();
}
// Reset the number of auto-resume retries.
setAutoResumeTrackRetries(
getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0), 0);
// Persist the current recording track.
prefManager.setRecordingTrack(recordingTrackId);
// Notify the world that we're now recording.
sendTrackBroadcast(
R.string.track_started_broadcast_action, recordingTrackId);
return recordingTrackId;
}
@@ -1033,8 +1144,27 @@ public class TrackRecordingService extends Service implements LocationListener {
"_id=" + recordingTrack.getId(), null);
}
showNotification();
long recordedTrackId = recordingTrackId;
prefManager.setRecordingTrack(recordingTrackId = -1);
if (sensorManager != null) {
sensorManager.shutdown();
}
releaseWakeLock();
// Notify the world that we're no longer recording.
sendTrackBroadcast(
R.string.track_stopped_broadcast_action, recordedTrackId);
}
private void sendTrackBroadcast(int actionResId, long trackId) {
Intent broadcastIntent =
new Intent()
.setAction(getString(actionResId))
.putExtra(getString(R.string.track_id_broadcast_extra), trackId);
sendBroadcast(broadcastIntent,
getString(R.string.broadcast_notifications_permission));
}
public TripStatistics getTripStatistics() {
@@ -1110,8 +1240,4 @@ public class TrackRecordingService extends Service implements LocationListener {
public SplitManager getSplitManager() {
return splitManager;
}
public TaskExecuterManager getSignalManager() {
return signalManager;
}
}
@@ -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);
}
}
}
}
@@ -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;
}
}
@@ -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 "";
}
}
}
@@ -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;
}
}
@@ -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());
}
}
@@ -1,111 +0,0 @@
/*
* Copyright 2009 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.stats;
/**
* A helper class that tracks a minimum and a maximum of a variable.
*
* @author Sandor Dornbush
*/
public class ExtremityMonitor {
/**
* The smallest value seen so far.
*/
private double min;
/**
* The largest value seen so far.
*/
private double max;
public ExtremityMonitor() {
reset();
}
/**
* Updates the min and the max with the new value.
*
* @param value the new value for the monitor
* @return true if an extremity was found
*/
public boolean update(double value) {
boolean changed = false;
if (value < min) {
min = value;
changed = true;
}
if (value > max) {
max = value;
changed = true;
}
return changed;
}
/**
* Gets the minimum value seen.
*
* @return The minimum value passed into the update() function
*/
public double getMin() {
return min;
}
/**
* Gets the maximum value seen.
*
* @return The maximum value passed into the update() function
*/
public double getMax() {
return max;
}
/**
* Resets this object to it's initial state where the min and max are unknown.
*/
public void reset() {
min = Double.POSITIVE_INFINITY;
max = Double.NEGATIVE_INFINITY;
}
/**
* Sets the minimum and maximum values.
*/
public void set(double min, double max) {
this.min = min;
this.max = max;
}
/**
* Sets the minimum value.
*/
public void setMin(double min) {
this.min = min;
}
/**
* Sets the maximum value.
*/
public void setMax(double max) {
this.max = max;
}
@Override
public String toString() {
return "Min: " + min + " Max: " + max;
}
}
@@ -1,2 +0,0 @@
package com.google.android.apps.mytracks.stats;
parcelable TripStatistics;
@@ -1,572 +0,0 @@
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.stats;
import android.os.Parcel;
import android.os.Parcelable;
/**
* Statistical data about a trip.
* The data in this class should be filled out by {@link TripStatisticsBuilder}.
*
* TODO: hashCode and equals
*
* @author Rodrigo Damazio
*/
public class TripStatistics implements Parcelable {
/**
* The start time for the trip. This is system time which might not match gps
* time.
*/
private long startTime = -1;
/**
* The stop time for the trip. This is the system time which might not match
* gps time.
*/
private long stopTime = -1;
/**
* The total time that we believe the user was traveling in milliseconds.
*/
private long movingTime;
/**
* The total time of the trip in milliseconds.
* This is only updated when new points are received, so it may be stale.
*/
private long totalTime;
/**
* The total distance in meters that the user traveled on this trip.
*/
private double totalDistance;
/**
* The total elevation gained on this trip in meters.
*/
private double totalElevationGain;
/**
* The maximum speed in meters/second reported that we believe to be a valid
* speed.
*/
private double maxSpeed;
/**
* The min and max latitude values seen in this trip.
*/
private final ExtremityMonitor latitudeExtremities = new ExtremityMonitor();
/**
* The min and max longitude values seen in this trip.
*/
private final ExtremityMonitor longitudeExtremities = new ExtremityMonitor();
/**
* The min and max elevation seen on this trip in meters.
*/
private final ExtremityMonitor elevationExtremities = new ExtremityMonitor();
/**
* The minimum and maximum grade calculations on this trip.
*/
private final ExtremityMonitor gradeExtremities = new ExtremityMonitor();
/**
* Default constructor.
*/
public TripStatistics() {
}
/**
* Copy constructor.
*
* @param other another statistics data object to copy from
*/
public TripStatistics(TripStatistics other) {
this.maxSpeed = other.maxSpeed;
this.movingTime = other.movingTime;
this.startTime = other.startTime;
this.stopTime = other.stopTime;
this.totalDistance = other.totalDistance;
this.totalElevationGain = other.totalElevationGain;
this.totalTime = other.totalTime;
this.latitudeExtremities.set(other.latitudeExtremities.getMin(),
other.latitudeExtremities.getMax());
this.longitudeExtremities.set(other.longitudeExtremities.getMin(),
other.longitudeExtremities.getMax());
this.elevationExtremities.set(other.elevationExtremities.getMin(),
other.elevationExtremities.getMax());
this.gradeExtremities.set(other.gradeExtremities.getMin(),
other.gradeExtremities.getMax());
}
/**
* Combines these statistics with those from another object.
* This assumes that the time periods covered by each do not intersect.
*
* @param other the other waypoint
*/
public void merge(TripStatistics other) {
startTime = Math.min(startTime, other.startTime);
stopTime = Math.max(stopTime, other.stopTime);
totalTime += other.totalTime;
movingTime += other.movingTime;
totalDistance += other.totalDistance;
totalElevationGain += other.totalElevationGain;
maxSpeed = Math.max(maxSpeed, other.maxSpeed);
latitudeExtremities.update(other.latitudeExtremities.getMax());
latitudeExtremities.update(other.latitudeExtremities.getMin());
longitudeExtremities.update(other.longitudeExtremities.getMax());
longitudeExtremities.update(other.longitudeExtremities.getMin());
elevationExtremities.update(other.elevationExtremities.getMax());
elevationExtremities.update(other.elevationExtremities.getMin());
gradeExtremities.update(other.gradeExtremities.getMax());
gradeExtremities.update(other.gradeExtremities.getMin());
}
/**
* Gets the time that this track started.
*
* @return The number of milliseconds since epoch to the time when this track
* started
*/
public long getStartTime() {
return startTime;
}
/**
* Gets the time that this track stopped.
*
* @return The number of milliseconds since epoch to the time when this track
* stopped
*/
public long getStopTime() {
return stopTime;
}
/**
* Gets the total time that this track has been active.
* This statistic is only updated when a new point is added to the statistics,
* so it may be off. If you need to calculate the proper total time, use
* {@link #getStartTime} with the current time.
*
* @return The total number of milliseconds the track was active
*/
public long getTotalTime() {
return totalTime;
}
/**
* Gets the total distance the user traveled.
*
* @return The total distance traveled in meters
*/
public double getTotalDistance() {
return totalDistance;
}
/**
* Gets the the average speed the user traveled.
* This calculation only takes into account the displacement until the last
* point that was accounted for in statistics.
*
* @return The average speed in m/s
*/
public double getAverageSpeed() {
return totalDistance / ((double) totalTime / 1000);
}
/**
* Gets the the average speed the user traveled when they were actively
* moving.
*
* @return The average moving speed in m/s
*/
public double getAverageMovingSpeed() {
return totalDistance / ((double) movingTime / 1000);
}
/**
* Gets the the maximum speed for this track.
*
* @return The maximum speed in m/s
*/
public double getMaxSpeed() {
return maxSpeed;
}
/**
* Gets the moving time.
*
* @return The total number of milliseconds the user was moving
*/
public long getMovingTime() {
return movingTime;
}
/**
* Gets the total elevation gain for this trip. This is calculated as the sum
* of all positive differences in the smoothed elevation.
*
* @return The elevation gain in meters for this trip
*/
public double getTotalElevationGain() {
return totalElevationGain;
}
/**
* Returns the leftmost position (lowest longitude) of the track, in signed
* decimal degrees.
*/
public int getLeft() {
return (int) (longitudeExtremities.getMin() * 1E6);
}
/**
* Returns the rightmost position (highest longitude) of the track, in signed
* decimal degrees.
*/
public int getRight() {
return (int) (longitudeExtremities.getMax() * 1E6);
}
/**
* Returns the bottommost position (lowest latitude) of the track, in meters.
*/
public int getBottom() {
return (int) (latitudeExtremities.getMin() * 1E6);
}
/**
* Returns the topmost position (highest latitude) of the track, in meters.
*/
public int getTop() {
return (int) (latitudeExtremities.getMax() * 1E6);
}
/**
* Gets the minimum elevation seen on this trip. This is calculated from the
* smoothed elevation so this can actually be more than the current elevation.
*
* @return The smallest elevation reading for this trip in meters
*/
public double getMinElevation() {
return elevationExtremities.getMin();
}
/**
* Gets the maximum elevation seen on this trip. This is calculated from the
* smoothed elevation so this can actually be less than the current elevation.
*
* @return The largest elevation reading for this trip in meters
*/
public double getMaxElevation() {
return elevationExtremities.getMax();
}
/**
* Gets the maximum grade for this trip.
*
* @return The maximum grade for this trip as a fraction
*/
public double getMaxGrade() {
return gradeExtremities.getMax();
}
/**
* Gets the minimum grade for this trip.
*
* @return The minimum grade for this trip as a fraction
*/
public double getMinGrade() {
return gradeExtremities.getMin();
}
// Setters - to be used when restoring state or loading from the DB
/**
* Sets the start time for this trip.
*
* @param startTime the start time, in milliseconds since the epoch
*/
public void setStartTime(long startTime) {
this.startTime = startTime;
}
/**
* Sets the stop time for this trip.
*
* @param stopTime the stop time, in milliseconds since the epoch
*/
public void setStopTime(long stopTime) {
this.stopTime = stopTime;
}
/**
* Sets the total moving time.
*
* @param movingTime the moving time in milliseconds
*/
public void setMovingTime(long movingTime) {
this.movingTime = movingTime;
}
/**
* Sets the total trip time.
*
* @param totalTime the total trip time in milliseconds
*/
public void setTotalTime(long totalTime) {
this.totalTime = totalTime;
}
/**
* Sets the total trip distance.
*
* @param totalDistance the trip distance in meters
*/
public void setTotalDistance(double totalDistance) {
this.totalDistance = totalDistance;
}
/**
* Sets the total elevation variation during the trip.
*
* @param totalElevationGain the elevation variation in meters
*/
public void setTotalElevationGain(double totalElevationGain) {
this.totalElevationGain = totalElevationGain;
}
/**
* Sets the maximum speed reached during the trip.
*
* @param maxSpeed the maximum speed in meters per second
*/
public void setMaxSpeed(double maxSpeed) {
this.maxSpeed = maxSpeed;
}
/**
* Sets the minimum elevation reached during the trip.
*
* @param elevation the minimum elevation in meters
*/
public void setMinElevation(double elevation) {
elevationExtremities.setMin(elevation);
}
/**
* Sets the maximum elevation reached during the trip.
*
* @param elevation the maximum elevation in meters
*/
public void setMaxElevation(double elevation) {
elevationExtremities.setMax(elevation);
}
/**
* Sets the minimum grade obtained during the trip.
*
* @param grade the grade as a fraction (-1.0 would mean vertical downwards)
*/
public void setMinGrade(double grade) {
gradeExtremities.setMin(grade);
}
/**
* Sets the maximum grade obtained during the trip).
*
* @param grade the grade as a fraction (1.0 would mean vertical upwards)
*/
public void setMaxGrade(double grade) {
gradeExtremities.setMax(grade);
}
/**
* Sets the bounding box for this trip.
* The unit for all parameters is signed decimal degrees (degrees * 1E6).
*
* @param leftE6 the westmost longitude reached
* @param topE6 the northmost latitude reached
* @param rightE6 the eastmost longitude reached
* @param bottomE6 the southmost latitude reached
*/
public void setBounds(int leftE6, int topE6, int rightE6, int bottomE6) {
latitudeExtremities.set(bottomE6 / 1E6, topE6 / 1E6);
longitudeExtremities.set(leftE6 / 1E6, rightE6 / 1E6);
}
// Data manipulation methods
/**
* Adds to the current total distance.
*
* @param distance the distance to add in meters
*/
void addTotalDistance(double distance) {
totalDistance += distance;
}
/**
* Adds to the total elevation variation.
*
* @param gain the elevation variation in meters
*/
void addTotalElevationGain(double gain) {
totalElevationGain += gain;
}
/**
* Adds to the total moving time of the trip.
*
* @param time the time in milliseconds
*/
void addMovingTime(long time) {
movingTime += time;
}
/**
* Accounts for a new latitude value for the bounding box.
*
* @param latitude the latitude value in signed decimal degrees
*/
void updateLatitudeExtremities(double latitude) {
latitudeExtremities.update(latitude);
}
/**
* Accounts for a new longitude value for the bounding box.
*
* @param longitude the longitude value in signed decimal degrees
*/
void updateLongitudeExtremities(double longitude) {
longitudeExtremities.update(longitude);
}
/**
* Accounts for a new elevation value for the bounding box.
*
* @param elevation the elevation value in meters
*/
void updateElevationExtremities(double elevation) {
elevationExtremities.update(elevation);
}
/**
* Accounts for a new grade value.
*
* @param grade the grade value as a fraction
*/
void updateGradeExtremities(double grade) {
gradeExtremities.update(grade);
}
// String conversion
@Override
public String toString() {
return "TripStatistics { Start Time: " + getStartTime()
+ "; Total Time: " + getTotalTime()
+ "; Moving Time: " + getMovingTime()
+ "; Total Distance: " + getTotalDistance()
+ "; Elevation Gain: " + getTotalElevationGain()
+ "; Min Elevation: " + getMinElevation()
+ "; Max Elevation: " + getMaxElevation()
+ "; Average Speed: " + getAverageMovingSpeed()
+ "; Min Grade: " + getMinGrade()
+ "; Max Grade: " + getMaxGrade()
+ "}";
}
// Parcelable interface and creator
/**
* Creator of statistics data from parcels.
*/
public static class Creator
implements Parcelable.Creator<TripStatistics> {
@Override
public TripStatistics createFromParcel(Parcel source) {
TripStatistics data = new TripStatistics();
data.startTime = source.readLong();
data.movingTime = source.readLong();
data.totalTime = source.readLong();
data.totalDistance = source.readDouble();
data.totalElevationGain = source.readDouble();
data.maxSpeed = source.readDouble();
double minLat = source.readDouble();
double maxLat = source.readDouble();
data.latitudeExtremities.set(minLat, maxLat);
double minLong = source.readDouble();
double maxLong = source.readDouble();
data.longitudeExtremities.set(minLong, maxLong);
double minElev = source.readDouble();
double maxElev = source.readDouble();
data.elevationExtremities.set(minElev, maxElev);
double minGrade = source.readDouble();
double maxGrade = source.readDouble();
data.gradeExtremities.set(minGrade, maxGrade);
return data;
}
@Override
public TripStatistics[] newArray(int size) {
return new TripStatistics[size];
}
}
/**
* Creator of {@link TripStatistics} from parcels.
*/
public static final Creator CREATOR = new Creator();
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeLong(startTime);
dest.writeLong(movingTime);
dest.writeLong(totalTime);
dest.writeDouble(totalDistance);
dest.writeDouble(totalElevationGain);
dest.writeDouble(maxSpeed);
dest.writeDouble(latitudeExtremities.getMin());
dest.writeDouble(latitudeExtremities.getMax());
dest.writeDouble(longitudeExtremities.getMin());
dest.writeDouble(longitudeExtremities.getMax());
dest.writeDouble(elevationExtremities.getMin());
dest.writeDouble(elevationExtremities.getMax());
dest.writeDouble(gradeExtremities.getMin());
dest.writeDouble(gradeExtremities.getMax());
}
}
@@ -102,10 +102,6 @@ public class ApiFeatures {
return true;
}
public boolean hasModernSignalStrength() {
return getApiLevel() >= 7;
}
public boolean hasStrictMode() {
return getApiLevel() >= 9;
}
@@ -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;
}
}