merge heads

This commit is contained in:
Jimmy Shih
2012-04-11 10:01:41 -07:00
commit 0cd11c799d
568 changed files with 62286 additions and 0 deletions
@@ -0,0 +1,59 @@
/*
* Copyright 2009 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.services;
/**
* This is a simple location listener policy that will always dictate the same
* polling interval.
*
* @author Sandor Dornbush
*/
public class AbsoluteLocationListenerPolicy implements LocationListenerPolicy {
/**
* The interval to request for gps signal.
*/
private final long interval;
/**
* @param interval The interval to request for gps signal
*/
public AbsoluteLocationListenerPolicy(final long interval) {
this.interval = interval;
}
/**
* @return The interval given in the constructor
*/
public long getDesiredPollingInterval() {
return interval;
}
/**
* Discards the idle time.
*/
public void updateIdleTime(long idleTime) {
}
/**
* Returns the minimum distance between updates.
* Get all updates to properly measure moving time.
*/
public int getMinDistance() {
return 0;
}
}
@@ -0,0 +1,81 @@
/*
* Copyright 2009 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.services;
/**
* A LocationListenerPolicy that will change based on how long the user has been
* stationary.
*
* This policy will dictate a policy based on a min, max and idle time.
* The policy will dictate an interval bounded by min and max whic is half of
* the idle time.
*
* @author Sandor Dornbush
*/
public class AdaptiveLocationListenerPolicy implements LocationListenerPolicy {
/**
* Smallest interval this policy will dictate, in milliseconds.
*/
private final long minInterval;
/**
* Largest interval this policy will dictate, in milliseconds.
*/
private final long maxInterval;
private final int minDistance;
/**
* The time the user has been at the current location, in milliseconds.
*/
private long idleTime;
/**
* Creates a policy that will be bounded by the given min and max.
*
* @param min Smallest interval this policy will dictate, in milliseconds
* @param max Largest interval this policy will dictate, in milliseconds
*/
public AdaptiveLocationListenerPolicy(long min, long max, int minDistance) {
this.minInterval = min;
this.maxInterval = max;
this.minDistance = minDistance;
}
/**
* @return An interval bounded by min and max which is half of the idle time
*/
public long getDesiredPollingInterval() {
long desiredInterval = idleTime / 2;
// Round to avoid setting the interval too often.
desiredInterval = (desiredInterval / 1000) * 1000;
return Math.max(Math.min(maxInterval, desiredInterval),
minInterval);
}
public void updateIdleTime(long newIdleTime) {
this.idleTime = newIdleTime;
}
/**
* Returns the minimum distance between updates.
*/
public int getMinDistance() {
return minDistance;
}
}
@@ -0,0 +1,121 @@
/*
* Copyright 2011 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.services;
import static com.google.android.apps.mytracks.Constants.TAG;
import com.google.android.apps.mytracks.widgets.TrackWidgetProvider;
import com.google.android.maps.mytracks.R;
import android.app.IntentService;
import android.content.ComponentName;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Log;
/**
* A service to control starting and stopping of a recording. This service,
* through the AndroidManifest.xml, is configured to only allow components of
* the same application to invoke it. Thus this service can be used my MyTracks
* app widget, {@link TrackWidgetProvider}, but not by other applications. This
* application delegates starting and stopping a recording to
* {@link TrackRecordingService} using RPC calls.
*
* @author Jimmy Shih
*/
public class ControlRecordingService extends IntentService implements ServiceConnection {
private ITrackRecordingService trackRecordingService;
private boolean connected = false;
public ControlRecordingService() {
super(ControlRecordingService.class.getSimpleName());
}
@Override
public void onCreate() {
super.onCreate();
Intent newIntent = new Intent(this, TrackRecordingService.class);
startService(newIntent);
bindService(newIntent, this, 0);
}
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
trackRecordingService = ITrackRecordingService.Stub.asInterface(service);
notifyConnected();
}
@Override
public void onServiceDisconnected(ComponentName name) {
connected = false;
}
/**
* Notifies all threads that connection to {@link TrackRecordingService} is
* available.
*/
private synchronized void notifyConnected() {
connected = true;
notifyAll();
}
/**
* Waits until the connection to {@link TrackRecordingService} is available.
*/
private synchronized void waitConnected() {
while (!connected) {
try {
wait();
} catch (InterruptedException e) {
// can safely ignore
}
}
}
@Override
protected void onHandleIntent(Intent intent) {
waitConnected();
String action = intent.getAction();
if (action != null) {
try {
if (action.equals(getString(R.string.track_action_start))) {
trackRecordingService.startNewTrack();
} else if (action.equals(getString(R.string.track_action_end))) {
trackRecordingService.endCurrentTrack();
}
} catch (RemoteException e) {
Log.d(TAG, "ControlRecordingService onHandleIntent RemoteException", e);
}
}
unbindService(this);
connected = false;
}
@Override
public void onDestroy() {
super.onDestroy();
if (connected) {
unbindService(this);
connected = false;
}
}
}
@@ -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;
import com.google.android.apps.mytracks.Constants;
import com.google.android.apps.mytracks.util.StringUtils;
import com.google.android.maps.mytracks.R;
import com.google.common.annotations.VisibleForTesting;
import android.content.Context;
import android.content.SharedPreferences;
import java.text.SimpleDateFormat;
/**
* Creates a default track name based on the track name setting.
*
* @author Matthew Simmons
*/
public class DefaultTrackNameFactory {
@VisibleForTesting
static final String ISO_8601_FORMAT = "yyyy-MM-dd HH:mm";
private final Context context;
public DefaultTrackNameFactory(Context context) {
this.context = context;
}
/**
* Gets the default track name.
*
* @param trackId the track id
* @param startTime the track start time
*/
public String getDefaultTrackName(long trackId, long startTime) {
String trackNameSetting = getTrackNameSetting();
if (trackNameSetting.equals(
context.getString(R.string.settings_recording_track_name_date_local_value))) {
return StringUtils.formatDateTime(context, startTime);
} else if (trackNameSetting.equals(
context.getString(R.string.settings_recording_track_name_date_iso_8601_value))) {
SimpleDateFormat dateFormat = new SimpleDateFormat(ISO_8601_FORMAT);
return dateFormat.format(startTime);
} else {
// trackNameSetting equals
// R.string.settings_recording_track_name_number_value
return String.format(context.getString(R.string.track_name_format), trackId);
}
}
/**
* Gets the track name setting.
*/
@VisibleForTesting
String getTrackNameSetting() {
SharedPreferences sharedPreferences = context.getSharedPreferences(
Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
return sharedPreferences.getString(
context.getString(R.string.track_name_key),
context.getString(R.string.settings_recording_track_name_date_local_value));
}
}
@@ -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,196 @@
/*
* 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.Constants;
import com.google.android.apps.mytracks.util.ApiAdapterFactory;
import com.google.android.maps.mytracks.R;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
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 selectedTrackKey;
private final String splitFrequencyKey;
public PreferenceManager(TrackRecordingService service) {
this.service = service;
this.sharedPreferences = service.getSharedPreferences(
Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
if (sharedPreferences == null) {
Log.w(Constants.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);
selectedTrackKey =
service.getString(R.string.selected_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 preferences,
String key) {
if (service == null) {
Log.w(Constants.TAG,
"onSharedPreferenceChanged: a preference change (key = " + key
+ ") after a call to shutdown()");
return;
}
if (key == null || key.equals(minRecordingDistanceKey)) {
int minRecordingDistance = sharedPreferences.getInt(
minRecordingDistanceKey,
Constants.DEFAULT_MIN_RECORDING_DISTANCE);
service.setMinRecordingDistance(minRecordingDistance);
Log.d(Constants.TAG,
"TrackRecordingService: minRecordingDistance = "
+ minRecordingDistance);
}
if (key == null || key.equals(maxRecordingDistanceKey)) {
service.setMaxRecordingDistance(sharedPreferences.getInt(
maxRecordingDistanceKey,
Constants.DEFAULT_MAX_RECORDING_DISTANCE));
}
if (key == null || key.equals(minRecordingIntervalKey)) {
int minRecordingInterval = sharedPreferences.getInt(
minRecordingIntervalKey,
Constants.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,
Constants.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,
Constants.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.setSplitFrequency(
sharedPreferences.getInt(splitFrequencyKey, 0));
}
if (key == null || key.equals(metricUnitsKey)) {
service.setMetricUnits(
sharedPreferences.getBoolean(metricUnitsKey, true));
}
}
public void setAutoResumeTrackCurrentRetry(int retryAttempts) {
Editor editor = sharedPreferences.edit();
editor.putInt(autoResumeTrackCurrentRetryKey, retryAttempts);
ApiAdapterFactory.getApiAdapter().applyPreferenceChanges(editor);
}
public void setRecordingTrack(long id) {
Editor editor = sharedPreferences.edit();
editor.putLong(recordingTrackKey, id);
ApiAdapterFactory.getApiAdapter().applyPreferenceChanges(editor);
}
public void setSelectedTrack(long id) {
Editor editor = sharedPreferences.edit();
editor.putLong(selectedTrackKey, id);
ApiAdapterFactory.getApiAdapter().applyPreferenceChanges(editor);
}
public void shutdown() {
sharedPreferences.unregisterOnSharedPreferenceChangeListener(this);
service = null;
}
}
@@ -0,0 +1,121 @@
/*
* Copyright 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.services;
import com.google.android.apps.mytracks.io.file.TrackWriterFactory.TrackFileFormat;
import com.google.android.apps.mytracks.util.FileUtils;
import com.google.common.annotations.VisibleForTesting;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.Environment;
import android.os.IBinder;
import android.util.Log;
import java.io.File;
/**
* A service to remove My Tracks temp files older than one hour on the SD card.
*
* @author Jimmy Shih
*/
public class RemoveTempFilesService extends Service {
private static final String TAG = RemoveTempFilesService.class.getSimpleName();
private static final int ONE_HOUR_IN_MILLISECONDS = 60 * 60 * 1000;
private RemoveTempFilesAsyncTask removeTempFilesAsyncTask = null;
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
// Setup an alarm to repeatedly call this service
Intent alarmIntent = new Intent(this, RemoveTempFilesService.class);
PendingIntent pendingIntent = PendingIntent.getService(
this, 0, alarmIntent, PendingIntent.FLAG_CANCEL_CURRENT);
AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
alarmManager.setInexactRepeating(AlarmManager.RTC_WAKEUP,
System.currentTimeMillis() + ONE_HOUR_IN_MILLISECONDS, AlarmManager.INTERVAL_HOUR,
pendingIntent);
// Invoke the AsyncTask to cleanup the temp files
if (removeTempFilesAsyncTask == null
|| removeTempFilesAsyncTask.getStatus().equals(AsyncTask.Status.FINISHED)) {
removeTempFilesAsyncTask = new RemoveTempFilesAsyncTask();
removeTempFilesAsyncTask.execute((Void[]) null);
}
return START_NOT_STICKY;
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
private class RemoveTempFilesAsyncTask extends AsyncTask<Void, Void, Void> {
@Override
protected Void doInBackground(Void... params) {
if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
// Can't do anything
return null;
}
cleanTempDirectory(TrackFileFormat.GPX.getExtension());
cleanTempDirectory(TrackFileFormat.KML.getExtension());
cleanTempDirectory(TrackFileFormat.CSV.getExtension());
cleanTempDirectory(TrackFileFormat.TCX.getExtension());
return null;
}
@Override
protected void onPostExecute(Void result) {
stopSelf();
}
}
private void cleanTempDirectory(String name) {
cleanTempDirectory(new File(FileUtils.buildExternalDirectoryPath(name, "tmp")));
}
/**
* Removes temp files in a directory older than one hour.
*
* @param dir the directory
* @return the number of files removed.
*/
@VisibleForTesting
int cleanTempDirectory(File dir) {
if (!dir.exists()) {
return 0;
}
int count = 0;
long oneHourAgo = System.currentTimeMillis() - ONE_HOUR_IN_MILLISECONDS;
for (File f : dir.listFiles()) {
if (f.lastModified() < oneHourAgo) {
if (!f.delete()) {
Log.e(TAG, "Unable to delete file: " + f.getAbsolutePath());
} else {
count++;
}
}
}
return count;
}
}
@@ -0,0 +1,88 @@
/*
* Copyright 2011 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.services;
import static com.google.android.apps.mytracks.Constants.TAG;
import com.google.android.apps.mytracks.Constants;
import com.google.android.maps.mytracks.R;
import android.app.ActivityManager;
import android.app.ActivityManager.RunningServiceInfo;
import android.content.ComponentName;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.RemoteException;
import android.util.Log;
import java.util.List;
/**
* Helper for reading service state.
*
* @author Rodrigo Damazio
*/
public class ServiceUtils {
/**
* Checks whether we're currently recording.
* The checking is done by calling the service, if provided, or alternatively by reading
* recording state saved to preferences.
*
* @param ctx the current context
* @param service the service, or null if not bound to it
* @param preferences the preferences, or null if not available
* @return true if the service is recording (or supposed to be recording), false otherwise
*/
public static boolean isRecording(Context ctx, ITrackRecordingService service, SharedPreferences preferences) {
if (service != null) {
try {
return service.isRecording();
} catch (RemoteException e) {
Log.e(TAG, "Failed to check if service is recording", e);
} catch (IllegalStateException e) {
Log.e(TAG, "Failed to check if service is recording", e);
}
}
if (preferences == null) {
preferences = ctx.getSharedPreferences(Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
}
return preferences.getLong(ctx.getString(R.string.recording_track_key), -1) > 0;
}
/**
* Checks whether the recording service is currently running.
*
* @param ctx the current context
* @return true if the service is running, false otherwise
*/
public static boolean isServiceRunning(Context ctx) {
ActivityManager activityManager = (ActivityManager) ctx.getSystemService(Context.ACTIVITY_SERVICE);
List<RunningServiceInfo> services = activityManager.getRunningServices(Integer.MAX_VALUE);
for (RunningServiceInfo serviceInfo : services) {
ComponentName componentName = serviceInfo.service;
String serviceName = componentName.getClassName();
if (serviceName.equals(TrackRecordingService.class.getName())) {
return true;
}
}
return false;
}
private ServiceUtils() {}
}
@@ -0,0 +1,171 @@
/*
* Copyright 2011 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.services;
import static com.google.android.apps.mytracks.Constants.TAG;
import com.google.android.apps.mytracks.util.SystemUtils;
import com.google.android.maps.mytracks.BuildConfig;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.IBinder;
import android.os.IBinder.DeathRecipient;
import android.os.RemoteException;
import android.util.Log;
/**
* Wrapper for the connection to the track recording service.
* This handles connection/disconnection internally, only returning a real
* service for use if one is available and connected.
*
* @author Rodrigo Damazio
*/
public class TrackRecordingServiceConnection {
private ITrackRecordingService boundService;
private final DeathRecipient deathRecipient = new DeathRecipient() {
@Override
public void binderDied() {
Log.d(TAG, "Service died");
setBoundService(null);
}
};
private final ServiceConnection serviceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName className, IBinder service) {
Log.i(TAG, "Connected to service");
try {
service.linkToDeath(deathRecipient, 0);
} catch (RemoteException e) {
Log.e(TAG, "Failed to bind a death recipient", e);
}
setBoundService(ITrackRecordingService.Stub.asInterface(service));
}
@Override
public void onServiceDisconnected(ComponentName className) {
Log.i(TAG, "Disconnected from service");
setBoundService(null);
}
};
private final Context context;
private final Runnable bindChangedCallback;
/**
* Constructor.
*
* @param context the current context
* @param bindChangedCallback a callback to be executed when the state of the
* service binding changes
*/
public TrackRecordingServiceConnection(Context context, Runnable bindChangedCallback) {
this.context = context;
this.bindChangedCallback = bindChangedCallback;
}
/**
* Binds to the service, starting it if necessary.
*/
public void startAndBind() {
bindService(true);
}
/**
* Binds to the service, only if it's already running.
*/
public void bindIfRunning() {
bindService(false);
}
/**
* Unbinds from and stops the service.
*/
public void stop() {
unbind();
Log.d(TAG, "Stopping service");
Intent intent = new Intent(context, TrackRecordingService.class);
context.stopService(intent);
}
/**
* Unbinds from the service (but leaves it running).
*/
public void unbind() {
Log.d(TAG, "Unbinding from the service");
try {
context.unbindService(serviceConnection);
} catch (IllegalArgumentException e) {
// Means we weren't bound, which is ok.
}
setBoundService(null);
}
/**
* Returns the service if connected to it, or null if not connected.
*/
public ITrackRecordingService getServiceIfBound() {
checkBindingAlive();
return boundService;
}
private void checkBindingAlive() {
if (boundService != null &&
!boundService.asBinder().isBinderAlive()) {
setBoundService(null);
}
}
private void bindService(boolean startIfNeeded) {
if (boundService != null) {
// Already bound.
return;
}
if (!startIfNeeded && !ServiceUtils.isServiceRunning(context)) {
// Not running, start not requested.
Log.d(TAG, "Service not running, not binding to it.");
return;
}
if (startIfNeeded) {
Log.i(TAG, "Starting the service");
Intent intent = new Intent(context, TrackRecordingService.class);
context.startService(intent);
}
Log.i(TAG, "Binding to the service");
Intent intent = new Intent(context, TrackRecordingService.class);
int flags = BuildConfig.DEBUG ? Context.BIND_DEBUG_UNBIND : 0;
context.bindService(intent, serviceConnection, flags);
}
private void setBoundService(ITrackRecordingService service) {
boundService = service;
if (bindChangedCallback != null) {
bindChangedCallback.run();
}
}
}
@@ -0,0 +1,412 @@
/*
* 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.Constants;
import com.google.android.apps.mytracks.content.Sensor;
import com.google.android.apps.mytracks.util.ApiAdapterFactory;
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.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 Bluetooth UUID for My Tracks
public 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 handler A Handler to send messages back to the UI Activity
* @param parser A message parser
*/
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(Constants.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(Constants.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(Constants.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(Constants.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(Constants.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(Constants.TAG, "Bluetooth connection failed.");
}
/**
* Indicate that the connection was lost and notify the UI Activity.
*/
private void connectionLost() {
setState(Sensor.SensorState.DISCONNECTED);
Log.i(Constants.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 = ApiAdapterFactory.getApiAdapter().getBluetoothSocket(device);
} catch (IOException e) {
Log.e(Constants.TAG, "create() failed", e);
}
socket = tmp;
}
@Override
public void run() {
Log.d(Constants.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(Constants.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(Constants.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(Constants.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(Constants.TAG, "temp sockets not created", e);
}
mmInStream = tmpIn;
mmOutStream = tmpOut;
}
@Override
public void run() {
Log.i(Constants.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(Constants.TAG, "Misaligned data, found new message at " +
index + " recovering...");
continue;
}
Log.w(Constants.TAG, "Could not find valid data, dropping data");
offset = 0;
continue;
}
offset = 0;
// Send copy of the obtained bytes to the UI Activity.
// Avoids memory inconsistency issues.
handler.obtainMessage(MESSAGE_READ, bytes, -1, buffer.clone())
.sendToTarget();
} catch (IOException e) {
Log.e(Constants.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(Constants.TAG, "Exception during write", e);
}
}
public void cancel() {
try {
btSocket.close();
} catch (IOException e) {
Log.e(Constants.TAG, "close() of connect socket failed", e);
}
}
}
}
@@ -0,0 +1,212 @@
/*
* 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 static com.google.android.apps.mytracks.Constants.TAG;
import com.google.android.apps.mytracks.Constants;
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.Looper;
import android.os.Message;
import android.util.Log;
import android.widget.Toast;
import java.util.ArrayList;
/**
* Manage the connection to a bluetooth sensor.
*
* @author Sandor Dornbush
*/
public class BluetoothSensorManager extends SensorManager {
// Local Bluetooth adapter
private static final BluetoothAdapter bluetoothAdapter = getDefaultBluetoothAdapter();
// 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;
// If BT is not available or not enabled quit.
if (!isEnabled()) {
return;
}
setupSensor();
}
private void setupSensor() {
Log.d(Constants.TAG, "setupSensor()");
// Initialize the BluetoothSensorAdapter to perform bluetooth connections.
connectionManager = new BluetoothConnectionManager(messageHandler, parser);
}
/**
* Code for assigning the local bluetooth adapter
*
* @return The default bluetooth adapter, if one is available, NULL if it isn't.
*/
private static BluetoothAdapter getDefaultBluetoothAdapter() {
// Check if the calling thread is the main application thread,
// if it is, do it directly.
if (Thread.currentThread().equals(Looper.getMainLooper().getThread())) {
return BluetoothAdapter.getDefaultAdapter();
}
// If the calling thread, isn't the main application thread,
// then get the main application thread to return the default adapter.
final ArrayList<BluetoothAdapter> adapters = new ArrayList<BluetoothAdapter>(1);
final Object mutex = new Object();
Handler handler = new Handler(Looper.getMainLooper());
handler.post(new Runnable() {
@Override
public void run() {
adapters.add(BluetoothAdapter.getDefaultAdapter());
synchronized (mutex) {
mutex.notify();
}
}
});
while (adapters.isEmpty()) {
synchronized (mutex) {
try {
mutex.wait(1000L);
} catch (InterruptedException e) {
Log.e(TAG, "Interrupted while waiting for default bluetooth adapter", e);
}
}
}
if (adapters.get(0) == null) {
Log.w(TAG, "No bluetooth adapter found!");
}
return adapters.get(0);
}
public boolean isEnabled() {
return bluetoothAdapter != null && bluetoothAdapter.isEnabled();
}
public void setupChannel() {
if (!isEnabled() || connectionManager == null) {
Log.w(Constants.TAG, "Disabled manager onStartTrack");
return;
}
SharedPreferences prefs = context.getSharedPreferences(
Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
String address =
prefs.getString(context.getString(R.string.bluetooth_sensor_key), "");
if (address == null || address.equals("")) {
return;
}
Log.w(Constants.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(Constants.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(Looper.getMainLooper()) {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case BluetoothConnectionManager.MESSAGE_STATE_CHANGE:
// TODO should we update the SensorManager state var?
Log.i(Constants.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(Constants.TAG, "MESSAGE_READ: " + sensorDataSet.toString());
} catch (IllegalArgumentException iae) {
sensorDataSet = null;
Log.i(Constants.TAG,
"Got bad sensor data: " + new String(readBuf, 0, readBuf.length),
iae);
} catch (RuntimeException re) {
sensorDataSet = null;
Log.i(Constants.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,140 @@
/*
* Copyright 2011 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.services.sensors;
import com.google.android.apps.mytracks.content.Sensor;
/**
* An implementation of a Sensor MessageParser for Polar Wearlink Bluetooth HRM.
*
* Polar Bluetooth Wearlink packet example;
* Hdr Len Chk Seq Status HeartRate RRInterval_16-bits
* FE 08 F7 06 F1 48 03 64
* where;
* Hdr always = 254 (0xFE),
* Chk = 255 - Len
* Seq range 0 to 15
* Status = Upper nibble may be battery voltage
* bit 0 is Beat Detection flag.
*
* Additional packet examples;
* FE 08 F7 06 F1 48 03 64
* FE 0A F5 06 F1 48 03 64 03 70
*
* @author John R. Gerthoffer
*/
public class PolarMessageParser implements MessageParser {
private int lastHeartRate = 0;
/**
* Applies Polar packet validation rules to buffer.
* Polar packets are checked for following;
* offset 0 = header byte, 254 (0xFE).
* offset 1 = packet length byte, 8, 10, 12, 14.
* offset 2 = check byte, 255 - packet length.
* offset 3 = sequence byte, range from 0 to 15.
*
* @param buffer an array of bytes to parse
* @param i buffer offset to beginning of packet.
* @return whether buffer has a valid packet at offset i
*/
private boolean packetValid (byte[] buffer, int i) {
boolean headerValid = (buffer[i] & 0xFF) == 0xFE;
boolean checkbyteValid = (buffer[i + 2] & 0xFF) == (0xFF - (buffer[i + 1] & 0xFF));
boolean sequenceValid = (buffer[i + 3] & 0xFF) < 16;
return headerValid && checkbyteValid && sequenceValid;
}
@Override
public Sensor.SensorDataSet parseBuffer(byte[] buffer) {
int heartRate = 0;
boolean heartrateValid = false;
// Minimum length Polar packets is 8, so stop search 8 bytes before buffer ends.
for (int i = 0; i < buffer.length - 8; i++) {
heartrateValid = packetValid(buffer,i);
if (heartrateValid) {
heartRate = buffer[i + 5] & 0xFF;
break;
}
}
// If our buffer is corrupted, use decaying last good value.
if(!heartrateValid) {
heartRate = (int) (lastHeartRate * 0.8);
if(heartRate < 50)
heartRate = 0;
}
lastHeartRate = heartRate; // Remember good value for next time.
// Heart Rate
Sensor.SensorData.Builder b = Sensor.SensorData.newBuilder()
.setValue(heartRate)
.setState(Sensor.SensorState.SENDING);
Sensor.SensorDataSet sds = Sensor.SensorDataSet.newBuilder()
.setCreationTime(System.currentTimeMillis())
.setHeartRate(b)
.build();
return sds;
}
/**
* Applies packet validation rules to buffer
*
* @param buffer an array of bytes to parse
* @return whether buffer has a valid packet starting at index zero
*/
@Override
public boolean isValid(byte[] buffer) {
return packetValid(buffer,0);
}
/**
* Polar uses variable packet sizes; 8, 10, 12, 14 and rarely 16.
* The most frequent are 8 and 10.
* We will wait for 16 bytes.
* This way, we are assured of getting one good one.
*
* @return the size of buffer needed to parse a good packet
*/
@Override
public int getFrameSize() {
return 16;
}
/**
* Searches buffer for the beginning of a valid packet.
*
* @param buffer an array of bytes to parse
* @return index to beginning of good packet, or -1 if none found.
*/
@Override
public int findNextAlignment(byte[] buffer) {
// Minimum length Polar packets is 8, so stop search 8 bytes before buffer ends.
for (int i = 0; i < buffer.length - 8; i++) {
if (packetValid(buffer,i)) {
return i;
}
}
return -1;
}
}
@@ -0,0 +1,28 @@
/*
* Copyright 2011 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.services.sensors;
import android.content.Context;
/**
* PolarSensorManager - A sensor manager for Polar heart rate monitors.
*/
public class PolarSensorManager extends BluetoothSensorManager {
public PolarSensorManager(Context context) {
super(context, new PolarMessageParser());
}
}
@@ -0,0 +1,138 @@
/*
* 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.Constants;
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(Constants.TAG,
"SensorManager state: " + getSensorState());
switch (getSensorState()) {
case CONNECTING:
long age = System.currentTimeMillis() - getSensorStateTimestamp();
if (age > 2 * RETRY_PERIOD) {
Log.i(Constants.TAG, "Retrying connecting SensorManager.");
setupChannel();
}
break;
case DISCONNECTED:
Log.i(Constants.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);
}
/**
* This method is used to set up any necessary connections to underlying
* sensor hardware.
*/
protected abstract void setupChannel();
public void shutdown() {
timer.cancel();
onDestroy();
}
/**
* This is called when my tracks stops recording.
* This is the place to shutdown any open connections.
*/
public abstract void onDestroy();
/**
* Return the last sensor reading.
* @return The last reading from the sensor.
*/
public abstract Sensor.SensorDataSet getSensorDataSet();
public void setSensorState(Sensor.SensorState sensorState) {
this.sensorState = sensorState;
}
/**
* Return the current sensor state.
* @return The current sensor state.
*/
public Sensor.SensorState getSensorState() {
return sensorState;
}
public long getSensorStateTimestamp() {
return sensorStateTimestamp;
}
/**
* @return True if the data is recent enough to be considered valid.
*/
public boolean isDataValid() {
return (System.currentTimeMillis() - getSensorDataSet().getCreationTime()) < MAX_AGE;
}
}
@@ -0,0 +1,118 @@
/*
* 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.Constants;
import com.google.android.apps.mytracks.services.sensors.ant.AntDirectSensorManager;
import com.google.android.apps.mytracks.services.sensors.ant.AntSrmBridgeSensorManager;
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 String activeSensorType;
private SensorManager activeSensorManager;
private int refCount;
private static SensorManagerFactory instance = new SensorManagerFactory();
private SensorManagerFactory() {
}
/**
* Get the factory instance.
*/
public static SensorManagerFactory getInstance() {
return instance;
}
/**
* Get and start a new sensor manager.
* @param context Context to fetch system preferences.
* @return The sensor manager that corresponds to the sensor type setting.
*/
public SensorManager getSensorManager(Context context) {
SharedPreferences prefs = context.getSharedPreferences(
Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
if (prefs == null) {
return null;
}
context = context.getApplicationContext();
String sensor = prefs.getString(context.getString(R.string.sensor_type_key), null);
Log.i(Constants.TAG, "Creating sensor of type: " + sensor);
if (sensor == null) {
reset();
return null;
}
if (sensor.equals(activeSensorType)) {
Log.i(Constants.TAG, "Returning existing sensor manager.");
refCount++;
return activeSensorManager;
}
reset();
if (sensor.equals(context.getString(R.string.sensor_type_value_ant))) {
activeSensorManager = new AntDirectSensorManager(context);
} else if (sensor.equals(context.getString(R.string.sensor_type_value_srm_ant_bridge))) {
activeSensorManager = new AntSrmBridgeSensorManager(context);
} else if (sensor.equals(context.getString(R.string.sensor_type_value_zephyr))) {
activeSensorManager = new ZephyrSensorManager(context);
} else if (sensor.equals(context.getString(R.string.sensor_type_value_polar))) {
activeSensorManager = new PolarSensorManager(context);
} else {
Log.w(Constants.TAG, "Unable to find sensor type: " + sensor);
return null;
}
activeSensorType = sensor;
refCount = 1;
activeSensorManager.onStartTrack();
return activeSensorManager;
}
/**
* Finish using a sensor manager.
*/
public void releaseSensorManager(SensorManager sensorManager) {
Log.i(Constants.TAG, "releaseSensorManager: " + activeSensorType + " " + refCount);
if (sensorManager != activeSensorManager) {
Log.e(Constants.TAG, "invalid parameter to releaseSensorManager");
}
if (--refCount > 0) {
return;
}
reset();
}
private void reset() {
activeSensorType = null;
if (activeSensorManager != null) {
activeSensorManager.shutdown();
}
activeSensorManager = null;
refCount = 0;
}
}
@@ -0,0 +1,115 @@
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.services.sensors;
import com.google.android.apps.mytracks.content.Sensor;
import com.google.android.maps.mytracks.R;
import android.content.Context;
/**
* A collection of methods for message parsers.
*
* @author Sandor Dornbush
* @author Nico Laum
*/
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;
}
/**
* Extract one unsigned short from a little 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 unsignedShortToIntLittleEndian(byte[] buffer, int index) {
int r = buffer[index] & 0xFF;
r |= (buffer[index + 1] & 0xFF) << 8;
return r;
}
/**
* Returns CRC8 (polynomial 0x8C) from byte array buffer[start] to
* (excluding) buffer[start + length]
*
* @param buffer the byte array of data (payload)
* @param start the position in the byte array where the payload begins
* @param length the length
* @return CRC8 value
*/
public static byte getCrc8(byte[] buffer, int start, int length) {
byte crc = 0x0;
for (int i = start; i < (start + length); i++) {
crc = crc8PushByte(crc, buffer[i]);
}
return crc;
}
/**
* Updates a CRC8 value by using the next byte passed to this method
*
* @param crc int of crc value
* @param add the next byte to add to the CRC8 calculation
*/
private static byte crc8PushByte(byte crc, byte add) {
crc = (byte) (crc ^ add);
for (int i = 0; i < 8; i++) {
if ((crc & 0x1) != 0x0) {
// Using a 0xFF bit assures that 0-bits are introduced during the shift operation.
// Otherwise, implicit casts to signed int could shift in 1-bits if the signed bit is 1.
crc = (byte) (((crc & 0xFF) >> 1) ^ 0x8C);
} else {
crc = (byte) ((crc & 0xFF) >> 1);
}
}
return crc;
}
public static String getStateAsString(Sensor.SensorState state, Context c) {
switch (state) {
case NONE:
return c.getString(R.string.settings_sensor_type_none);
case CONNECTING:
return c.getString(R.string.sensor_state_connecting);
case CONNECTED:
return c.getString(R.string.sensor_state_connected);
case DISCONNECTED:
return c.getString(R.string.sensor_state_disconnected);
case SENDING:
return c.getString(R.string.sensor_state_sending);
default:
return "";
}
}
}
@@ -0,0 +1,80 @@
/*
* Copyright 2011 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.services.sensors;
import java.util.LinkedList;
import java.util.List;
/**
* A history of Zephyr stride counter reading.
* These can be used as an alternate method to calculate the correct cadence.
* This is a work around for an issue with some HxM firmware.
*
* @author Dominik Ršttsches
*/
public class StrideReadings {
// visible for testing
protected static final int NUM_READINGS_FOR_AVERAGE = 10;
protected static final int MIN_READINGS_FOR_AVERAGE = 5;
protected static final int CADENCE_NOT_AVAILABLE = -1;
// TODO: Check whether 1Hz assumption is okay for cadence calculation
// otherwise add heart beat timestamp to this list and compute
// cadence from these timestamps.
private final List<Integer> strideReadingsHistory;
public StrideReadings() {
strideReadingsHistory = new LinkedList<Integer>();
}
public void updateStrideReading(int numStrides) {
// HRM/HxM documentation says, transmission frequency is 1 Hz,
// let's keep last NUM_READINGS_FOR_AVERAGE readings.
// TODO: Calibrate this using a reliable footpod / cadence sensor,
// otherwise use heartbeat timestamp for calculation.
strideReadingsHistory.add(0, numStrides);
while (strideReadingsHistory.size() > NUM_READINGS_FOR_AVERAGE) {
strideReadingsHistory.remove(strideReadingsHistory.size()-1);
}
}
public int getCadence() {
if (strideReadingsHistory.size() < MIN_READINGS_FOR_AVERAGE) {
// Bail out if we cannot really get a meaningful average yet.
return CADENCE_NOT_AVAILABLE;
}
// Compute assuming 1 stride reading/second.
int timeSinceOldestReadingSecs = strideReadingsHistory.size() - 1;
int stridesThen = strideReadingsHistory.get(strideReadingsHistory.size()-1);
int stridesNow = strideReadingsHistory.get(0);
// Contrary to documentation stride value seems to roll over at 128.
return Math.round( (float)(mod((stridesNow - stridesThen), 128)) /
timeSinceOldestReadingSecs * 60);
}
/**
* Modulo operation with positive return values, Java's remainder operator doesn't change sign.
*
* @return x mod y
*/
private static int mod(int x, int y)
{
int result = x % y;
return result < 0 ? result + y : result;
}
}
@@ -0,0 +1,109 @@
/*
* 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.content.Sensor;
import com.google.android.apps.mytracks.util.ApiAdapterFactory;
import java.util.Arrays;
/**
* An implementation of a Sensor MessageParser for Zephyr.
*
* @author Sandor Dornbush
* @author Dominik Ršttsches
*/
public class ZephyrMessageParser implements MessageParser {
public static final int ZEPHYR_HXM_BYTE_STX = 0;
public static final int ZEPHYR_HXM_BYTE_CRC = 58;
public static final int ZEPHYR_HXM_BYTE_ETX = 59;
private static final byte[] CADENCE_BUG_FW_ID = {0x1A, 0x00, 0x31, 0x65, 0x50, 0x00, 0x31, 0x62};
private StrideReadings strideReadings;
@Override
public Sensor.SensorDataSet parseBuffer(byte[] buffer) {
Sensor.SensorDataSet.Builder sds =
Sensor.SensorDataSet.newBuilder()
.setCreationTime(System.currentTimeMillis());
Sensor.SensorData.Builder heartrate = Sensor.SensorData.newBuilder()
.setValue(buffer[12] & 0xFF)
.setState(Sensor.SensorState.SENDING);
sds.setHeartRate(heartrate);
Sensor.SensorData.Builder batteryLevel = Sensor.SensorData.newBuilder()
.setValue(buffer[11])
.setState(Sensor.SensorState.SENDING);
sds.setBatteryLevel(batteryLevel);
setCadence(sds, buffer);
return sds.build();
}
private void setCadence(Sensor.SensorDataSet.Builder sds, byte[] buffer) {
// Device Firmware ID, Firmware Version, Hardware ID, Hardware Version
// 0x1A00316550003162 produces erroneous values for Cadence and needs
// a workaround based on the stride counter.
// Firmware values range from field 3 to 10 (inclusive) of the byte buffer.
byte[] hardwareFirmwareId = ApiAdapterFactory.getApiAdapter().copyByteArray(buffer, 3, 11);
Sensor.SensorData.Builder cadence = Sensor.SensorData.newBuilder();
if (Arrays.equals(hardwareFirmwareId, CADENCE_BUG_FW_ID)) {
if (strideReadings == null) {
strideReadings = new StrideReadings();
}
strideReadings.updateStrideReading(buffer[54] & 0xFF);
if (strideReadings.getCadence() != StrideReadings.CADENCE_NOT_AVAILABLE) {
cadence.setValue(strideReadings.getCadence()).setState(Sensor.SensorState.SENDING);
}
} else {
cadence
.setValue(SensorUtils.unsignedShortToIntLittleEndian(buffer, 56) / 16)
.setState(Sensor.SensorState.SENDING);
}
sds.setCadence(cadence);
}
@Override
public boolean isValid(byte[] buffer) {
// Check STX (Start of Text), ETX (End of Text) and CRC Checksum
return buffer.length > ZEPHYR_HXM_BYTE_ETX
&& buffer[ZEPHYR_HXM_BYTE_STX] == 0x02
&& buffer[ZEPHYR_HXM_BYTE_ETX] == 0x03
&& SensorUtils.getCrc8(buffer, 3, 55) == buffer[ZEPHYR_HXM_BYTE_CRC];
}
@Override
public int getFrameSize() {
return 60;
}
@Override
public int findNextAlignment(byte[] buffer) {
// TODO test or understand this code.
for (int i = 0; i < buffer.length - 1; i++) {
if (buffer[i] == 0x03 && buffer[i+1] == 0x02) {
return i;
}
}
return -1;
}
}
@@ -0,0 +1,26 @@
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.services.sensors;
import android.content.Context;
public class ZephyrSensorManager extends BluetoothSensorManager {
public ZephyrSensorManager(Context context) {
super(context, new ZephyrMessageParser());
}
}
@@ -0,0 +1,56 @@
/*
* Copyright 2011 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.services.sensors.ant;
/**
* This class decodes and encapsulates an ANT Channel ID message.
* (ANT Message ID 0x51, Protocol & Usage guide v4.2 section 9.5.7.2)
*
* @author Matthew Simmons
*/
public class AntChannelIdMessage extends AntMessage {
private byte channelNumber;
private short deviceNumber;
private byte deviceTypeId;
private byte transmissionType;
public AntChannelIdMessage(byte[] messageData) {
channelNumber = messageData[0];
deviceNumber = decodeShort(messageData[1], messageData[2]);
deviceTypeId = messageData[3];
transmissionType = messageData[4];
}
/** Returns the channel number */
public byte getChannelNumber() {
return channelNumber;
}
/** Returns the device number */
public short getDeviceNumber() {
return deviceNumber;
}
/** Returns the device type */
public byte getDeviceTypeId() {
return deviceTypeId;
}
/** Returns the transmission type */
public byte getTransmissionType() {
return transmissionType;
}
}
@@ -0,0 +1,49 @@
/*
* Copyright 2011 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.services.sensors.ant;
/**
* This class decodes and encapsulates an ANT Channel Response / Event message.
* (ANT Message ID 0x40, Protocol & Usage guide v4.2 section 9.5.6.1)
*
* @author Matthew Simmons
*/
public class AntChannelResponseMessage extends AntMessage {
private byte channelNumber;
private byte messageId;
private byte messageCode;
public AntChannelResponseMessage(byte[] messageData) {
channelNumber = messageData[0];
messageId = messageData[1];
messageCode = messageData[2];
}
/** Returns the channel number */
public byte getChannelNumber() {
return channelNumber;
}
/** Returns the ID of the message being responded to */
public byte getMessageId() {
return messageId;
}
/** Returns the code for a specific response or event */
public byte getMessageCode() {
return messageCode;
}
}
@@ -0,0 +1,252 @@
/*
* 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.ant;
import static com.google.android.apps.mytracks.Constants.TAG;
import com.dsi.ant.AntDefine;
import com.dsi.ant.AntMesg;
import com.dsi.ant.exception.AntInterfaceException;
import com.google.android.apps.mytracks.Constants;
import com.google.android.apps.mytracks.content.Sensor;
import com.google.android.apps.mytracks.util.ApiAdapterFactory;
import com.google.android.maps.mytracks.R;
import android.content.Context;
import android.content.SharedPreferences;
import android.util.Log;
/**
* A sensor manager to connect ANT+ sensors.
*
* @author Sandor Dornbush
* @author Laszlo Molnar
*/
public class AntDirectSensorManager extends AntSensorManager
implements AntSensorDataCollector {
// allocating one channel for each sensor type
private static final byte HEART_RATE_CHANNEL = 0;
private static final byte CADENCE_CHANNEL = 1;
private static final byte CADENCE_SPEED_CHANNEL = 2;
// ids for device number preferences
private static final int sensorIdKeys[] = {
R.string.ant_heart_rate_sensor_id_key,
R.string.ant_cadence_sensor_id_key,
R.string.ant_cadence_speed_sensor_id_key,
};
private AntSensorBase sensors[] = null;
// current data to be sent for SensorDataSet
private static final byte HEART_RATE_DATA_INDEX = 0;
private static final byte CADENCE_DATA_INDEX = 1;
private int currentSensorData[] = { -1, -1 };
private long lastDataSentMillis = 0;
private byte connectingChannelsBitmap = 0;
public AntDirectSensorManager(Context context) {
super(context);
}
@Override
protected boolean handleMessage(byte messageId, byte[] messageData) {
if (super.handleMessage(messageId, messageData)) {
return true;
}
int channel = messageData[0] & AntDefine.CHANNEL_NUMBER_MASK;
if (sensors == null || channel >= sensors.length) {
Log.d(TAG, "Unknown channel in message: " + channel);
return false;
}
AntSensorBase sensor = sensors[channel];
switch (messageId) {
case AntMesg.MESG_BROADCAST_DATA_ID:
if (sensor.getDeviceNumber() == WILDCARD) {
resolveWildcardDeviceNumber((byte) channel);
}
sensor.handleBroadcastData(messageData, this);
break;
case AntMesg.MESG_RESPONSE_EVENT_ID:
handleMessageResponse(messageData);
break;
case AntMesg.MESG_CHANNEL_ID_ID:
sensor.setDeviceNumber(handleChannelId(messageData));
break;
default:
Log.e(TAG, "Unexpected ANT message id: " + messageId);
}
return true;
}
private short handleChannelId(byte[] rawMessage) {
AntChannelIdMessage message = new AntChannelIdMessage(rawMessage);
short deviceNumber = message.getDeviceNumber();
byte channel = message.getChannelNumber();
if (channel >= sensors.length) {
Log.d(TAG, "Unknown channel in message: " + channel);
return WILDCARD;
}
Log.i(TAG, "Found ANT device id: " + deviceNumber + " on channel: " + channel);
SharedPreferences prefs = context.getSharedPreferences(
Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = prefs.edit();
editor.putInt(context.getString(sensorIdKeys[channel]), deviceNumber);
ApiAdapterFactory.getApiAdapter().applyPreferenceChanges(editor);
return deviceNumber;
}
private void channelOut(byte channel)
{
if (channel >= sensors.length) {
Log.d(TAG, "Unknown channel in message: " + channel);
return;
}
connectingChannelsBitmap &= ~(1 << channel);
Log.i(TAG, "ANT channel " + channel + " disconnected.");
if (sensors[channel].getDeviceNumber() != WILDCARD) {
Log.i(TAG, "Retrying....");
if (setupChannel(sensors[channel], channel)) {
connectingChannelsBitmap |= 1 << channel;
}
}
if (connectingChannelsBitmap == 0) {
setSensorState(Sensor.SensorState.DISCONNECTED);
}
}
private void handleMessageResponse(byte[] rawMessage) {
AntChannelResponseMessage message = new AntChannelResponseMessage(rawMessage);
byte channel = message.getChannelNumber();
switch (message.getMessageId()) {
case AntMesg.MESG_EVENT_ID:
if (message.getMessageCode() == AntDefine.EVENT_RX_SEARCH_TIMEOUT) {
// Search timeout
Log.w(TAG, "ANT search timed out. Unassigning channel " + channel);
try {
getAntReceiver().ANTUnassignChannel(channel);
} catch (AntInterfaceException e) {
Log.e(TAG, "ANT error unassigning channel", e);
channelOut(channel);
}
}
break;
case AntMesg.MESG_UNASSIGN_CHANNEL_ID:
channelOut(channel);
break;
}
}
private void resolveWildcardDeviceNumber(byte channel) {
try {
getAntReceiver().ANTRequestMessage(channel, AntMesg.MESG_CHANNEL_ID_ID);
} catch (AntInterfaceException e) {
Log.e(TAG, "ANT error handling broadcast data", e);
}
Log.d(TAG, "Requesting channel id id on channel: " + channel);
}
@Override
protected void setupAntSensorChannels() {
short devIds[] = new short[sensorIdKeys.length];
SharedPreferences prefs = context.getSharedPreferences(
Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
if (prefs != null) {
for (int i = 0; i < sensorIdKeys.length; ++i) {
devIds[i] = (short) prefs.getInt(context.getString(sensorIdKeys[i]), WILDCARD);
}
}
sensors = new AntSensorBase[] {
new HeartRateSensor(devIds[HEART_RATE_CHANNEL]),
new CadenceSensor(devIds[CADENCE_CHANNEL]),
new CadenceSpeedSensor(devIds[CADENCE_SPEED_CHANNEL]),
};
connectingChannelsBitmap = 0;
for (int i = 0; i < sensors.length; ++i) {
if (setupChannel(sensors[i], (byte) i)) {
connectingChannelsBitmap |= 1 << i;
}
}
if (connectingChannelsBitmap == 0) {
setSensorState(Sensor.SensorState.DISCONNECTED);
}
}
protected boolean setupChannel(AntSensorBase sensor, byte channel) {
Log.i(TAG, "setup channel=" + channel + " deviceType=" + sensor.getDeviceType());
return setupAntSensorChannel(sensor.getNetworkNumber(),
channel,
sensor.getDeviceNumber(),
sensor.getDeviceType(),
(byte) 0x01,
sensor.getChannelPeriod(),
sensor.getFrequency(),
(byte) 0);
}
private void sendSensorData(byte index, int value) {
if (index >= currentSensorData.length) {
Log.w(TAG, "invalid index in sendSensorData:" + index);
return;
}
currentSensorData[index] = value;
long now = System.currentTimeMillis();
// data comes in at ~4Hz rate from the sensors, so after >300 msec
// fresh data is here from all the connected sensors
if (now < lastDataSentMillis + 300) {
return;
}
lastDataSentMillis = now;
setSensorState(Sensor.SensorState.CONNECTED);
Sensor.SensorDataSet.Builder b = Sensor.SensorDataSet.newBuilder();
if (currentSensorData[HEART_RATE_DATA_INDEX] >= 0) {
b.setHeartRate(
Sensor.SensorData.newBuilder()
.setValue(currentSensorData[HEART_RATE_DATA_INDEX])
.setState(Sensor.SensorState.SENDING));
}
if (currentSensorData[CADENCE_DATA_INDEX] >= 0) {
b.setCadence(
Sensor.SensorData.newBuilder()
.setValue(currentSensorData[CADENCE_DATA_INDEX])
.setState(Sensor.SensorState.SENDING));
}
sensorData = b.setCreationTime(now).build();
}
public void setCadence(int value) {
sendSensorData(CADENCE_DATA_INDEX, value);
}
public void setHeartRate(int value) {
sendSensorData(HEART_RATE_DATA_INDEX, value);
}
}
@@ -0,0 +1,32 @@
/*
* Copyright 2011 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.services.sensors.ant;
/**
* This is a common superclass for ANT message subclasses.
*
* @author Matthew Simmons
*/
public class AntMessage {
protected AntMessage() {}
/** Build a short value from its constituent bytes */
protected static short decodeShort(byte b0, byte b1) {
int value = b0 & 0xFF;
value |= (b1 & 0xFF) << 8;
return (short)value;
}
}
@@ -0,0 +1,46 @@
/*
* Copyright (C) 2011 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.services.sensors.ant;
/**
* A enum which stores static data about ANT sensors.
*
* @author Matthew Simmons
*/
public enum AntSensor {
SENSOR_HEART_RATE (Constants.ANT_DEVICE_TYPE_HRM),
SENSOR_CADENCE (Constants.ANT_DEVICE_TYPE_CADENCE),
SENSOR_SPEED (Constants.ANT_DEVICE_TYPE_SPEED),
SENSOR_POWER (Constants.ANT_DEVICE_TYPE_POWER);
private static class Constants {
public static byte ANT_DEVICE_TYPE_POWER = 11;
public static byte ANT_DEVICE_TYPE_HRM = 120;
public static byte ANT_DEVICE_TYPE_CADENCE = 122;
public static byte ANT_DEVICE_TYPE_SPEED = 123;
};
private final byte deviceType;
private AntSensor(byte deviceType) {
this.deviceType = deviceType;
}
public byte getDeviceType() {
return deviceType;
}
}
@@ -0,0 +1,73 @@
/*
* Copyright 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.services.sensors.ant;
import static com.google.android.apps.mytracks.Constants.TAG;
import android.util.Log;
/**
* Base class for ANT+ sensors.
*
* @author Laszlo Molnar
*/
public abstract class AntSensorBase {
/*
* These constants are defined by the ANT+ spec.
*/
public static final byte NETWORK_NUMBER = 1;
public static final byte RF_FREQUENCY = 57;
private short deviceNumber;
private final byte deviceType;
private final short channelPeriod;
AntSensorBase(short deviceNumber, byte deviceType,
String deviceTypeString, short channelPeriod) {
this.deviceNumber = deviceNumber;
this.deviceType = deviceType;
this.channelPeriod = channelPeriod;
Log.i(TAG, "Will pair with " + deviceTypeString + " device: " + ((int) deviceNumber & 0xFFFF));
}
public abstract void handleBroadcastData(byte[] antMessage, AntSensorDataCollector c);
public void setDeviceNumber(short dn) {
deviceNumber = dn;
}
public short getDeviceNumber() {
return deviceNumber;
}
public byte getNetworkNumber() {
return NETWORK_NUMBER;
}
public byte getFrequency() {
return RF_FREQUENCY;
}
public byte getDeviceType() {
return deviceType;
}
public short getChannelPeriod() {
return channelPeriod;
}
};
@@ -0,0 +1,35 @@
/*
* Copyright 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.services.sensors.ant;
/**
* Interface for collecting data from the sensors.
* @author Laszlo Molnar
*/
public interface AntSensorDataCollector {
/**
* Sets the current cadence to the value specified.
*/
void setCadence(int value);
/**
* Sets the current heart rate to the value specified.
*/
void setHeartRate(int value);
}
@@ -0,0 +1,383 @@
/*
* 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.ant;
import static com.google.android.apps.mytracks.Constants.TAG;
import com.dsi.ant.AntDefine;
import com.dsi.ant.AntInterface;
import com.dsi.ant.AntInterfaceIntent;
import com.dsi.ant.AntMesg;
import com.dsi.ant.exception.AntInterfaceException;
import com.dsi.ant.exception.AntServiceNotConnectedException;
import com.google.android.apps.mytracks.content.Sensor;
import com.google.android.apps.mytracks.content.Sensor.SensorDataSet;
import com.google.android.apps.mytracks.services.sensors.SensorManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.util.Log;
/**
* This is the common superclass for ANT-based sensors. It handles tasks which
* apply to the ANT framework as a whole, such as framework initialization and
* destruction. Subclasses are expected to handle the initialization and
* management of individual sensors.
*
* Implementation:
*
* The initialization process is somewhat complicated. This is due in part to
* the asynchronous nature of ANT Radio Service initialization, and in part due
* to an apparent bug in that service. The following is an overview of the
* initialization process.
*
* Initialization begins in {@link #setupChannel}, which is invoked by the
* {@link SensorManager} when track recording begins. {@link #setupChannel}
* asks the ANT Radio Service (via {@link AntInterface}) to start, using a
* AntInterface.ServiceListener to indicate when the service has
* connected. {@link #serviceConnected} claims and enables the Radio Service,
* and then resets it to a known state for our use. Completion of reset is
* indicated by receipt of a startup message (see {@link AntStartupMessage}).
* Once we've received that message, the ANT service is ready for use, and we
* can start sensor-specific initialization using
* {@link #setupAntSensorChannels}. The initialization of each sensor will
* usually result in a call to {@link #setupAntSensorChannel}.
*
* @author Sandor Dornbush
*/
public abstract class AntSensorManager extends SensorManager {
// Pairing
protected static final short WILDCARD = 0;
private AntInterface antReceiver;
/**
* The data from the sensors.
*/
protected SensorDataSet sensorData;
protected Context context;
private static final boolean DEBUGGING = false;
/** Receives and logs all status ANT intents. */
private final BroadcastReceiver statusReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context ctx, Intent intent) {
String antAction = intent.getAction();
Log.i(TAG, "enter status onReceive" + antAction);
}
};
/** Receives all data ANT intents. */
private final BroadcastReceiver dataReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context ctx, Intent intent) {
String antAction = intent.getAction();
Log.i(TAG, "enter data onReceive" + antAction);
if (antAction != null && antAction.equals(AntInterfaceIntent.ANT_RX_MESSAGE_ACTION)) {
byte[] antMessage = intent.getByteArrayExtra(AntInterfaceIntent.ANT_MESSAGE);
if (DEBUGGING) {
Log.d(TAG, "Received RX message " + messageToString(antMessage));
}
if (getAntReceiver() != null) {
handleMessage(antMessage);
}
}
}
};
/**
* ANT uses this listener to tell us when it has bound to the ANT Radio
* Service. We can't start sending ANT commands until we've been notified
* (via this listener) that the Radio Service has connected.
*/
private AntInterface.ServiceListener antServiceListener = new AntInterface.ServiceListener() {
@Override
public void onServiceConnected() {
serviceConnected();
}
@Override
public void onServiceDisconnected() {
Log.d(TAG, "ANT interface reports disconnection");
}
};
public AntSensorManager(Context context) {
this.context = context;
// We register for ANT intents early because we want to have a record of
// the status intents in the log as we start up.
registerForAntIntents();
}
@Override
public void onDestroy() {
Log.i(TAG, "destroying AntSensorManager");
cleanAntInterface();
unregisterForAntIntents();
}
@Override
public SensorDataSet getSensorDataSet() {
return sensorData;
}
@Override
public boolean isEnabled() {
return true;
}
public AntInterface getAntReceiver() {
return antReceiver;
}
/**
* This is the interface used by the {@link SensorManager} to tell this
* class when to start. It handles initialization of the ANT framework,
* eventually resulting in sensor-specific initialization via
* {@link #setupAntSensorChannels}.
*/
@Override
protected final void setupChannel() {
setup();
}
private synchronized void setup() {
// We handle this unpleasantly because the UI should've checked for ANT
// support before it even instantiated this class.
if (!AntInterface.hasAntSupport(context)) {
throw new IllegalStateException("device does not have ANT support");
}
cleanAntInterface();
antReceiver = AntInterface.getInstance(context, antServiceListener);
if (antReceiver == null) {
Log.e(TAG, "Failed to get ANT Receiver");
return;
}
setSensorState(Sensor.SensorState.CONNECTING);
}
/**
* Cleans up the ANT+ receiver interface, by releasing the interface
* and destroying it.
*/
private void cleanAntInterface() {
Log.i(TAG, "Destroying AntSensorManager");
if (antReceiver == null) {
Log.e(TAG, "no ANT receiver");
return;
}
try {
antReceiver.releaseInterface();
antReceiver.destroy();
antReceiver = null;
} catch (AntServiceNotConnectedException e) {
Log.i(TAG, "ANT service not connected", e);
} catch (AntInterfaceException e) {
Log.e(TAG, "failed to release ANT interface", e);
} catch (RuntimeException e) {
Log.e(TAG, "run-time exception when cleaning the ANT interface", e);
}
}
/**
* This method is invoked via the ServiceListener when we're connected to
* the ANT service. If we're just starting up, this is our first opportunity
* to initiate any ANT commands.
*/
private synchronized void serviceConnected() {
Log.d(TAG, "ANT service connected");
if (antReceiver == null) {
Log.e(TAG, "no ANT receiver");
return;
}
try {
if (!antReceiver.claimInterface()) {
Log.e(TAG, "failed to claim ANT interface");
return;
}
if (!antReceiver.isEnabled()) {
// Make sure not to call AntInterface.enable() again, if it has been
// already called before
Log.i(TAG, "Powering on Radio");
antReceiver.enable();
} else {
Log.i(TAG, "Radio already enabled");
}
} catch (AntInterfaceException e) {
Log.e(TAG, "failed to enable ANT", e);
}
try {
// We expect this call to throw an exception due to a bug in the ANT
// Radio Service. It won't actually fail, though, as we'll get the
// startup message (see {@link AntStartupMessage}) one normally expects
// after a reset. Channel initialization can proceed once we receive
// that message.
antReceiver.ANTResetSystem();
} catch (AntInterfaceException e) {
Log.e(TAG, "failed to reset ANT (expected exception)", e);
}
}
/**
* Process a raw ANT message.
* @param antMessage the ANT message, including the size and message ID bytes
* @deprecated Use {@link #handleMessage(byte, byte[])} instead.
*/
protected void handleMessage(byte[] antMessage) {
int len = antMessage[0];
if (len != antMessage.length - 2 || antMessage.length <= 2) {
Log.e(TAG, "Invalid message: " + messageToString(antMessage));
return;
}
byte messageId = antMessage[1];
// Arrays#copyOfRange doesn't exist??
byte[] messageData = new byte[antMessage.length - 2];
System.arraycopy(antMessage, 2, messageData, 0, antMessage.length - 2);
handleMessage(messageId, messageData);
}
/**
* Process a raw ANT message.
* @param messageId the message ID. See the ANT Message Protocol and Usage
* guide, section 9.3.
* @param messageData the ANT message, without the size and message ID bytes.
* @return true if this method has taken responsibility for the passed
* message; false otherwise.
*/
protected boolean handleMessage(byte messageId, byte[] messageData) {
if (messageId == AntMesg.MESG_STARTUP_MESG_ID) {
Log.d(TAG, String.format(
"Received startup message (reason %02x); initializing channel",
new AntStartupMessage(messageData).getMessage()));
setupAntSensorChannels();
return true;
}
return false;
}
/**
* Subclasses define this method to perform sensor-specific initialization.
* When this method is called, the ANT framework has been enabled, and is
* ready for use.
*/
protected abstract void setupAntSensorChannels();
/**
* Used by subclasses to set up an ANT channel for a single sensor. A given
* subclass may invoke this method multiple times if the subclass is
* responsible for more than one sensor.
*
* @return true on success
*/
protected boolean setupAntSensorChannel(byte networkNumber, byte channelNumber,
short deviceNumber, byte deviceType, byte txType, short channelPeriod,
byte radioFreq, byte proxSearch) {
if (antReceiver == null) {
Log.e(TAG, "no ANT receiver");
return false;
}
try {
// Assign as slave channel on selected network (0 = public, 1 = ANT+, 2 =
// ANTFS)
antReceiver.ANTAssignChannel(channelNumber, AntDefine.PARAMETER_RX_NOT_TX, networkNumber);
antReceiver.ANTSetChannelId(channelNumber, deviceNumber, deviceType, txType);
antReceiver.ANTSetChannelPeriod(channelNumber, channelPeriod);
antReceiver.ANTSetChannelRFFreq(channelNumber, radioFreq);
// Disable high priority search
antReceiver.ANTSetChannelSearchTimeout(channelNumber, (byte) 0);
// Set search timeout to 10 seconds (low priority search))
antReceiver.ANTSetLowPriorityChannelSearchTimeout(channelNumber, (byte) 4);
if (deviceNumber == WILDCARD) {
// Configure proximity search, if using wild card search
antReceiver.ANTSetProximitySearch(channelNumber, proxSearch);
}
antReceiver.ANTOpenChannel(channelNumber);
return true;
} catch (AntInterfaceException e) {
Log.e(TAG, "failed to setup ANT channel", e);
return false;
}
}
private void registerForAntIntents() {
Log.i(TAG, "Registering for ant intents.");
// Register for ANT intent broadcasts.
IntentFilter statusIntentFilter = new IntentFilter();
statusIntentFilter.addAction(AntInterfaceIntent.ANT_ENABLED_ACTION);
statusIntentFilter.addAction(AntInterfaceIntent.ANT_DISABLED_ACTION);
statusIntentFilter.addAction(AntInterfaceIntent.ANT_INTERFACE_CLAIMED_ACTION);
statusIntentFilter.addAction(AntInterfaceIntent.ANT_RESET_ACTION);
context.registerReceiver(statusReceiver, statusIntentFilter);
IntentFilter dataIntentFilter = new IntentFilter();
dataIntentFilter.addAction(AntInterfaceIntent.ANT_RX_MESSAGE_ACTION);
context.registerReceiver(dataReceiver, dataIntentFilter);
}
private void unregisterForAntIntents()
{
Log.i(TAG, "Unregistering ANT Intents.");
try {
context.unregisterReceiver(statusReceiver);
} catch (IllegalArgumentException e) {
Log.w(TAG, "Failed to unregister ANT status receiver", e);
}
try {
context.unregisterReceiver(dataReceiver);
} catch (IllegalArgumentException e) {
Log.w(TAG, "Failed to unregister ANT data receiver", e);
}
}
private String messageToString(byte[] message) {
StringBuilder out = new StringBuilder();
for (byte b : message) {
out.append(String.format("%s%02x", (out.length() == 0 ? "" : " "), b));
}
return out.toString();
}
}
@@ -0,0 +1,254 @@
/*
* Copyright 2011 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.services.sensors.ant;
import static com.google.android.apps.mytracks.Constants.TAG;
import com.dsi.ant.AntDefine;
import com.dsi.ant.AntMesg;
import com.dsi.ant.exception.AntInterfaceException;
import com.google.android.apps.mytracks.Constants;
import com.google.android.apps.mytracks.content.Sensor;
import com.google.android.apps.mytracks.util.ApiAdapterFactory;
import com.google.android.maps.mytracks.BuildConfig;
import com.google.android.maps.mytracks.R;
import android.content.Context;
import android.content.SharedPreferences;
import android.util.Log;
/**
* A sensor manager to the PC7 SRM ANT+ bridge.
*
* @author Sandor Dornbush
* @author Umran Abdulla
*/
public class AntSrmBridgeSensorManager extends AntSensorManager {
/*
* These constants are defined by the ANT+ spec.
*/
public static final byte CHANNEL_NUMBER = 0;
public static final byte NETWORK_NUMBER = 0;
public static final byte DEVICE_TYPE = 12;
public static final byte NETWORK_ID = 1;
public static final short CHANNEL_PERIOD = 8192;
public static final byte RF_FREQUENCY = 50;
private static final int INDEX_MESSAGE_TYPE = 1;
private static final int INDEX_MESSAGE_ID = 2;
private static final int INDEX_MESSAGE_POWER = 3;
private static final int INDEX_MESSAGE_SPEED = 5;
private static final int INDEX_MESSAGE_CADENCE = 7;
private static final int INDEX_MESSAGE_BPM = 8;
private static final int MSG_INITIAL = 5;
private static final int MSG_DATA = 6;
private short deviceNumber;
public AntSrmBridgeSensorManager(Context context) {
super(context);
Log.i(TAG, "new ANT SRM Bridge Sensor Manager created");
deviceNumber = WILDCARD;
// First read the the device id that we will be pairing with.
SharedPreferences prefs = context.getSharedPreferences(
Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
if (prefs != null) {
deviceNumber =
(short) prefs.getInt(context.getString(
R.string.ant_srm_bridge_sensor_id_key), 0);
}
Log.i(TAG, "Will pair with device: " + deviceNumber);
}
@Override
protected boolean handleMessage(byte messageId, byte[] messageData) {
if (super.handleMessage(messageId, messageData)) {
return true;
}
if (BuildConfig.DEBUG) {
Log.d(TAG, "Received ANT msg: " + AntUtils.antMessageToString(messageId) + "(" + messageId + ")");
}
int channel = messageData[0] & AntDefine.CHANNEL_NUMBER_MASK;
switch (channel) {
case CHANNEL_NUMBER:
decodeChannelMsg(messageId, messageData);
break;
default:
Log.d(TAG, "Unhandled message: " + channel);
}
return true;
}
/**
* Decode an ant device message.
* @param messageData The byte array received from the device.
*/
private void decodeChannelMsg(int messageId, byte[] messageData) {
switch (messageId) {
case AntMesg.MESG_BROADCAST_DATA_ID:
handleBroadcastData(messageData);
break;
case AntMesg.MESG_RESPONSE_EVENT_ID:
handleMessageResponse(messageData);
break;
case AntMesg.MESG_CHANNEL_ID_ID:
handleChannelId(messageData);
break;
default:
Log.e(TAG, "Unexpected message id: " + messageId);
}
}
private void handleBroadcastData(byte[] antMessage) {
if (deviceNumber == WILDCARD) {
try {
getAntReceiver().ANTRequestMessage(CHANNEL_NUMBER, AntMesg.MESG_CHANNEL_ID_ID);
} catch (AntInterfaceException e) {
Log.e(TAG, "ANT error handling broadcast data", e);
}
Log.d(TAG, "Requesting channel id id.");
}
setSensorState(Sensor.SensorState.CONNECTED);
int messageType = antMessage[INDEX_MESSAGE_TYPE] & 0xFF;
Log.d(TAG, "Received message-type=" + messageType);
switch (messageType) {
case MSG_INITIAL:
break;
case MSG_DATA:
parseDataMsg(antMessage);
break;
}
}
private void parseDataMsg(byte[] msg)
{
int messageId = msg[INDEX_MESSAGE_ID] & 0xFF;
Log.d(TAG, "Received message-id=" + messageId);
int powerVal = (((msg[INDEX_MESSAGE_POWER] & 0xFF) << 8) |
(msg[INDEX_MESSAGE_POWER+1] & 0xFF));
@SuppressWarnings("unused")
int speedVal = (((msg[INDEX_MESSAGE_SPEED] & 0xFF) << 8) |
(msg[INDEX_MESSAGE_SPEED+1] & 0xFF));
int cadenceVal = (msg[INDEX_MESSAGE_CADENCE] & 0xFF);
int bpmVal = (msg[INDEX_MESSAGE_BPM] & 0xFF);
long time = System.currentTimeMillis();
Sensor.SensorData.Builder power =
Sensor.SensorData.newBuilder()
.setValue(powerVal)
.setState(Sensor.SensorState.SENDING);
/*
* Although speed is available from the SRM Bridge, MyTracks doesn't use the value, and
* computes speed from the GPS location data.
*/
// Sensor.SensorData.Builder speed = Sensor.SensorData.newBuilder().setValue(speedVal).setState(
// Sensor.SensorState.SENDING);
Sensor.SensorData.Builder cadence =
Sensor.SensorData.newBuilder()
.setValue(cadenceVal)
.setState(Sensor.SensorState.SENDING);
Sensor.SensorData.Builder bpm =
Sensor.SensorData.newBuilder()
.setValue(bpmVal)
.setState(Sensor.SensorState.SENDING);
sensorData =
Sensor.SensorDataSet.newBuilder()
.setCreationTime(time)
.setPower(power)
.setCadence(cadence)
.setHeartRate(bpm)
.build();
}
void handleChannelId(byte[] rawMessage) {
AntChannelIdMessage message = new AntChannelIdMessage(rawMessage);
deviceNumber = message.getDeviceNumber();
Log.d(TAG, "Found device id: " + deviceNumber);
SharedPreferences prefs = context.getSharedPreferences(
Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = prefs.edit();
editor.putInt(context.getString(R.string.ant_srm_bridge_sensor_id_key), deviceNumber);
ApiAdapterFactory.getApiAdapter().applyPreferenceChanges(editor);
}
private void handleMessageResponse(byte[] rawMessage) {
AntChannelResponseMessage message =
new AntChannelResponseMessage(rawMessage);
if (BuildConfig.DEBUG) {
Log.d(TAG, "Received ANT Response: " + AntUtils.antMessageToString(message.getMessageId()) +
"(" + message.getMessageId() + ")" +
", Code: " + AntUtils.antEventToStr(message.getMessageCode()) +
"(" + message.getMessageCode() + ")");
}
switch (message.getMessageId()) {
case AntMesg.MESG_EVENT_ID:
if (message.getMessageCode() == AntDefine.EVENT_RX_SEARCH_TIMEOUT) {
// Search timeout
Log.w(TAG, "Search timed out. Unassigning channel.");
try {
getAntReceiver().ANTUnassignChannel((byte) 0);
} catch (AntInterfaceException e) {
Log.e(TAG, "ANT error unassigning channel", e);
}
}
break;
case AntMesg.MESG_UNASSIGN_CHANNEL_ID:
setSensorState(Sensor.SensorState.DISCONNECTED);
Log.i(TAG, "Disconnected from the sensor: " + getSensorState());
break;
}
}
@Override protected void setupAntSensorChannels() {
Log.i(TAG, "Setting up ANT sensor channels");
setupAntSensorChannel(
NETWORK_NUMBER,
CHANNEL_NUMBER,
deviceNumber,
DEVICE_TYPE,
(byte) 0x01,
CHANNEL_PERIOD,
RF_FREQUENCY,
(byte) 0);
}
public short getDeviceNumber() {
return deviceNumber;
}
void setDeviceNumber(short deviceNumber) {
this.deviceNumber = deviceNumber;
}
}
@@ -0,0 +1,35 @@
/*
* Copyright 2011 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.services.sensors.ant;
/**
* This class decodes and encapsulates an ANT Startup message.
* (ANT Message ID 0x6f, Protocol & Usage guide v4.2 section 9.5.3.1)
*
* @author Matthew Simmons
*/
public class AntStartupMessage extends AntMessage {
private byte message;
public AntStartupMessage(byte[] messageData) {
message = messageData[0];
}
/** Returns the cause of the startup */
public byte getMessage() {
return message;
}
}
@@ -0,0 +1,84 @@
/*
* Copyright 2011 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.services.sensors.ant;
import com.dsi.ant.AntDefine;
import com.dsi.ant.AntInterface;
import android.content.Context;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.HashSet;
import java.util.Set;
/**
* Utility methods for ANT functionality.
*
* Prefer use of this class to importing DSI ANT classes into code outside of
* the sensors package.
*/
public class AntUtils {
private AntUtils() {}
/** Returns true if this device supports ANT sensors. */
public static boolean hasAntSupport(Context context) {
return AntInterface.hasAntSupport(context);
}
/**
* Finds the names of in the messages with the given value
*/
public static String antMessageToString(byte msg) {
return findConstByteInClass(AntDefine.class, msg, "MESG_.*_ID");
}
/**
* Finds the names of in the events with the given value
*/
public static String antEventToStr(byte event) {
return findConstByteInClass(AntDefine.class, event, ".*EVENT.*");
}
/**
* Finds a set of constant static byte field declarations in the class that have the given value
* and whose name match the given pattern
* @param cl class to search in
* @param value value of constant static byte field declarations to match
* @param regexPattern pattern to match against the name of the field
* @return a set of the names of fields, expressed as a string
*/
private static String findConstByteInClass(Class<?> cl, byte value, String regexPattern)
{
Field[] fields = cl.getDeclaredFields();
Set<String> fieldSet = new HashSet<String>();
for (Field f : fields) {
try {
if (f.getType() == Byte.TYPE &&
(f.getModifiers() & Modifier.STATIC) != 0 &&
f.getName().matches(regexPattern) &&
f.getByte(null) == value) {
fieldSet.add(f.getName());
}
} catch (IllegalArgumentException e) {
// ignore
} catch (IllegalAccessException e) {
// ignore
}
}
return fieldSet.toString();
}
}
@@ -0,0 +1,48 @@
/*
* Copyright 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.services.sensors.ant;
/**
* Ant+ cadence sensor.
*
* @author Laszlo Molnar
*/
public class CadenceSensor extends AntSensorBase {
/*
* These constants are defined by the ANT+ bike speed and cadence sensor spec.
*/
public static final byte CADENCE_DEVICE_TYPE = 122;
public static final short CADENCE_CHANNEL_PERIOD = 8102;
SensorEventCounter dataProcessor = new SensorEventCounter();
CadenceSensor(short devNum) {
super(devNum, CADENCE_DEVICE_TYPE, "cadence sensor", CADENCE_CHANNEL_PERIOD);
}
/**
* Decode an ANT+ cadence sensor message.
* @param antMessage The byte array received from the cadence sensor.
*/
@Override
public void handleBroadcastData(byte[] antMessage, AntSensorDataCollector c) {
int sensorTime = ((int) antMessage[5] & 0xFF) + ((int) antMessage[6] & 0xFF) * 256;
int crankRevs = ((int) antMessage[7] & 0xFF) + ((int) antMessage[8] & 0xFF) * 256;
c.setCadence(dataProcessor.getEventsPerMinute(crankRevs, sensorTime));
}
};
@@ -0,0 +1,48 @@
/*
* Copyright 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.services.sensors.ant;
/**
* Ant+ combined cadence and speed sensor.
*
* @author Laszlo Molnar
*/
public class CadenceSpeedSensor extends AntSensorBase {
/*
* These constants are defined by the ANT+ bike speed and cadence sensor spec.
*/
public static final byte CADENCE_SPEED_DEVICE_TYPE = 121;
public static final short CADENCE_SPEED_CHANNEL_PERIOD = 8086;
SensorEventCounter dataProcessor = new SensorEventCounter();
CadenceSpeedSensor(short devNum) {
super(devNum, CADENCE_SPEED_DEVICE_TYPE, "speed&cadence sensor", CADENCE_SPEED_CHANNEL_PERIOD);
}
/**
* Decode an ANT+ cadence&speed sensor message.
* @param antMessage The byte array received from the sensor.
*/
@Override
public void handleBroadcastData(byte[] antMessage, AntSensorDataCollector c) {
int sensorTime = ((int) antMessage[1] & 0xFF) + ((int) antMessage[2] & 0xFF) * 256;
int crankRevs = ((int) antMessage[3] & 0xFF) + ((int) antMessage[4] & 0xFF) * 256;
c.setCadence(dataProcessor.getEventsPerMinute(crankRevs, sensorTime));
}
};
@@ -0,0 +1,49 @@
/*
* Copyright 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.services.sensors.ant;
import static com.google.android.apps.mytracks.Constants.TAG;
import android.util.Log;
/**
* Ant+ heart reate monitor sensor.
*
* @author Laszlo Molnar
*/
public class HeartRateSensor extends AntSensorBase {
/*
* These constants are defined by the ANT+ heart rate monitor spec.
*/
public static final byte HEART_RATE_DEVICE_TYPE = 120;
public static final short HEART_RATE_CHANNEL_PERIOD = 8070;
HeartRateSensor(short devNum) {
super(devNum, HEART_RATE_DEVICE_TYPE, "heart rate monitor", HEART_RATE_CHANNEL_PERIOD);
}
/**
* Decode an ANT+ heart rate monitor message.
* @param antMessage The byte array received from the heart rate monitor.
*/
@Override
public void handleBroadcastData(byte[] antMessage, AntSensorDataCollector c) {
int bpm = (int) antMessage[8] & 0xFF;
Log.d(TAG, "now:" + System.currentTimeMillis() + " heart rate=" + bpm);
c.setHeartRate(bpm);
}
};
@@ -0,0 +1,185 @@
/*
* Copyright 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.services.sensors.ant;
import static com.google.android.apps.mytracks.Constants.TAG;
import android.util.Log;
import java.util.LinkedList;
/**
* Processes an ANT+ sensor data (counter) + timestamp pair,
* and returns the instantaneous value of the sensor
*
* @author Laszlo Molnar
*/
public class SensorEventCounter {
/**
* HistoryElement stores a time stamped sensor counter
*/
private static class HistoryElement {
final long systemTime;
final int counter;
final int sensorTime;
HistoryElement(long systemTime, int counter, int sensorTime) {
this.systemTime = systemTime;
this.counter = counter;
this.sensorTime = sensorTime;
}
}
/**
* The latest counter value reported by the sensor
*/
private int counter;
/**
* The calculated instantaneous sensor value to be displayed
*/
private int eventsPerMinute;
private static final int ONE_SECOND_MILLIS = 1000;
private static final int HISTORY_LENGTH_MILLIS = ONE_SECOND_MILLIS * 5;
private static final int HISTORY_MAX_LENGTH = 100;
private static final int ONE_MINUTE_MILLIS = ONE_SECOND_MILLIS * 60;
private static final int SENSOR_TIME_RESOLUTION = 1024; // in a second
private static final int SENSOR_TIME_ONE_MINUTE = SENSOR_TIME_RESOLUTION * 60;
/**
* History of previous sensor data - oldest first
* only the latest HISTORY_LENGTH_MILLIS milliseconds of data is stored
*/
private LinkedList<HistoryElement> history;
SensorEventCounter() {
counter = -1;
eventsPerMinute = 0;
history = new LinkedList<HistoryElement>();
}
/**
* Calculates the instantaneous sensor value to be displayed
*
* @param newCounter sensor reported counter value
* @param sensorTime sensor reported timestamp
* @return the calculated value
*/
public int getEventsPerMinute(int newCounter, int sensorTime) {
return getEventsPerMinute(newCounter, sensorTime, System.currentTimeMillis());
}
// VisibleForTesting
protected int getEventsPerMinute(int newCounter, int sensorTime, long now) {
int counterChange = (newCounter - counter) & 0xFFFF;
Log.d(TAG, "now=" + now + " counter=" + newCounter + " sensortime=" + sensorTime);
if (counter < 0) {
// store the initial counter value reported by the sensor
// the timestamp is probably out of date, so the history is not updated
counter = newCounter;
return eventsPerMinute = 0;
}
counter = newCounter;
if (counterChange != 0) {
// if new data has arrived from the sensor ...
if (removeOldHistory(now)) {
// ... and the history is not empty, then use the latest entry
HistoryElement h = history.getLast();
int sensorTimeChange = (sensorTime - h.sensorTime) & 0xFFFF;
counterChange = (counter - h.counter) & 0xFFFF;
eventsPerMinute = counterChange * SENSOR_TIME_ONE_MINUTE / sensorTimeChange;
}
// the previous removeOldHistory() call makes the length of the history capped
history.addLast(new HistoryElement(now, counter, sensorTime));
} else if (!history.isEmpty()) {
// the sensor has resent an old (counter,timestamp) pair,
// but the history is not empty
HistoryElement h = history.getLast();
if (ONE_MINUTE_MILLIS < (now - h.systemTime) * eventsPerMinute) {
// Too much time has passed since the last counter change.
// This means that a smaller value than eventsPerMinute must be
// returned. This value is extrapolated from the history of
// HISTORY_LENGTH_MILLIS data.
// Note, that eventsPerMinute is NOT updated unless the history
// is empty or contains outdated entries. In that case it is zeroed.
return getValueFromHistory(now);
} // else the current eventsPerMinute is still valid, nothing to do here
} else {
// no new data from the sensor & the history is empty -> return 0
eventsPerMinute = 0;
}
Log.d(TAG, "getEventsPerMinute returns:" + eventsPerMinute);
return eventsPerMinute;
}
/**
* Calculates the instantaneous sensor value to be displayed
* using the history when the sensor only resends the old data
*/
private int getValueFromHistory(long now) {
if (!removeOldHistory(now)) {
// there is nothing in the history, return 0
return eventsPerMinute = 0;
}
HistoryElement f = history.getFirst();
HistoryElement l = history.getLast();
int sensorTimeChange = (l.sensorTime - f.sensorTime) & 0xFFFF;
int counterChange = (counter - f.counter) & 0xFFFF;
// difference between now and systemTime of the oldest history entry
// for better precision sensor timestamps are considered between
// the first and the last history entry (could be overkill)
int systemTimeChange = (int) (now - l.systemTime
+ (sensorTimeChange * ONE_SECOND_MILLIS) / SENSOR_TIME_RESOLUTION);
// eventsPerMinute is not overwritten by this calculated value
// because it is still needed when a new sensor event arrives
int v = (counterChange * ONE_MINUTE_MILLIS) / systemTimeChange;
Log.d(TAG, "getEventsPerMinute returns (2):" + v);
// do not return larger number than eventsPerMinute, because the reason
// this function got called is that more time has passed after the last
// sensor counter value change than the current eventsPerMinute
// would be valid
return v < eventsPerMinute ? v : eventsPerMinute;
}
/**
* Removes old data from the history.
*
* @param now the current system time
* @return true if the remaining history is not empty
*/
private boolean removeOldHistory(long now) {
HistoryElement h;
while ((h = history.peek()) != null) {
// if the first element of the list is in our desired time range then return
if (now - h.systemTime <= HISTORY_LENGTH_MILLIS && history.size() < HISTORY_MAX_LENGTH) {
return true;
}
// otherwise remove the too old element, and look at the next (newer) one
history.removeFirst();
}
return false;
}
}
@@ -0,0 +1,83 @@
/*
* Copyright 2011 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.services.tasks;
import static com.google.android.apps.mytracks.Constants.TAG;
import android.annotation.TargetApi;
import android.content.Context;
import android.media.AudioManager;
import android.speech.tts.TextToSpeech;
import android.speech.tts.TextToSpeech.OnUtteranceCompletedListener;
import android.util.Log;
import java.util.HashMap;
/**
* This class will periodically announce the user's trip statistics. This class
* will request and release audio focus. <br>
* For API Level 8 or higher.
*
* @author Sandor Dornbush
*/
@TargetApi(8)
public class Api8StatusAnnouncerTask extends StatusAnnouncerTask {
private final static HashMap<String, String> SPEECH_PARAMS = new HashMap<String, String>();
static {
SPEECH_PARAMS.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, "not_used");
}
private final AudioManager audioManager;
private final OnUtteranceCompletedListener utteranceListener =
new OnUtteranceCompletedListener() {
@Override
public void onUtteranceCompleted(String utteranceId) {
int result = audioManager.abandonAudioFocus(null);
if (result == AudioManager.AUDIOFOCUS_REQUEST_FAILED) {
Log.w(TAG, "FroyoStatusAnnouncerTask: Failed to relinquish audio focus");
}
}
};
public Api8StatusAnnouncerTask(Context context) {
super(context);
audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
}
@Override
protected void onTtsReady() {
super.onTtsReady();
tts.setOnUtteranceCompletedListener(utteranceListener);
}
@Override
protected synchronized void speakAnnouncement(String announcement) {
int result = audioManager.requestAudioFocus(null,
TextToSpeech.Engine.DEFAULT_STREAM,
AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK);
if (result == AudioManager.AUDIOFOCUS_REQUEST_FAILED) {
Log.w(TAG, "FroyoStatusAnnouncerTask: Request for audio focus failed.");
}
// We don't care about the utterance id.
// It is supplied here to force onUtteranceCompleted to be called.
tts.speak(announcement, TextToSpeech.QUEUE_FLUSH, SPEECH_PARAMS);
}
}
@@ -0,0 +1,41 @@
/*
* 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.tasks;
import com.google.android.apps.mytracks.services.TrackRecordingService;
/**
* 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,189 @@
/*
* 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.tasks;
import static com.google.android.apps.mytracks.Constants.TAG;
import com.google.android.apps.mytracks.services.TrackRecordingService;
import com.google.android.apps.mytracks.util.UnitConversions;
import android.util.Log;
/**
* Execute a task on a time or distance schedule.
*
* @author Sandor Dornbush
*/
public class PeriodicTaskExecutor {
/**
* The frequency of the task.
* A value greater than zero is a frequency in time.
* A value less than zero is considered a frequency in distance.
*/
private int taskFrequency = 0;
/**
* The next distance when the task should execute.
*/
private double nextTaskDistance = 0;
/**
* Time based executor.
*/
private TimerTaskExecutor timerExecutor = null;
private boolean metricUnits;
private final TrackRecordingService service;
private final PeriodicTaskFactory factory;
private PeriodicTask task;
public PeriodicTaskExecutor(TrackRecordingService service, PeriodicTaskFactory factory) {
this.service = service;
this.factory = factory;
}
/**
* Restores the manager.
*/
public void restore() {
// TODO: Decouple service from this class once and forever.
if (!service.isRecording()) {
return;
}
if (!isTimeFrequency()) {
if (timerExecutor != null) {
timerExecutor.shutdown();
timerExecutor = null;
}
}
if (taskFrequency == 0) {
return;
}
// Try to make the task.
task = factory.create(service);
// Returning null is ok.
if (task == null) {
return;
}
task.start();
if (isTimeFrequency()) {
if (timerExecutor == null) {
timerExecutor = new TimerTaskExecutor(task, service);
}
timerExecutor.scheduleTask(taskFrequency * 60000L);
} else {
// For distance based splits.
calculateNextTaskDistance();
}
}
/**
* Shuts down the manager.
*/
public void shutdown() {
if (task != null) {
task.shutdown();
task = null;
}
if (timerExecutor != null) {
timerExecutor.shutdown();
timerExecutor = null;
}
}
/**
* Calculates the next distance when the task should execute.
*/
void calculateNextTaskDistance() {
// TODO: Decouple service from this class once and forever.
if (!service.isRecording() || task == null) {
return;
}
if (!isDistanceFrequency()) {
nextTaskDistance = Double.MAX_VALUE;
Log.d(TAG, "SplitManager: Distance splits disabled.");
return;
}
double distance = service.getTripStatistics().getTotalDistance() * UnitConversions.M_TO_KM;
if (!metricUnits) {
distance *= UnitConversions.KM_TO_MI;
}
// The index will be negative since the frequency is negative.
int index = (int) (distance / taskFrequency);
index -= 1;
nextTaskDistance = taskFrequency * index;
Log.d(TAG, "SplitManager: Next split distance: " + nextTaskDistance);
}
/**
* Updates executer with new trip statistics.
*/
public void update() {
if (!isDistanceFrequency() || task == null) {
return;
}
// Convert the distance in meters to km or mi.
double distance = service.getTripStatistics().getTotalDistance() * UnitConversions.M_TO_KM;
if (!metricUnits) {
distance *= UnitConversions.KM_TO_MI;
}
if (distance > nextTaskDistance) {
task.run(service);
calculateNextTaskDistance();
}
}
private boolean isTimeFrequency() {
return taskFrequency > 0;
}
private boolean isDistanceFrequency() {
return taskFrequency < 0;
}
/**
* Sets the task frequency.
* &lt; 0 Use the absolute value as a distance in the current measurement km
* or mi
* 0 Turn off the task
* &gt; 0 Use the value as a time in minutes
* @param taskFrequency The frequency in time or distance
*/
public void setTaskFrequency(int taskFrequency) {
Log.d(TAG, "setTaskFrequency: taskFrequency = " + taskFrequency);
this.taskFrequency = taskFrequency;
restore();
}
public void setMetricUnits(boolean metricUnits) {
this.metricUnits = metricUnits;
calculateNextTaskDistance();
}
double getNextTaskDistance() {
return nextTaskDistance;
}
}
@@ -0,0 +1,33 @@
/*
* 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.tasks;
import android.content.Context;
/**
* An interface for classes that can create periodic tasks.
*
* @author Sandor Dornbush
*/
public interface PeriodicTaskFactory {
/**
* Creates a periodic task which does voice announcements.
*
* @return the task, or null if task is not supported
*/
PeriodicTask create(Context context);
}
@@ -0,0 +1,57 @@
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.services.tasks;
import com.google.android.apps.mytracks.content.WaypointCreationRequest;
import com.google.android.apps.mytracks.services.TrackRecordingService;
import android.content.Context;
/**
* A simple task to insert statistics markers periodically.
* @author Sandor Dornbush
*/
public class SplitTask implements PeriodicTask {
private SplitTask() {
}
@Override
public void run(TrackRecordingService service) {
service.insertWaypoint(WaypointCreationRequest.DEFAULT_STATISTICS);
}
@Override
public void shutdown() {
}
@Override
public void start() {
}
/**
* Create new SplitTasks.
*/
public static class Factory implements PeriodicTaskFactory {
@Override
public PeriodicTask create(Context context) {
return new SplitTask();
}
}
}
@@ -0,0 +1,37 @@
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.services.tasks;
import com.google.android.apps.mytracks.util.ApiAdapterFactory;
import android.content.Context;
/**
* Factory which wraps construction and setup of text-to-speech announcements in
* an API-level-safe way.
*
* @author Rodrigo Damazio
*/
public class StatusAnnouncerFactory implements PeriodicTaskFactory {
public StatusAnnouncerFactory() {
}
@Override
public PeriodicTask create(Context context) {
return ApiAdapterFactory.getApiAdapter().getStatusAnnouncerTask(context);
}
}
@@ -0,0 +1,361 @@
/*
* 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.tasks;
import static com.google.android.apps.mytracks.Constants.TAG;
import com.google.android.apps.mytracks.Constants;
import com.google.android.apps.mytracks.services.TrackRecordingService;
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 com.google.common.annotations.VisibleForTesting;
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;
/**
* The interface to the text to speech engine.
*/
protected TextToSpeech tts;
/**
* The response received from the TTS engine after initialization.
*/
private int initStatus = TextToSpeech.ERROR;
/**
* Whether the TTS engine is ready.
*/
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 != null && tts.isSpeaking()) {
// If we're already speaking, stop it.
tts.stop();
}
}
};
public StatusAnnouncerTask(Context context) {
this.context = context;
}
/**
* {@inheritDoc}
*
* Announces the trip status.
*/
@Override
public void run(TrackRecordingService service) {
if (service == null) {
Log.e(TAG, "StatusAnnouncer TrackRecordingService not initialized");
return;
}
runWithStatistics(service.getTripStatistics());
}
/**
* This method exists as a convenience for testing code, allowing said code
* to avoid needing to instantiate an entire {@link TrackRecordingService}
* just to test the announcer.
*/
// @VisibleForTesting
void runWithStatistics(TripStatistics statistics) {
if (statistics == null) {
Log.e(TAG, "StatusAnnouncer stats not initialized.");
return;
}
synchronized (this) {
checkReady();
if (!ready) {
Log.e(TAG, "StatusAnnouncer Tts not ready.");
return;
}
}
if (!speechAllowed) {
Log.i(Constants.TAG,
"Not making announcement - not allowed at this time");
return;
}
String announcement = getAnnouncement(statistics);
Log.d(Constants.TAG, "Announcement: " + announcement);
speakAnnouncement(announcement);
}
protected void speakAnnouncement(String 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) {
boolean metricUnits = true;
boolean reportSpeed = true;
SharedPreferences preferences = context.getSharedPreferences(
Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
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() * UnitConversions.M_TO_KM;
double s = stats.getAverageMovingSpeed() * UnitConversions.MS_TO_KMH;
if (d == 0) {
return context.getString(R.string.voice_total_distance_zero);
}
if (!metricUnits) {
d *= UnitConversions.KM_TO_MI;
s *= UnitConversions.KM_TO_MI;
}
if (!reportSpeed) {
s = 3600000.0 / s; // converts from speed to pace
}
// Makes sure s is not NaN.
if (Double.isNaN(s)) {
s = 0;
}
String speed;
if (reportSpeed) {
int speedId = metricUnits ? R.plurals.voiceSpeedKilometersPerHour
: R.plurals.voiceSpeedMilesPerHour;
speed = context.getResources().getQuantityString(speedId, getQuantityCount(s), s);
} else {
int paceId = metricUnits ? R.string.voice_pace_per_kilometer : R.string.voice_pace_per_mile;
speed = String.format(context.getString(paceId), getAnnounceTime((long) s));
}
int totalDistanceId = metricUnits ? R.plurals.voiceTotalDistanceKilometers
: R.plurals.voiceTotalDistanceMiles;
String totalDistance = context.getResources().getQuantityString(
totalDistanceId, getQuantityCount(d), d);
return context.getString(
R.string.voice_template, totalDistance, getAnnounceTime(stats.getMovingTime()), speed);
}
/**
* Gets the plural count to be used by getQuantityString. getQuantityString
* only supports integer quantities, not a double quantity like "2.2".
* <p>
* As a temporary workaround, we convert a double quantity to an integer
* quantity. If the double quantity is exactly 0, 1, or 2, then we can return
* these integer quantities. Otherwise, we cast the double quantity to an
* integer quantity. However, we need to make sure that if the casted value is
* 0, 1, or 2, we don't return those, instead, return the next biggest integer
* 3.
*
* @param d the double value
*/
private int getQuantityCount(double d) {
if (d == 0) {
return 0;
} else if (d == 1) {
return 1;
} else if (d == 2) {
return 2;
} else {
int count = (int) d;
return count < 3 ? 3 : count;
}
}
@Override
public void start() {
Log.i(Constants.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);
}
/**
* Called when the TTS engine is initialized.
*/
private void onTtsInit(int status) {
Log.i(TAG, "TrackRecordingService.TTS init: " + status);
synchronized (this) {
// TTS should be valid here but NPE exceptions were reported to the market.
initStatus = status;
checkReady();
}
}
/**
* Ensures that the TTS is ready (finishing its initialization if needed).
*/
private void checkReady() {
synchronized (this) {
if (ready) {
// Already done;
return;
}
ready = initStatus == TextToSpeech.SUCCESS && tts != null;
Log.d(TAG, "Status announcer ready: " + ready);
if (ready) {
onTtsReady();
}
}
}
/**
* Finishes the TTS engine initialization.
* Called once (and only once) when the TTS engine is ready.
*/
protected void onTtsReady() {
// 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(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);
}
@Override
public void shutdown() {
// Stop listening to phone state.
listenToPhoneState(phoneListener, PhoneStateListener.LISTEN_NONE);
if (tts != null) {
tts.shutdown();
tts = null;
}
Log.i(Constants.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);
}
}
/**
* Gets a string to announce the time.
*
* @param time the time
*/
@VisibleForTesting
String getAnnounceTime(long time) {
int[] parts = StringUtils.getTimeParts(time);
String seconds = context.getResources().getQuantityString(
R.plurals.voiceSeconds, parts[0], parts[0]);
String minutes = context.getResources().getQuantityString(
R.plurals.voiceMinutes, parts[1], parts[1]);
String hours = context.getResources().getQuantityString(
R.plurals.voiceHours, parts[2], parts[2]);
StringBuilder sb = new StringBuilder();
if (parts[2] != 0) {
sb.append(hours);
sb.append(" ");
sb.append(minutes);
} else {
sb.append(minutes);
sb.append(" ");
sb.append(seconds);
}
return sb.toString();
}
}
@@ -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.tasks;
import static com.google.android.apps.mytracks.Constants.TAG;
import android.util.Log;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;
import com.google.android.apps.mytracks.services.TrackRecordingService;
/**
* This class will periodically perform a task.
*
* @author Sandor Dornbush
*/
public class TimerTaskExecutor {
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 TimerTaskExecutor(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(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(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);
}
}
}