forked from upstream-mirrors/OpenTracks
Add HTTP status code to the error message
This commit is contained in:
+59
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright 2009 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
|
||||
package com.google.android.apps.mytracks.services;
|
||||
|
||||
/**
|
||||
* This is a simple location listener policy that will always dictate the same
|
||||
* polling interval.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public class AbsoluteLocationListenerPolicy implements LocationListenerPolicy {
|
||||
|
||||
/**
|
||||
* The interval to request for gps signal.
|
||||
*/
|
||||
private final long interval;
|
||||
|
||||
/**
|
||||
* @param interval The interval to request for gps signal
|
||||
*/
|
||||
public AbsoluteLocationListenerPolicy(final long interval) {
|
||||
this.interval = interval;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The interval given in the constructor
|
||||
*/
|
||||
public long getDesiredPollingInterval() {
|
||||
return interval;
|
||||
}
|
||||
|
||||
/**
|
||||
* Discards the idle time.
|
||||
*/
|
||||
public void updateIdleTime(long idleTime) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the minimum distance between updates.
|
||||
* Get all updates to properly measure moving time.
|
||||
*/
|
||||
public int getMinDistance() {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* Copyright 2009 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
|
||||
package com.google.android.apps.mytracks.services;
|
||||
|
||||
/**
|
||||
* A LocationListenerPolicy that will change based on how long the user has been
|
||||
* stationary.
|
||||
*
|
||||
* This policy will dictate a policy based on a min, max and idle time.
|
||||
* The policy will dictate an interval bounded by min and max whic is half of
|
||||
* the idle time.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public class AdaptiveLocationListenerPolicy implements LocationListenerPolicy {
|
||||
|
||||
/**
|
||||
* Smallest interval this policy will dictate, in milliseconds.
|
||||
*/
|
||||
private final long minInterval;
|
||||
|
||||
/**
|
||||
* Largest interval this policy will dictate, in milliseconds.
|
||||
*/
|
||||
private final long maxInterval;
|
||||
|
||||
private final int minDistance;
|
||||
|
||||
/**
|
||||
* The time the user has been at the current location, in milliseconds.
|
||||
*/
|
||||
private long idleTime;
|
||||
|
||||
/**
|
||||
* Creates a policy that will be bounded by the given min and max.
|
||||
*
|
||||
* @param min Smallest interval this policy will dictate, in milliseconds
|
||||
* @param max Largest interval this policy will dictate, in milliseconds
|
||||
*/
|
||||
public AdaptiveLocationListenerPolicy(long min, long max, int minDistance) {
|
||||
this.minInterval = min;
|
||||
this.maxInterval = max;
|
||||
this.minDistance = minDistance;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return An interval bounded by min and max which is half of the idle time
|
||||
*/
|
||||
public long getDesiredPollingInterval() {
|
||||
long desiredInterval = idleTime / 2;
|
||||
// Round to avoid setting the interval too often.
|
||||
desiredInterval = (desiredInterval / 1000) * 1000;
|
||||
return Math.max(Math.min(maxInterval, desiredInterval),
|
||||
minInterval);
|
||||
}
|
||||
|
||||
public void updateIdleTime(long newIdleTime) {
|
||||
this.idleTime = newIdleTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the minimum distance between updates.
|
||||
*/
|
||||
public int getMinDistance() {
|
||||
return minDistance;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Copyright 2010 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package com.google.android.apps.mytracks.services;
|
||||
|
||||
import com.google.android.apps.mytracks.MyTracksSettings;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* Creates a default track name based on the current default track name policy.
|
||||
*
|
||||
* @author Matthew Simmons
|
||||
*/
|
||||
class DefaultTrackNameFactory {
|
||||
private static final String TIMESTAMP_DATE_FORMAT = "yyyy-MM-dd HH:mm";
|
||||
|
||||
private final Context context;
|
||||
|
||||
DefaultTrackNameFactory(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new track name.
|
||||
*
|
||||
* @param trackId The ID for the current track.
|
||||
* @param startTime The start time, in milliseconds since the epoch, of the
|
||||
* current track.
|
||||
* @return The new track name.
|
||||
*/
|
||||
String newTrackName(long trackId, long startTime) {
|
||||
if (useTimestampTrackName()) {
|
||||
SimpleDateFormat formatter = new SimpleDateFormat(TIMESTAMP_DATE_FORMAT);
|
||||
return formatter.format(new Date(startTime));
|
||||
} else {
|
||||
return String.format(context.getString(R.string.new_track), trackId);
|
||||
}
|
||||
}
|
||||
|
||||
/** Determines whether the preferences allow a timestamp-based track name */
|
||||
protected boolean useTimestampTrackName() {
|
||||
SharedPreferences prefs =
|
||||
context.getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0);
|
||||
return prefs.getBoolean(
|
||||
context.getString(R.string.timestamp_track_name_key), true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright 2009 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
|
||||
package com.google.android.apps.mytracks.services;
|
||||
|
||||
/**
|
||||
* This is an interface for classes that will manage the location listener policy.
|
||||
* Different policy options are:
|
||||
* Absolute
|
||||
* Addaptive
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public interface LocationListenerPolicy {
|
||||
|
||||
/**
|
||||
* Returns the polling time this policy would like at this time.
|
||||
*
|
||||
* @return The polling that this policy dictates
|
||||
*/
|
||||
public long getDesiredPollingInterval();
|
||||
|
||||
/**
|
||||
* Returns the minimum distance between updates.
|
||||
*/
|
||||
public int getMinDistance();
|
||||
|
||||
/**
|
||||
* Notifies the amount of time the user has been idle at their current
|
||||
* location.
|
||||
*
|
||||
* @param idleTime The time that the user has been idle at this spot
|
||||
*/
|
||||
public void updateIdleTime(long idleTime);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright 2009 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
|
||||
package com.google.android.apps.mytracks.services;
|
||||
|
||||
/**
|
||||
* This is interface for a task that will be executed on some schedule.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public interface PeriodicTask {
|
||||
/**
|
||||
* Sets up this task for subsequent calls to the run method.
|
||||
*/
|
||||
public void start();
|
||||
|
||||
/**
|
||||
* This method will be called periodically.
|
||||
*/
|
||||
public void run(TrackRecordingService service);
|
||||
|
||||
/**
|
||||
* Shuts down this task and clean up resources.
|
||||
*/
|
||||
public void shutdown();
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
* Copyright 2009 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
|
||||
package com.google.android.apps.mytracks.services;
|
||||
|
||||
import com.google.android.apps.mytracks.MyTracksConstants;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
|
||||
/**
|
||||
* This class will periodically announce the user's trip statistics.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public class PeriodicTaskExecuter {
|
||||
|
||||
private final PeriodicTask task;
|
||||
private final TrackRecordingService service;
|
||||
|
||||
/**
|
||||
* A timer to schedule the announcements.
|
||||
* This is non-null if the task is in started (scheduled) state.
|
||||
*/
|
||||
private Timer timer;
|
||||
|
||||
public PeriodicTaskExecuter(PeriodicTask task,
|
||||
TrackRecordingService service) {
|
||||
this.task = task;
|
||||
this.service = service;
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedules the task at the given interval.
|
||||
*
|
||||
* @param interval The interval in milliseconds
|
||||
*/
|
||||
public void scheduleTask(long interval) {
|
||||
// TODO: Decouple service from this class once and forever.
|
||||
if (!service.isRecording()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (timer != null) {
|
||||
timer.cancel();
|
||||
timer.purge();
|
||||
} else {
|
||||
// First start, or we were previously shut down.
|
||||
task.start();
|
||||
}
|
||||
|
||||
timer = new Timer();
|
||||
if (interval <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
long now = System.currentTimeMillis();
|
||||
long next = service.getTripStatistics().getStartTime();
|
||||
if (next < now) {
|
||||
next = now + interval - ((now - next) % interval);
|
||||
}
|
||||
|
||||
Date start = new Date(next);
|
||||
Log.i(MyTracksConstants.TAG,
|
||||
task.getClass().getSimpleName() + " scheduled to start at " + start
|
||||
+ " every " + interval + " milliseconds.");
|
||||
timer.scheduleAtFixedRate(new PeriodicTimerTask(), start, interval);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up this object.
|
||||
*/
|
||||
public void shutdown() {
|
||||
Log.i(MyTracksConstants.TAG,
|
||||
task.getClass().getSimpleName() + " shutting down.");
|
||||
if (timer != null) {
|
||||
timer.cancel();
|
||||
timer.purge();
|
||||
timer = null;
|
||||
task.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The timer task to announce the trip status.
|
||||
*/
|
||||
private class PeriodicTimerTask extends TimerTask {
|
||||
@Override
|
||||
public void run() {
|
||||
task.run(service);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
/*
|
||||
* Copyright 2010 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package com.google.android.apps.mytracks.services;
|
||||
|
||||
import com.google.android.apps.mytracks.MyTracksConstants;
|
||||
import com.google.android.apps.mytracks.MyTracksSettings;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
|
||||
import android.util.Log;
|
||||
|
||||
/**
|
||||
* A class that manages reading the shared preferences for the service.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public class PreferenceManager implements OnSharedPreferenceChangeListener {
|
||||
private TrackRecordingService service;
|
||||
private SharedPreferences sharedPreferences;
|
||||
private final String announcementFrequencyKey;
|
||||
private final String autoResumeTrackCurrentRetryKey;
|
||||
private final String autoResumeTrackTimeoutKey;
|
||||
private final String maxRecordingDistanceKey;
|
||||
private final String metricUnitsKey;
|
||||
private final String minRecordingDistanceKey;
|
||||
private final String minRecordingIntervalKey;
|
||||
private final String minRequiredAccuracyKey;
|
||||
private final String recordingTrackKey;
|
||||
private final String splitFrequencyKey;
|
||||
|
||||
public PreferenceManager(TrackRecordingService service) {
|
||||
this.service = service;
|
||||
this.sharedPreferences = service.getSharedPreferences(
|
||||
MyTracksSettings.SETTINGS_NAME, 0);
|
||||
if (sharedPreferences == null) {
|
||||
Log.w(MyTracksConstants.TAG,
|
||||
"TrackRecordingService: Couldn't get shared preferences.");
|
||||
throw new IllegalStateException("Couldn't get shared preferences");
|
||||
}
|
||||
sharedPreferences.registerOnSharedPreferenceChangeListener(this);
|
||||
|
||||
announcementFrequencyKey =
|
||||
service.getString(R.string.announcement_frequency_key);
|
||||
autoResumeTrackCurrentRetryKey =
|
||||
service.getString(R.string.auto_resume_track_current_retry_key);
|
||||
autoResumeTrackTimeoutKey =
|
||||
service.getString(R.string.auto_resume_track_timeout_key);
|
||||
maxRecordingDistanceKey =
|
||||
service.getString(R.string.max_recording_distance_key);
|
||||
metricUnitsKey =
|
||||
service.getString(R.string.metric_units_key);
|
||||
minRecordingDistanceKey =
|
||||
service.getString(R.string.min_recording_distance_key);
|
||||
minRecordingIntervalKey =
|
||||
service.getString(R.string.min_recording_interval_key);
|
||||
minRequiredAccuracyKey =
|
||||
service.getString(R.string.min_required_accuracy_key);
|
||||
recordingTrackKey =
|
||||
service.getString(R.string.recording_track_key);
|
||||
splitFrequencyKey =
|
||||
service.getString(R.string.split_frequency_key);
|
||||
|
||||
// Refresh all properties.
|
||||
onSharedPreferenceChanged(sharedPreferences, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies that preferences have changed.
|
||||
* Call this with key == null to update all preferences in one call.
|
||||
*
|
||||
* @param key the key that changed (may be null to update all preferences)
|
||||
*/
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences,
|
||||
String key) {
|
||||
if (service == null) {
|
||||
Log.w(MyTracksConstants.TAG,
|
||||
"onSharedPreferenceChanged: a preference change (key = " + key
|
||||
+ ") after a call to shutdown()");
|
||||
return;
|
||||
}
|
||||
if (key == null || key.equals(minRecordingDistanceKey)) {
|
||||
service.setMinRecordingDistance(
|
||||
sharedPreferences.getInt(
|
||||
minRecordingDistanceKey,
|
||||
MyTracksSettings.DEFAULT_MIN_RECORDING_DISTANCE));
|
||||
Log.d(MyTracksConstants.TAG,
|
||||
"TrackRecordingService: minRecordingDistance = "
|
||||
+ service.getMinRecordingDistance());
|
||||
}
|
||||
if (key == null || key.equals(maxRecordingDistanceKey)) {
|
||||
service.setMaxRecordingDistance(sharedPreferences.getInt(
|
||||
maxRecordingDistanceKey,
|
||||
MyTracksSettings.DEFAULT_MAX_RECORDING_DISTANCE));
|
||||
}
|
||||
if (key == null || key.equals(minRecordingIntervalKey)) {
|
||||
int minRecordingInterval = sharedPreferences.getInt(
|
||||
minRecordingIntervalKey,
|
||||
MyTracksSettings.DEFAULT_MIN_RECORDING_INTERVAL);
|
||||
switch (minRecordingInterval) {
|
||||
case -2:
|
||||
// Battery Miser
|
||||
// min: 30 seconds
|
||||
// max: 5 minutes
|
||||
// minDist: 5 meters Choose battery life over moving time accuracy.
|
||||
service.setLocationListenerPolicy(
|
||||
new AdaptiveLocationListenerPolicy(30000, 300000, 5));
|
||||
break;
|
||||
case -1:
|
||||
// High Accuracy
|
||||
// min: 1 second
|
||||
// max: 30 seconds
|
||||
// minDist: 0 meters get all updates to properly measure moving time.
|
||||
service.setLocationListenerPolicy(
|
||||
new AdaptiveLocationListenerPolicy(1000, 30000, 0));
|
||||
break;
|
||||
default:
|
||||
service.setLocationListenerPolicy(
|
||||
new AbsoluteLocationListenerPolicy(minRecordingInterval * 1000));
|
||||
}
|
||||
}
|
||||
if (key == null || key.equals(minRequiredAccuracyKey)) {
|
||||
service.setMinRequiredAccuracy(sharedPreferences.getInt(
|
||||
minRequiredAccuracyKey,
|
||||
MyTracksSettings.DEFAULT_MIN_REQUIRED_ACCURACY));
|
||||
}
|
||||
if (key == null || key.equals(announcementFrequencyKey)) {
|
||||
service.setAnnouncementFrequency(
|
||||
sharedPreferences.getInt(announcementFrequencyKey, -1));
|
||||
}
|
||||
if (key == null || key.equals(autoResumeTrackTimeoutKey)) {
|
||||
service.setAutoResumeTrackTimeout(sharedPreferences.getInt(
|
||||
autoResumeTrackTimeoutKey,
|
||||
MyTracksSettings.DEFAULT_AUTO_RESUME_TRACK_TIMEOUT));
|
||||
}
|
||||
if (key == null || key.equals(recordingTrackKey)) {
|
||||
long recordingTrackId = sharedPreferences.getLong(recordingTrackKey, -1);
|
||||
// Only read the id if it is valid.
|
||||
// Setting it to -1 should only happen in
|
||||
// TrackRecordingService.endCurrentTrack()
|
||||
if (recordingTrackId > 0) {
|
||||
service.setRecordingTrackId(recordingTrackId);
|
||||
}
|
||||
}
|
||||
if (key == null || key.equals(splitFrequencyKey)) {
|
||||
service.getSplitManager().setSplitFrequency(
|
||||
sharedPreferences.getInt(splitFrequencyKey, 0));
|
||||
}
|
||||
if (key == null || key.equals(metricUnitsKey)) {
|
||||
service.getSplitManager().setMetricUnits(
|
||||
sharedPreferences.getBoolean(metricUnitsKey, true));
|
||||
}
|
||||
}
|
||||
|
||||
public void setAutoResumeTrackCurrentRetry(int retryAttempts) {
|
||||
sharedPreferences
|
||||
.edit()
|
||||
.putInt(autoResumeTrackCurrentRetryKey, retryAttempts)
|
||||
.commit();
|
||||
}
|
||||
|
||||
public void setRecordingTrack(long id) {
|
||||
sharedPreferences
|
||||
.edit()
|
||||
.putLong(recordingTrackKey, id)
|
||||
.commit();
|
||||
}
|
||||
|
||||
public void shutdown() {
|
||||
sharedPreferences.unregisterOnSharedPreferenceChangeListener(this);
|
||||
service = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,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.
|
||||
* < 0 Use the absolute value as a distance in the current measurement km
|
||||
* or mi
|
||||
* 0 Turn off splits
|
||||
* > 0 Use the value as a time in minutes
|
||||
* @param splitFrequency The frequency in time or distance
|
||||
*/
|
||||
public void setSplitFrequency(int splitFrequency) {
|
||||
Log.d(MyTracksConstants.TAG,
|
||||
"setSplitFrequency: splitFrequency = " + splitFrequency);
|
||||
this.splitFrequency = splitFrequency;
|
||||
|
||||
// TODO: Decouple service from this class once and forever.
|
||||
if (!service.isRecording()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (splitFrequency < 1) {
|
||||
if (splitExecuter != null) {
|
||||
splitExecuter.shutdown();
|
||||
splitExecuter = null;
|
||||
}
|
||||
}
|
||||
if (splitFrequency > 0) {
|
||||
if (splitExecuter == null) {
|
||||
TimeSplitTask splitter = new TimeSplitTask();
|
||||
splitExecuter = new PeriodicTaskExecuter(splitter, service);
|
||||
}
|
||||
splitExecuter.scheduleTask(splitFrequency * 60000);
|
||||
} else {
|
||||
// For distance based splits.
|
||||
calculateNextSplit();
|
||||
}
|
||||
}
|
||||
|
||||
public void setMetricUnits(boolean metricUnits) {
|
||||
this.metricUnits = metricUnits;
|
||||
calculateNextSplit();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright 2010 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package com.google.android.apps.mytracks.services;
|
||||
|
||||
import com.google.android.apps.mytracks.util.ApiFeatures;
|
||||
|
||||
import android.content.Context;
|
||||
import android.media.AudioManager;
|
||||
|
||||
/**
|
||||
* Factory which wraps construction and setup of text-to-speech announcements in
|
||||
* an API-level-safe way.
|
||||
*
|
||||
* @author Rodrigo Damazio
|
||||
*/
|
||||
public class StatusAnnouncerFactory {
|
||||
|
||||
private final boolean hasTts;
|
||||
|
||||
public StatusAnnouncerFactory(ApiFeatures apiFeatures) {
|
||||
this.hasTts = apiFeatures.hasTextToSpeech();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a periodic task which does voice announcements.
|
||||
*
|
||||
* @return the task, or null if announcements are not supported
|
||||
*/
|
||||
public PeriodicTask create(Context context) {
|
||||
if (hasTts) {
|
||||
return new StatusAnnouncerTask(context);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the appropriate volume stream for controlling announcement
|
||||
* volume.
|
||||
*/
|
||||
public int getVolumeStream() {
|
||||
if (hasTts) {
|
||||
return StatusAnnouncerTask.getVolumeStream();
|
||||
} else {
|
||||
return AudioManager.USE_DEFAULT_STREAM_TYPE;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,279 @@
|
||||
/*
|
||||
* Copyright 2009 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
|
||||
package com.google.android.apps.mytracks.services;
|
||||
|
||||
import com.google.android.apps.mytracks.MyTracksConstants;
|
||||
import com.google.android.apps.mytracks.MyTracksSettings;
|
||||
import com.google.android.apps.mytracks.stats.TripStatistics;
|
||||
import com.google.android.apps.mytracks.util.StringUtils;
|
||||
import com.google.android.apps.mytracks.util.UnitConversions;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.speech.tts.TextToSpeech;
|
||||
import android.speech.tts.TextToSpeech.OnInitListener;
|
||||
import android.telephony.PhoneStateListener;
|
||||
import android.telephony.TelephonyManager;
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* This class will periodically announce the user's trip statistics.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public class StatusAnnouncerTask implements PeriodicTask {
|
||||
|
||||
/**
|
||||
* The rate at which announcements are spoken.
|
||||
*/
|
||||
// @VisibleForTesting
|
||||
static final float TTS_SPEECH_RATE = 0.9f;
|
||||
|
||||
/**
|
||||
* A pointer to the service context.
|
||||
*/
|
||||
private final Context context;
|
||||
|
||||
/**
|
||||
* String utilities.
|
||||
*/
|
||||
private final StringUtils stringUtils;
|
||||
|
||||
/**
|
||||
* The interface to the text to speech engine.
|
||||
*/
|
||||
private TextToSpeech tts;
|
||||
|
||||
/**
|
||||
* The response received from the TTS engine after initialization.
|
||||
*/
|
||||
private boolean ready = false;
|
||||
|
||||
/**
|
||||
* Whether we're allowed to speak right now.
|
||||
*/
|
||||
private boolean speechAllowed;
|
||||
|
||||
/**
|
||||
* Listener which updates {@link #speechAllowed} when the phone state changes.
|
||||
*/
|
||||
private final PhoneStateListener phoneListener = new PhoneStateListener() {
|
||||
@Override
|
||||
public void onCallStateChanged(int state, String incomingNumber) {
|
||||
speechAllowed = state == TelephonyManager.CALL_STATE_IDLE;
|
||||
|
||||
if (!speechAllowed && tts.isSpeaking()) {
|
||||
// If we're already speaking, stop it.
|
||||
tts.stop();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public StatusAnnouncerTask(Context context) {
|
||||
this(context, new StringUtils(context));
|
||||
}
|
||||
|
||||
public StatusAnnouncerTask(Context context, StringUtils stringUtils) {
|
||||
this.context = context;
|
||||
this.stringUtils = stringUtils;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the TTS engine is initialized.
|
||||
*/
|
||||
private void onTtsInit(int status) {
|
||||
Log.i(MyTracksConstants.TAG, "TrackRecordingService.TTS init: " + status);
|
||||
this.ready = status == TextToSpeech.SUCCESS;
|
||||
|
||||
if (ready) {
|
||||
// Force the language to be the same as the string we will be speaking,
|
||||
// if that's available.
|
||||
Locale speechLanguage = Locale.getDefault();
|
||||
int languageAvailability = tts.isLanguageAvailable(speechLanguage);
|
||||
if (languageAvailability == TextToSpeech.LANG_MISSING_DATA ||
|
||||
languageAvailability == TextToSpeech.LANG_NOT_SUPPORTED) {
|
||||
// English is probably supported.
|
||||
// TODO: Somehow use announcement strings from English too.
|
||||
Log.w(MyTracksConstants.TAG, "Default language not available, using English.");
|
||||
speechLanguage = Locale.ENGLISH;
|
||||
}
|
||||
tts.setLanguage(speechLanguage);
|
||||
|
||||
// Slow down the speed just a bit as it is hard to hear when exercising.
|
||||
tts.setSpeechRate(TTS_SPEECH_RATE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* Announces the trip status.
|
||||
*/
|
||||
@Override
|
||||
public void run(TrackRecordingService service) {
|
||||
if (!ready || tts == null) {
|
||||
Log.e(MyTracksConstants.TAG, "StatusAnnouncer Tts not ready.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!speechAllowed) {
|
||||
Log.i(MyTracksConstants.TAG,
|
||||
"Not making announcement - not allowed at this time");
|
||||
return;
|
||||
}
|
||||
|
||||
if (service == null || service.getTripStatistics() == null) {
|
||||
Log.e(MyTracksConstants.TAG, "StatusAnnouncer stats not initialized.");
|
||||
return;
|
||||
}
|
||||
|
||||
String announcement = getAnnouncement(service.getTripStatistics());
|
||||
Log.d(MyTracksConstants.TAG, "Announcement: " + announcement);
|
||||
tts.speak(announcement, TextToSpeech.QUEUE_FLUSH, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the announcement string.
|
||||
*
|
||||
* @return The string that will be read to the user
|
||||
*/
|
||||
// @VisibleForTesting
|
||||
protected String getAnnouncement(TripStatistics stats) {
|
||||
SharedPreferences preferences =
|
||||
context.getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0);
|
||||
boolean metricUnits = true;
|
||||
boolean reportSpeed = true;
|
||||
if (preferences != null) {
|
||||
metricUnits =
|
||||
preferences.getBoolean(context.getString(R.string.metric_units_key),
|
||||
true);
|
||||
reportSpeed =
|
||||
preferences.getBoolean(context.getString(R.string.report_speed_key),
|
||||
true);
|
||||
}
|
||||
|
||||
double d = stats.getTotalDistance() / 1000;
|
||||
double s = stats.getAverageMovingSpeed() * 3.6;
|
||||
if (d == 0) {
|
||||
return context.getString(R.string.announce_no_distance);
|
||||
}
|
||||
|
||||
int speedLabel;
|
||||
if (metricUnits) {
|
||||
if (reportSpeed) {
|
||||
speedLabel = R.string.kilometer_per_hour_long;
|
||||
} else {
|
||||
speedLabel = R.string.per_kilometer;
|
||||
}
|
||||
} else {
|
||||
s *= UnitConversions.KMH_TO_MPH;
|
||||
d *= UnitConversions.KM_TO_MI;
|
||||
if (reportSpeed) {
|
||||
speedLabel = R.string.mile_per_hour_long;
|
||||
} else {
|
||||
speedLabel = R.string.per_mile;
|
||||
}
|
||||
}
|
||||
|
||||
String speed = null;
|
||||
if ((s == 0) || Double.isNaN(s)) {
|
||||
speed = context.getString(R.string.unknown);
|
||||
} else {
|
||||
if (reportSpeed) {
|
||||
speed = String.format("%.1f", s);
|
||||
} else {
|
||||
double pace = 3600000.0 / s;
|
||||
Log.w(MyTracksConstants.TAG,
|
||||
"Converted speed: " + s + " to pace: " + pace);
|
||||
speed = stringUtils.formatTimeLong((long) pace);
|
||||
}
|
||||
}
|
||||
|
||||
return context.getString(R.string.announce_template,
|
||||
context.getString(R.string.total_distance_label),
|
||||
d,
|
||||
context.getString(metricUnits
|
||||
? R.string.kilometers_long
|
||||
: R.string.miles_long),
|
||||
stringUtils.formatTimeLong(stats.getMovingTime()),
|
||||
speed,
|
||||
context.getString(speedLabel));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
Log.i(MyTracksConstants.TAG, "Starting TTS");
|
||||
if (tts == null) {
|
||||
// We can't have this class also be the listener, otherwise it's unsafe to
|
||||
// reference it in Cupcake (even if we don't instantiate it).
|
||||
tts = newTextToSpeech(context, new OnInitListener() {
|
||||
@Override
|
||||
public void onInit(int status) {
|
||||
onTtsInit(status);
|
||||
}
|
||||
});
|
||||
}
|
||||
speechAllowed = true;
|
||||
|
||||
// Register ourselves as a listener so we won't speak during a call.
|
||||
listenToPhoneState(phoneListener, PhoneStateListener.LISTEN_CALL_STATE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void shutdown() {
|
||||
// Stop listening to phone state.
|
||||
listenToPhoneState(phoneListener, PhoneStateListener.LISTEN_NONE);
|
||||
|
||||
if (tts != null) {
|
||||
tts.shutdown();
|
||||
tts = null;
|
||||
}
|
||||
|
||||
Log.i(MyTracksConstants.TAG, "TTS shut down");
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper for instantiating a {@link TextToSpeech} object, which causes
|
||||
* several issues during testing.
|
||||
*/
|
||||
// @VisibleForTesting
|
||||
protected TextToSpeech newTextToSpeech(Context ctx, OnInitListener onInitListener) {
|
||||
return new TextToSpeech(ctx, onInitListener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper for calls to the 100%-unmockable {@link TelephonyManager#listen}.
|
||||
*/
|
||||
// @VisibleForTesting
|
||||
protected void listenToPhoneState(PhoneStateListener listener, int events) {
|
||||
TelephonyManager telephony =
|
||||
(TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
|
||||
if (telephony != null) {
|
||||
telephony.listen(listener, events);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the volume stream to use for controlling announcement volume.
|
||||
*/
|
||||
public static int getVolumeStream() {
|
||||
return TextToSpeech.Engine.DEFAULT_STREAM;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* Copyright 2010 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package com.google.android.apps.mytracks.services;
|
||||
|
||||
import com.google.android.apps.mytracks.MyTracksConstants;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
/**
|
||||
* This class manages a period task executer.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public class TaskExecuterManager {
|
||||
|
||||
private int frequency;
|
||||
private final PeriodicTask task;
|
||||
private PeriodicTaskExecuter executer;
|
||||
|
||||
public TaskExecuterManager(int frequency, PeriodicTask task,
|
||||
TrackRecordingService service) {
|
||||
this.task = task;
|
||||
setFrequency(frequency, service);
|
||||
}
|
||||
|
||||
public int getFrequency() {
|
||||
return frequency;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the frequency that the task should be run at.
|
||||
* If needed the task will be scheduled.
|
||||
*
|
||||
* @param frequency The frequency in minutes for the task to run
|
||||
* @param service The service to run the task on
|
||||
*/
|
||||
public void setFrequency(int frequency, TrackRecordingService service) {
|
||||
this.frequency = frequency;
|
||||
Log.i(MyTracksConstants.TAG, "Frequency set to " + frequency
|
||||
+ " for task " + task.getClass().getSimpleName());
|
||||
|
||||
if (frequency == -1) {
|
||||
if (executer != null) {
|
||||
executer.shutdown();
|
||||
executer = null;
|
||||
Log.i(MyTracksConstants.TAG,
|
||||
"Shut down service: " + task.getClass().getSimpleName());
|
||||
}
|
||||
} else {
|
||||
if (executer == null) {
|
||||
executer = new PeriodicTaskExecuter(task, service);
|
||||
}
|
||||
executer.scheduleTask(frequency * 60000);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores the task at the current frequency.
|
||||
*/
|
||||
public void restore() {
|
||||
if (frequency > 0) {
|
||||
executer.scheduleTask(frequency * 60000);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shuts down this executer.
|
||||
*/
|
||||
public void shutdown() {
|
||||
if (executer != null) {
|
||||
executer.shutdown();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,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() {
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
+169
@@ -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;
|
||||
}
|
||||
}
|
||||
+226
@@ -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;
|
||||
}
|
||||
}
|
||||
+432
@@ -0,0 +1,432 @@
|
||||
/*
|
||||
* Copyright (C) 2010 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
|
||||
package com.google.android.apps.mytracks.services.sensors;
|
||||
|
||||
import com.google.android.apps.mytracks.MyTracksConstants;
|
||||
import com.google.android.apps.mytracks.content.Sensor;
|
||||
|
||||
import android.bluetooth.BluetoothAdapter;
|
||||
import android.bluetooth.BluetoothDevice;
|
||||
import android.bluetooth.BluetoothSocket;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Message;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* This class does all the work for setting up and managing Bluetooth
|
||||
* connections with other devices. It has a thread that listens for incoming
|
||||
* connections, a thread for connecting with a device, and a thread for
|
||||
* performing data transmissions when connected.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public class BluetoothConnectionManager {
|
||||
// Unique UUID for this application
|
||||
private static final UUID SPP_UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB");
|
||||
|
||||
private MessageParser parser;
|
||||
|
||||
// Member fields
|
||||
private final BluetoothAdapter adapter;
|
||||
private final Handler handler;
|
||||
private ConnectThread connectThread;
|
||||
private ConnectedThread connectedThread;
|
||||
|
||||
private Sensor.SensorState state;
|
||||
|
||||
// Message types sent from the BluetoothSenorService Handler
|
||||
public static final int MESSAGE_STATE_CHANGE = 1;
|
||||
public static final int MESSAGE_READ = 2;
|
||||
public static final int MESSAGE_WRITE = 3;
|
||||
public static final int MESSAGE_DEVICE_NAME = 4;
|
||||
|
||||
// Key names received from the BluetoothSenorService Handler
|
||||
public static final String DEVICE_NAME = "device_name";
|
||||
|
||||
/**
|
||||
* Constructor. Prepares a new BluetoothSensor session.
|
||||
*
|
||||
* @param context The UI Activity Context
|
||||
* @param handler A Handler to send messages back to the UI Activity
|
||||
*/
|
||||
public BluetoothConnectionManager(Handler handler, MessageParser parser) {
|
||||
this.adapter = BluetoothAdapter.getDefaultAdapter();
|
||||
this.state = Sensor.SensorState.NONE;
|
||||
this.handler = handler;
|
||||
this.parser = parser;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current state of the sensor connection
|
||||
*
|
||||
* @param state An integer defining the current connection state
|
||||
*/
|
||||
private synchronized void setState(Sensor.SensorState state) {
|
||||
// TODO pretty print this.
|
||||
Log.d(MyTracksConstants.TAG, "setState(" + state + ")");
|
||||
this.state = state;
|
||||
|
||||
// Give the new state to the Handler so the UI Activity can update
|
||||
handler.obtainMessage(MESSAGE_STATE_CHANGE, state.getNumber(), -1).sendToTarget();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the current connection state.
|
||||
*/
|
||||
public synchronized Sensor.SensorState getState() {
|
||||
return state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the sensor service. Specifically start AcceptThread to begin a session
|
||||
* in listening (server) mode. Called by the Activity onResume()
|
||||
*/
|
||||
public synchronized void start() {
|
||||
Log.d(MyTracksConstants.TAG, "BluetoothConnectionManager.start()");
|
||||
|
||||
// Cancel any thread attempting to make a connection
|
||||
if (connectThread != null) {
|
||||
connectThread.cancel();
|
||||
connectThread = null;
|
||||
}
|
||||
|
||||
// Cancel any thread currently running a connection
|
||||
if (connectedThread != null) {
|
||||
connectedThread.cancel();
|
||||
connectedThread = null;
|
||||
}
|
||||
|
||||
setState(Sensor.SensorState.NONE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the ConnectThread to initiate a connection to a remote device.
|
||||
*
|
||||
* @param device The BluetoothDevice to connect
|
||||
*/
|
||||
public synchronized void connect(BluetoothDevice device) {
|
||||
Log.d(MyTracksConstants.TAG, "connect to: " + device);
|
||||
|
||||
// Cancel any thread attempting to make a connection
|
||||
if (state == Sensor.SensorState.CONNECTING) {
|
||||
if (connectThread != null) {
|
||||
connectThread.cancel();
|
||||
connectThread = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Cancel any thread currently running a connection
|
||||
if (connectedThread != null) {
|
||||
connectedThread.cancel();
|
||||
connectedThread = null;
|
||||
}
|
||||
|
||||
// Start the thread to connect with the given device
|
||||
connectThread = new ConnectThread(device);
|
||||
connectThread.start();
|
||||
setState(Sensor.SensorState.CONNECTING);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the ConnectedThread to begin managing a Bluetooth connection
|
||||
*
|
||||
* @param socket The BluetoothSocket on which the connection was made
|
||||
* @param device The BluetoothDevice that has been connected
|
||||
*/
|
||||
public synchronized void connected(BluetoothSocket socket,
|
||||
BluetoothDevice device) {
|
||||
Log.d(MyTracksConstants.TAG, "connected");
|
||||
|
||||
// Cancel the thread that completed the connection
|
||||
if (connectThread != null) {
|
||||
connectThread.cancel();
|
||||
connectThread = null;
|
||||
}
|
||||
|
||||
// Cancel any thread currently running a connection
|
||||
if (connectedThread != null) {
|
||||
connectedThread.cancel();
|
||||
connectedThread = null;
|
||||
}
|
||||
|
||||
// Start the thread to manage the connection and perform transmissions
|
||||
connectedThread = new ConnectedThread(socket);
|
||||
connectedThread.start();
|
||||
|
||||
// Send the name of the connected device back to the UI Activity
|
||||
Message msg = handler.obtainMessage(MESSAGE_DEVICE_NAME);
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putString(DEVICE_NAME, device.getName());
|
||||
msg.setData(bundle);
|
||||
handler.sendMessage(msg);
|
||||
|
||||
setState(Sensor.SensorState.CONNECTED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop all threads
|
||||
*/
|
||||
public synchronized void stop() {
|
||||
Log.d(MyTracksConstants.TAG, "stop()");
|
||||
if (connectThread != null) {
|
||||
connectThread.cancel();
|
||||
connectThread = null;
|
||||
}
|
||||
if (connectedThread != null) {
|
||||
connectedThread.cancel();
|
||||
connectedThread = null;
|
||||
}
|
||||
setState(Sensor.SensorState.NONE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Write to the ConnectedThread in an unsynchronized manner
|
||||
*
|
||||
* @param out The bytes to write
|
||||
* @see ConnectedThread#write(byte[])
|
||||
*/
|
||||
public void write(byte[] out) {
|
||||
// Create temporary object
|
||||
ConnectedThread r;
|
||||
// Synchronize a copy of the ConnectedThread
|
||||
synchronized (this) {
|
||||
if (state != Sensor.SensorState.CONNECTED) {
|
||||
return;
|
||||
}
|
||||
r = connectedThread;
|
||||
}
|
||||
// Perform the write unsynchronized
|
||||
r.write(out);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the connection attempt failed and notify the UI Activity.
|
||||
*/
|
||||
private void connectionFailed() {
|
||||
setState(Sensor.SensorState.DISCONNECTED);
|
||||
Log.i(MyTracksConstants.TAG, "Bluetooth connection failed.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the connection was lost and notify the UI Activity.
|
||||
*/
|
||||
private void connectionLost() {
|
||||
setState(Sensor.SensorState.DISCONNECTED);
|
||||
Log.i(MyTracksConstants.TAG, "Bluetooth connection lost.");
|
||||
}
|
||||
|
||||
/**
|
||||
* This thread runs while attempting to make an outgoing connection with a
|
||||
* device. It runs straight through; the connection either succeeds or fails.
|
||||
*/
|
||||
private class ConnectThread extends Thread {
|
||||
private final BluetoothSocket socket;
|
||||
private final BluetoothDevice device;
|
||||
|
||||
public ConnectThread(BluetoothDevice device) {
|
||||
setName("ConnectThread-" + device.getName());
|
||||
this.device = device;
|
||||
BluetoothSocket tmp = null;
|
||||
|
||||
// Get a BluetoothSocket for a connection with the
|
||||
// given BluetoothDevice
|
||||
try {
|
||||
tmp = getSocket();
|
||||
} catch (IOException e) {
|
||||
Log.e(MyTracksConstants.TAG, "create() failed", e);
|
||||
}
|
||||
socket = tmp;
|
||||
}
|
||||
|
||||
private BluetoothSocket getSocket() throws IOException {
|
||||
try {
|
||||
Class<? extends BluetoothDevice> c = device.getClass();
|
||||
Method insecure = c.getMethod("createInsecureRfcommSocket",
|
||||
Integer.class);
|
||||
insecure.setAccessible(true);
|
||||
return (BluetoothSocket) insecure.invoke(device, 1);
|
||||
} catch (SecurityException e) {
|
||||
Log.e(MyTracksConstants.TAG, "Unable to get insecure connect.", e);
|
||||
} catch (NoSuchMethodException e) {
|
||||
Log.e(MyTracksConstants.TAG, "Unable to get insecure connect.", e);
|
||||
} catch (IllegalArgumentException e) {
|
||||
Log.e(MyTracksConstants.TAG, "Unable to get insecure connect.", e);
|
||||
} catch (IllegalAccessException e) {
|
||||
Log.e(MyTracksConstants.TAG, "Unable to get insecure connect.", e);
|
||||
} catch (InvocationTargetException e) {
|
||||
Log.e(MyTracksConstants.TAG, "Unable to get insecure connect.", e);
|
||||
}
|
||||
return device.createRfcommSocketToServiceRecord(SPP_UUID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
Log.d(MyTracksConstants.TAG, "BEGIN mConnectThread");
|
||||
|
||||
// Always cancel discovery because it will slow down a connection
|
||||
adapter.cancelDiscovery();
|
||||
|
||||
// Make a connection to the BluetoothSocket
|
||||
try {
|
||||
// This is a blocking call and will only return on a
|
||||
// successful connection or an exception
|
||||
socket.connect();
|
||||
} catch (IOException e) {
|
||||
connectionFailed();
|
||||
// Close the socket
|
||||
try {
|
||||
socket.close();
|
||||
} catch (IOException e2) {
|
||||
Log.e(MyTracksConstants.TAG,
|
||||
"unable to close() socket during connection failure", e2);
|
||||
}
|
||||
// Start the service over to restart listening mode
|
||||
BluetoothConnectionManager.this.start();
|
||||
return;
|
||||
}
|
||||
|
||||
// Reset the ConnectThread because we're done
|
||||
synchronized (BluetoothConnectionManager.this) {
|
||||
connectThread = null;
|
||||
}
|
||||
|
||||
// Start the connected thread
|
||||
connected(socket, device);
|
||||
}
|
||||
|
||||
public void cancel() {
|
||||
try {
|
||||
socket.close();
|
||||
} catch (IOException e) {
|
||||
Log.e(MyTracksConstants.TAG, "close() of connect socket failed", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This thread runs during a connection with a remote device. It handles all
|
||||
* incoming and outgoing transmissions.
|
||||
*/
|
||||
private class ConnectedThread extends Thread {
|
||||
private final BluetoothSocket btSocket;
|
||||
private final InputStream mmInStream;
|
||||
private final OutputStream mmOutStream;
|
||||
|
||||
public ConnectedThread(BluetoothSocket socket) {
|
||||
Log.d(MyTracksConstants.TAG, "create ConnectedThread");
|
||||
btSocket = socket;
|
||||
InputStream tmpIn = null;
|
||||
OutputStream tmpOut = null;
|
||||
|
||||
// Get the BluetoothSocket input and output streams
|
||||
try {
|
||||
tmpIn = socket.getInputStream();
|
||||
tmpOut = socket.getOutputStream();
|
||||
} catch (IOException e) {
|
||||
Log.e(MyTracksConstants.TAG, "temp sockets not created", e);
|
||||
}
|
||||
|
||||
mmInStream = tmpIn;
|
||||
mmOutStream = tmpOut;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
Log.i(MyTracksConstants.TAG, "BEGIN mConnectedThread");
|
||||
byte[] buffer = new byte[parser.getFrameSize()];
|
||||
int bytes;
|
||||
int offset = 0;
|
||||
|
||||
// Keep listening to the InputStream while connected
|
||||
while (true) {
|
||||
try {
|
||||
// Read from the InputStream
|
||||
bytes = mmInStream.read(buffer, offset, parser.getFrameSize() - offset);
|
||||
|
||||
if (bytes < 0) {
|
||||
throw new IOException("EOF reached");
|
||||
}
|
||||
|
||||
offset += bytes;
|
||||
|
||||
if (offset != parser.getFrameSize()) {
|
||||
// partial frame received, call read() again to receive the rest
|
||||
continue;
|
||||
}
|
||||
|
||||
// check if its a valid frame
|
||||
if (!parser.isValid(buffer)) {
|
||||
int index = parser.findNextAlignment(buffer);
|
||||
if (index > 0) {
|
||||
// re-align
|
||||
offset = parser.getFrameSize() - index;
|
||||
System.arraycopy(buffer, index, buffer, 0, offset);
|
||||
Log.w(MyTracksConstants.TAG, "Misaligned data, found new message at " +
|
||||
index + " recovering...");
|
||||
continue;
|
||||
}
|
||||
Log.w(MyTracksConstants.TAG, "Could not find valid data, dropping data");
|
||||
offset = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
offset = 0;
|
||||
|
||||
// Send the obtained bytes to the UI Activity
|
||||
handler.obtainMessage(MESSAGE_READ, bytes, -1, buffer)
|
||||
.sendToTarget();
|
||||
} catch (IOException e) {
|
||||
Log.e(MyTracksConstants.TAG, "disconnected", e);
|
||||
connectionLost();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write to the connected OutStream.
|
||||
*
|
||||
* @param buffer The bytes to write
|
||||
*/
|
||||
public void write(byte[] buffer) {
|
||||
try {
|
||||
mmOutStream.write(buffer);
|
||||
|
||||
// Share the sent message back to the UI Activity
|
||||
handler.obtainMessage(MESSAGE_WRITE, -1, -1, buffer).sendToTarget();
|
||||
} catch (IOException e) {
|
||||
Log.e(MyTracksConstants.TAG, "Exception during write", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void cancel() {
|
||||
try {
|
||||
btSocket.close();
|
||||
} catch (IOException e) {
|
||||
Log.e(MyTracksConstants.TAG, "close() of connect socket failed", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+166
@@ -0,0 +1,166 @@
|
||||
/*
|
||||
* Copyright 2010 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package com.google.android.apps.mytracks.services.sensors;
|
||||
|
||||
import com.google.android.apps.mytracks.MyTracksConstants;
|
||||
import com.google.android.apps.mytracks.MyTracksSettings;
|
||||
import com.google.android.apps.mytracks.content.Sensor;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.bluetooth.BluetoothAdapter;
|
||||
import android.bluetooth.BluetoothDevice;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Handler;
|
||||
import android.os.Message;
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
|
||||
/**
|
||||
* Manage the connection to a bluetooth sensor.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public class BluetoothSensorManager extends SensorManager {
|
||||
|
||||
// Local Bluetooth adapter
|
||||
private BluetoothAdapter bluetoothAdapter = null;
|
||||
|
||||
// Member object for the sensor threads and connections.
|
||||
private BluetoothConnectionManager connectionManager = null;
|
||||
|
||||
// Name of the connected device
|
||||
private String connectedDeviceName = null;
|
||||
|
||||
private Context context = null;
|
||||
|
||||
private Sensor.SensorDataSet sensorDataSet = null;
|
||||
|
||||
private MessageParser parser;
|
||||
|
||||
public BluetoothSensorManager(
|
||||
Context context, MessageParser parser) {
|
||||
this.context = context;
|
||||
this.parser = parser;
|
||||
// Get local Bluetooth adapter
|
||||
bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
|
||||
// If BT is not available or not enabled quit.
|
||||
if (!isEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
setupSensor();
|
||||
}
|
||||
|
||||
private void setupSensor() {
|
||||
Log.d(MyTracksConstants.TAG, "setupSensor()");
|
||||
|
||||
// Initialize the BluetoothSensorAdapter to perform bluetooth connections.
|
||||
connectionManager = new BluetoothConnectionManager(messageHandler, parser);
|
||||
}
|
||||
|
||||
public boolean isEnabled() {
|
||||
return bluetoothAdapter != null && bluetoothAdapter.isEnabled();
|
||||
}
|
||||
|
||||
public void setupChannel() {
|
||||
if (!isEnabled() || connectionManager == null) {
|
||||
Log.w(MyTracksConstants.TAG, "Disabled manager onStartTrack");
|
||||
return;
|
||||
}
|
||||
SharedPreferences prefs =
|
||||
context.getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0);
|
||||
String address =
|
||||
prefs.getString(context.getString(R.string.bluetooth_sensor_key), null);
|
||||
if (address == null) {
|
||||
return;
|
||||
}
|
||||
Log.w(MyTracksConstants.TAG, "Connecting to bluetooth sensor: " + address);
|
||||
// Get the BluetoothDevice object
|
||||
BluetoothDevice device = bluetoothAdapter.getRemoteDevice(address);
|
||||
// Attempt to connect to the device
|
||||
connectionManager.connect(device);
|
||||
|
||||
// Performing this check in onResume() covers the case in which BT was
|
||||
// not enabled during onStart(), so we were paused to enable it...
|
||||
// onResume() will be called when ACTION_REQUEST_ENABLE activity returns.
|
||||
if (connectionManager != null) {
|
||||
// Only if the state is STATE_NONE, do we know that we haven't started
|
||||
// already
|
||||
if (connectionManager.getState() == Sensor.SensorState.NONE) {
|
||||
// Start the Bluetooth sensor services
|
||||
Log.w(MyTracksConstants.TAG, "Disabled manager onStartTrack");
|
||||
connectionManager.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void onDestroy() {
|
||||
// Stop the Bluetooth sensor services
|
||||
if (connectionManager != null) {
|
||||
connectionManager.stop();
|
||||
}
|
||||
}
|
||||
|
||||
public Sensor.SensorDataSet getSensorDataSet() {
|
||||
return sensorDataSet;
|
||||
}
|
||||
|
||||
public Sensor.SensorState getSensorState() {
|
||||
return (connectionManager == null)
|
||||
? Sensor.SensorState.NONE
|
||||
: connectionManager.getState();
|
||||
}
|
||||
|
||||
// The Handler that gets information back from the BluetoothSensorService
|
||||
private final Handler messageHandler = new Handler() {
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
switch (msg.what) {
|
||||
case BluetoothConnectionManager.MESSAGE_STATE_CHANGE:
|
||||
// TODO should we update the SensorManager state var?
|
||||
Log.i(MyTracksConstants.TAG, "MESSAGE_STATE_CHANGE: " + msg.arg1);
|
||||
break;
|
||||
case BluetoothConnectionManager.MESSAGE_WRITE:
|
||||
break;
|
||||
case BluetoothConnectionManager.MESSAGE_READ:
|
||||
byte[] readBuf = null;
|
||||
try {
|
||||
readBuf = (byte[]) msg.obj;
|
||||
sensorDataSet = parser.parseBuffer(readBuf);
|
||||
Log.d(MyTracksConstants.TAG, "MESSAGE_READ: " + sensorDataSet.toString());
|
||||
} catch (IllegalArgumentException iae) {
|
||||
sensorDataSet = null;
|
||||
Log.i(MyTracksConstants.TAG,
|
||||
"Got bad sensor data: " + new String(readBuf, 0, readBuf.length),
|
||||
iae);
|
||||
} catch (RuntimeException re) {
|
||||
sensorDataSet = null;
|
||||
Log.i(MyTracksConstants.TAG, "Unexpected exception on read.", re);
|
||||
}
|
||||
break;
|
||||
case BluetoothConnectionManager.MESSAGE_DEVICE_NAME:
|
||||
// save the connected device's name
|
||||
connectedDeviceName =
|
||||
msg.getData().getString(BluetoothConnectionManager.DEVICE_NAME);
|
||||
Toast.makeText(context.getApplicationContext(),
|
||||
"Connected to " + connectedDeviceName, Toast.LENGTH_SHORT)
|
||||
.show();
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright (C) 2010 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
|
||||
package com.google.android.apps.mytracks.services.sensors;
|
||||
|
||||
import com.google.android.apps.mytracks.content.Sensor;
|
||||
|
||||
/**
|
||||
* An interface for parsing a byte array to a SensorData object.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public interface MessageParser {
|
||||
|
||||
public int getFrameSize();
|
||||
|
||||
public Sensor.SensorDataSet parseBuffer(byte[] readBuff);
|
||||
|
||||
public boolean isValid(byte[] buffer);
|
||||
|
||||
public int findNextAlignment(byte[] buffer);
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
/*
|
||||
* Copyright 2010 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package com.google.android.apps.mytracks.services.sensors;
|
||||
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import com.google.android.apps.mytracks.MyTracksConstants;
|
||||
import com.google.android.apps.mytracks.content.Sensor;
|
||||
|
||||
/**
|
||||
* Manage the connection to a sensor.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public abstract class SensorManager {
|
||||
|
||||
/**
|
||||
* The maximum age where the data is considered valid.
|
||||
*/
|
||||
public static final long MAX_AGE = 5000;
|
||||
|
||||
/**
|
||||
* Time to wait after a time out to retry.
|
||||
*/
|
||||
public static final int RETRY_PERIOD = 30000;
|
||||
|
||||
private Sensor.SensorState sensorState = Sensor.SensorState.NONE;
|
||||
|
||||
private long sensorStateTimestamp = 0;
|
||||
|
||||
/**
|
||||
* A task to run periodically to check to see if connection was lost.
|
||||
*/
|
||||
private TimerTask checkSensorManager = new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
Log.i(MyTracksConstants.TAG,
|
||||
"SensorManager state: " + getSensorState());
|
||||
switch (getSensorState()) {
|
||||
case CONNECTING:
|
||||
long age = System.currentTimeMillis() - getSensorStateTimestamp();
|
||||
if (age > 2 * RETRY_PERIOD) {
|
||||
Log.i(MyTracksConstants.TAG, "Retrying connecting SensorManager.");
|
||||
setupChannel();
|
||||
}
|
||||
break;
|
||||
case DISCONNECTED:
|
||||
Log.i(MyTracksConstants.TAG,
|
||||
"Re-registering disconnected SensoManager.");
|
||||
setupChannel();
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* This timer invokes periodically the checkLocationListener timer task.
|
||||
*/
|
||||
private final Timer timer = new Timer();
|
||||
|
||||
/**
|
||||
* Is the sensor that this manages enabled.
|
||||
* @return true if the sensor is enabled
|
||||
*/
|
||||
public abstract boolean isEnabled();
|
||||
|
||||
/**
|
||||
* This is called when my tracks starts recording a new track.
|
||||
* This is the place to open connections to the sensor.
|
||||
*/
|
||||
public void onStartTrack() {
|
||||
setupChannel();
|
||||
timer.schedule(checkSensorManager, RETRY_PERIOD, RETRY_PERIOD);
|
||||
}
|
||||
|
||||
public abstract void setupChannel();
|
||||
|
||||
public void shutdown() {
|
||||
timer.cancel();
|
||||
onDestroy();
|
||||
}
|
||||
|
||||
/**
|
||||
* This is called when my tracks stops recording.
|
||||
* This is the place to shutdown any open connections.
|
||||
*/
|
||||
public abstract void onDestroy();
|
||||
|
||||
/**
|
||||
* Return the last sensor reading.
|
||||
* @return The last reading from the sensor.
|
||||
*/
|
||||
public abstract Sensor.SensorDataSet getSensorDataSet();
|
||||
|
||||
public void setSensorState(Sensor.SensorState sensorState) {
|
||||
this.sensorState = sensorState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the current sensor state.
|
||||
* @return The current sensor state.
|
||||
*/
|
||||
public Sensor.SensorState getSensorState() {
|
||||
return sensorState;
|
||||
}
|
||||
|
||||
public long getSensorStateTimestamp() {
|
||||
return sensorStateTimestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return True if the data is recent enough to be considered valid.
|
||||
*/
|
||||
public boolean isDataValid() {
|
||||
return (System.currentTimeMillis() - getSensorDataSet().getCreationTime()) < MAX_AGE;
|
||||
}
|
||||
|
||||
}
|
||||
+64
@@ -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 "";
|
||||
}
|
||||
}
|
||||
}
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* Copyright 2010 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package com.google.android.apps.mytracks.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;
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright 2010 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package com.google.android.apps.mytracks.services.sensors;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
public class ZephyrSensorManager extends BluetoothSensorManager {
|
||||
|
||||
public ZephyrSensorManager(Context context) {
|
||||
super(context, new ZephyrMessageParser());
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user