Modify the check units flow so that it asks the user to pick either

metric or imperial. Metric is the default. Thus no longer need to
send the user to the 'Display' settings page.
This commit is contained in:
Jimmy Shih
2011-11-03 18:29:30 -07:00
commit 776f9bf360
501 changed files with 63826 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 jshih@google.com (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.start_new_track_action))) {
trackRecordingService.startNewTrack();
} else if (action.equals(getString(R.string.end_current_track_action))) {
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,65 @@
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.services;
import com.google.android.apps.mytracks.Constants;
import com.google.android.maps.mytracks.R;
import android.content.Context;
import android.content.SharedPreferences;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* Creates a default track name based on the current default track name policy.
*
* @author Matthew Simmons
*/
class DefaultTrackNameFactory {
private static final String TIMESTAMP_DATE_FORMAT = "yyyy-MM-dd HH:mm";
private final Context context;
DefaultTrackNameFactory(Context context) {
this.context = context;
}
/**
* Creates a new track name.
*
* @param trackId The ID for the current track.
* @param startTime The start time, in milliseconds since the epoch, of the
* current track.
* @return The new track name.
*/
String newTrackName(long trackId, long startTime) {
if (useTimestampTrackName()) {
SimpleDateFormat formatter = new SimpleDateFormat(TIMESTAMP_DATE_FORMAT);
return formatter.format(new Date(startTime));
} else {
return String.format(context.getString(R.string.new_track_format), trackId);
}
}
/** Determines whether the preferences allow a timestamp-based track name */
protected boolean useTimestampTrackName() {
SharedPreferences prefs =
context.getSharedPreferences(Constants.SETTINGS_NAME, 0);
return prefs.getBoolean(
context.getString(R.string.timestamp_track_name_key), true);
}
}
@@ -0,0 +1,48 @@
/*
* Copyright 2009 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.services;
/**
* This is an interface for classes that will manage the location listener policy.
* Different policy options are:
* Absolute
* Addaptive
*
* @author Sandor Dornbush
*/
public interface LocationListenerPolicy {
/**
* Returns the polling time this policy would like at this time.
*
* @return The polling that this policy dictates
*/
public long getDesiredPollingInterval();
/**
* Returns the minimum distance between updates.
*/
public int getMinDistance();
/**
* Notifies the amount of time the user has been idle at their current
* location.
*
* @param idleTime The time that the user has been idle at this spot
*/
public void updateIdleTime(long idleTime);
}
@@ -0,0 +1,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.maps.mytracks.R;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.util.Log;
/**
* A class that manages reading the shared preferences for the service.
*
* @author Sandor Dornbush
*/
public class PreferenceManager implements OnSharedPreferenceChangeListener {
private TrackRecordingService service;
private SharedPreferences sharedPreferences;
private final String announcementFrequencyKey;
private final String autoResumeTrackCurrentRetryKey;
private final String autoResumeTrackTimeoutKey;
private final String maxRecordingDistanceKey;
private final String metricUnitsKey;
private final String minRecordingDistanceKey;
private final String minRecordingIntervalKey;
private final String minRequiredAccuracyKey;
private final String recordingTrackKey;
private final String selectedTrackKey;
private final String splitFrequencyKey;
public PreferenceManager(TrackRecordingService service) {
this.service = service;
this.sharedPreferences = service.getSharedPreferences(
Constants.SETTINGS_NAME, 0);
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) {
sharedPreferences
.edit()
.putInt(autoResumeTrackCurrentRetryKey, retryAttempts)
.commit();
}
public void setRecordingTrack(long id) {
sharedPreferences
.edit()
.putLong(recordingTrackKey, id)
.commit();
}
public void setSelectedTrack(long id) {
sharedPreferences
.edit()
.putLong(selectedTrackKey, id)
.commit();
}
public void shutdown() {
sharedPreferences.unregisterOnSharedPreferenceChangeListener(this);
service = null;
}
}
@@ -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, 0);
}
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,170 @@
/*
* 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 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 = SystemUtils.isRelease(context) ? 0 : Context.BIND_DEBUG_UNBIND;
context.bindService(intent, serviceConnection, flags);
}
private void setBoundService(ITrackRecordingService service) {
boundService = service;
if (bindChangedCallback != null) {
bindChangedCallback.run();
}
}
}
@@ -0,0 +1,441 @@
/*
* 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.ApiFeatures;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothSocket;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.UUID;
/**
* This class does all the work for setting up and managing Bluetooth
* connections with other devices. It has a thread that listens for incoming
* connections, a thread for connecting with a device, and a thread for
* performing data transmissions when connected.
*
* @author Sandor Dornbush
*/
public class BluetoothConnectionManager {
// Unique UUID for this application
private static final UUID SPP_UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB");
private MessageParser parser;
// Member fields
private final BluetoothAdapter adapter;
private final Handler handler;
private ConnectThread connectThread;
private ConnectedThread connectedThread;
private Sensor.SensorState state;
// Message types sent from the BluetoothSenorService Handler
public static final int MESSAGE_STATE_CHANGE = 1;
public static final int MESSAGE_READ = 2;
public static final int MESSAGE_WRITE = 3;
public static final int MESSAGE_DEVICE_NAME = 4;
// Key names received from the BluetoothSenorService Handler
public static final String DEVICE_NAME = "device_name";
/**
* Constructor. Prepares a new BluetoothSensor session.
*
* @param 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 = getSocket();
} catch (IOException e) {
Log.e(Constants.TAG, "create() failed", e);
}
socket = tmp;
}
private BluetoothSocket getSocket() throws IOException {
if (ApiFeatures.getInstance().hasBluetoothDeviceCreateInsecureRfcommSocketToServiceRecord()) {
try {
return device.createInsecureRfcommSocketToServiceRecord(SPP_UUID);
} catch (IOException e) {
Log.e(Constants.TAG, "Unable to get insecure connect.", e);
}
} else {
try {
Class<? extends BluetoothDevice> c = device.getClass();
Method insecure = c.getMethod("createInsecureRfcommSocket", Integer.class);
insecure.setAccessible(true);
return (BluetoothSocket) insecure.invoke(device, 1);
} catch (SecurityException e) {
Log.e(Constants.TAG, "Unable to get insecure connect.", e);
} catch (NoSuchMethodException e) {
Log.e(Constants.TAG, "Unable to get insecure connect.", e);
} catch (IllegalArgumentException e) {
Log.e(Constants.TAG, "Unable to get insecure connect.", e);
} catch (IllegalAccessException e) {
Log.e(Constants.TAG, "Unable to get insecure connect.", e);
} catch (InvocationTargetException e) {
Log.e(Constants.TAG, "Unable to get insecure connect.", e);
}
}
return device.createRfcommSocketToServiceRecord(SPP_UUID);
}
@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, 0);
String address =
prefs.getString(context.getString(R.string.bluetooth_sensor_key), null);
if (address == null) {
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,69 @@
/*
* 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.AntSRMSensorManager;
import com.google.android.maps.mytracks.R;
import android.content.Context;
import android.content.SharedPreferences;
import android.util.Log;
/**
* A factory of SensorManagers.
*
* @author Sandor Dornbush
*/
public class SensorManagerFactory {
private SensorManagerFactory() {
}
/**
* Get a new sensor manager.
* @param context Context to fetch system preferences.
* @return The sensor manager that corresponds to the sensor type setting.
*/
public static SensorManager getSensorManager(Context context) {
SharedPreferences prefs =
context.getSharedPreferences(Constants.SETTINGS_NAME, 0);
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) {
return null;
} else if (sensor.equals(context.getString(R.string.sensor_type_value_ant))) {
return new AntDirectSensorManager(context);
} else if (sensor.equals(context.getString(R.string.sensor_type_value_srm_ant_bridge))) {
return new AntSRMSensorManager(context);
} else if (sensor.equals(context.getString(R.string.sensor_type_value_zephyr))) {
return new ZephyrSensorManager(context);
} else if (sensor.equals(context.getString(R.string.sensor_type_value_polar))) {
return new PolarSensorManager(context);
} else {
Log.w(Constants.TAG, "Unable to find sensor type: " + sensor);
return null;
}
}
}
@@ -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.sensor_type_value_none);
case CONNECTING:
return c.getString(R.string.connecting);
case CONNECTED:
return c.getString(R.string.connected);
case DISCONNECTED:
return c.getString(R.string.disconnected);
case SENDING:
return c.getString(R.string.sending);
default:
return "";
}
}
}
@@ -0,0 +1,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,110 @@
/*
* 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.ApiFeatures;
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 = ApiFeatures.getInstance().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,182 @@
/*
* 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.google.android.apps.mytracks.Constants;
import com.google.android.apps.mytracks.content.Sensor;
import com.google.android.maps.mytracks.R;
import com.dsi.ant.AntDefine;
import com.dsi.ant.AntMesg;
import com.dsi.ant.exception.AntInterfaceException;
import android.content.Context;
import android.content.SharedPreferences;
import android.util.Log;
/**
* A sensor manager to connect ANT+ sensors.
* This can include heart rate monitors.
*
* @author Sandor Dornbush
*/
public class AntDirectSensorManager extends AntSensorManager {
/*
* These constants are defined by the ANT+ heart rate monitor spec.
*/
public static final byte HRM_CHANNEL = 0;
public static final byte NETWORK_NUMBER = 1;
public static final byte HEART_RATE_DEVICE_TYPE = 120;
public static final byte POWER_DEVICE_TYPE = 11;
public static final byte MANUFACTURER_ID = 1;
public static final short CHANNEL_PERIOD = 8070;
public static final byte RF_FREQUENCY = 57;
private short deviceNumberHRM;
public AntDirectSensorManager(Context context) {
super(context);
deviceNumberHRM = WILDCARD;
// First read the the device id that we will be pairing with.
SharedPreferences prefs = context.getSharedPreferences(
Constants.SETTINGS_NAME, 0);
if (prefs != null) {
deviceNumberHRM =
(short) prefs.getInt(context.getString(R.string.ant_heart_rate_sensor_id_key), 0);
}
Log.i(TAG, "Will pair with heart rate monitor: " + deviceNumberHRM);
}
@Override
protected boolean handleMessage(byte messageId, byte[] messageData) {
if (super.handleMessage(messageId, messageData)) {
return true;
}
int channel = messageData[0] & AntDefine.CHANNEL_NUMBER_MASK;
switch (channel) {
case HRM_CHANNEL:
antDecodeHRM(messageId, messageData);
break;
default:
Log.d(TAG, "Unhandled message: " + channel);
}
return true;
}
/**
* Decode an ant heart rate monitor message.
* @param messageData The byte array received from the heart rate monitor.
*/
private void antDecodeHRM(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 (deviceNumberHRM == WILDCARD) {
try {
getAntReceiver().ANTRequestMessage(HRM_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.");
}
setSensorState(Sensor.SensorState.CONNECTED);
int bpm = (int) antMessage[8] & 0xFF;
Sensor.SensorData.Builder b = Sensor.SensorData.newBuilder()
.setValue(bpm)
.setState(Sensor.SensorState.SENDING);
sensorData =
Sensor.SensorDataSet.newBuilder()
.setCreationTime(System.currentTimeMillis())
.setHeartRate(b)
.build();
}
void handleChannelId(byte[] rawMessage) {
AntChannelIdMessage message = new AntChannelIdMessage(rawMessage);
deviceNumberHRM = message.getDeviceNumber();
Log.i(TAG, "Found device id: " + deviceNumberHRM);
SharedPreferences prefs = context.getSharedPreferences(
Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = prefs.edit();
editor.putInt(context.getString(R.string.ant_heart_rate_sensor_id_key), deviceNumberHRM);
editor.commit();
}
private void handleMessageResponse(byte[] rawMessage) {
AntChannelResponseMessage message = new AntChannelResponseMessage(rawMessage);
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);
}
setSensorState(Sensor.SensorState.DISCONNECTED);
}
break;
case AntMesg.MESG_UNASSIGN_CHANNEL_ID:
setSensorState(Sensor.SensorState.DISCONNECTED);
Log.i(TAG, "Disconnected from the sensor: " + getSensorState());
break;
}
}
@Override protected void setupAntSensorChannels() {
setupAntSensorChannel(NETWORK_NUMBER,
HRM_CHANNEL,
deviceNumberHRM,
HEART_RATE_DEVICE_TYPE,
(byte) 0x01,
CHANNEL_PERIOD,
RF_FREQUENCY,
(byte) 0);
}
public short getDeviceNumberHRM() {
return deviceNumberHRM;
}
void setDeviceNumberHRM(short deviceNumberHRM) {
this.deviceNumberHRM = deviceNumberHRM;
}
}
@@ -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) {
short value = b0;
value |= ((short) b1) << 8;
return value;
}
}
@@ -0,0 +1,227 @@
/*
* 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.services.sensors.SensorUtils;
import com.google.android.maps.mytracks.R;
import android.content.Context;
import android.content.SharedPreferences;
import android.util.Log;
/**
* A sensor manager that can connect to an SRM Power Control 7 head unit.
*
* @author Sandor Dornbush
*/
public class AntSRMSensorManager extends AntSensorManager {
/*
* These constants are defined by the SRM Headunit spec.
*/
public static final byte NETWORK_NUMBER = (byte) 0;
public static final byte DEVICE_TYPE = (byte) 12;
public static final byte MANUFACTURER_ID = (byte) 1;
public static final short CHANNEL_PERIOD = (short) 8192;
public static final byte RF_FREQUENCY = (byte) 50;
public static final int MESSAGE_TYPE_INDEX = 3;
public static final int INITIALS_MESSAGE_TYPE = 5;
public static final int SENSOR_DATA_MESSAGE_TYPE = 6;
public static final int MESSAGE_ID_INDEX = 4;
/**
* This can be any value between 0..8
*/
private byte channel = (byte) 5;
/**
* This is the id of this device.
* The head unit must be programmed to expect this number.
*/
private byte deviceId;
/**
* The id of the last message received from the SRM head unit.
*/
private int lastMessageId;
public AntSRMSensorManager(Context context) {
super(context);
// First read the the device id that we will be announcing.
SharedPreferences prefs = context.getSharedPreferences(
Constants.SETTINGS_NAME, 0);
if (prefs != null) {
deviceId = (byte) prefs.getInt(
context.getString(R.string.ant_srm_bridge_sensor_id_key),
WILDCARD);
}
}
@Override
public void handleMessage(byte[] antMessage) {
// Parse channel number
byte recievedChannel = (byte) (antMessage[AntMesg.MESG_DATA_OFFSET]
& AntDefine.CHANNEL_NUMBER_MASK);
if (recievedChannel != channel) {
Log.d(TAG, "Unexpected channel: " + recievedChannel);
return;
}
switch (antMessage[AntMesg.MESG_ID_OFFSET]) {
case AntMesg.MESG_BROADCAST_DATA_ID:
handleBroadcastData(antMessage);
break;
case AntMesg.MESG_RESPONSE_EVENT_ID:
handleMessageResponse(antMessage);
break;
case AntMesg.MESG_CHANNEL_ID_ID:
handleChannelId(antMessage);
break;
default:
Log.e(TAG, "Unexpected message id: " + antMessage[3]);
}
}
private void handleBroadcastData(byte[] antMessage) {
if (deviceId == WILDCARD) {
try {
Log.d(TAG, "Requesting channel id id.");
getAntReceiver().ANTRequestMessage(channel, AntMesg.MESG_CHANNEL_ID_ID);
} catch (AntInterfaceException e) {
Log.e(TAG, "Failed to request channel id id", e);
}
}
setSensorState(Sensor.SensorState.CONNECTED);
switch(antMessage[MESSAGE_TYPE_INDEX]) {
case INITIALS_MESSAGE_TYPE:
// TODO handle initials message.
break;
case SENSOR_DATA_MESSAGE_TYPE:
parseSensorData(antMessage);
break;
default:
Log.e(TAG, "Unexpected message type: " + antMessage[MESSAGE_TYPE_INDEX]);
}
}
private void handleChannelId(byte[] antMessage) {
// Store the device id.
deviceId = antMessage[3];
Log.i(TAG, "Found device id: " + deviceId);
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), deviceId);
editor.commit();
}
private void handleMessageResponse(byte[] antMessage) {
if (antMessage[3] == AntMesg.MESG_EVENT_ID
&& antMessage[4] == AntDefine.EVENT_RX_SEARCH_TIMEOUT) {
// Search timeout
Log.w(TAG, "Search timed out. Unassigning channel.");
try {
getAntReceiver().ANTUnassignChannel(channel);
} catch (AntInterfaceException e) {
Log.e(TAG, "Failed to unassign ANT channel", e);
}
setSensorState(Sensor.SensorState.DISCONNECTED);
} else if (antMessage[3] == AntMesg.MESG_UNASSIGN_CHANNEL_ID) {
setSensorState(Sensor.SensorState.DISCONNECTED);
Log.i(TAG, "Disconnected from the sensor: " + getSensorState());
}
}
private void parseSensorData(byte[] antMessage) {
if (antMessage.length != 11) {
Log.e(TAG, "Unexpected ant message length: " + antMessage.length);
return;
}
int newMessageId = antMessage[MESSAGE_ID_INDEX] & 0xFF;
if (lastMessageId == newMessageId) {
// Repeated message.
Log.i(TAG, String.format("SRM ignoring repeat: 0x%X", newMessageId));
return;
}
if (newMessageId < lastMessageId) {
if (!(newMessageId < 20 && lastMessageId > 200)) {
Log.i(TAG, String.format("SRM ignoring repeat: 0x%X", newMessageId));
return;
} // else assume the byte overflowed to 0.
}
lastMessageId = newMessageId;
Sensor.SensorDataSet.Builder builder = Sensor.SensorDataSet.newBuilder()
.setCreationTime(System.currentTimeMillis());
int power = SensorUtils.unsignedShortToInt(antMessage, 5);
if (power >= 0) {
Sensor.SensorData.Builder b = Sensor.SensorData.newBuilder()
.setValue(power)
.setState(Sensor.SensorState.SENDING);
builder.setPower(b);
}
int speed = SensorUtils.unsignedShortToInt(antMessage, 7);
if (speed >= 0) {
// The speed from srm is in 1/10 km/hr.
// Sensor data expects the speed as m/s.
// TODO finish this.
//sensorData.setWheelSpeed(UnitConversions.TENTH_KMH_TO_MPS * speed);
}
int cadence = antMessage[9] & 0xFF;
if (cadence >= 0) {
Sensor.SensorData.Builder b = Sensor.SensorData.newBuilder()
.setValue(cadence)
.setState(Sensor.SensorState.SENDING);
builder.setCadence(b);
}
int bpm = antMessage[10] & 0xFF;
if (bpm > 0) {
Sensor.SensorData.Builder b = Sensor.SensorData.newBuilder()
.setValue(bpm)
.setState(Sensor.SensorState.SENDING);
builder.setHeartRate(b);
}
sensorData = builder.build();
}
@Override
protected void setupAntSensorChannels() {
lastMessageId = 0;
setupAntSensorChannel(NETWORK_NUMBER,
channel,
deviceId,
DEVICE_TYPE,
MANUFACTURER_ID,
CHANNEL_PERIOD,
RF_FREQUENCY,
(byte) 0);
}
}
@@ -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,370 @@
/*
* 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
* {@link 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.equals(AntInterfaceIntent.ANT_RX_MESSAGE_ACTION)) {
byte[] antMessage = intent.getByteArrayExtra(AntInterfaceIntent.ANT_MESSAGE);
if (DEBUGGING) {
Log.d(TAG, "Received RX message " + messageToString(antMessage));
}
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) {
try {
antReceiver.releaseInterface();
} catch (AntServiceNotConnectedException e) {
Log.i(TAG, "ANT service not connected", e);
} catch (AntInterfaceException e) {
Log.e(TAG, "failed to release ANT interface", e);
}
antReceiver.destroy();
antReceiver = null;
}
}
/**
* 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");
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) {
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 30 seconds (low priority search))
antReceiver.ANTSetLowPriorityChannelSearchTimeout(channelNumber, (byte) 12);
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,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,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;
import com.dsi.ant.AntInterface;
import android.content.Context;
/**
* 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);
}
}
@@ -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.tasks;
import static com.google.android.apps.mytracks.Constants.TAG;
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 for Froyo and future handsets.
* This class will request and release audio focus.
*
* @author Sandor Dornbush
*/
public class FroyoStatusAnnouncerTask 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 FroyoStatusAnnouncerTask(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() / 1000;
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() / 1000.0;
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,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.util.ApiFeatures;
import android.content.Context;
import android.media.AudioManager;
/**
* Factory which wraps construction and setup of text-to-speech announcements in
* an API-level-safe way.
*
* @author Rodrigo Damazio
*/
public class StatusAnnouncerFactory implements PeriodicTaskFactory {
private final boolean hasTts;
public StatusAnnouncerFactory(ApiFeatures apiFeatures) {
this.hasTts = apiFeatures.hasTextToSpeech();
}
@Override
public PeriodicTask create(Context context) {
if (hasTts) {
return ApiFeatures.getInstance().getApiAdapter().getStatusAnnouncerTask(context);
} else {
return null;
}
}
/**
* Returns the appropriate volume stream for controlling announcement
* volume.
*/
public int getVolumeStream() {
if (hasTts) {
return StatusAnnouncerTask.getVolumeStream();
} else {
return AudioManager.USE_DEFAULT_STREAM_TYPE;
}
}
}
@@ -0,0 +1,336 @@
/*
* 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 android.content.Context;
import android.content.SharedPreferences;
import android.speech.tts.TextToSpeech;
import android.speech.tts.TextToSpeech.OnInitListener;
import android.telephony.PhoneStateListener;
import android.telephony.TelephonyManager;
import android.util.Log;
import java.util.Locale;
/**
* This class will periodically announce the user's trip statistics.
*
* @author Sandor Dornbush
*/
public class StatusAnnouncerTask implements PeriodicTask {
/**
* The rate at which announcements are spoken.
*/
// @VisibleForTesting
static final float TTS_SPEECH_RATE = 0.9f;
/**
* A pointer to the service context.
*/
private final Context context;
/**
* String utilities.
*/
private final StringUtils stringUtils;
/**
* The interface to the text to speech engine.
*/
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, new StringUtils(context));
}
public StatusAnnouncerTask(Context context, StringUtils stringUtils) {
this.context = context;
this.stringUtils = stringUtils;
}
/**
* {@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) {
SharedPreferences preferences =
context.getSharedPreferences(Constants.SETTINGS_NAME, 0);
boolean metricUnits = true;
boolean reportSpeed = true;
if (preferences != null) {
metricUnits =
preferences.getBoolean(context.getString(R.string.metric_units_key),
true);
reportSpeed =
preferences.getBoolean(context.getString(R.string.report_speed_key),
true);
}
double d = stats.getTotalDistance() / 1000;
double s = stats.getAverageMovingSpeed() * 3.6;
if (d == 0) {
return context.getString(R.string.announce_no_distance);
}
int speedLabel;
if (metricUnits) {
if (reportSpeed) {
speedLabel = R.string.kilometer_per_hour_long;
} else {
speedLabel = R.string.per_kilometer;
}
} else {
s *= UnitConversions.KMH_TO_MPH;
d *= UnitConversions.KM_TO_MI;
if (reportSpeed) {
speedLabel = R.string.mile_per_hour_long;
} else {
speedLabel = R.string.per_mile;
}
}
String speed = null;
if ((s == 0) || Double.isNaN(s)) {
speed = context.getString(R.string.unknown);
} else {
if (reportSpeed) {
speed = String.format("%.1f", s);
} else {
double pace = 3600000.0 / s;
Log.w(Constants.TAG,
"Converted speed: " + s + " to pace: " + pace);
speed = stringUtils.formatTimeLong((long) pace);
}
}
return context.getString(R.string.announce_template,
context.getString(R.string.total_distance_label),
d,
context.getString(metricUnits
? R.string.kilometers_long
: R.string.miles_long),
stringUtils.formatTimeLong(stats.getMovingTime()),
speed,
context.getString(speedLabel));
}
@Override
public void start() {
Log.i(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);
}
}
/**
* Returns the volume stream to use for controlling announcement volume.
*/
public static int getVolumeStream() {
return TextToSpeech.Engine.DEFAULT_STREAM;
}
}
@@ -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);
}
}
}