Allow speed greater than 250km/hr.

This commit is contained in:
Jimmy Shih
2012-06-21 14:45:47 -07:00
commit 09f08f04db
669 changed files with 63333 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,82 @@
/*
* 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,134 @@
/*
* 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 com.google.common.annotations.VisibleForTesting;
import android.app.IntentService;
import android.content.ComponentName;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Log;
/**
* A service to control starting and stopping of a recording. This service,
* through the AndroidManifest.xml, is configured to only allow components of
* the same application to invoke it. Thus this service can be used my MyTracks
* app widget, {@link TrackWidgetProvider}, but not by other applications. This
* application delegates starting and stopping a recording to
* {@link TrackRecordingService} using RPC calls.
*
* @author Jimmy Shih
*/
public class ControlRecordingService extends IntentService implements ServiceConnection {
private ITrackRecordingService trackRecordingService;
private boolean connected = false;
public ControlRecordingService() {
super(ControlRecordingService.class.getSimpleName());
}
@Override
public void onCreate() {
super.onCreate();
Intent newIntent = new Intent(this, TrackRecordingService.class);
startService(newIntent);
bindService(newIntent, this, 0);
}
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
trackRecordingService = ITrackRecordingService.Stub.asInterface(service);
notifyConnected();
}
@Override
public void onServiceDisconnected(ComponentName name) {
connected = false;
}
/**
* Notifies all threads that connection to {@link TrackRecordingService} is
* available.
*/
private synchronized void notifyConnected() {
connected = true;
notifyAll();
}
/**
* Waits until the connection to {@link TrackRecordingService} is available.
*/
private synchronized void waitConnected() {
while (!connected) {
try {
wait();
} catch (InterruptedException e) {
// can safely ignore
}
}
}
@Override
protected void onHandleIntent(Intent intent) {
waitConnected();
onHandleIntent(intent, trackRecordingService);
}
/**
* Handles the intent to start or stop a recording.
*
* @param intent to be handled
* @param trackRecordingService the trackRecordingService
*/
@VisibleForTesting
void onHandleIntent(Intent intent, @SuppressWarnings("hiding") ITrackRecordingService trackRecordingService) {
String action = intent.getAction();
if (action != null) {
try {
if (action.equals(getString(R.string.track_action_start))) {
trackRecordingService.startNewTrack();
} else if (action.equals(getString(R.string.track_action_end))) {
trackRecordingService.endCurrentTrack();
}
} catch (RemoteException e) {
Log.d(TAG, "ControlRecordingService onHandleIntent RemoteException", e);
}
}
unbindService(this);
connected = false;
}
@Override
public void onDestroy() {
super.onDestroy();
if (connected) {
unbindService(this);
connected = false;
}
}
}
@@ -0,0 +1,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,139 @@
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.services;
import com.google.android.apps.mytracks.Constants;
import com.google.android.apps.mytracks.util.PreferencesUtils;
import com.google.android.maps.mytracks.R;
import android.content.Context;
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;
public PreferenceManager(TrackRecordingService service) {
this.service = service;
this.sharedPreferences = service.getSharedPreferences(
Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
sharedPreferences.registerOnSharedPreferenceChangeListener(this);
// 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
|| PreferencesUtils.getKey(service, R.string.recording_track_id_key).equals(key)) {
long recordingTrackId = PreferencesUtils.getLong(service, R.string.recording_track_id_key);
/*
* Only set the id if it is valid. Setting it to -1L should only happen
* in TrackRecordingService.endCurrentTrack()
*/
if (recordingTrackId != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT) {
service.setRecordingTrackId(recordingTrackId);
}
}
if (key == null || PreferencesUtils.getKey(service, R.string.metric_units_key).equals(key)) {
service.setMetricUnits(PreferencesUtils.getBoolean(
service, R.string.metric_units_key, PreferencesUtils.METRIC_UNITS_DEFAULT));
}
if (key == null
|| PreferencesUtils.getKey(service, R.string.announcement_frequency_key).equals(key)) {
service.setAnnouncementFrequency(PreferencesUtils.getInt(
service, R.string.announcement_frequency_key,
PreferencesUtils.ANNOUNCEMENT_FREQUENCY_DEFAULT));
}
if (key == null || PreferencesUtils.getKey(service, R.string.split_frequency_key).equals(key)) {
service.setSplitFrequency(PreferencesUtils.getInt(
service, R.string.split_frequency_key, PreferencesUtils.SPLIT_FREQUENCY_DEFAULT));
}
if (key == null
|| PreferencesUtils.getKey(service, R.string.min_recording_interval_key).equals(key)) {
int minRecordingInterval = PreferencesUtils.getInt(service,
R.string.min_recording_interval_key, PreferencesUtils.MIN_RECORDING_INTERVAL_DEFAULT);
switch (minRecordingInterval) {
case PreferencesUtils.MIN_RECORDING_INTERVAL_ADAPT_BATTERY_LIFE:
/*
* Choose battery life over moving time accuracy. min: 30 seconds,
* max: 5 minutes, inDist: 5 meters.
*/
service.setLocationListenerPolicy(new AdaptiveLocationListenerPolicy(30000, 300000, 5));
break;
case PreferencesUtils.MIN_RECORDING_INTERVAL_ADAPT_ACCURACY:
/*
* Get all the updates. min: 1 second, max: 30 seconds, minDist: 0
* meter.
*/
service.setLocationListenerPolicy(new AdaptiveLocationListenerPolicy(1000, 30000, 0));
break;
default:
service.setLocationListenerPolicy(
new AbsoluteLocationListenerPolicy(minRecordingInterval * 1000));
}
}
if (key == null
|| PreferencesUtils.getKey(service, R.string.min_recording_distance_key).equals(key)) {
service.setMinRecordingDistance(PreferencesUtils.getInt(
service, R.string.min_recording_distance_key,
PreferencesUtils.MIN_RECORDING_DISTANCE_DEFAULT));
}
if (key == null
|| PreferencesUtils.getKey(service, R.string.max_recording_distance_key).equals(key)) {
service.setMaxRecordingDistance(PreferencesUtils.getInt(
service, R.string.max_recording_distance_key,
PreferencesUtils.MAX_RECORDING_DISTANCE_DEFAULT));
}
if (key == null
|| PreferencesUtils.getKey(service, R.string.min_required_accuracy_key).equals(key)) {
service.setMinRequiredAccuracy(PreferencesUtils.getInt(
service, R.string.min_required_accuracy_key,
PreferencesUtils.MIN_REQUIRED_ACCURACY_DEFAULT));
}
if (key == null
|| PreferencesUtils.getKey(service, R.string.auto_resume_track_timeout_key).equals(key)) {
service.setAutoResumeTrackTimeout(PreferencesUtils.getInt(
service, R.string.auto_resume_track_timeout_key,
PreferencesUtils.AUTO_RESUME_TRACK_TIMEOUT_DEFAULT));
}
}
public void shutdown() {
sharedPreferences.unregisterOnSharedPreferenceChangeListener(this);
service = null;
}
}
@@ -0,0 +1,121 @@
/*
* Copyright 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.services;
import com.google.android.apps.mytracks.io.file.TrackWriterFactory.TrackFileFormat;
import com.google.android.apps.mytracks.util.FileUtils;
import com.google.common.annotations.VisibleForTesting;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.Environment;
import android.os.IBinder;
import android.util.Log;
import java.io.File;
/**
* A service to remove My Tracks temp files older than one hour on the SD card.
*
* @author Jimmy Shih
*/
public class RemoveTempFilesService extends Service {
private static final String TAG = RemoveTempFilesService.class.getSimpleName();
private static final int ONE_HOUR_IN_MILLISECONDS = 60 * 60 * 1000;
private RemoveTempFilesAsyncTask removeTempFilesAsyncTask = null;
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
// Setup an alarm to repeatedly call this service
Intent alarmIntent = new Intent(this, RemoveTempFilesService.class);
PendingIntent pendingIntent = PendingIntent.getService(
this, 0, alarmIntent, PendingIntent.FLAG_CANCEL_CURRENT);
AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
alarmManager.setInexactRepeating(AlarmManager.RTC_WAKEUP,
System.currentTimeMillis() + ONE_HOUR_IN_MILLISECONDS, AlarmManager.INTERVAL_HOUR,
pendingIntent);
// Invoke the AsyncTask to cleanup the temp files
if (removeTempFilesAsyncTask == null
|| removeTempFilesAsyncTask.getStatus().equals(AsyncTask.Status.FINISHED)) {
removeTempFilesAsyncTask = new RemoveTempFilesAsyncTask();
removeTempFilesAsyncTask.execute((Void[]) null);
}
return START_NOT_STICKY;
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
private class RemoveTempFilesAsyncTask extends AsyncTask<Void, Void, Void> {
@Override
protected Void doInBackground(Void... params) {
if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
// Can't do anything
return null;
}
cleanTempDirectory(TrackFileFormat.GPX.getExtension());
cleanTempDirectory(TrackFileFormat.KML.getExtension());
cleanTempDirectory(TrackFileFormat.CSV.getExtension());
cleanTempDirectory(TrackFileFormat.TCX.getExtension());
return null;
}
@Override
protected void onPostExecute(Void result) {
stopSelf();
}
}
private void cleanTempDirectory(String name) {
cleanTempDirectory(new File(FileUtils.buildExternalDirectoryPath(name, "tmp")));
}
/**
* Removes temp files in a directory older than one hour.
*
* @param dir the directory
* @return the number of files removed.
*/
@VisibleForTesting
int cleanTempDirectory(File dir) {
if (!dir.exists()) {
return 0;
}
int count = 0;
long oneHourAgo = System.currentTimeMillis() - ONE_HOUR_IN_MILLISECONDS;
for (File f : dir.listFiles()) {
if (f.lastModified() < oneHourAgo) {
if (!f.delete()) {
Log.e(TAG, "Unable to delete file: " + f.getAbsolutePath());
} else {
count++;
}
}
}
return count;
}
}
@@ -0,0 +1,172 @@
/*
* 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.TrackRecordingServiceConnectionUtils;
import com.google.android.maps.mytracks.BuildConfig;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.IBinder;
import android.os.IBinder.DeathRecipient;
import android.os.RemoteException;
import android.util.Log;
/**
* Wrapper for the connection to the track recording service.
* This handles connection/disconnection internally, only returning a real
* service for use if one is available and connected.
*
* @author Rodrigo Damazio
*/
public class TrackRecordingServiceConnection {
private ITrackRecordingService boundService;
private final DeathRecipient deathRecipient = new DeathRecipient() {
@Override
public void binderDied() {
Log.d(TAG, "Service died");
setBoundService(null);
}
};
private final ServiceConnection serviceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName className, IBinder service) {
Log.i(TAG, "Connected to service");
try {
service.linkToDeath(deathRecipient, 0);
} catch (RemoteException e) {
Log.e(TAG, "Failed to bind a death recipient", e);
}
setBoundService(ITrackRecordingService.Stub.asInterface(service));
}
@Override
public void onServiceDisconnected(ComponentName className) {
Log.i(TAG, "Disconnected from service");
setBoundService(null);
}
};
private final Context context;
private final Runnable bindChangedCallback;
/**
* Constructor.
*
* @param context the current context
* @param bindChangedCallback a callback to be executed when the state of the
* service binding changes
*/
public TrackRecordingServiceConnection(Context context, Runnable bindChangedCallback) {
this.context = context;
this.bindChangedCallback = bindChangedCallback;
}
/**
* Binds to the service, starting it if necessary.
*/
public void startAndBind() {
bindService(true);
}
/**
* Binds to the service, only if it's already running.
*/
public void bindIfRunning() {
bindService(false);
}
/**
* Unbinds from and stops the service.
*/
public void stop() {
unbind();
Log.d(TAG, "Stopping service");
Intent intent = new Intent(context, TrackRecordingService.class);
context.stopService(intent);
}
/**
* Unbinds from the service (but leaves it running).
*/
public void unbind() {
Log.d(TAG, "Unbinding from the service");
try {
context.unbindService(serviceConnection);
} catch (IllegalArgumentException e) {
// Means we weren't bound, which is ok.
}
setBoundService(null);
}
/**
* Returns the service if connected to it, or null if not connected.
*/
public ITrackRecordingService getServiceIfBound() {
checkBindingAlive();
return boundService;
}
private void checkBindingAlive() {
if (boundService != null &&
!boundService.asBinder().isBinderAlive()) {
setBoundService(null);
}
}
private void bindService(boolean startIfNeeded) {
if (boundService != null) {
// Already bound.
return;
}
if (!startIfNeeded
&& !TrackRecordingServiceConnectionUtils.isRecordingServiceRunning(context)) {
// Not running, start not requested.
Log.d(TAG, "Service not running, not binding to it.");
return;
}
if (startIfNeeded) {
Log.i(TAG, "Starting the service");
Intent intent = new Intent(context, TrackRecordingService.class);
context.startService(intent);
}
Log.i(TAG, "Binding to the service");
Intent intent = new Intent(context, TrackRecordingService.class);
int flags = BuildConfig.DEBUG ? Context.BIND_DEBUG_UNBIND : 0;
context.bindService(intent, serviceConnection, flags);
}
private void setBoundService(ITrackRecordingService service) {
boundService = service;
if (bindChangedCallback != null) {
bindChangedCallback.run();
}
}
}
@@ -0,0 +1,294 @@
/*
* 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;
import com.google.android.apps.mytracks.content.Sensor.SensorState;
import com.google.android.apps.mytracks.util.ApiAdapterFactory;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothSocket;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
import java.io.IOException;
import java.io.InputStream;
import java.util.UUID;
/**
* Manages bluetooth connection. It has a thread for connecting with a bluetooth
* device and a thread for performing data transmission when connected.
*
* @author Sandor Dornbush
*/
public class BluetoothConnectionManager {
// My Tracks UUID
public static final UUID MY_TRACKS_UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB");
// Message types sent to hander
public static final int MESSAGE_DEVICE_NAME = 1;
public static final int MESSAGE_READ = 2;
// Key for storing the device name
public static final String KEY_DEVICE_NAME = "device_name";
private static final String TAG = BluetoothConnectionManager.class.getSimpleName();
private final BluetoothAdapter bluetoothAdapter;
private final Handler handler;
private final MessageParser messageParser;
private SensorState sensorState;
private ConnectThread connectThread;
private ConnectedThread connectedThread;
/**
* Constructor.
*
* @param bluetoothAdapter the bluetooth adapter
* @param handler a hander for sending messages back to the UI activity
* @param messageParser a message parser
*/
public BluetoothConnectionManager(
BluetoothAdapter bluetoothAdapter, Handler handler, MessageParser messageParser) {
this.bluetoothAdapter = bluetoothAdapter;
this.handler = handler;
this.messageParser = messageParser;
this.sensorState = SensorState.NONE;
}
/**
* Gets the sensor state.
*/
public synchronized SensorState getSensorState() {
return sensorState;
}
/**
* Sets the sensor state.
*
* @param sensorState the sensor state
*/
private synchronized void setState(Sensor.SensorState sensorState) {
this.sensorState = sensorState;
}
/**
* Resets the bluetooth connection manager.
*/
public synchronized void reset() {
cancelThreads();
setState(Sensor.SensorState.NONE);
}
/**
* Cancels all the threads.
*/
private void cancelThreads() {
if (connectThread != null) {
connectThread.cancel();
connectThread = null;
}
if (connectedThread != null) {
connectedThread.cancel();
connectedThread = null;
}
}
/**
* Connects to a bluetooth device.
*
* @param bluetoothDevice the bluetooth device
*/
public synchronized void connect(BluetoothDevice bluetoothDevice) {
Log.d(TAG, "connect to: " + bluetoothDevice);
cancelThreads();
connectThread = new ConnectThread(bluetoothDevice);
connectThread.start();
setState(Sensor.SensorState.CONNECTING);
}
/**
* Starts the ConnectedThread to read data.
*
* @param bluetoothSocket the bluetooth socket
* @param bluetoothDevice the bluetooth device
*/
private synchronized void connected(
BluetoothSocket bluetoothSocket, BluetoothDevice bluetoothDevice) {
cancelThreads();
connectedThread = new ConnectedThread(bluetoothSocket);
connectedThread.start();
// Send the device name to the handler
Message message = handler.obtainMessage(MESSAGE_DEVICE_NAME);
Bundle bundle = new Bundle();
bundle.putString(KEY_DEVICE_NAME, bluetoothDevice.getName());
message.setData(bundle);
handler.sendMessage(message);
setState(Sensor.SensorState.CONNECTED);
}
/**
* A thread to connect to a bluetooth device.
*/
private class ConnectThread extends Thread {
private final BluetoothSocket bluetoothSocket;
private final BluetoothDevice bluetoothDevice;
public ConnectThread(BluetoothDevice device) {
setName("ConnectThread-" + device.getName());
this.bluetoothDevice = device;
BluetoothSocket tmp = null;
try {
tmp = ApiAdapterFactory.getApiAdapter().getBluetoothSocket(device);
} catch (IOException e) {
Log.e(TAG, "Unable to get blueooth socket.", e);
}
bluetoothSocket = tmp;
}
@Override
public void run() {
if (bluetoothAdapter == null) {
BluetoothConnectionManager.this.reset();
return;
}
// Cancel discovery to prevent slow down
bluetoothAdapter.cancelDiscovery();
try {
bluetoothSocket.connect();
} catch (IOException connectException) {
Log.i(TAG, "Unable to connect.", connectException);
setState(Sensor.SensorState.DISCONNECTED);
try {
bluetoothSocket.close();
} catch (IOException e) {
Log.e(TAG, "Unable to close blueooth socket.", e);
}
// Reset the bluetooth connection manager
BluetoothConnectionManager.this.reset();
return;
}
// Reset the ConnectThread since we are done
synchronized (BluetoothConnectionManager.this) {
connectThread = null;
}
// Start the connected thread
connected(bluetoothSocket, bluetoothDevice);
}
/**
* Cancels this thread.
*/
public void cancel() {
try {
bluetoothSocket.close();
} catch (IOException e) {
Log.e(TAG, "Unable to close bluetooth socket.", e);
}
}
}
/**
* This thread handles data transmission when connected.
*/
private class ConnectedThread extends Thread {
private final BluetoothSocket bluetoothSSocket;
private final InputStream inputStream;
public ConnectedThread(BluetoothSocket bluetoothSocket) {
this.bluetoothSSocket = bluetoothSocket;
InputStream tmp = null;
try {
tmp = bluetoothSocket.getInputStream();
} catch (IOException e) {
Log.e(TAG, "Unable to get input stream.", e);
}
inputStream = tmp;
}
@Override
public void run() {
byte[] buffer = new byte[messageParser.getFrameSize()];
int bytes; // bytes read
int offset = 0;
// Keep listening to the inputStream while connected
while (true) {
try {
// Read from the inputStream
bytes = inputStream.read(buffer, offset, messageParser.getFrameSize() - offset);
if (bytes == -1) { throw new IOException("EOF reached."); }
offset += bytes;
if (offset != messageParser.getFrameSize()) {
// Partial frame received. Call read again to receive the rest.
continue;
}
if (!messageParser.isValid(buffer)) {
int index = messageParser.findNextAlignment(buffer);
if (index == -1) {
Log.w(TAG, "Could not find any valid data. Drop data.");
offset = 0;
continue;
}
Log.w(TAG, "Misaligned data. Found new message at " + index + ". Recovering...");
offset = messageParser.getFrameSize() - index;
System.arraycopy(buffer, index, buffer, 0, offset);
continue;
}
offset = 0;
// Send a copy of the obtained bytes to the handler to avoid memory
// inconsistency issues
handler.obtainMessage(MESSAGE_READ, bytes, -1, buffer.clone()).sendToTarget();
} catch (IOException e) {
Log.i(TAG, "Bluetooth connection lost.", e);
setState(Sensor.SensorState.DISCONNECTED);
break;
}
}
}
/**
* Cancels this thread.
*/
public void cancel() {
try {
bluetoothSSocket.close();
} catch (IOException e) {
Log.e(TAG, "Unable to close bluetooth socket.", e);
}
}
}
}
@@ -0,0 +1,177 @@
/*
* 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.SensorDataSet;
import com.google.android.apps.mytracks.content.Sensor.SensorState;
import com.google.android.apps.mytracks.util.PreferencesUtils;
import com.google.android.maps.mytracks.R;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.util.Log;
import android.widget.Toast;
import java.util.ArrayList;
/**
* Bluetooth sensor manager.
*
* @author Sandor Dornbush
*/
public class BluetoothSensorManager extends SensorManager {
private static final BluetoothAdapter bluetoothAdapter = getDefaultBluetoothAdapter();
private static final String TAG = BluetoothConnectionManager.class.getSimpleName();
/**
* Gets the default bluetooth adapter.
*/
private static BluetoothAdapter getDefaultBluetoothAdapter() {
// If from the main application thread, return directly
if (Thread.currentThread().equals(Looper.getMainLooper().getThread())) {
return BluetoothAdapter.getDefaultAdapter();
}
// Get the default adapter from the main application thread
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 null;
}
return adapters.get(0);
}
private final Context context;
private final MessageParser messageParser;
private final BluetoothConnectionManager bluetoothConnectionManager;
private SensorDataSet sensorDataSet = null;
// Handler that gets information back from the bluetoothConnectionManager
private final Handler messageHandler = new Handler(Looper.getMainLooper()) {
@Override
public void handleMessage(Message message) {
switch (message.what) {
case BluetoothConnectionManager.MESSAGE_DEVICE_NAME:
String deviceName = message.getData()
.getString(BluetoothConnectionManager.KEY_DEVICE_NAME);
Toast.makeText(context, context.getString(R.string.settings_sensor_connected, deviceName),
Toast.LENGTH_SHORT).show();
break;
case BluetoothConnectionManager.MESSAGE_READ:
try {
byte[] readBuf = (byte[]) message.obj;
sensorDataSet = messageParser.parseBuffer(readBuf);
Log.d(TAG, "MESSAGE_READ: " + sensorDataSet);
} catch (IllegalArgumentException e) {
sensorDataSet = null;
Log.i(TAG, "Unexpected exception on read", e);
} catch (RuntimeException e) {
sensorDataSet = null;
Log.i(TAG, "Unexpected exception on read.", e);
}
break;
default:
break;
}
}
};
/**
* Constructor.
*
* @param context the context
* @param messageParser the message parser
*/
public BluetoothSensorManager(Context context, MessageParser messageParser) {
this.context = context;
this.messageParser = messageParser;
bluetoothConnectionManager = new BluetoothConnectionManager(
bluetoothAdapter, messageHandler, messageParser);
}
@Override
public boolean isEnabled() {
return bluetoothAdapter != null && bluetoothAdapter.isEnabled();
}
@Override
protected void setUpChannel() {
if (!isEnabled()) {
Log.w(TAG, "Bluetooth not enabled.");
return;
}
String address = PreferencesUtils.getString(
context, R.string.bluetooth_sensor_key, PreferencesUtils.BLUETOOTH_SENSOR_DEFAULT);
if (PreferencesUtils.BLUETOOTH_SENSOR_DEFAULT.equals(address)) {
Log.w(TAG, "No blueooth address.");
return;
}
Log.w(TAG, "Connecting to bluetooth address: " + address);
BluetoothDevice device;
try {
device = bluetoothAdapter.getRemoteDevice(address);
} catch (IllegalArgumentException e) {
Log.d(TAG, "Unable to get remote device for: " + address, e);
return;
}
bluetoothConnectionManager.connect(device);
}
@Override
protected void tearDownChannel() {
bluetoothConnectionManager.reset();
}
@Override
public SensorState getSensorState() {
return bluetoothConnectionManager.getSensorState();
}
@Override
public SensorDataSet getSensorDataSet() {
return sensorDataSet;
}
}
@@ -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,30 @@
/*
* 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;
/**
* A Polar heart rate sensor manager.
*
* @author Jimmy Shih
*/
public class PolarSensorManager extends BluetoothSensorManager {
public PolarSensorManager(Context context) {
super(context, new PolarMessageParser());
}
}
@@ -0,0 +1,130 @@
/*
* 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.SensorDataSet;
import com.google.android.apps.mytracks.content.Sensor.SensorState;
import android.util.Log;
import java.util.Timer;
import java.util.TimerTask;
/**
* Manage the connection to a sensor.
*
* @author Sandor Dornbush
*/
public abstract class SensorManager {
private static final String TAG = SensorManager.class.getSimpleName();
private static final long MAX_SENSOR_DATE_SET_AGE = 5000;
private static final long MAX_SENSOR_STATE_AGE = 20000;
private static final int RETRY_PERIOD = 20000;
private SensorState sensorState = SensorState.NONE;
private long sensorStateTimestamp = System.currentTimeMillis();
/**
* A time task to check sensor connection.
*/
private TimerTask checkSensorConnectionTimeTask = new TimerTask() {
@Override
public void run() {
switch (getSensorState()) {
case CONNECTING:
if (System.currentTimeMillis() - sensorStateTimestamp > MAX_SENSOR_STATE_AGE) {
Log.i(TAG, "Retry setUpChannel");
setUpChannel();
}
break;
case NONE:
case DISCONNECTED:
setUpChannel();
break;
default:
break;
}
}
};
private final Timer timer = new Timer();
/**
* Returns true if the sensor is enabled.
*/
public abstract boolean isEnabled();
/**
* Sets up the sensor channel.
*/
protected abstract void setUpChannel();
/**
* Tears down the sensor channel.
*/
protected abstract void tearDownChannel();
/**
* Gets the sensor data set.
*/
public abstract SensorDataSet getSensorDataSet();
/**
* Starts the sensor.
*/
public void startSensor() {
setUpChannel();
timer.schedule(checkSensorConnectionTimeTask, RETRY_PERIOD, RETRY_PERIOD);
}
/**
* Stops the sensor.
*/
public void stopSensor() {
timer.cancel();
tearDownChannel();
}
/**
* Sets the sensor state.
*
* @param sensorState the sensor state
*/
public void setSensorState(SensorState sensorState) {
sensorStateTimestamp = System.currentTimeMillis();
this.sensorState = sensorState;
}
/**
* Gets the sensor state.
*/
public SensorState getSensorState() {
return sensorState;
}
/**
* Returns true if the sensor data set is valid.
*/
public boolean isSensorDataSetValid() {
SensorDataSet sensorDataSet = getSensorDataSet();
if (sensorDataSet == null) {
return false;
}
return (System.currentTimeMillis() - sensorDataSet.getCreationTime()) < MAX_SENSOR_DATE_SET_AGE;
}
}
@@ -0,0 +1,118 @@
/*
* Copyright 2009 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.services.sensors;
import com.google.android.apps.mytracks.services.sensors.ant.AntSensorManager;
import com.google.android.apps.mytracks.util.AnalyticsUtils;
import com.google.android.apps.mytracks.util.PreferencesUtils;
import com.google.android.maps.mytracks.R;
import android.content.Context;
/**
* A factory of {@link SensorManager}.
*
* @author Sandor Dornbush
*/
public class SensorManagerFactory {
private static SensorManager systemSensorManager = null;
private static SensorManager tempSensorManager = null;
private SensorManagerFactory() {}
/**
* Gets the system sensor manager.
*
* @param context the context
*/
public static SensorManager getSystemSensorManager(Context context) {
releaseTempSensorManager();
releaseSystemSensorManager();
systemSensorManager = getSensorManager(context, true);
if (systemSensorManager != null) {
systemSensorManager.startSensor();
}
return systemSensorManager;
}
/**
* Releases the system sensor manager.
*/
public static void releaseSystemSensorManager() {
if (systemSensorManager != null) {
systemSensorManager.stopSensor();
}
systemSensorManager = null;
}
/**
* Gets the temp sensor manager.
*
* @param context
*/
public static SensorManager getTempSensorManager(Context context) {
releaseTempSensorManager();
if (systemSensorManager != null) {
return null;
}
tempSensorManager = getSensorManager(context, false);
if (tempSensorManager != null) {
tempSensorManager.startSensor();
}
return tempSensorManager;
}
/**
* Releases the temp sensor manager.
*/
public static void releaseTempSensorManager() {
if (tempSensorManager != null) {
tempSensorManager.stopSensor();
}
tempSensorManager = null;
}
/**
* Gets the sensor manager.
*
* @param context the context
*/
private static SensorManager getSensorManager(Context context, boolean sendPageViews) {
String sensorTypeValueNone = context.getString(R.string.sensor_type_value_none);
String sensorType = PreferencesUtils.getString(
context, R.string.sensor_type_key, sensorTypeValueNone);
if (sensorType.equals(context.getString(R.string.sensor_type_value_ant))) {
if (sendPageViews) {
AnalyticsUtils.sendPageViews(context, "/sensor/ant");
}
return new AntSensorManager(context);
} else if (sensorType.equals(context.getString(R.string.sensor_type_value_zephyr))) {
if (sendPageViews) {
AnalyticsUtils.sendPageViews(context, "/sensor/zephyr");
}
return new ZephyrSensorManager(context);
} else if (sensorType.equals(context.getString(R.string.sensor_type_value_polar))) {
if (sendPageViews) {
AnalyticsUtils.sendPageViews(context, "/sensor/polar");
}
return new PolarSensorManager(context);
}
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.settings_sensor_type_none);
case CONNECTING:
return c.getString(R.string.sensor_state_connecting);
case CONNECTED:
return c.getString(R.string.sensor_state_connected);
case DISCONNECTED:
return c.getString(R.string.sensor_state_disconnected);
case SENDING:
return c.getString(R.string.sensor_state_sending);
default:
return "";
}
}
}
@@ -0,0 +1,80 @@
/*
* Copyright 2011 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.services.sensors;
import java.util.LinkedList;
import java.util.List;
/**
* A history of Zephyr stride counter reading.
* These can be used as an alternate method to calculate the correct cadence.
* This is a work around for an issue with some HxM firmware.
*
* @author Dominik Ršttsches
*/
public class StrideReadings {
// visible for testing
protected static final int NUM_READINGS_FOR_AVERAGE = 10;
protected static final int MIN_READINGS_FOR_AVERAGE = 5;
protected static final int CADENCE_NOT_AVAILABLE = -1;
// TODO: Check whether 1Hz assumption is okay for cadence calculation
// otherwise add heart beat timestamp to this list and compute
// cadence from these timestamps.
private final List<Integer> strideReadingsHistory;
public StrideReadings() {
strideReadingsHistory = new LinkedList<Integer>();
}
public void updateStrideReading(int numStrides) {
// HRM/HxM documentation says, transmission frequency is 1 Hz,
// let's keep last NUM_READINGS_FOR_AVERAGE readings.
// TODO: Calibrate this using a reliable footpod / cadence sensor,
// otherwise use heartbeat timestamp for calculation.
strideReadingsHistory.add(0, numStrides);
while (strideReadingsHistory.size() > NUM_READINGS_FOR_AVERAGE) {
strideReadingsHistory.remove(strideReadingsHistory.size()-1);
}
}
public int getCadence() {
if (strideReadingsHistory.size() < MIN_READINGS_FOR_AVERAGE) {
// Bail out if we cannot really get a meaningful average yet.
return CADENCE_NOT_AVAILABLE;
}
// Compute assuming 1 stride reading/second.
int timeSinceOldestReadingSecs = strideReadingsHistory.size() - 1;
int stridesThen = strideReadingsHistory.get(strideReadingsHistory.size()-1);
int stridesNow = strideReadingsHistory.get(0);
// Contrary to documentation stride value seems to roll over at 128.
return Math.round( (float)(mod((stridesNow - stridesThen), 128)) /
timeSinceOldestReadingSecs * 60);
}
/**
* Modulo operation with positive return values, Java's remainder operator doesn't change sign.
*
* @return x mod y
*/
private static int mod(int x, int y)
{
int result = x % y;
return result < 0 ? result + y : result;
}
}
@@ -0,0 +1,109 @@
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.services.sensors;
import com.google.android.apps.mytracks.content.Sensor;
import com.google.android.apps.mytracks.util.ApiAdapterFactory;
import java.util.Arrays;
/**
* An implementation of a Sensor MessageParser for Zephyr.
*
* @author Sandor Dornbush
* @author Dominik Ršttsches
*/
public class ZephyrMessageParser implements MessageParser {
public static final int ZEPHYR_HXM_BYTE_STX = 0;
public static final int ZEPHYR_HXM_BYTE_CRC = 58;
public static final int ZEPHYR_HXM_BYTE_ETX = 59;
private static final byte[] CADENCE_BUG_FW_ID = {0x1A, 0x00, 0x31, 0x65, 0x50, 0x00, 0x31, 0x62};
private StrideReadings strideReadings;
@Override
public Sensor.SensorDataSet parseBuffer(byte[] buffer) {
Sensor.SensorDataSet.Builder sds =
Sensor.SensorDataSet.newBuilder()
.setCreationTime(System.currentTimeMillis());
Sensor.SensorData.Builder heartrate = Sensor.SensorData.newBuilder()
.setValue(buffer[12] & 0xFF)
.setState(Sensor.SensorState.SENDING);
sds.setHeartRate(heartrate);
Sensor.SensorData.Builder batteryLevel = Sensor.SensorData.newBuilder()
.setValue(buffer[11])
.setState(Sensor.SensorState.SENDING);
sds.setBatteryLevel(batteryLevel);
setCadence(sds, buffer);
return sds.build();
}
private void setCadence(Sensor.SensorDataSet.Builder sds, byte[] buffer) {
// Device Firmware ID, Firmware Version, Hardware ID, Hardware Version
// 0x1A00316550003162 produces erroneous values for Cadence and needs
// a workaround based on the stride counter.
// Firmware values range from field 3 to 10 (inclusive) of the byte buffer.
byte[] hardwareFirmwareId = ApiAdapterFactory.getApiAdapter().copyByteArray(buffer, 3, 11);
Sensor.SensorData.Builder cadence = Sensor.SensorData.newBuilder();
if (Arrays.equals(hardwareFirmwareId, CADENCE_BUG_FW_ID)) {
if (strideReadings == null) {
strideReadings = new StrideReadings();
}
strideReadings.updateStrideReading(buffer[54] & 0xFF);
if (strideReadings.getCadence() != StrideReadings.CADENCE_NOT_AVAILABLE) {
cadence.setValue(strideReadings.getCadence()).setState(Sensor.SensorState.SENDING);
}
} else {
cadence
.setValue(SensorUtils.unsignedShortToIntLittleEndian(buffer, 56) / 16)
.setState(Sensor.SensorState.SENDING);
}
sds.setCadence(cadence);
}
@Override
public boolean isValid(byte[] buffer) {
// Check STX (Start of Text), ETX (End of Text) and CRC Checksum
return buffer.length > ZEPHYR_HXM_BYTE_ETX
&& buffer[ZEPHYR_HXM_BYTE_STX] == 0x02
&& buffer[ZEPHYR_HXM_BYTE_ETX] == 0x03
&& SensorUtils.getCrc8(buffer, 3, 55) == buffer[ZEPHYR_HXM_BYTE_CRC];
}
@Override
public int getFrameSize() {
return 60;
}
@Override
public int findNextAlignment(byte[] buffer) {
// TODO test or understand this code.
for (int i = 0; i < buffer.length - 1; i++) {
if (buffer[i] == 0x03 && buffer[i+1] == 0x02) {
return i;
}
}
return -1;
}
}
@@ -0,0 +1,31 @@
/*
* 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;
/**
* A Zephyr heart rate sensor manager.
*
* @author Jimmy Shih
*/
public class ZephyrSensorManager extends BluetoothSensorManager {
public ZephyrSensorManager(Context context) {
super(context, new ZephyrMessageParser());
}
}
@@ -0,0 +1,590 @@
/*
* Copyright 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.services.sensors.ant;
import 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.content.Sensor.SensorState;
import com.google.android.apps.mytracks.services.sensors.SensorManager;
import com.google.android.apps.mytracks.util.PreferencesUtils;
import com.google.android.maps.mytracks.R;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.provider.Settings;
import android.util.Log;
import android.widget.Toast;
import java.lang.reflect.Field;
/**
* Ant Sensor Manager.
*
* @author Jimmy Shih
*/
public class AntSensorManager extends SensorManager {
/**
* Channel States
*/
public enum ChannelStates {
CLOSED, // Channel is closed
PENDING_OPEN, // User has requested opening the channel, waiting for a reset
SEARCHING, // Channel is opened, but has not received data
TRACKING_STATUS, // Channel is opened and has received status data recently
TRACKING_DATA, // Channel is opened and has received measurement data
// recently
OFFLINE // Channel is closed as the result of a search timeout
}
public static final short WILDCARD = 0;
private static final String TAG = AntSensorManager.class.getSimpleName();
private static final int CHANNELS = 4;
private static final String RADIO_ANT = "ant";
private static final byte ANT_NETWORK = (byte) 0x01;
private final Context context;
private final ChannelConfiguration channelConfig[];
private final IntentFilter statusIntentFilter;
private final AntInterface antInterface;
private boolean serviceConnected = false;
private boolean hasClaimedInterface = false;
private SensorDataSet sensorDataSet = null;
private long lastSensorDataSetTime = 0;
private AntSensorValue antSensorValue = new AntSensorValue();
private boolean requestedReset = false;
/**
* Constructor.
*
* @param context the context
*/
public AntSensorManager(Context context) {
this.context = context;
channelConfig = new ChannelConfiguration[CHANNELS];
channelConfig[0] = new HeartRateChannelConfiguration();
channelConfig[1] = new SpeedDistanceChannelConfiguration();
channelConfig[2] = new BikeCadenceChannelConfiguration();
channelConfig[3] = new CombinedBikeChannelConfiguration();
statusIntentFilter = new IntentFilter();
statusIntentFilter.addAction(AntInterfaceIntent.ANT_ENABLED_ACTION);
statusIntentFilter.addAction(AntInterfaceIntent.ANT_ENABLING_ACTION);
statusIntentFilter.addAction(AntInterfaceIntent.ANT_DISABLED_ACTION);
statusIntentFilter.addAction(AntInterfaceIntent.ANT_DISABLING_ACTION);
statusIntentFilter.addAction(AntInterfaceIntent.ANT_RESET_ACTION);
statusIntentFilter.addAction(AntInterfaceIntent.ANT_INTERFACE_CLAIMED_ACTION);
statusIntentFilter.addAction(Intent.ACTION_AIRPLANE_MODE_CHANGED);
antInterface = new AntInterface();
}
@Override
protected synchronized void setUpChannel() {
tearDownChannel();
if (AntInterface.hasAntSupport(context)) {
context.registerReceiver(statusReceiver, statusIntentFilter);
if (!antInterface.initService(context, serviceListener)) {
AntInterface.goToMarket(context);
} else {
setSensorState(Sensor.SensorState.CONNECTING);
handleServiceConnected();
}
}
}
/**
* Handles service connected. Needs to be synchronized.
*/
private synchronized void handleServiceConnected() {
serviceConnected = antInterface.isServiceConnected();
if (serviceConnected) {
try {
hasClaimedInterface = antInterface.hasClaimedInterface();
if (hasClaimedInterface) {
enableDataMessage(true);
} else {
// Need to claim the ant interface if it is available
hasClaimedInterface = antInterface.claimInterface();
}
} catch (AntInterfaceException e) {
handleAntError();
}
}
}
@Override
protected void tearDownChannel() {
try {
context.unregisterReceiver(statusReceiver);
} catch (IllegalArgumentException e) {
// Can safely ignore
}
enableDataMessage(false);
if (serviceConnected) {
try {
if (hasClaimedInterface) {
antInterface.releaseInterface();
}
hasClaimedInterface = false;
antInterface.stopRequestForceClaimInterface();
} catch (AntServiceNotConnectedException e) {
// Can safely ignore
} catch (AntInterfaceException e) {
Log.w(TAG, "Exception in tearDonwChannel.", e);
}
antInterface.releaseService();
serviceConnected = false;
}
}
@Override
public SensorDataSet getSensorDataSet() {
return sensorDataSet;
}
/**
* Tries to claim the ant interface.
*/
public void tryClaimAnt() {
try {
antInterface.requestForceClaimInterface(context.getString(R.string.my_tracks_app_name));
} catch (AntInterfaceException e) {
handleAntError();
}
}
private AntInterface.ServiceListener serviceListener = new AntInterface.ServiceListener() {
@Override
public void onServiceConnected() {
handleServiceConnected();
}
@Override
public void onServiceDisconnected() {
serviceConnected = false;
if (hasClaimedInterface) {
enableDataMessage(false);
}
}
};
/**
* Configures ant radio.
*/
private void configureAntRadio() {
try {
if (serviceConnected && hasClaimedInterface && antInterface.isEnabled()) {
try {
antInterface.ANTDisableEventBuffering();
} catch (AntInterfaceException e) {
Log.e(TAG, "Cannot disable event buffering.", e);
}
} else {
Log.i(TAG, "Cannot disable event buffering now.");
}
} catch (AntInterfaceException e) {
Log.e(TAG, "Unable to check enabled state.", e);
}
}
/**
* Handles ant error.
*/
private void handleAntError() {
clearAllChannels();
}
/**
* Opens a channel.
*
* @param channel the channel
*/
private void openChannel(byte channel) {
short deviceNumber = (short) PreferencesUtils.getInt(
context, channelConfig[channel].getDeviceIdKey(), WILDCARD);
channelConfig[channel].setDeviceNumber(deviceNumber);
channelConfig[channel].setChannelState(ChannelStates.PENDING_OPEN);
setupAntChannel(ANT_NETWORK, channel);
}
/**
* Closes a channel.
*
* @param channel the channel
*/
private void closeChannel(byte channel) {
channelConfig[channel].setInitializing(false);
channelConfig[channel].setDeinitializing(true);
channelConfig[channel].setChannelState(ChannelStates.CLOSED);
setSensorState(SensorState.DISCONNECTED);
try {
antInterface.ANTCloseChannel(channel);
// Note, unassign channel after getting channel closed event
} catch (AntInterfaceException e) {
Log.w(TAG, "Unable to close channel: " + channel, e);
handleAntError();
}
}
/**
* Clears all channels.
*/
private void clearAllChannels() {
for (int i = 0; i < CHANNELS; i++) {
channelConfig[i].setChannelState(ChannelStates.CLOSED);
}
setSensorState(SensorState.DISCONNECTED);
}
/**
* Requests reset.
*/
public void requestReset() {
try {
requestedReset = true;
antInterface.ANTResetSystem();
configureAntRadio();
} catch (AntInterfaceException e) {
Log.e(TAG, "Unable to reset ant.", e);
requestedReset = false;
}
}
@Override
public boolean isEnabled() {
if (antInterface == null || !antInterface.isServiceConnected()) {
return false;
}
try {
return antInterface.isEnabled();
} catch (AntInterfaceException e) {
Log.w(TAG, "Unable to check enabled.", e);
return false;
}
}
private final BroadcastReceiver statusReceiver = new BroadcastReceiver() {
public void onReceive(Context c, Intent intent) {
String ANTAction = intent.getAction();
if (ANTAction.equals(AntInterfaceIntent.ANT_DISABLED_ACTION)) {
clearAllChannels();
} else if (ANTAction.equals(AntInterfaceIntent.ANT_RESET_ACTION)) {
if (!requestedReset) {
// Someone else triggered an ant reset
clearAllChannels();
} else {
requestedReset = false;
configureAntRadio();
}
} else if (ANTAction.equals(AntInterfaceIntent.ANT_INTERFACE_CLAIMED_ACTION)) {
boolean wasClaimed = hasClaimedInterface;
// Could also read ANT_INTERFACE_CLAIMED_PID from intent and see if it
// matches the current process PID.
try {
hasClaimedInterface = antInterface.hasClaimedInterface();
if (hasClaimedInterface) {
enableDataMessage(true);
} else {
if (wasClaimed) {
// Claimed by another application
enableDataMessage(false);
}
}
} catch (AntInterfaceException e) {
handleAntError();
}
} else if (ANTAction.equals(Intent.ACTION_AIRPLANE_MODE_CHANGED)) {
if (isAirPlaneMode()) {
clearAllChannels();
}
}
}
};
private final BroadcastReceiver dataReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context c, Intent intent) {
if (intent.getAction().equals(AntInterfaceIntent.ANT_RX_MESSAGE_ACTION)) {
byte[] antRxMessage = intent.getByteArrayExtra(AntInterfaceIntent.ANT_MESSAGE);
byte channel;
switch (antRxMessage[AntMesg.MESG_ID_OFFSET]) {
case AntMesg.MESG_BROADCAST_DATA_ID:
case AntMesg.MESG_ACKNOWLEDGED_DATA_ID:
channel = antRxMessage[AntMesg.MESG_DATA_OFFSET];
if (channelConfig[channel].getChannelState() != ChannelStates.CLOSED) {
channelConfig[channel].setChannelState(ChannelStates.TRACKING_DATA);
}
if (channelConfig[channel].getDeviceNumber() == WILDCARD) {
try {
antInterface.ANTRequestMessage(channel, AntMesg.MESG_CHANNEL_ID_ID);
} catch (AntInterfaceException e) {
handleAntError();
}
}
channelConfig[channel].decodeMessage(antRxMessage, antSensorValue);
setSensorDataSet();
break;
case AntMesg.MESG_RESPONSE_EVENT_ID:
handleResponseEventMessage(antRxMessage);
break;
case AntMesg.MESG_CHANNEL_ID_ID:
channel = antRxMessage[AntMesg.MESG_DATA_OFFSET];
short deviceNumber = (short) ((antRxMessage[AntMesg.MESG_DATA_OFFSET + 1] & 0xFF
| ((antRxMessage[AntMesg.MESG_DATA_OFFSET + 2] & 0xFF) << 8)) & 0xFFFF);
channelConfig[channel].setDeviceNumber(deviceNumber);
PreferencesUtils.setInt(context, channelConfig[channel].getDeviceIdKey(), deviceNumber);
Toast.makeText(context,
context.getString(R.string.settings_sensor_connected, deviceNumber),
Toast.LENGTH_SHORT).show();
setSensorState(SensorState.CONNECTED);
break;
default:
break;
}
}
}
/**
* Handles response event message.
*
* @param message the message
*/
private void handleResponseEventMessage(byte[] message) {
// For a list of possible message codes see ANT Message Protocol and Usage
// section 9.5.6.1 available from thisisant.com
byte channel = message[AntMesg.MESG_DATA_OFFSET];
if ((message[AntMesg.MESG_DATA_OFFSET + 1] == AntMesg.MESG_EVENT_ID)
&& (message[AntMesg.MESG_DATA_OFFSET + 2] == AntDefine.EVENT_RX_SEARCH_TIMEOUT)) {
// A channel timed out searching, unassign it
channelConfig[channel].setInitializing(false);
channelConfig[channel].setDeinitializing(false);
channelConfig[channel].setChannelState(ChannelStates.OFFLINE);
try {
antInterface.ANTUnassignChannel(channel);
} catch (AntInterfaceException e) {
handleAntError();
}
setSensorState(SensorState.DISCONNECTED);
}
if (channelConfig[channel].isInitializing()) {
if (message[AntMesg.MESG_DATA_OFFSET + 2] != 0) {
// Error response
Log.e(TAG, String.format("Error code(%#02x) on message ID(%#02x) on channel %d",
message[AntMesg.MESG_DATA_OFFSET + 2], message[AntMesg.MESG_DATA_OFFSET + 1],
channel));
} else {
// Switch on message id
switch (message[AntMesg.MESG_DATA_OFFSET + 1]) {
case AntMesg.MESG_ASSIGN_CHANNEL_ID:
try {
antInterface.ANTSetChannelId(channel, channelConfig[channel].getDeviceNumber(),
channelConfig[channel].getDeviceType(), ChannelConfiguration.TRANSMISSION_TYPE);
} catch (AntInterfaceException e) {
handleAntError();
}
break;
case AntMesg.MESG_CHANNEL_ID_ID:
try {
antInterface.ANTSetChannelPeriod(channel, channelConfig[channel].getMessagPeriod());
} catch (AntInterfaceException e) {
handleAntError();
}
break;
case AntMesg.MESG_CHANNEL_MESG_PERIOD_ID:
try {
antInterface.ANTSetChannelRFFreq(channel, ChannelConfiguration.FREQUENCY);
} catch (AntInterfaceException e) {
handleAntError();
}
break;
case AntMesg.MESG_CHANNEL_RADIO_FREQ_ID:
try {
// Disable high priority search
antInterface.ANTSetChannelSearchTimeout(channel, (byte) 0);
} catch (AntInterfaceException e) {
handleAntError();
}
break;
case AntMesg.MESG_CHANNEL_SEARCH_TIMEOUT_ID:
try {
// Set search timeout to 30 seconds (low priority search)
antInterface.ANTSetLowPriorityChannelSearchTimeout(channel, (byte) 12);
} catch (AntInterfaceException e) {
handleAntError();
}
break;
case AntMesg.MESG_SET_LP_SEARCH_TIMEOUT_ID:
if (channelConfig[channel].getDeviceNumber() == WILDCARD) {
try {
// Configure proximity search, if using wild card search
antInterface.ANTSetProximitySearch(
channel, ChannelConfiguration.PROXIMITY_SEARCH);
} catch (AntInterfaceException e) {
handleAntError();
}
} else {
try {
antInterface.ANTOpenChannel(channel);
} catch (AntInterfaceException e) {
handleAntError();
}
}
break;
case AntMesg.MESG_PROX_SEARCH_CONFIG_ID:
try {
antInterface.ANTOpenChannel(channel);
} catch (AntInterfaceException e) {
handleAntError();
}
break;
case AntMesg.MESG_OPEN_CHANNEL_ID:
channelConfig[channel].setInitializing(false);
channelConfig[channel].setChannelState(ChannelStates.SEARCHING);
break;
default:
break;
}
}
} else if (channelConfig[channel].isDeinitializing()) {
if ((message[AntMesg.MESG_DATA_OFFSET + 1] == AntMesg.MESG_EVENT_ID)
&& (message[AntMesg.MESG_DATA_OFFSET + 2] == AntDefine.EVENT_CHANNEL_CLOSED)) {
try {
antInterface.ANTUnassignChannel(channel);
} catch (AntInterfaceException e) {
handleAntError();
}
} else if ((message[AntMesg.MESG_DATA_OFFSET + 1] == AntMesg.MESG_UNASSIGN_CHANNEL_ID)
&& (message[AntMesg.MESG_DATA_OFFSET + 2] == AntDefine.RESPONSE_NO_ERROR)) {
channelConfig[channel].setDeinitializing(false);
}
}
}
};
/**
* Sets sensor data set.
*/
private void setSensorDataSet() {
long now = System.currentTimeMillis();
// Data comes in at ~4Hz rate from the sensors, so after >300 msec fresh
// data is here from all the connected sensors
if (now < lastSensorDataSetTime + 300) {
return;
}
lastSensorDataSetTime = now;
SensorDataSet.Builder builder = Sensor.SensorDataSet.newBuilder();
int heartRate = antSensorValue.getHeartRate();
if (heartRate != -1) {
builder.setHeartRate(Sensor.SensorData.newBuilder()
.setValue(heartRate).setState(Sensor.SensorState.SENDING));
}
int cadence = antSensorValue.getCadence();
if (cadence != -1) {
builder.setCadence(Sensor.SensorData.newBuilder()
.setValue(cadence).setState(Sensor.SensorState.SENDING));
}
sensorDataSet = builder.setCreationTime(now).build();
setSensorState(SensorState.SENDING);
}
/**
* Sets up ant channel.
*
* @param networkNumber the network number
* @param channel the channel
*/
private void setupAntChannel(byte networkNumber, byte channel) {
try {
channelConfig[channel].setInitializing(true);
channelConfig[channel].setDeinitializing(false);
// Assign as slave channel on selected network
antInterface.ANTAssignChannel(channel, AntDefine.PARAMETER_RX_NOT_TX, networkNumber);
// The rest of the channel configuration will occur after the response is
// received in handleResponseEventMessage
} catch (AntInterfaceException aie) {
handleAntError();
}
}
/**
* Enables data message.
*
* @param enabled true to enable
*/
private void enableDataMessage(boolean enabled) {
if (enabled) {
context.registerReceiver(
dataReceiver, new IntentFilter(AntInterfaceIntent.ANT_RX_MESSAGE_ACTION));
for (int i = 0; i < CHANNELS; i++) {
openChannel((byte) i);
}
} else {
try {
context.unregisterReceiver(dataReceiver);
for (int i = 0; i < CHANNELS; i++) {
closeChannel((byte) i);
}
setSensorState(SensorState.DISCONNECTED);
} catch (IllegalArgumentException e) {
// Can safely ignore
}
}
}
/**
* Returns true if in airplane mode.
*/
private boolean isAirPlaneMode() {
if (!Settings.System.getString(
context.getContentResolver(), Settings.System.AIRPLANE_MODE_RADIOS)
.contains(RADIO_ANT)) {
return false;
}
if (Settings.System.getInt(context.getContentResolver(), Settings.System.AIRPLANE_MODE_ON, 0)
== 0) {
return false;
}
try {
Field field = Settings.System.class.getField("AIRPLANE_MODE_TOGGLEABLE_RADIOS");
return !Settings.System.getString(context.getContentResolver(), (String) field.get(null))
.contains(RADIO_ANT);
} catch (Exception e) {
// This is expected if the list does not yet exist, so just return true
return true;
}
}
}
@@ -0,0 +1,60 @@
/*
* Copyright 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.services.sensors.ant;
/**
* Contains the latest ant sensor value.
*
* @author Jimmy Shih
*/
public class AntSensorValue {
private int heartRate = -1;
private int cadence = -1;
/**
* Gets the heart rate.
*/
public int getHeartRate() {
return heartRate;
}
/**
* Sets the heart rate.
*
* @param heartRate the heart rate to set
*/
public void setHeartRate(int heartRate) {
this.heartRate = heartRate;
}
/**
* Gets the cadence.
*/
public int getCadence() {
return cadence;
}
/**
* Sets the cadence.
*
* @param cadence the cadence to set
*/
public void setCadence(int cadence) {
this.cadence = cadence;
}
}
@@ -0,0 +1,55 @@
/*
* Copyright 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.services.sensors.ant;
import com.google.android.maps.mytracks.R;
/**
* Bike cadence sensor channel configuration.
*
* @author Jimmy Shih
*/
public class BikeCadenceChannelConfiguration extends ChannelConfiguration {
private static final int DEVICE_ID_KEY = R.string.ant_bike_cadence_sensor_id_key;
private static final byte DEVICE_TYPE = 0x7A;
private static final short MESSAGE_PERIOD = 8102;
private CadenceCounter cadenceCounter = new CadenceCounter();
@Override
public int getDeviceIdKey() {
return DEVICE_ID_KEY;
}
@Override
public byte getDeviceType() {
return DEVICE_TYPE;
}
@Override
public short getMessagPeriod() {
return MESSAGE_PERIOD;
}
@Override
public void decodeMessage(byte[] message, AntSensorValue antSensorValue) {
int revolutionCount = ((int) message[9] & 0xFF) + ((int) message[10] & 0xFF) * 256;
int eventTime = ((int) message[7] & 0xFF) + ((int) message[8] & 0xFF) * 256;
antSensorValue.setCadence(cadenceCounter.getEventsPerMinute(revolutionCount, eventTime));
}
}
@@ -0,0 +1,178 @@
/*
* Copyright 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.services.sensors.ant;
import java.util.LinkedList;
/**
* A counter that processes an Ant+ sensor data (count + event time) and returns
* the instantaneous cadence value.
*
* @author Laszlo Molnar
*/
public class CadenceCounter {
private static final int MILLIS_PER_MINUTE = 60000;
private static final int MAX_HISTORY_TIME_IN_MILLIS = 5000; // 5 seconds
private static final int MAX_HISTORY_SIZE = 100;
private static final int EVENT_TIME_PER_MINUTE = 60 * 1024;
/**
* Cadence data.
*
* @author Laszlo Molnar
*/
private static class CadenceData {
final long systemTime;
final int count;
final int eventTime;
private CadenceData(long systemTime, int count, int eventTime) {
this.systemTime = systemTime;
this.count = count;
this.eventTime = eventTime;
}
}
// The last count
private int lastCount;
// The last calculated cadence
private int eventsPerMinute;
// The history of the previous sensor data, oldest first
private LinkedList<CadenceData> history;
public CadenceCounter() {
lastCount = -1;
eventsPerMinute = 0;
history = new LinkedList<CadenceData>();
}
/**
* Gets the cadence value.
*
* @param count count
* @param eventTime event time
*/
public int getEventsPerMinute(int count, int eventTime) {
long now = System.currentTimeMillis();
int countChange = (count - lastCount) & 0xFFFF;
if (lastCount < 0) {
/*
* eventTime for the initial count value is probably out of date, so not
* updating the history.
*/
lastCount = count;
eventsPerMinute = 0;
return 0;
}
lastCount = count;
if (countChange != 0) {
if (removeOldHistory(now)) {
CadenceData lastCadenceData = history.getLast();
int eventTimeChange = (eventTime - lastCadenceData.eventTime) & 0xFFFF;
if (eventTimeChange != 0) {
countChange = (count - lastCadenceData.count) & 0xFFFF;
eventsPerMinute = countChange * EVENT_TIME_PER_MINUTE / eventTimeChange;
}
}
history.addLast(new CadenceData(now, count, eventTime));
return eventsPerMinute;
} else {
// The sensor has resent old data
if (history.isEmpty()) {
eventsPerMinute = 0;
return 0;
}
CadenceData lastCadenceData = history.getLast();
if ((now - lastCadenceData.systemTime) * eventsPerMinute < MILLIS_PER_MINUTE) {
// The last eventsPerMinute is still valid
return eventsPerMinute;
}
// Update the history
if (!removeOldHistory(now)) {
eventsPerMinute = 0;
return 0;
}
/*
* Too much time has passed since the last count change. A smaller value
* than eventsPerMinute must be returned. The value is calculated from the
* history.
*/
return getValueFromHistory(now);
}
}
/**
* Gets the cadence value from the history when the sensor only resends the
* old data.
*
* @param now the current system time
*/
private int getValueFromHistory(long now) {
CadenceData firstCadenceData = history.getFirst();
CadenceData lastCadenceData = history.getLast();
int eventTimeChange = (lastCadenceData.eventTime - firstCadenceData.eventTime) & 0xFFFF;
int countChange = (lastCount - firstCadenceData.count) & 0xFFFF;
// (now - lastCadenceData) + (lastCadenceData - firstCadenceData)
int systemTimeChange = (int) (now - lastCadenceData.systemTime
+ (eventTimeChange * MILLIS_PER_MINUTE) / EVENT_TIME_PER_MINUTE);
/*
* eventsPerMinute is not updated because it is still needed when a new
* sensor event arrives.
*/
if (systemTimeChange == 0) {
return eventsPerMinute;
}
int value = (countChange * MILLIS_PER_MINUTE) / systemTimeChange;
/*
* Do not return a larger number than eventsPerMinute because this function
* is only called when more time has passed after the last count change than
* the interval from eventsPerMinute.
*/
return value < eventsPerMinute ? value : eventsPerMinute;
}
/**
* Removes old data from the history.
*
* @param now the current system time
* @return true if the remaining history is not empty.
*/
private boolean removeOldHistory(long now) {
CadenceData historyElement = history.peek();
while (historyElement != null) {
if (now - historyElement.systemTime <= MAX_HISTORY_TIME_IN_MILLIS
&& history.size() < MAX_HISTORY_SIZE) {
return true;
}
history.removeFirst();
historyElement = history.peek();
}
return false;
}
}
@@ -0,0 +1,123 @@
/*
* Copyright 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.services.sensors.ant;
import com.google.android.apps.mytracks.services.sensors.ant.AntSensorManager.ChannelStates;
/**
* Ant channel configuration.
*
* @author Jimmy Shih
*/
public abstract class ChannelConfiguration {
public static final byte FREQUENCY = 57; // 2457Mhz (Ant+ frequency)
public static final byte TRANSMISSION_TYPE = 0; // 0 for wild card search
public static final byte PROXIMITY_SEARCH = 7;
private short deviceNumber = AntSensorManager.WILDCARD;
private ChannelStates channelState = ChannelStates.CLOSED;
private boolean isInitializing = false;
private boolean isDeinitializing = false;
/**
* Gets the My Tracks settings device id key.
*/
public abstract int getDeviceIdKey();
/**
* Gets the device type.
*/
public abstract byte getDeviceType();
/**
* Gets the message period.
*/
public abstract short getMessagPeriod();
/**
* Decodes the message and places the value in antSensorValue.
*
* @param message the message
* @param antSensorValue the ant sensor value
*/
public abstract void decodeMessage(byte[] message, AntSensorValue antSensorValue);
/**
* Gets the device number.
*/
public short getDeviceNumber() {
return deviceNumber;
}
/**
* Sets the device number.
*
* @param deviceNumber the device number to set
*/
public void setDeviceNumber(short deviceNumber) {
this.deviceNumber = deviceNumber;
}
/**
* Gets the channel state.
*/
public ChannelStates getChannelState() {
return channelState;
}
/**
* Sets the channel state.
*
* @param channelState the channelState to set
*/
public void setChannelState(ChannelStates channelState) {
this.channelState = channelState;
}
/**
* Returns true if initializing.
*/
public boolean isInitializing() {
return isInitializing;
}
/**
* Sets isInitializing.
*
* @param isInitializing true if isInitializing
*/
public void setInitializing(boolean isInitializing) {
this.isInitializing = isInitializing;
}
/**
* Returns true if deinitializing.
*/
public boolean isDeinitializing() {
return isDeinitializing;
}
/**
* Sets isDeinitializing.
*
* @param isDeinitializing true if isDeinitializing
*/
public void setDeinitializing(boolean isDeinitializing) {
this.isDeinitializing = isDeinitializing;
}
}
@@ -0,0 +1,56 @@
/*
* Copyright 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.services.sensors.ant;
import com.google.android.maps.mytracks.R;
/**
* Combined bike sensor channel configuration.
*
* @author Jimmy Shih
*/
public class CombinedBikeChannelConfiguration extends ChannelConfiguration {
private static final int DEVICE_ID_KEY = R.string.ant_combined_bike_sensor_id_key;
private static final byte DEVICE_TYPE = 0x79;
private static final short MESSAGE_PERIOD = 8086;
private CadenceCounter cadenceCounter = new CadenceCounter();
@Override
public int getDeviceIdKey() {
return DEVICE_ID_KEY;
}
@Override
public byte getDeviceType() {
return DEVICE_TYPE;
}
@Override
public short getMessagPeriod() {
return MESSAGE_PERIOD;
}
@Override
public void decodeMessage(byte[] message, AntSensorValue antSensorValue) {
int revolutionCount = ((int) message[5] & 0xFF) + ((int) message[6] & 0xFF) * 256;
int eventTime = ((int) message[3] & 0xFF) + ((int) message[4] & 0xFF) * 256;
antSensorValue.setCadence(cadenceCounter.getEventsPerMinute(revolutionCount, eventTime));
}
}
@@ -0,0 +1,51 @@
/*
* Copyright 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.services.sensors.ant;
import com.google.android.maps.mytracks.R;
/**
* Heart rate monitor channel configuration.
*
* @author Jimmy Shih
*/
public class HeartRateChannelConfiguration extends ChannelConfiguration {
private static final int DEVICE_ID_KEY = R.string.ant_heart_rate_monitor_id_key;
private static final byte DEVICE_TYPE = 0x78;
private static final short MESSAGE_PERIOD = 8070;
@Override
public int getDeviceIdKey() {
return DEVICE_ID_KEY;
}
@Override
public byte getDeviceType() {
return DEVICE_TYPE;
}
@Override
public short getMessagPeriod() {
return MESSAGE_PERIOD;
}
@Override
public void decodeMessage(byte[] message, AntSensorValue antSensorValue) {
antSensorValue.setHeartRate(message[10] & 0xFF);
}
}
@@ -0,0 +1,55 @@
/*
* Copyright 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.services.sensors.ant;
import com.google.android.maps.mytracks.R;
/**
* Speed distance monitor channel configuration.
*
* @author Jimmy Shih
*/
public class SpeedDistanceChannelConfiguration extends ChannelConfiguration {
private static final int DEVICE_ID_KEY = R.string.ant_speed_distance_monitor_id_key;
private static final byte DEVICE_TYPE = 0x7C;
private static final short MESSAGE_PERIOD = 8134;
@Override
public int getDeviceIdKey() {
return DEVICE_ID_KEY;
}
@Override
public byte getDeviceType() {
return DEVICE_TYPE;
}
@Override
public short getMessagPeriod() {
return MESSAGE_PERIOD;
}
@Override
public void decodeMessage(byte[] message, AntSensorValue antSensorValue) {
// Check page 2 data
if (message[3] == 0x02) {
antSensorValue.setCadence(
(int) ((message[6] & 0xFF) + (((message[7] >>> 4) & 0x0F) / 16.0f)));
}
}
}
@@ -0,0 +1,83 @@
/*
* Copyright 2011 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.services.tasks;
import static com.google.android.apps.mytracks.Constants.TAG;
import android.annotation.TargetApi;
import android.content.Context;
import android.media.AudioManager;
import android.speech.tts.TextToSpeech;
import android.speech.tts.TextToSpeech.OnUtteranceCompletedListener;
import android.util.Log;
import java.util.HashMap;
/**
* This class will periodically announce the user's trip statistics. This class
* will request and release audio focus. <br>
* For API Level 8 or higher.
*
* @author Sandor Dornbush
*/
@TargetApi(8)
public class Api8StatusAnnouncerTask extends StatusAnnouncerTask {
private final static HashMap<String, String> SPEECH_PARAMS = new HashMap<String, String>();
static {
SPEECH_PARAMS.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, "not_used");
}
private final AudioManager audioManager;
private final OnUtteranceCompletedListener utteranceListener =
new OnUtteranceCompletedListener() {
@Override
public void onUtteranceCompleted(String utteranceId) {
int result = audioManager.abandonAudioFocus(null);
if (result == AudioManager.AUDIOFOCUS_REQUEST_FAILED) {
Log.w(TAG, "FroyoStatusAnnouncerTask: Failed to relinquish audio focus");
}
}
};
public Api8StatusAnnouncerTask(Context context) {
super(context);
audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
}
@Override
protected void onTtsReady() {
super.onTtsReady();
tts.setOnUtteranceCompletedListener(utteranceListener);
}
@Override
protected synchronized void speakAnnouncement(String announcement) {
int result = audioManager.requestAudioFocus(null,
TextToSpeech.Engine.DEFAULT_STREAM,
AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK);
if (result == AudioManager.AUDIOFOCUS_REQUEST_FAILED) {
Log.w(TAG, "FroyoStatusAnnouncerTask: Request for audio focus failed.");
}
// We don't care about the utterance id.
// It is supplied here to force onUtteranceCompleted to be called.
tts.speak(announcement, TextToSpeech.QUEUE_FLUSH, SPEECH_PARAMS);
}
}
@@ -0,0 +1,41 @@
/*
* Copyright 2009 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.services.tasks;
import com.google.android.apps.mytracks.services.TrackRecordingService;
/**
* This is interface for a task that will be executed on some schedule.
*
* @author Sandor Dornbush
*/
public interface PeriodicTask {
/**
* Sets up this task for subsequent calls to the run method.
*/
public void start();
/**
* This method will be called periodically.
*/
public void run(TrackRecordingService service);
/**
* Shuts down this task and clean up resources.
*/
public void shutdown();
}
@@ -0,0 +1,190 @@
/*
* 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.PreferencesUtils;
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 = PreferencesUtils.FREQUENCY_OFF;
/**
* 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 == PreferencesUtils.FREQUENCY_OFF) {
return;
}
// Try to make the task.
task = factory.create(service);
// Returning null is ok.
if (task == null) {
return;
}
task.start();
if (isTimeFrequency()) {
if (timerExecutor == null) {
timerExecutor = new TimerTaskExecutor(task, service);
}
timerExecutor.scheduleTask(taskFrequency * 60000L);
} else {
// For distance based splits.
calculateNextTaskDistance();
}
}
/**
* Shuts down the manager.
*/
public void shutdown() {
if (task != null) {
task.shutdown();
task = null;
}
if (timerExecutor != null) {
timerExecutor.shutdown();
timerExecutor = null;
}
}
/**
* Calculates the next distance when the task should execute.
*/
void calculateNextTaskDistance() {
// TODO: Decouple service from this class once and forever.
if (!service.isRecording() || task == null) {
return;
}
if (!isDistanceFrequency()) {
nextTaskDistance = Double.MAX_VALUE;
Log.d(TAG, "SplitManager: Distance splits disabled.");
return;
}
double distance = service.getTripStatistics().getTotalDistance() * UnitConversions.M_TO_KM;
if (!metricUnits) {
distance *= UnitConversions.KM_TO_MI;
}
// The index will be negative since the frequency is negative.
int index = (int) (distance / taskFrequency);
index -= 1;
nextTaskDistance = taskFrequency * index;
Log.d(TAG, "SplitManager: Next split distance: " + nextTaskDistance);
}
/**
* Updates executer with new trip statistics.
*/
public void update() {
if (!isDistanceFrequency() || task == null) {
return;
}
// Convert the distance in meters to km or mi.
double distance = service.getTripStatistics().getTotalDistance() * UnitConversions.M_TO_KM;
if (!metricUnits) {
distance *= UnitConversions.KM_TO_MI;
}
if (distance > nextTaskDistance) {
task.run(service);
calculateNextTaskDistance();
}
}
private boolean isTimeFrequency() {
return taskFrequency > 0;
}
private boolean isDistanceFrequency() {
return taskFrequency < 0;
}
/**
* Sets the task frequency.
* &lt; 0 Use the absolute value as a distance in the current measurement km
* or mi
* 0 Turn off the task
* &gt; 0 Use the value as a time in minutes
* @param taskFrequency The frequency in time or distance
*/
public void setTaskFrequency(int taskFrequency) {
Log.d(TAG, "setTaskFrequency: taskFrequency = " + taskFrequency);
this.taskFrequency = taskFrequency;
restore();
}
public void setMetricUnits(boolean metricUnits) {
this.metricUnits = metricUnits;
calculateNextTaskDistance();
}
double getNextTaskDistance() {
return nextTaskDistance;
}
}
@@ -0,0 +1,33 @@
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.services.tasks;
import android.content.Context;
/**
* An interface for classes that can create periodic tasks.
*
* @author Sandor Dornbush
*/
public interface PeriodicTaskFactory {
/**
* Creates a periodic task which does voice announcements.
*
* @return the task, or null if task is not supported
*/
PeriodicTask create(Context context);
}
@@ -0,0 +1,57 @@
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.services.tasks;
import com.google.android.apps.mytracks.content.WaypointCreationRequest;
import com.google.android.apps.mytracks.services.TrackRecordingService;
import android.content.Context;
/**
* A simple task to insert statistics markers periodically.
* @author Sandor Dornbush
*/
public class SplitTask implements PeriodicTask {
private SplitTask() {
}
@Override
public void run(TrackRecordingService service) {
service.insertWaypoint(WaypointCreationRequest.DEFAULT_STATISTICS);
}
@Override
public void shutdown() {
}
@Override
public void start() {
}
/**
* Create new SplitTasks.
*/
public static class Factory implements PeriodicTaskFactory {
@Override
public PeriodicTask create(Context context) {
return new SplitTask();
}
}
}
@@ -0,0 +1,37 @@
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.services.tasks;
import com.google.android.apps.mytracks.util.ApiAdapterFactory;
import android.content.Context;
/**
* Factory which wraps construction and setup of text-to-speech announcements in
* an API-level-safe way.
*
* @author Rodrigo Damazio
*/
public class StatusAnnouncerFactory implements PeriodicTaskFactory {
public StatusAnnouncerFactory() {
}
@Override
public PeriodicTask create(Context context) {
return ApiAdapterFactory.getApiAdapter().getStatusAnnouncerTask(context);
}
}
@@ -0,0 +1,356 @@
/*
* 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.PreferencesUtils;
import com.google.android.apps.mytracks.util.StringUtils;
import com.google.android.apps.mytracks.util.UnitConversions;
import com.google.android.maps.mytracks.R;
import com.google.common.annotations.VisibleForTesting;
import android.content.Context;
import android.speech.tts.TextToSpeech;
import android.speech.tts.TextToSpeech.OnInitListener;
import android.telephony.PhoneStateListener;
import android.telephony.TelephonyManager;
import android.util.Log;
import java.util.Locale;
/**
* This class will periodically announce the user's trip statistics.
*
* @author Sandor Dornbush
*/
public class StatusAnnouncerTask implements PeriodicTask {
/**
* The rate at which announcements are spoken.
*/
// @VisibleForTesting
static final float TTS_SPEECH_RATE = 0.9f;
/**
* A pointer to the service context.
*/
private final Context context;
/**
* The interface to the text to speech engine.
*/
protected TextToSpeech tts;
/**
* The response received from the TTS engine after initialization.
*/
private int initStatus = TextToSpeech.ERROR;
/**
* Whether the TTS engine is ready.
*/
private boolean ready = false;
/**
* Whether we're allowed to speak right now.
*/
private boolean speechAllowed;
/**
* Listener which updates {@link #speechAllowed} when the phone state changes.
*/
private final PhoneStateListener phoneListener = new PhoneStateListener() {
@Override
public void onCallStateChanged(int state, String incomingNumber) {
speechAllowed = state == TelephonyManager.CALL_STATE_IDLE;
if (!speechAllowed && tts != null && tts.isSpeaking()) {
// If we're already speaking, stop it.
tts.stop();
}
}
};
public StatusAnnouncerTask(Context context) {
this.context = context;
}
/**
* {@inheritDoc}
*
* Announces the trip status.
*/
@Override
public void run(TrackRecordingService service) {
if (service == null) {
Log.e(TAG, "StatusAnnouncer TrackRecordingService not initialized");
return;
}
runWithStatistics(service.getTripStatistics());
}
/**
* This method exists as a convenience for testing code, allowing said code
* to avoid needing to instantiate an entire {@link TrackRecordingService}
* just to test the announcer.
*/
// @VisibleForTesting
void runWithStatistics(TripStatistics statistics) {
if (statistics == null) {
Log.e(TAG, "StatusAnnouncer stats not initialized.");
return;
}
synchronized (this) {
checkReady();
if (!ready) {
Log.e(TAG, "StatusAnnouncer Tts not ready.");
return;
}
}
if (!speechAllowed) {
Log.i(Constants.TAG,
"Not making announcement - not allowed at this time");
return;
}
String announcement = getAnnouncement(statistics);
Log.d(Constants.TAG, "Announcement: " + announcement);
speakAnnouncement(announcement);
}
protected void speakAnnouncement(String announcement) {
tts.speak(announcement, TextToSpeech.QUEUE_FLUSH, null);
}
/**
* Builds the announcement string.
*
* @return The string that will be read to the user
*/
// @VisibleForTesting
protected String getAnnouncement(TripStatistics stats) {
boolean metricUnits = PreferencesUtils.getBoolean(
context, R.string.metric_units_key, PreferencesUtils.METRIC_UNITS_DEFAULT);
boolean reportSpeed = PreferencesUtils.getBoolean(
context, R.string.report_speed_key, PreferencesUtils.REPORT_SPEED_DEFAULT);
double d = stats.getTotalDistance() * UnitConversions.M_TO_KM;
double s = stats.getAverageMovingSpeed() * UnitConversions.MS_TO_KMH;
if (d == 0) {
return context.getString(R.string.voice_total_distance_zero);
}
if (!metricUnits) {
d *= UnitConversions.KM_TO_MI;
s *= UnitConversions.KM_TO_MI;
}
if (!reportSpeed) {
s = 3600000.0 / s; // converts from speed to pace
}
// Makes sure s is not NaN.
if (Double.isNaN(s)) {
s = 0;
}
String speed;
if (reportSpeed) {
int speedId = metricUnits ? R.plurals.voiceSpeedKilometersPerHour
: R.plurals.voiceSpeedMilesPerHour;
speed = context.getResources().getQuantityString(speedId, getQuantityCount(s), s);
} else {
int paceId = metricUnits ? R.string.voice_pace_per_kilometer : R.string.voice_pace_per_mile;
speed = context.getString(paceId, getAnnounceTime((long) s));
}
int totalDistanceId = metricUnits ? R.plurals.voiceTotalDistanceKilometers
: R.plurals.voiceTotalDistanceMiles;
String totalDistance = context.getResources().getQuantityString(
totalDistanceId, getQuantityCount(d), d);
return context.getString(
R.string.voice_template, totalDistance, getAnnounceTime(stats.getMovingTime()), speed);
}
/**
* Gets the plural count to be used by getQuantityString. getQuantityString
* only supports integer quantities, not a double quantity like "2.2".
* <p>
* As a temporary workaround, we convert a double quantity to an integer
* quantity. If the double quantity is exactly 0, 1, or 2, then we can return
* these integer quantities. Otherwise, we cast the double quantity to an
* integer quantity. However, we need to make sure that if the casted value is
* 0, 1, or 2, we don't return those, instead, return the next biggest integer
* 3.
*
* @param d the double value
*/
private int getQuantityCount(double d) {
if (d == 0) {
return 0;
} else if (d == 1) {
return 1;
} else if (d == 2) {
return 2;
} else {
int count = (int) d;
return count < 3 ? 3 : count;
}
}
@Override
public void start() {
Log.i(Constants.TAG, "Starting TTS");
if (tts == null) {
// We can't have this class also be the listener, otherwise it's unsafe to
// reference it in Cupcake (even if we don't instantiate it).
tts = newTextToSpeech(context, new OnInitListener() {
@Override
public void onInit(int status) {
onTtsInit(status);
}
});
}
speechAllowed = true;
// Register ourselves as a listener so we won't speak during a call.
listenToPhoneState(phoneListener, PhoneStateListener.LISTEN_CALL_STATE);
}
/**
* Called when the TTS engine is initialized.
*/
private void onTtsInit(int status) {
Log.i(TAG, "TrackRecordingService.TTS init: " + status);
synchronized (this) {
// TTS should be valid here but NPE exceptions were reported to the market.
initStatus = status;
checkReady();
}
}
/**
* Ensures that the TTS is ready (finishing its initialization if needed).
*/
private void checkReady() {
synchronized (this) {
if (ready) {
// Already done;
return;
}
ready = initStatus == TextToSpeech.SUCCESS && tts != null;
Log.d(TAG, "Status announcer ready: " + ready);
if (ready) {
onTtsReady();
}
}
}
/**
* Finishes the TTS engine initialization.
* Called once (and only once) when the TTS engine is ready.
*/
protected void onTtsReady() {
// Force the language to be the same as the string we will be speaking,
// if that's available.
Locale speechLanguage = Locale.getDefault();
int languageAvailability = tts.isLanguageAvailable(speechLanguage);
if (languageAvailability == TextToSpeech.LANG_MISSING_DATA ||
languageAvailability == TextToSpeech.LANG_NOT_SUPPORTED) {
// English is probably supported.
// TODO: Somehow use announcement strings from English too.
Log.w(TAG, "Default language not available, using English.");
speechLanguage = Locale.ENGLISH;
}
tts.setLanguage(speechLanguage);
// Slow down the speed just a bit as it is hard to hear when exercising.
tts.setSpeechRate(TTS_SPEECH_RATE);
}
@Override
public void shutdown() {
// Stop listening to phone state.
listenToPhoneState(phoneListener, PhoneStateListener.LISTEN_NONE);
if (tts != null) {
tts.shutdown();
tts = null;
}
Log.i(Constants.TAG, "TTS shut down");
}
/**
* Wrapper for instantiating a {@link TextToSpeech} object, which causes
* several issues during testing.
*/
// @VisibleForTesting
protected TextToSpeech newTextToSpeech(Context ctx, OnInitListener onInitListener) {
return new TextToSpeech(ctx, onInitListener);
}
/**
* Wrapper for calls to the 100%-unmockable {@link TelephonyManager#listen}.
*/
// @VisibleForTesting
protected void listenToPhoneState(PhoneStateListener listener, int events) {
TelephonyManager telephony =
(TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
if (telephony != null) {
telephony.listen(listener, events);
}
}
/**
* Gets a string to announce the time.
*
* @param time the time
*/
@VisibleForTesting
String getAnnounceTime(long time) {
int[] parts = StringUtils.getTimeParts(time);
String seconds = context.getResources().getQuantityString(
R.plurals.voiceSeconds, parts[0], parts[0]);
String minutes = context.getResources().getQuantityString(
R.plurals.voiceMinutes, parts[1], parts[1]);
String hours = context.getResources().getQuantityString(
R.plurals.voiceHours, parts[2], parts[2]);
StringBuilder sb = new StringBuilder();
if (parts[2] != 0) {
sb.append(hours);
sb.append(" ");
sb.append(minutes);
} else {
sb.append(minutes);
sb.append(" ");
sb.append(seconds);
}
return sb.toString();
}
}
@@ -0,0 +1,109 @@
/*
* Copyright 2009 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.services.tasks;
import static com.google.android.apps.mytracks.Constants.TAG;
import android.util.Log;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;
import com.google.android.apps.mytracks.services.TrackRecordingService;
/**
* This class will periodically perform a task.
*
* @author Sandor Dornbush
*/
public class TimerTaskExecutor {
private final PeriodicTask task;
private final TrackRecordingService service;
/**
* A timer to schedule the announcements.
* This is non-null if the task is in started (scheduled) state.
*/
private Timer timer;
public TimerTaskExecutor(PeriodicTask task,
TrackRecordingService service) {
this.task = task;
this.service = service;
}
/**
* Schedules the task at the given interval.
*
* @param interval The interval in milliseconds
*/
public void scheduleTask(long interval) {
// TODO: Decouple service from this class once and forever.
if (!service.isRecording()) {
return;
}
if (timer != null) {
timer.cancel();
timer.purge();
} else {
// First start, or we were previously shut down.
task.start();
}
timer = new Timer();
if (interval <= 0) {
return;
}
long now = System.currentTimeMillis();
long next = service.getTripStatistics().getStartTime();
if (next < now) {
next = now + interval - ((now - next) % interval);
}
Date start = new Date(next);
Log.i(TAG, task.getClass().getSimpleName() + " scheduled to start at " + start
+ " every " + interval + " milliseconds.");
timer.scheduleAtFixedRate(new PeriodicTimerTask(), start, interval);
}
/**
* Cleans up this object.
*/
public void shutdown() {
Log.i(TAG, task.getClass().getSimpleName() + " shutting down.");
if (timer != null) {
timer.cancel();
timer.purge();
timer = null;
task.shutdown();
}
}
/**
* The timer task to announce the trip status.
*/
private class PeriodicTimerTask extends TimerTask {
@Override
public void run() {
task.run(service);
}
}
}