Add HTTP status code to the error message

This commit is contained in:
Matthew Simmons
2011-02-09 13:27:20 -05:00
commit 36c425d699
365 changed files with 47654 additions and 0 deletions
@@ -0,0 +1,59 @@
/*
* Copyright 2009 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.services;
/**
* This is a simple location listener policy that will always dictate the same
* polling interval.
*
* @author Sandor Dornbush
*/
public class AbsoluteLocationListenerPolicy implements LocationListenerPolicy {
/**
* The interval to request for gps signal.
*/
private final long interval;
/**
* @param interval The interval to request for gps signal
*/
public AbsoluteLocationListenerPolicy(final long interval) {
this.interval = interval;
}
/**
* @return The interval given in the constructor
*/
public long getDesiredPollingInterval() {
return interval;
}
/**
* Discards the idle time.
*/
public void updateIdleTime(long idleTime) {
}
/**
* Returns the minimum distance between updates.
* Get all updates to properly measure moving time.
*/
public int getMinDistance() {
return 0;
}
}
@@ -0,0 +1,81 @@
/*
* Copyright 2009 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.services;
/**
* A LocationListenerPolicy that will change based on how long the user has been
* stationary.
*
* This policy will dictate a policy based on a min, max and idle time.
* The policy will dictate an interval bounded by min and max whic is half of
* the idle time.
*
* @author Sandor Dornbush
*/
public class AdaptiveLocationListenerPolicy implements LocationListenerPolicy {
/**
* Smallest interval this policy will dictate, in milliseconds.
*/
private final long minInterval;
/**
* Largest interval this policy will dictate, in milliseconds.
*/
private final long maxInterval;
private final int minDistance;
/**
* The time the user has been at the current location, in milliseconds.
*/
private long idleTime;
/**
* Creates a policy that will be bounded by the given min and max.
*
* @param min Smallest interval this policy will dictate, in milliseconds
* @param max Largest interval this policy will dictate, in milliseconds
*/
public AdaptiveLocationListenerPolicy(long min, long max, int minDistance) {
this.minInterval = min;
this.maxInterval = max;
this.minDistance = minDistance;
}
/**
* @return An interval bounded by min and max which is half of the idle time
*/
public long getDesiredPollingInterval() {
long desiredInterval = idleTime / 2;
// Round to avoid setting the interval too often.
desiredInterval = (desiredInterval / 1000) * 1000;
return Math.max(Math.min(maxInterval, desiredInterval),
minInterval);
}
public void updateIdleTime(long newIdleTime) {
this.idleTime = newIdleTime;
}
/**
* Returns the minimum distance between updates.
*/
public int getMinDistance() {
return minDistance;
}
}
@@ -0,0 +1,65 @@
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.services;
import com.google.android.apps.mytracks.MyTracksSettings;
import com.google.android.maps.mytracks.R;
import android.content.Context;
import android.content.SharedPreferences;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* Creates a default track name based on the current default track name policy.
*
* @author Matthew Simmons
*/
class DefaultTrackNameFactory {
private static final String TIMESTAMP_DATE_FORMAT = "yyyy-MM-dd HH:mm";
private final Context context;
DefaultTrackNameFactory(Context context) {
this.context = context;
}
/**
* Creates a new track name.
*
* @param trackId The ID for the current track.
* @param startTime The start time, in milliseconds since the epoch, of the
* current track.
* @return The new track name.
*/
String newTrackName(long trackId, long startTime) {
if (useTimestampTrackName()) {
SimpleDateFormat formatter = new SimpleDateFormat(TIMESTAMP_DATE_FORMAT);
return formatter.format(new Date(startTime));
} else {
return String.format(context.getString(R.string.new_track), trackId);
}
}
/** Determines whether the preferences allow a timestamp-based track name */
protected boolean useTimestampTrackName() {
SharedPreferences prefs =
context.getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0);
return prefs.getBoolean(
context.getString(R.string.timestamp_track_name_key), true);
}
}
@@ -0,0 +1,48 @@
/*
* Copyright 2009 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.services;
/**
* This is an interface for classes that will manage the location listener policy.
* Different policy options are:
* Absolute
* Addaptive
*
* @author Sandor Dornbush
*/
public interface LocationListenerPolicy {
/**
* Returns the polling time this policy would like at this time.
*
* @return The polling that this policy dictates
*/
public long getDesiredPollingInterval();
/**
* Returns the minimum distance between updates.
*/
public int getMinDistance();
/**
* Notifies the amount of time the user has been idle at their current
* location.
*
* @param idleTime The time that the user has been idle at this spot
*/
public void updateIdleTime(long idleTime);
}
@@ -0,0 +1,39 @@
/*
* Copyright 2009 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.services;
/**
* This is interface for a task that will be executed on some schedule.
*
* @author Sandor Dornbush
*/
public interface PeriodicTask {
/**
* Sets up this task for subsequent calls to the run method.
*/
public void start();
/**
* This method will be called periodically.
*/
public void run(TrackRecordingService service);
/**
* Shuts down this task and clean up resources.
*/
public void shutdown();
}
@@ -0,0 +1,109 @@
/*
* Copyright 2009 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.services;
import com.google.android.apps.mytracks.MyTracksConstants;
import android.util.Log;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;
/**
* This class will periodically announce the user's trip statistics.
*
* @author Sandor Dornbush
*/
public class PeriodicTaskExecuter {
private final PeriodicTask task;
private final TrackRecordingService service;
/**
* A timer to schedule the announcements.
* This is non-null if the task is in started (scheduled) state.
*/
private Timer timer;
public PeriodicTaskExecuter(PeriodicTask task,
TrackRecordingService service) {
this.task = task;
this.service = service;
}
/**
* Schedules the task at the given interval.
*
* @param interval The interval in milliseconds
*/
public void scheduleTask(long interval) {
// TODO: Decouple service from this class once and forever.
if (!service.isRecording()) {
return;
}
if (timer != null) {
timer.cancel();
timer.purge();
} else {
// First start, or we were previously shut down.
task.start();
}
timer = new Timer();
if (interval <= 0) {
return;
}
long now = System.currentTimeMillis();
long next = service.getTripStatistics().getStartTime();
if (next < now) {
next = now + interval - ((now - next) % interval);
}
Date start = new Date(next);
Log.i(MyTracksConstants.TAG,
task.getClass().getSimpleName() + " scheduled to start at " + start
+ " every " + interval + " milliseconds.");
timer.scheduleAtFixedRate(new PeriodicTimerTask(), start, interval);
}
/**
* Cleans up this object.
*/
public void shutdown() {
Log.i(MyTracksConstants.TAG,
task.getClass().getSimpleName() + " shutting down.");
if (timer != null) {
timer.cancel();
timer.purge();
timer = null;
task.shutdown();
}
}
/**
* The timer task to announce the trip status.
*/
private class PeriodicTimerTask extends TimerTask {
@Override
public void run() {
task.run(service);
}
}
}
@@ -0,0 +1,187 @@
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.services;
import com.google.android.apps.mytracks.MyTracksConstants;
import com.google.android.apps.mytracks.MyTracksSettings;
import com.google.android.maps.mytracks.R;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.util.Log;
/**
* A class that manages reading the shared preferences for the service.
*
* @author Sandor Dornbush
*/
public class PreferenceManager implements OnSharedPreferenceChangeListener {
private TrackRecordingService service;
private SharedPreferences sharedPreferences;
private final String announcementFrequencyKey;
private final String autoResumeTrackCurrentRetryKey;
private final String autoResumeTrackTimeoutKey;
private final String maxRecordingDistanceKey;
private final String metricUnitsKey;
private final String minRecordingDistanceKey;
private final String minRecordingIntervalKey;
private final String minRequiredAccuracyKey;
private final String recordingTrackKey;
private final String splitFrequencyKey;
public PreferenceManager(TrackRecordingService service) {
this.service = service;
this.sharedPreferences = service.getSharedPreferences(
MyTracksSettings.SETTINGS_NAME, 0);
if (sharedPreferences == null) {
Log.w(MyTracksConstants.TAG,
"TrackRecordingService: Couldn't get shared preferences.");
throw new IllegalStateException("Couldn't get shared preferences");
}
sharedPreferences.registerOnSharedPreferenceChangeListener(this);
announcementFrequencyKey =
service.getString(R.string.announcement_frequency_key);
autoResumeTrackCurrentRetryKey =
service.getString(R.string.auto_resume_track_current_retry_key);
autoResumeTrackTimeoutKey =
service.getString(R.string.auto_resume_track_timeout_key);
maxRecordingDistanceKey =
service.getString(R.string.max_recording_distance_key);
metricUnitsKey =
service.getString(R.string.metric_units_key);
minRecordingDistanceKey =
service.getString(R.string.min_recording_distance_key);
minRecordingIntervalKey =
service.getString(R.string.min_recording_interval_key);
minRequiredAccuracyKey =
service.getString(R.string.min_required_accuracy_key);
recordingTrackKey =
service.getString(R.string.recording_track_key);
splitFrequencyKey =
service.getString(R.string.split_frequency_key);
// Refresh all properties.
onSharedPreferenceChanged(sharedPreferences, null);
}
/**
* Notifies that preferences have changed.
* Call this with key == null to update all preferences in one call.
*
* @param key the key that changed (may be null to update all preferences)
*/
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences,
String key) {
if (service == null) {
Log.w(MyTracksConstants.TAG,
"onSharedPreferenceChanged: a preference change (key = " + key
+ ") after a call to shutdown()");
return;
}
if (key == null || key.equals(minRecordingDistanceKey)) {
service.setMinRecordingDistance(
sharedPreferences.getInt(
minRecordingDistanceKey,
MyTracksSettings.DEFAULT_MIN_RECORDING_DISTANCE));
Log.d(MyTracksConstants.TAG,
"TrackRecordingService: minRecordingDistance = "
+ service.getMinRecordingDistance());
}
if (key == null || key.equals(maxRecordingDistanceKey)) {
service.setMaxRecordingDistance(sharedPreferences.getInt(
maxRecordingDistanceKey,
MyTracksSettings.DEFAULT_MAX_RECORDING_DISTANCE));
}
if (key == null || key.equals(minRecordingIntervalKey)) {
int minRecordingInterval = sharedPreferences.getInt(
minRecordingIntervalKey,
MyTracksSettings.DEFAULT_MIN_RECORDING_INTERVAL);
switch (minRecordingInterval) {
case -2:
// Battery Miser
// min: 30 seconds
// max: 5 minutes
// minDist: 5 meters Choose battery life over moving time accuracy.
service.setLocationListenerPolicy(
new AdaptiveLocationListenerPolicy(30000, 300000, 5));
break;
case -1:
// High Accuracy
// min: 1 second
// max: 30 seconds
// minDist: 0 meters get all updates to properly measure moving time.
service.setLocationListenerPolicy(
new AdaptiveLocationListenerPolicy(1000, 30000, 0));
break;
default:
service.setLocationListenerPolicy(
new AbsoluteLocationListenerPolicy(minRecordingInterval * 1000));
}
}
if (key == null || key.equals(minRequiredAccuracyKey)) {
service.setMinRequiredAccuracy(sharedPreferences.getInt(
minRequiredAccuracyKey,
MyTracksSettings.DEFAULT_MIN_REQUIRED_ACCURACY));
}
if (key == null || key.equals(announcementFrequencyKey)) {
service.setAnnouncementFrequency(
sharedPreferences.getInt(announcementFrequencyKey, -1));
}
if (key == null || key.equals(autoResumeTrackTimeoutKey)) {
service.setAutoResumeTrackTimeout(sharedPreferences.getInt(
autoResumeTrackTimeoutKey,
MyTracksSettings.DEFAULT_AUTO_RESUME_TRACK_TIMEOUT));
}
if (key == null || key.equals(recordingTrackKey)) {
long recordingTrackId = sharedPreferences.getLong(recordingTrackKey, -1);
// Only read the id if it is valid.
// Setting it to -1 should only happen in
// TrackRecordingService.endCurrentTrack()
if (recordingTrackId > 0) {
service.setRecordingTrackId(recordingTrackId);
}
}
if (key == null || key.equals(splitFrequencyKey)) {
service.getSplitManager().setSplitFrequency(
sharedPreferences.getInt(splitFrequencyKey, 0));
}
if (key == null || key.equals(metricUnitsKey)) {
service.getSplitManager().setMetricUnits(
sharedPreferences.getBoolean(metricUnitsKey, true));
}
}
public void setAutoResumeTrackCurrentRetry(int retryAttempts) {
sharedPreferences
.edit()
.putInt(autoResumeTrackCurrentRetryKey, retryAttempts)
.commit();
}
public void setRecordingTrack(long id) {
sharedPreferences
.edit()
.putLong(recordingTrackKey, id)
.commit();
}
public void shutdown() {
sharedPreferences.unregisterOnSharedPreferenceChangeListener(this);
service = null;
}
}
@@ -0,0 +1,160 @@
/*
* 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.WaypointCreationRequest;
import com.google.android.apps.mytracks.util.UnitConversions;
import android.util.Log;
/**
* This class manages inserting time or distance splits.
*
* @author Sandor Dornbush
*/
public class SplitManager {
/**
* The frequency of splits.
*/
private int splitFrequency = 0;
/**
* The next distance when a split should be taken.
*/
private double nextSplitDistance = 0;
/**
* Splits executer.
*/
private PeriodicTaskExecuter splitExecuter = null;
private boolean metricUnits;
private final TrackRecordingService service;
public SplitManager(TrackRecordingService service) {
this.service = service;
}
/**
* Restores the manager.
*/
public void restore() {
if ((splitFrequency > 0) && (splitExecuter != null)) {
splitExecuter.scheduleTask(splitFrequency * 60000);
}
calculateNextSplit();
}
/**
* Shuts down the manager.
*/
public void shutdown() {
if (splitExecuter != null) {
splitExecuter.shutdown();
}
}
/**
* Calculates the next distance that a split should be inserted at.
*/
public void calculateNextSplit() {
// TODO: Decouple service from this class once and forever.
if (!service.isRecording()) {
return;
}
if (splitFrequency >= 0) {
nextSplitDistance = Double.MAX_VALUE;
Log.d(MyTracksConstants.TAG,
"SplitManager: Distance splits disabled.");
return;
}
double dist = service.getTripStatistics().getTotalDistance() / 1000;
if (!metricUnits) {
dist *= UnitConversions.KM_TO_MI;
}
// The index will be negative since the frequency is negative.
int index = (int) (dist / splitFrequency);
index -= 1;
nextSplitDistance = splitFrequency * index;
Log.d(MyTracksConstants.TAG,
"SplitManager: Next split distance: " + nextSplitDistance);
}
/**
* Updates split manager with new trip statistics.
*/
public void updateSplits() {
if (this.splitFrequency >= 0) {
return;
}
// Convert the distance in meters to km or mi.
double distance = service.getTripStatistics().getTotalDistance() / 1000.0;
if (!metricUnits) {
distance *= UnitConversions.KM_TO_MI;
}
if (distance > this.nextSplitDistance) {
service.insertWaypoint(WaypointCreationRequest.DEFAULT_STATISTICS);
calculateNextSplit();
}
}
/**
* Sets the split frequency.
* &lt; 0 Use the absolute value as a distance in the current measurement km
* or mi
* 0 Turn off splits
* &gt; 0 Use the value as a time in minutes
* @param splitFrequency The frequency in time or distance
*/
public void setSplitFrequency(int splitFrequency) {
Log.d(MyTracksConstants.TAG,
"setSplitFrequency: splitFrequency = " + splitFrequency);
this.splitFrequency = splitFrequency;
// TODO: Decouple service from this class once and forever.
if (!service.isRecording()) {
return;
}
if (splitFrequency < 1) {
if (splitExecuter != null) {
splitExecuter.shutdown();
splitExecuter = null;
}
}
if (splitFrequency > 0) {
if (splitExecuter == null) {
TimeSplitTask splitter = new TimeSplitTask();
splitExecuter = new PeriodicTaskExecuter(splitter, service);
}
splitExecuter.scheduleTask(splitFrequency * 60000);
} else {
// For distance based splits.
calculateNextSplit();
}
}
public void setMetricUnits(boolean metricUnits) {
this.metricUnits = metricUnits;
calculateNextSplit();
}
}
@@ -0,0 +1,61 @@
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.services;
import com.google.android.apps.mytracks.util.ApiFeatures;
import android.content.Context;
import android.media.AudioManager;
/**
* Factory which wraps construction and setup of text-to-speech announcements in
* an API-level-safe way.
*
* @author Rodrigo Damazio
*/
public class StatusAnnouncerFactory {
private final boolean hasTts;
public StatusAnnouncerFactory(ApiFeatures apiFeatures) {
this.hasTts = apiFeatures.hasTextToSpeech();
}
/**
* Creates a periodic task which does voice announcements.
*
* @return the task, or null if announcements are not supported
*/
public PeriodicTask create(Context context) {
if (hasTts) {
return new StatusAnnouncerTask(context);
} else {
return null;
}
}
/**
* Returns the appropriate volume stream for controlling announcement
* volume.
*/
public int getVolumeStream() {
if (hasTts) {
return StatusAnnouncerTask.getVolumeStream();
} else {
return AudioManager.USE_DEFAULT_STREAM_TYPE;
}
}
}
@@ -0,0 +1,279 @@
/*
* Copyright 2009 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.services;
import com.google.android.apps.mytracks.MyTracksConstants;
import com.google.android.apps.mytracks.MyTracksSettings;
import com.google.android.apps.mytracks.stats.TripStatistics;
import com.google.android.apps.mytracks.util.StringUtils;
import com.google.android.apps.mytracks.util.UnitConversions;
import com.google.android.maps.mytracks.R;
import android.content.Context;
import android.content.SharedPreferences;
import android.speech.tts.TextToSpeech;
import android.speech.tts.TextToSpeech.OnInitListener;
import android.telephony.PhoneStateListener;
import android.telephony.TelephonyManager;
import android.util.Log;
import java.util.Locale;
/**
* This class will periodically announce the user's trip statistics.
*
* @author Sandor Dornbush
*/
public class StatusAnnouncerTask implements PeriodicTask {
/**
* The rate at which announcements are spoken.
*/
// @VisibleForTesting
static final float TTS_SPEECH_RATE = 0.9f;
/**
* A pointer to the service context.
*/
private final Context context;
/**
* String utilities.
*/
private final StringUtils stringUtils;
/**
* The interface to the text to speech engine.
*/
private TextToSpeech tts;
/**
* The response received from the TTS engine after initialization.
*/
private boolean ready = false;
/**
* Whether we're allowed to speak right now.
*/
private boolean speechAllowed;
/**
* Listener which updates {@link #speechAllowed} when the phone state changes.
*/
private final PhoneStateListener phoneListener = new PhoneStateListener() {
@Override
public void onCallStateChanged(int state, String incomingNumber) {
speechAllowed = state == TelephonyManager.CALL_STATE_IDLE;
if (!speechAllowed && tts.isSpeaking()) {
// If we're already speaking, stop it.
tts.stop();
}
}
};
public StatusAnnouncerTask(Context context) {
this(context, new StringUtils(context));
}
public StatusAnnouncerTask(Context context, StringUtils stringUtils) {
this.context = context;
this.stringUtils = stringUtils;
}
/**
* Called when the TTS engine is initialized.
*/
private void onTtsInit(int status) {
Log.i(MyTracksConstants.TAG, "TrackRecordingService.TTS init: " + status);
this.ready = status == TextToSpeech.SUCCESS;
if (ready) {
// Force the language to be the same as the string we will be speaking,
// if that's available.
Locale speechLanguage = Locale.getDefault();
int languageAvailability = tts.isLanguageAvailable(speechLanguage);
if (languageAvailability == TextToSpeech.LANG_MISSING_DATA ||
languageAvailability == TextToSpeech.LANG_NOT_SUPPORTED) {
// English is probably supported.
// TODO: Somehow use announcement strings from English too.
Log.w(MyTracksConstants.TAG, "Default language not available, using English.");
speechLanguage = Locale.ENGLISH;
}
tts.setLanguage(speechLanguage);
// Slow down the speed just a bit as it is hard to hear when exercising.
tts.setSpeechRate(TTS_SPEECH_RATE);
}
}
/**
* {@inheritDoc}
*
* Announces the trip status.
*/
@Override
public void run(TrackRecordingService service) {
if (!ready || tts == null) {
Log.e(MyTracksConstants.TAG, "StatusAnnouncer Tts not ready.");
return;
}
if (!speechAllowed) {
Log.i(MyTracksConstants.TAG,
"Not making announcement - not allowed at this time");
return;
}
if (service == null || service.getTripStatistics() == null) {
Log.e(MyTracksConstants.TAG, "StatusAnnouncer stats not initialized.");
return;
}
String announcement = getAnnouncement(service.getTripStatistics());
Log.d(MyTracksConstants.TAG, "Announcement: " + announcement);
tts.speak(announcement, TextToSpeech.QUEUE_FLUSH, null);
}
/**
* Builds the announcement string.
*
* @return The string that will be read to the user
*/
// @VisibleForTesting
protected String getAnnouncement(TripStatistics stats) {
SharedPreferences preferences =
context.getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0);
boolean metricUnits = true;
boolean reportSpeed = true;
if (preferences != null) {
metricUnits =
preferences.getBoolean(context.getString(R.string.metric_units_key),
true);
reportSpeed =
preferences.getBoolean(context.getString(R.string.report_speed_key),
true);
}
double d = stats.getTotalDistance() / 1000;
double s = stats.getAverageMovingSpeed() * 3.6;
if (d == 0) {
return context.getString(R.string.announce_no_distance);
}
int speedLabel;
if (metricUnits) {
if (reportSpeed) {
speedLabel = R.string.kilometer_per_hour_long;
} else {
speedLabel = R.string.per_kilometer;
}
} else {
s *= UnitConversions.KMH_TO_MPH;
d *= UnitConversions.KM_TO_MI;
if (reportSpeed) {
speedLabel = R.string.mile_per_hour_long;
} else {
speedLabel = R.string.per_mile;
}
}
String speed = null;
if ((s == 0) || Double.isNaN(s)) {
speed = context.getString(R.string.unknown);
} else {
if (reportSpeed) {
speed = String.format("%.1f", s);
} else {
double pace = 3600000.0 / s;
Log.w(MyTracksConstants.TAG,
"Converted speed: " + s + " to pace: " + pace);
speed = stringUtils.formatTimeLong((long) pace);
}
}
return context.getString(R.string.announce_template,
context.getString(R.string.total_distance_label),
d,
context.getString(metricUnits
? R.string.kilometers_long
: R.string.miles_long),
stringUtils.formatTimeLong(stats.getMovingTime()),
speed,
context.getString(speedLabel));
}
@Override
public void start() {
Log.i(MyTracksConstants.TAG, "Starting TTS");
if (tts == null) {
// We can't have this class also be the listener, otherwise it's unsafe to
// reference it in Cupcake (even if we don't instantiate it).
tts = newTextToSpeech(context, new OnInitListener() {
@Override
public void onInit(int status) {
onTtsInit(status);
}
});
}
speechAllowed = true;
// Register ourselves as a listener so we won't speak during a call.
listenToPhoneState(phoneListener, PhoneStateListener.LISTEN_CALL_STATE);
}
@Override
public void shutdown() {
// Stop listening to phone state.
listenToPhoneState(phoneListener, PhoneStateListener.LISTEN_NONE);
if (tts != null) {
tts.shutdown();
tts = null;
}
Log.i(MyTracksConstants.TAG, "TTS shut down");
}
/**
* Wrapper for instantiating a {@link TextToSpeech} object, which causes
* several issues during testing.
*/
// @VisibleForTesting
protected TextToSpeech newTextToSpeech(Context ctx, OnInitListener onInitListener) {
return new TextToSpeech(ctx, onInitListener);
}
/**
* Wrapper for calls to the 100%-unmockable {@link TelephonyManager#listen}.
*/
// @VisibleForTesting
protected void listenToPhoneState(PhoneStateListener listener, int events) {
TelephonyManager telephony =
(TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
if (telephony != null) {
telephony.listen(listener, events);
}
}
/**
* Returns the volume stream to use for controlling announcement volume.
*/
public static int getVolumeStream() {
return TextToSpeech.Engine.DEFAULT_STREAM;
}
}
@@ -0,0 +1,87 @@
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.services;
import com.google.android.apps.mytracks.MyTracksConstants;
import android.util.Log;
/**
* This class manages a period task executer.
*
* @author Sandor Dornbush
*/
public class TaskExecuterManager {
private int frequency;
private final PeriodicTask task;
private PeriodicTaskExecuter executer;
public TaskExecuterManager(int frequency, PeriodicTask task,
TrackRecordingService service) {
this.task = task;
setFrequency(frequency, service);
}
public int getFrequency() {
return frequency;
}
/**
* Sets the frequency that the task should be run at.
* If needed the task will be scheduled.
*
* @param frequency The frequency in minutes for the task to run
* @param service The service to run the task on
*/
public void setFrequency(int frequency, TrackRecordingService service) {
this.frequency = frequency;
Log.i(MyTracksConstants.TAG, "Frequency set to " + frequency
+ " for task " + task.getClass().getSimpleName());
if (frequency == -1) {
if (executer != null) {
executer.shutdown();
executer = null;
Log.i(MyTracksConstants.TAG,
"Shut down service: " + task.getClass().getSimpleName());
}
} else {
if (executer == null) {
executer = new PeriodicTaskExecuter(task, service);
}
executer.scheduleTask(frequency * 60000);
}
}
/**
* Restores the task at the current frequency.
*/
public void restore() {
if (frequency > 0) {
executer.scheduleTask(frequency * 60000);
}
}
/**
* Shuts down this executer.
*/
public void shutdown() {
if (executer != null) {
executer.shutdown();
}
}
}
@@ -0,0 +1,40 @@
/*
* 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.content.WaypointCreationRequest;
/**
* A simple task to insert statistics markers every n minutes.
* @author Sandor Dornbush
*/
public class TimeSplitTask implements PeriodicTask {
@Override
public void run(TrackRecordingService service) {
service.insertWaypoint(WaypointCreationRequest.DEFAULT_STATISTICS);
}
@Override
public void shutdown() {
}
@Override
public void start() {
}
}
@@ -0,0 +1,169 @@
/*
* 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 static com.google.android.apps.mytracks.MyTracksConstants.TAG;
import com.google.android.apps.mytracks.MyTracksSettings;
import com.google.android.apps.mytracks.content.Sensor;
import com.google.android.maps.mytracks.R;
import com.dsi.ant.AntDefine;
import com.dsi.ant.AntMesg;
import android.content.Context;
import android.content.SharedPreferences;
import android.util.Log;
/**
* A sensor manager to connect ANT+ sensors.
* This can include heart rate monitors.
*
* @author Sandor Dornbush
*/
public class AntDirectSensorManager extends AntSensorManager {
/*
* These constants are defined by the ANT+ heart rate monitor spec.
*/
public static final byte HRM_CHANNEL = 0;
public static final byte NETWORK_NUMBER = 1;
public static final byte HEART_RATE_DEVICE_TYPE = 120;
public static final byte POWER_DEVICE_TYPE = 11;
public static final byte MANUFACTURER_ID = 1;
public static final short CHANNEL_PERIOD = 8070;
public static final byte RF_FREQUENCY = 57;
private short deviceNumberHRM;
public AntDirectSensorManager(Context context) {
super(context);
deviceNumberHRM = WILDCARD;
// First read the the device id that we will be pairing with.
SharedPreferences prefs = context.getSharedPreferences(
MyTracksSettings.SETTINGS_NAME, 0);
if (prefs != null) {
deviceNumberHRM =
(short) prefs.getInt(context.getString(R.string.ant_heart_rate_sensor_id_key), 0);
}
Log.i(TAG, "Pairing with heart rate monitor: " + deviceNumberHRM);
}
@Override
public void handleMessage(byte[] antMessage) {
int channel =
antMessage[AntMesg.MESG_DATA_OFFSET] & AntDefine.CHANNEL_NUMBER_MASK;
switch (channel) {
case HRM_CHANNEL:
antDecodeHRM(antMessage);
break;
default:
Log.d(TAG, "Unhandled message: " + channel);
}
}
/**
* Decode an ant heart rate monitor message.
* @param antMessage The byte array received from the heart rate monitor.
*/
private void antDecodeHRM(byte[] antMessage) {
// message ID == broadcast
switch (antMessage[AntMesg.MESG_ID_OFFSET]) {
case AntMesg.MESG_BROADCAST_DATA_ID:
handleBroadcastData(antMessage);
break;
case AntMesg.MESG_RESPONSE_EVENT_ID:
handleMessageResponse(antMessage);
break;
case AntMesg.MESG_CHANNEL_ID_ID:
handleChannelId(antMessage);
break;
default:
Log.e(TAG, "Unexpected message id: " + antMessage[3]);
}
}
private void handleBroadcastData(byte[] antMessage) {
if (deviceNumberHRM == WILDCARD) {
getAntReceiver().ANTRequestMessage(HRM_CHANNEL,
AntMesg.MESG_CHANNEL_ID_ID);
Log.d(TAG, "Requesting channel id id.");
}
setSensorState(Sensor.SensorState.CONNECTED);
int bpm = (int) antMessage[10] & 0xFF;
Sensor.SensorData.Builder b = Sensor.SensorData.newBuilder()
.setValue(bpm)
.setState(Sensor.SensorState.SENDING);
sensorData =
Sensor.SensorDataSet.newBuilder()
.setCreationTime(System.currentTimeMillis())
.setHeartRate(b)
.build();
}
void handleChannelId(byte[] antMessage) {
// Store the device id.
deviceNumberHRM =
(short) (((int) antMessage[3] & 0xFF |
((int) (antMessage[4] & 0xFF) << 8)) & 0xFFFF);
Log.i(TAG, "Found device id: " + deviceNumberHRM);
SharedPreferences prefs = context.getSharedPreferences(
MyTracksSettings.SETTINGS_NAME, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = prefs.edit();
editor.putInt(context.getString(R.string.ant_heart_rate_sensor_id_key), deviceNumberHRM);
editor.commit();
}
private void handleMessageResponse(byte[] antMessage) {
if (antMessage[3] == AntMesg.MESG_EVENT_ID &&
antMessage[4] == AntDefine.EVENT_RX_SEARCH_TIMEOUT) {
// Search timeout
Log.w(TAG, "Search timed out. Unassigning channel.");
getAntReceiver().ANTUnassignChannel((byte) 0);
setSensorState(Sensor.SensorState.DISCONNECTED);
} else if (antMessage[3] == AntMesg.MESG_UNASSIGN_CHANNEL_ID) {
setSensorState(Sensor.SensorState.DISCONNECTED);
Log.i(TAG, "Disconnected from the sensor: " + getSensorState());
}
}
/**
* Open a channel to the SRM head unit.
*/
@Override
public void setupChannel() {
super.setupChannel();
antChannelSetup(NETWORK_NUMBER,
HRM_CHANNEL,
deviceNumberHRM,
HEART_RATE_DEVICE_TYPE,
(byte) 0x01,
CHANNEL_PERIOD,
RF_FREQUENCY,
(byte) 0);
setSensorState(Sensor.SensorState.CONNECTING);
}
public short getDeviceNumberHRM() {
return deviceNumberHRM;
}
void setDeviceNumberHRM(short deviceNumberHRM) {
this.deviceNumberHRM = deviceNumberHRM;
}
}
@@ -0,0 +1,226 @@
/*
* 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.apps.mytracks.content.Sensor;
import com.google.android.maps.mytracks.R;
import com.dsi.ant.AntDefine;
import com.dsi.ant.AntMesg;
import android.content.Context;
import android.content.SharedPreferences;
import android.util.Log;
/**
* A sensor manager that can connect to an SRM Power Control 7 head unit.
*
* @author Sandor Dornbush
*/
public class AntSRMSensorManager extends AntSensorManager {
/*
* These constants are defined by the SRM Headunit spec.
*/
public static final byte NETWORK_NUMBER = (byte) 0;
public static final byte DEVICE_TYPE = (byte) 12;
public static final byte MANUFACTURER_ID = (byte) 1;
public static final short CHANNEL_PERIOD = (short) 8192;
public static final byte RF_FREQUENCY = (byte) 50;
public static final int MESSAGE_TYPE_INDEX = 3;
public static final int INITIALS_MESSAGE_TYPE = 5;
public static final int SENSOR_DATA_MESSAGE_TYPE = 6;
public static final int MESSAGE_ID_INDEX = 4;
/**
* This can be any value between 0..8
*/
private byte channel = (byte) 5;
/**
* This is the id of this device.
* The head unit must be programmed to expect this number.
*/
private byte deviceId;
/**
* The id of the last message received from the SRM head unit.
*/
private int lastMessageId;
public AntSRMSensorManager(Context context) {
super(context);
// First read the the device id that we will be announcing.
SharedPreferences prefs = context.getSharedPreferences(
MyTracksSettings.SETTINGS_NAME, 0);
if (prefs != null) {
deviceId = (byte) prefs.getInt(
context.getString(R.string.ant_srm_bridge_sensor_id_key),
WILDCARD);
}
}
@Override
public void handleMessage(byte[] antMessage) {
// Parse channel number
byte recievedChannel = (byte) (antMessage[AntMesg.MESG_DATA_OFFSET]
& AntDefine.CHANNEL_NUMBER_MASK);
if (recievedChannel != channel) {
Log.d(MyTracksConstants.TAG, "Unexpected channel: " + recievedChannel);
return;
}
switch (antMessage[AntMesg.MESG_ID_OFFSET]) {
case AntMesg.MESG_BROADCAST_DATA_ID:
handleBroadcastData(antMessage);
break;
case AntMesg.MESG_RESPONSE_EVENT_ID:
handleMessageResponse(antMessage);
break;
case AntMesg.MESG_CHANNEL_ID_ID:
handleChannelId(antMessage);
break;
default:
Log.e(MyTracksConstants.TAG,
"Unexpected message id: " + antMessage[3]);
}
}
private void handleBroadcastData(byte[] antMessage) {
if (deviceId == WILDCARD) {
getAntReceiver().ANTRequestMessage(channel, AntMesg.MESG_CHANNEL_ID_ID);
Log.d(MyTracksConstants.TAG, "Requesting channel id id.");
}
setSensorState(Sensor.SensorState.CONNECTED);
switch(antMessage[MESSAGE_TYPE_INDEX]) {
case INITIALS_MESSAGE_TYPE:
// TODO handle initials message.
break;
case SENSOR_DATA_MESSAGE_TYPE:
parseSensorData(antMessage);
break;
default:
Log.e(MyTracksConstants.TAG,
"Unexpected message type: " + antMessage[MESSAGE_TYPE_INDEX]);
}
}
private void handleChannelId(byte[] antMessage) {
// Store the device id.
deviceId = antMessage[3];
Log.i(MyTracksConstants.TAG, "Found device id: " + deviceId);
SharedPreferences prefs = context.getSharedPreferences(
MyTracksSettings.SETTINGS_NAME, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = prefs.edit();
editor.putInt(context.getString(R.string.ant_srm_bridge_sensor_id_key), deviceId);
editor.commit();
}
private void handleMessageResponse(byte[] antMessage) {
if (antMessage[3] == AntMesg.MESG_EVENT_ID
&& antMessage[4] == AntDefine.EVENT_RX_SEARCH_TIMEOUT) {
// Search timeout
Log.w(MyTracksConstants.TAG, "Search timed out. Unassigning channel.");
getAntReceiver().ANTUnassignChannel(channel);
setSensorState(Sensor.SensorState.DISCONNECTED);
} else if (antMessage[3] == AntMesg.MESG_UNASSIGN_CHANNEL_ID) {
setSensorState(Sensor.SensorState.DISCONNECTED);
Log.i(MyTracksConstants.TAG,
"Disconnected from the sensor: " + getSensorState());
}
}
private void parseSensorData(byte[] antMessage) {
if (antMessage.length != 11) {
Log.e(MyTracksConstants.TAG,
"Unexpected ant message length: " + antMessage.length);
return;
}
int newMessageId = antMessage[MESSAGE_ID_INDEX] & 0xFF;
if (lastMessageId == newMessageId) {
// Repeated message.
Log.i(MyTracksConstants.TAG,
String.format("SRM ignoring repeat: 0x%X", newMessageId));
return;
}
if (newMessageId < lastMessageId) {
if (!(newMessageId < 20 && lastMessageId > 200)) {
Log.i(MyTracksConstants.TAG,
String.format("SRM ignoring repeat: 0x%X", newMessageId));
return;
} // else assume the byte overflowed to 0.
}
lastMessageId = newMessageId;
Sensor.SensorDataSet.Builder builder = Sensor.SensorDataSet.newBuilder()
.setCreationTime(System.currentTimeMillis());
int power = SensorUtils.unsignedShortToInt(antMessage, 5);
if (power >= 0) {
Sensor.SensorData.Builder b = Sensor.SensorData.newBuilder()
.setValue(power)
.setState(Sensor.SensorState.SENDING);
builder.setPower(b);
}
int speed = SensorUtils.unsignedShortToInt(antMessage, 7);
if (speed >= 0) {
// The speed from srm is in 1/10 km/hr.
// Sensor data expects the speed as m/s.
// TODO finish this.
//sensorData.setWheelSpeed(UnitConversions.TENTH_KMH_TO_MPS * speed);
}
int cadence = antMessage[9] & 0xFF;
if (cadence >= 0) {
Sensor.SensorData.Builder b = Sensor.SensorData.newBuilder()
.setValue(cadence)
.setState(Sensor.SensorState.SENDING);
builder.setCadence(b);
}
int bpm = antMessage[10] & 0xFF;
if (bpm > 0) {
Sensor.SensorData.Builder b = Sensor.SensorData.newBuilder()
.setValue(bpm)
.setState(Sensor.SensorState.SENDING);
builder.setHeartRate(b);
}
sensorData = builder.build();
}
/**
* Open a channel to the SRM head unit.
*/
@Override
public void setupChannel() {
antChannelSetup(NETWORK_NUMBER,
channel,
deviceId,
DEVICE_TYPE,
MANUFACTURER_ID,
CHANNEL_PERIOD,
RF_FREQUENCY,
(byte) 0);
lastMessageId = 0;
setSensorState(Sensor.SensorState.CONNECTING);
}
}
@@ -0,0 +1,228 @@
/*
* 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.content.Sensor.SensorDataSet;
import com.dsi.ant.AntDefine;
import com.dsi.ant.AntInterface;
import com.dsi.ant.AntInterfaceIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
/**
* An abstract class for ANT+ based sensors.
*
* @author Sandor Dornbush
*/
public abstract class AntSensorManager extends SensorManager {
// ANT Service
public static final int EVENT_ANT_ENABLED = 1;
public static final int EVENT_ANT_DISABLED = 2;
// Pairing
protected static final short WILDCARD = 0;
private AntInterface antReceiver;
private boolean status;
// Flag to know if the ANT App was interrupted
// TODO: This code path is not used but probably should be.
private boolean antInterrupted;
/**
* The data from the sensors.
*/
protected SensorDataSet sensorData;
protected Context context;
private static final boolean DEBUGGING = false;
/**
* Receives all of the ANT intents and dispatches to the proper handler.
*/
private final BroadcastReceiver receiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String antAction = intent.getAction();
Log.i(MyTracksConstants.TAG, "enter onReceive" + antAction);
if (antAction.equals(AntInterfaceIntent.ANT_ENABLED_ACTION)) {
Log.i(MyTracksConstants.TAG, "enter onReceive ANT_ENABLED_ACTION "
+ antAction);
handler.sendMessage(handler.obtainMessage(EVENT_ANT_ENABLED, 0));
}
if (antAction.equals(AntInterfaceIntent.ANT_DISABLED_ACTION)) {
Log.i(MyTracksConstants.TAG, "enter onReceive ANT_DISABLED_ACTION "
+ antAction);
handler.sendMessage(handler.obtainMessage(EVENT_ANT_DISABLED, 0));
}
if (antAction.equals(AntInterfaceIntent.ANT_RX_MESSAGE_ACTION)) {
byte[] antMessage =
intent.getByteArrayExtra(AntInterfaceIntent.ANT_MESSAGE);
if (DEBUGGING) {
StringBuilder sb =
new StringBuilder("ANT_RX_MESSAGE_ACTION Rx:");
for (int i = 0; i < antMessage.length; i++)
sb.append(String.format("[%X]]", (antMessage[i] & 0xFF)));
Log.d(MyTracksConstants.TAG, sb.toString());
}
handleMessage(antMessage);
}
}
};
/**
* Handler for all the ANT related events. The events will be handled only
* when we are in the ANT application main menu
**/
private Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case EVENT_ANT_ENABLED:
Log.i(MyTracksConstants.TAG,
"enter handleMessage ----EVENT_ANT_ENABLED");
break;
case EVENT_ANT_DISABLED:
Log.i(MyTracksConstants.TAG,
"enter handleMessage ----EVENT_ANT_DISABLED");
break;
}
}
};
public AntSensorManager(Context context) {
this.context = context;
// Need to enable the ANT if it is not enabled earlier
antReceiver = AntInterface.getInstance(context, null);
if (antReceiver == null) {
return;
}
status = antReceiver.isEnabled();
if (!status) {
// Make sure not to call AntInterface.enable() again, if it has been
// already called before
if (antInterrupted == false) {
status = antReceiver.enable();
if (!status) {
Log.e(MyTracksConstants.TAG, "Can not enable ANT interface");
} else {
Log.i(MyTracksConstants.TAG, "Powering on Radio");
}
} else {
Log.i(MyTracksConstants.TAG, "Radio already enabled");
}
}
}
@Override
public void setupChannel() {
Log.i(MyTracksConstants.TAG, "Registering for ant intents.");
// Register for ANT intent broadcasts.
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(AntInterfaceIntent.ANT_ENABLED_ACTION);
intentFilter.addAction(AntInterfaceIntent.ANT_DISABLED_ACTION);
intentFilter.addAction(AntInterfaceIntent.ANT_RX_MESSAGE_ACTION);
context.registerReceiver(receiver, intentFilter);
}
@Override
public SensorDataSet getSensorDataSet() {
return sensorData;
}
@Override
public boolean isEnabled() {
return true;
}
public abstract void handleMessage(byte[] antMessage);
@Override
public void onDestroy() {
Log.i(MyTracksConstants.TAG, "Unregistering for ant intents.");
context.unregisterReceiver(receiver);
}
/**
* Set up an ANT+ channel.
*
* @return True on success.
*/
protected boolean antChannelSetup(byte networkNumber, byte channelNumber,
short deviceNumber, byte deviceType, byte txType, short channelPeriod,
byte radioFreq, byte proxSearch) {
// Assign as slave channel on selected network (0 = public, 1 = ANT+, 2 =
// ANTFS)
if (!antReceiver.ANTAssignChannel(channelNumber,
AntDefine.PARAMETER_RX_NOT_TX, networkNumber)) {
return false;
}
if (!antReceiver.ANTSetChannelId(channelNumber, deviceNumber, deviceType,
txType)) {
return false;
}
if (!antReceiver.ANTSetChannelPeriod(channelNumber, channelPeriod)) {
return false;
}
if (!antReceiver.ANTSetChannelRFFreq(channelNumber, radioFreq)) {
return false;
}
// Disable high priority search
if (!antReceiver.ANTSetChannelSearchTimeout(channelNumber, (byte) 0)) {
return false;
}
// Set search timeout to 30 seconds (low priority search))
if (!antReceiver.ANTSetLowPriorityChannelSearchTimeout(channelNumber, (byte) 12)) {
return false;
}
if (deviceNumber == WILDCARD) {
// Configure proximity search, if using wild card search
if (!antReceiver.ANTSetProximitySearch(channelNumber, proxSearch)) {
return false;
}
}
return !antReceiver.ANTOpenChannel(channelNumber);
}
public AntInterface getAntReceiver() {
return antReceiver;
}
}
@@ -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,64 @@
/*
* Copyright 2009 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.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.ant_sensor_type))) {
return new AntDirectSensorManager(context);
} else if (sensor.equals(context.getString(R.string.srm_ant_bridge_sensor_type))) {
return new AntSRMSensorManager(context);
} else if (sensor.equals(context.getString(R.string.zephyr_sensor_type))) {
return new ZephyrSensorManager(context);
} else {
Log.w(MyTracksConstants.TAG, "Unable to find sensor type: " + sensor);
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,77 @@
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.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.
// TODO: Fix the cadence calculation.
// 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());
}
}