Fix an issue when run SyncTest.

This commit is contained in:
youtaol
2013-10-30 11:13:50 +08:00
commit 7823ca4eec
1115 changed files with 75566 additions and 0 deletions
@@ -0,0 +1,52 @@
/*
* 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 {
private final long interval;
/**
* Constructor.
*
* @param interval the interval to request for gps signal
*/
public AbsoluteLocationListenerPolicy(long interval) {
this.interval = interval;
}
@Override
public long getDesiredPollingInterval() {
return interval;
}
@Override
public int getMinDistance() {
return 0;
}
@Override
public void updateIdleTime(long idleTime) {
// Ignore
}
}
@@ -0,0 +1,92 @@
/*
* Copyright 2013 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.services;
import com.google.android.apps.mytracks.util.PreferencesUtils;
import com.google.android.gms.location.ActivityRecognitionResult;
import com.google.android.gms.location.DetectedActivity;
import com.google.android.maps.mytracks.R;
import android.app.IntentService;
import android.content.Intent;
/**
* Service that receives ActivityRecognition updates.
*
* @author Jimmy Shih
*/
public class ActivityRecognitionIntentService extends IntentService {
private static final int REQUIRED_CONFIDENCE = 90;
public ActivityRecognitionIntentService() {
super(ActivityRecognitionIntentService.class.getSimpleName());
}
@Override
protected void onHandleIntent(Intent intent) {
if (!ActivityRecognitionResult.hasResult(intent)) {
return;
}
ActivityRecognitionResult result = ActivityRecognitionResult.extractResult(intent);
DetectedActivity detectedActivity = result.getMostProbableActivity();
if (detectedActivity == null) {
return;
}
if (detectedActivity.getConfidence() < REQUIRED_CONFIDENCE) {
return;
}
long recordingTrackId = PreferencesUtils.getLong(this, R.string.recording_track_id_key);
if (recordingTrackId == PreferencesUtils.RECORDING_TRACK_ID_DEFAULT) {
return;
}
boolean recordingTrackPaused = PreferencesUtils.getBoolean(
this, R.string.recording_track_paused_key, PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT);
if (recordingTrackPaused) {
return;
}
int currentType = PreferencesUtils.getInt(this, R.string.activity_recognition_type_key,
PreferencesUtils.ACTIVITY_RECOGNITION_TYPE_DEFAULT);
switch (detectedActivity.getType()) {
case DetectedActivity.IN_VEHICLE:
if (currentType != DetectedActivity.IN_VEHICLE) {
PreferencesUtils.setInt(
this, R.string.activity_recognition_type_key, DetectedActivity.IN_VEHICLE);
}
break;
case DetectedActivity.ON_BICYCLE:
if (currentType != DetectedActivity.IN_VEHICLE
&& currentType != DetectedActivity.ON_BICYCLE) {
PreferencesUtils.setInt(
this, R.string.activity_recognition_type_key, DetectedActivity.ON_BICYCLE);
}
break;
case DetectedActivity.ON_FOOT:
if (currentType != DetectedActivity.IN_VEHICLE && currentType != DetectedActivity.ON_BICYCLE
&& currentType != DetectedActivity.ON_FOOT) {
PreferencesUtils.setInt(
this, R.string.activity_recognition_type_key, DetectedActivity.ON_FOOT);
}
break;
default:
break;
}
}
}
@@ -0,0 +1,73 @@
/*
* 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 {@link 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,
* and is half of the idle time.
*
* @author Sandor Dornbush
*/
public class AdaptiveLocationListenerPolicy implements LocationListenerPolicy {
private final long minInterval;
private final long maxInterval;
private final int minDistance;
// The time the user has been idle at the current location, in milliseconds.
private long idleTime;
/**
* Creates a policy that will be bounded by the given minInterval and
* maxInterval.
*
* @param minInterval the smallest interval this policy will dictate, in
* milliseconds
* @param maxInterval the largest interval this policy will dictate, in
* milliseconds
* @param minDistance the minimum distance in meters
*/
public AdaptiveLocationListenerPolicy(long minInterval, long maxInterval, int minDistance) {
this.minInterval = minInterval;
this.maxInterval = maxInterval;
this.minDistance = minDistance;
}
/*
* Returns an interval half of the idle time, but bounded by minInteval and
* maxInterval.
*/
@Override
public long getDesiredPollingInterval() {
long desiredInterval = idleTime / 2;
// Round to second to avoid setting the interval too often
desiredInterval = (desiredInterval / 1000) * 1000;
return Math.max(Math.min(maxInterval, desiredInterval), minInterval);
}
@Override
public int getMinDistance() {
return minDistance;
}
@Override
public void updateIdleTime(long newIdleTime) {
idleTime = newIdleTime;
}
}
@@ -0,0 +1,138 @@
/*
* 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 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 static final String TAG = ControlRecordingService.class.getSimpleName();
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 service the trackRecordingService
*/
@VisibleForTesting
void onHandleIntent(Intent intent, ITrackRecordingService service) {
String action = intent.getAction();
if (action != null) {
try {
if (action.equals(getString(R.string.track_action_start))) {
service.startNewTrack();
} else if (action.equals(getString(R.string.track_action_end))) {
service.endCurrentTrack();
} else if (action.equals(getString(R.string.track_action_pause))) {
service.pauseCurrentTrack();
} else if (action.equals(getString(R.string.track_action_resume))) {
service.resumeCurrentTrack();
}
} 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,47 @@
/*
* 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: {@link AbsoluteLocationListenerPolicy}
* and {@link AdaptiveLocationListenerPolicy}.
*
* @author Sandor Dornbush
*/
public interface LocationListenerPolicy {
/**
* Returns the polling interval this policy would like at this moment.
*
* @return the polling interval
*/
public long getDesiredPollingInterval();
/**
* Returns the minimum distance between updates.
*/
public int getMinDistance();
/**
* Notifies the amount of time the user has been idle at his current location.
*
* @param idleTime the time that the user has been idle at his current
* location
*/
public void updateIdleTime(long idleTime);
}
@@ -0,0 +1,257 @@
/*
* 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.util.GoogleLocationUtils;
import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.GooglePlayServicesClient.ConnectionCallbacks;
import com.google.android.gms.common.GooglePlayServicesClient.OnConnectionFailedListener;
import com.google.android.gms.location.LocationClient;
import com.google.android.gms.location.LocationListener;
import com.google.android.gms.location.LocationRequest;
import com.google.common.annotations.VisibleForTesting;
import android.content.ContentResolver;
import android.content.Context;
import android.database.ContentObserver;
import android.database.Cursor;
import android.location.LocationManager;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
/**
* My Tracks Location Manager. Applies Google location settings before allowing
* access to {@link LocationManager}.
*
* @author Jimmy Shih
*/
public class MyTracksLocationManager {
/**
* Observer for Google location settings.
*
* @author Jimmy Shih
*/
private class GoogleSettingsObserver extends ContentObserver {
public GoogleSettingsObserver(Handler handler) {
super(handler);
}
@Override
public void onChange(boolean selfChange) {
isAllowed = isUseLocationForServicesOn();
}
}
private static final String TAG = MyTracksLocationManager.class.getSimpleName();
private static final String GOOGLE_SETTINGS_CONTENT_URI = "content://com.google.settings/partner";
private static final String USE_LOCATION_FOR_SERVICES = "use_location_for_services";
// User has agreed to use location for Google services.
@VisibleForTesting
static final String USE_LOCATION_FOR_SERVICES_ON = "1";
private static final String NAME = "name";
private static final String VALUE = "value";
private final ConnectionCallbacks connectionCallbacks = new ConnectionCallbacks() {
@Override
public void onDisconnected() {}
@Override
public void onConnected(Bundle bunlde) {
handler.post(new Runnable() {
@Override
public void run() {
if (requestLastLocation != null && locationClient.isConnected()) {
requestLastLocation.onLocationChanged(locationClient.getLastLocation());
requestLastLocation = null;
}
if (requestLocationUpdates != null && locationClient.isConnected()) {
LocationRequest locationRequest = new LocationRequest().setPriority(
LocationRequest.PRIORITY_HIGH_ACCURACY).setInterval(requestLocationUpdatesTime)
.setFastestInterval(requestLocationUpdatesTime)
.setSmallestDisplacement(requestLocationUpdatesDistance);
locationClient.requestLocationUpdates(
locationRequest, requestLocationUpdates, handler.getLooper());
}
}
});
}
};
private final OnConnectionFailedListener
onConnectionFailedListener = new OnConnectionFailedListener() {
@Override
public void onConnectionFailed(ConnectionResult connectionResult) {}
};
private final Handler handler;
private final LocationClient locationClient;
private final LocationManager locationManager;
private final ContentResolver contentResolver;
private final GoogleSettingsObserver observer;
private boolean isAvailable;
private boolean isAllowed;
private LocationListener requestLastLocation;
private LocationListener requestLocationUpdates;
private float requestLocationUpdatesDistance;
private long requestLocationUpdatesTime;
public MyTracksLocationManager(Context context, Looper looper, boolean enableLocaitonClient) {
this.handler = new Handler(looper);
if (enableLocaitonClient) {
locationClient = new LocationClient(context, connectionCallbacks, onConnectionFailedListener);
locationClient.connect();
} else {
locationClient = null;
}
locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
contentResolver = context.getContentResolver();
observer = new GoogleSettingsObserver(handler);
isAvailable = GoogleLocationUtils.isAvailable(context);
isAllowed = isUseLocationForServicesOn();
contentResolver.registerContentObserver(
Uri.parse(GOOGLE_SETTINGS_CONTENT_URI + "/" + USE_LOCATION_FOR_SERVICES), false, observer);
}
/**
* Closes the {@link MyTracksLocationManager}.
*/
public void close() {
if (locationClient != null) {
locationClient.disconnect();
}
contentResolver.unregisterContentObserver(observer);
}
/**
* Returns true if allowed to access the location manager. Returns true if
* there is no Google location settings or the Google location settings allows
* access to location data.
*/
public boolean isAllowed() {
return isAllowed;
}
/**
* Returns true if gps provider is enabled.
*/
public boolean isGpsProviderEnabled() {
if (!isAllowed) {
return false;
}
String provider = LocationManager.GPS_PROVIDER;
if (locationManager.getProvider(provider) == null) {
return false;
}
return locationManager.isProviderEnabled(provider);
}
/**
* Request last location.
*
* @param locationListener location listener
*/
public void requestLastLocation(final LocationListener locationListener) {
handler.post(new Runnable() {
@Override
public void run() {
if (!isAllowed) {
requestLastLocation = null;
locationListener.onLocationChanged(null);
} else {
requestLastLocation = locationListener;
connectionCallbacks.onConnected(null);
}
}
});
}
/**
* Requests location updates. This is an ongoing request, thus the caller
* needs to check the status of {@link #isAllowed}.
*
* @param minTime the minimal time
* @param minDistance the minimal distance
* @param locationListener the location listener
*/
public void requestLocationUpdates(
final long minTime, final float minDistance, final LocationListener locationListener) {
handler.post(new Runnable() {
@Override
public void run() {
requestLocationUpdatesTime = minTime;
requestLocationUpdatesDistance = minDistance;
requestLocationUpdates = locationListener;
connectionCallbacks.onConnected(null);
}
});
}
/**
* Removes location updates.
*
* @param locationListener the location listener
*/
public void removeLocationUpdates(final LocationListener locationListener) {
handler.post(new Runnable() {
@Override
public void run() {
requestLocationUpdates = null;
if (locationClient != null && locationClient.isConnected()) {
locationClient.removeLocationUpdates(locationListener);
}
}
});
}
/**
* Returns true if the Google location settings for
* {@link #USE_LOCATION_FOR_SERVICES} is on.
*/
private boolean isUseLocationForServicesOn() {
if (!isAvailable) {
return true;
}
Cursor cursor = null;
try {
cursor = contentResolver.query(Uri.parse(GOOGLE_SETTINGS_CONTENT_URI), new String[] { VALUE },
NAME + "=?", new String[] { USE_LOCATION_FOR_SERVICES }, null);
if (cursor != null && cursor.moveToNext()) {
return USE_LOCATION_FOR_SERVICES_ON.equals(cursor.getString(0));
}
} catch (RuntimeException e) {
Log.w(TAG, "Failed to read " + USE_LOCATION_FOR_SERVICES, e);
} finally {
if (cursor != null) {
cursor.close();
}
}
return false;
}
}
@@ -0,0 +1,167 @@
/*
* 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 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 track recording service. This handles service
* start/bind/unbind/stop. The service must be started before it can be bound.
* Returns the service if it is started and bound.
*
* @author Rodrigo Damazio
*/
public class TrackRecordingServiceConnection {
private static final String TAG = TrackRecordingServiceConnection.class.getSimpleName();
private final DeathRecipient deathRecipient = new DeathRecipient() {
@Override
public void binderDied() {
Log.d(TAG, "Service died.");
setTrackRecordingService(null);
}
};
private final ServiceConnection serviceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName className, IBinder service) {
Log.i(TAG, "Connected to the service.");
try {
service.linkToDeath(deathRecipient, 0);
} catch (RemoteException e) {
Log.e(TAG, "Failed to bind a death recipient.", e);
}
setTrackRecordingService(ITrackRecordingService.Stub.asInterface(service));
}
@Override
public void onServiceDisconnected(ComponentName className) {
Log.i(TAG, "Disconnected from the service.");
setTrackRecordingService(null);
}
};
private final Context context;
private final Runnable callback;
private ITrackRecordingService trackRecordingService;
/**
* Constructor.
*
* @param context the context
* @param callback the callback to invoke when the service binding changes
*/
public TrackRecordingServiceConnection(Context context, Runnable callback) {
this.context = context;
this.callback = callback;
}
/**
* Starts and binds the service.
*/
public void startAndBind() {
bindService(true);
}
/**
* Binds the service if it is started.
*/
public void bindIfStarted() {
bindService(false);
}
/**
* Unbinds and stops the service.
*/
public void unbindAndStop() {
unbind();
context.stopService(new Intent(context, TrackRecordingService.class));
}
/**
* Unbinds the service (but leave it running).
*/
public void unbind() {
try {
context.unbindService(serviceConnection);
} catch (IllegalArgumentException e) {
// Means not bound to the service. OK to ignore.
}
setTrackRecordingService(null);
}
/**
* Gets the track recording service if bound. Returns null otherwise
*/
public ITrackRecordingService getServiceIfBound() {
if (trackRecordingService != null && !trackRecordingService.asBinder().isBinderAlive()) {
setTrackRecordingService(null);
return null;
}
return trackRecordingService;
}
/**
* Binds the service if it is started.
*
* @param startIfNeeded start the service if needed
*/
private void bindService(boolean startIfNeeded) {
if (trackRecordingService != null) {
// Service is already started and bound.
return;
}
if (!startIfNeeded
&& !TrackRecordingServiceConnectionUtils.isRecordingServiceRunning(context)) {
Log.d(TAG, "Service is not started. Not binding it.");
return;
}
if (startIfNeeded) {
Log.i(TAG, "Starting the service.");
context.startService(new Intent(context, TrackRecordingService.class));
}
Log.i(TAG, "Binding the service.");
int flags = BuildConfig.DEBUG ? Context.BIND_DEBUG_UNBIND : 0;
context.bindService(new Intent(context, TrackRecordingService.class), serviceConnection, flags);
}
/**
* Sets the trackRecordingService.
*
* @param value the value
*/
private void setTrackRecordingService(ITrackRecordingService value) {
trackRecordingService = value;
if (callback != null) {
callback.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,137 @@
/*
* 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();
private TimerTask timerTask;
private Timer 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();
timerTask = 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:
// CONNECTED or SENDING
break;
}
}
};
timer = new Timer(SensorManager.class.getSimpleName());
timer.schedule(timerTask, RETRY_PERIOD, RETRY_PERIOD);
}
/**
* Stops the sensor.
*/
public void stopSensor() {
if (timerTask != null) {
timerTask.cancel();
timerTask = null;
}
if (timer != null) {
timer.cancel();
timer.purge();
timer = null;
}
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,117 @@
/*
* 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 sensorType = PreferencesUtils.getString(
context, R.string.sensor_type_key, PreferencesUtils.SENSOR_TYPE_DEFAULT);
if (sensorType.equals(context.getString(R.string.sensor_type_value_ant))) {
if (sendPageViews) {
AnalyticsUtils.sendPageViews(context, AnalyticsUtils.SENSOR_ANT);
}
return new AntSensorManager(context);
} else if (sensorType.equals(context.getString(R.string.sensor_type_value_zephyr))) {
if (sendPageViews) {
AnalyticsUtils.sendPageViews(context, AnalyticsUtils.SENSOR_ZEPHYR);
}
return new ZephyrSensorManager(context);
} else if (sensorType.equals(context.getString(R.string.sensor_type_value_polar))) {
if (sendPageViews) {
AnalyticsUtils.sendPageViews(context, AnalyticsUtils.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.value_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 Rottsches
*/
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,586 @@
/*
* 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;
private AntInterface.ServiceListener serviceListener = new AntInterface.ServiceListener() {
@Override
public void onServiceConnected() {
setSensorState(Sensor.SensorState.CONNECTING);
serviceConnected = true;
try {
hasClaimedInterface = antInterface.hasClaimedInterface();
if (!hasClaimedInterface) {
hasClaimedInterface = antInterface.claimInterface();
}
if (!hasClaimedInterface) {
tryClaimAnt();
return;
}
if (!antInterface.isEnabled()) {
antInterface.enable();
}
requestReset();
enableDataMessage(true);
} catch (AntInterfaceException e) {
handleAntError();
}
}
@Override
public void onServiceDisconnected() {
serviceConnected = false;
if (hasClaimedInterface) {
enableDataMessage(false);
}
setSensorState(SensorState.DISCONNECTED);
}
};
/**
* 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);
}
}
}
@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;
}
setSensorState(SensorState.DISCONNECTED);
}
@Override
public SensorDataSet getSensorDataSet() {
return sensorDataSet;
}
/**
* Tries to claim the ant interface.
*/
private void tryClaimAnt() {
try {
antInterface.requestForceClaimInterface(context.getString(R.string.my_tracks_app_name));
} catch (AntInterfaceException e) {
handleAntError();
}
}
/**
* 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);
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.
*/
private 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);
setSensorState(SensorState.DISCONNECTED);
}
}
} 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);
}
} 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,333 @@
/*
* 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;
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.media.AudioManager;
import android.speech.tts.TextToSpeech;
import android.speech.tts.TextToSpeech.OnInitListener;
import android.speech.tts.TextToSpeech.OnUtteranceCompletedListener;
import android.telephony.PhoneStateListener;
import android.telephony.TelephonyManager;
import android.util.Log;
import java.util.HashMap;
import java.util.Locale;
/**
* This class will periodically announce the user's trip statistics.
*
* @author Sandor Dornbush
*/
public class AnnouncementPeriodicTask implements PeriodicTask {
/**
* The rate at which announcements are spoken.
*/
@VisibleForTesting
static final float TTS_SPEECH_RATE = 0.9f;
private static final String TAG = AnnouncementPeriodicTask.class.getSimpleName();
private static final long HOUR_TO_MILLISECOND = 60 * 60 * 1000;
@VisibleForTesting
static final HashMap<String, String> SPEECH_PARAMS = new HashMap<String, String>();
static {
SPEECH_PARAMS.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, "not_used");
}
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, "Failed to relinquish audio focus.");
}
}
};
private final Context context;
private TextToSpeech tts;
// Response from TTS after its initialization
private int initStatus = TextToSpeech.ERROR;
// True if TTS engine is ready
private boolean ready = false;
// True if speech is allowed
private boolean speechAllowed;
private final AudioManager audioManager;
/**
* Listener which updates {@link #speechAllowed} when the phone state changes.
*/
private final PhoneStateListener phoneStateListener = 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 AnnouncementPeriodicTask(Context context) {
this.context = context;
audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
}
@Override
public void start() {
if (tts == null) {
tts = newTextToSpeech(context, new OnInitListener() {
@Override
public void onInit(int status) {
initStatus = status;
}
});
}
speechAllowed = true;
listenToPhoneState(phoneStateListener, PhoneStateListener.LISTEN_CALL_STATE);
}
@Override
public void run(TrackRecordingService trackRecordingService) {
if (trackRecordingService == null) {
Log.e(TAG, "TrackRecordingService is null.");
return;
}
announce(trackRecordingService.getTripStatistics());
}
/**
* Runs this task.
*
* @param tripStatistics the trip statistics
*/
@VisibleForTesting
void announce(TripStatistics tripStatistics) {
if (tripStatistics == null) {
Log.e(TAG, "TripStatistics is null.");
return;
}
synchronized (this) {
if (!ready) {
ready = initStatus == TextToSpeech.SUCCESS;
if (ready) {
onTtsReady();
}
}
if (!ready) {
Log.i(TAG, "TTS not ready.");
return;
}
}
if (!speechAllowed) {
Log.i(TAG, "Speech is not allowed at this time.");
return;
}
speakAnnouncement(getAnnouncement(tripStatistics));
}
@Override
public void shutdown() {
listenToPhoneState(phoneStateListener, PhoneStateListener.LISTEN_NONE);
if (tts != null) {
tts.shutdown();
tts = null;
}
}
/**
* Called when TTS is ready.
*/
private void onTtsReady() {
Locale locale = Locale.getDefault();
int languageAvailability = tts.isLanguageAvailable(locale);
if (languageAvailability == TextToSpeech.LANG_MISSING_DATA
|| languageAvailability == TextToSpeech.LANG_NOT_SUPPORTED) {
Log.w(TAG, "Default locale not available, use English.");
locale = Locale.ENGLISH;
/*
* TODO: instead of using english, load the language if missing and show a
* toast if not supported. Not able to change the resource strings to
* English.
*/
}
tts.setLanguage(locale);
// Slow down the speed just a bit as it is hard to hear when exercising.
tts.setSpeechRate(TTS_SPEECH_RATE);
tts.setOnUtteranceCompletedListener(utteranceListener);
}
/**
* Speaks the announcement.
*
* @param announcement the announcement
*/
private 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, "Failed to request audio focus.");
}
/*
* 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);
}
/**
* Create a new {@link TextToSpeech}.
*
* @param aContext a context
* @param onInitListener an on init listener
*/
@VisibleForTesting
protected TextToSpeech newTextToSpeech(Context aContext, OnInitListener onInitListener) {
return new TextToSpeech(aContext, onInitListener);
}
/**
* Gets the announcement.
*
* @param tripStatistics the trip statistics
*/
@VisibleForTesting
protected String getAnnouncement(TripStatistics tripStatistics) {
boolean metricUnits = PreferencesUtils.isMetricUnits(context);
boolean reportSpeed = PreferencesUtils.isReportSpeed(context);
double distance = tripStatistics.getTotalDistance() * UnitConversions.M_TO_KM;
double speed = tripStatistics.getAverageMovingSpeed() * UnitConversions.MS_TO_KMH;
if (distance == 0) {
return context.getString(R.string.voice_total_distance_zero);
}
if (!metricUnits) {
distance *= UnitConversions.KM_TO_MI;
speed *= UnitConversions.KM_TO_MI;
}
String rate;
if (reportSpeed) {
int speedId = metricUnits ? R.plurals.voiceSpeedKilometersPerHour
: R.plurals.voiceSpeedMilesPerHour;
rate = context.getResources().getQuantityString(speedId, getQuantityCount(speed), speed);
} else {
speed = speed == 0 ? 0.0 : 1 / speed;
int paceId = metricUnits ? R.string.voice_pace_per_kilometer : R.string.voice_pace_per_mile;
rate = context.getString(paceId, getAnnounceTime((long) (speed * HOUR_TO_MILLISECOND)));
}
int totalDistanceId = metricUnits ? R.plurals.voiceTotalDistanceKilometers
: R.plurals.voiceTotalDistanceMiles;
String totalDistance = context.getResources()
.getQuantityString(totalDistanceId, getQuantityCount(distance), distance);
return context.getString(R.string.voice_template, totalDistance,
getAnnounceTime(tripStatistics.getMovingTime()), rate);
}
/**
* Listens to phone state.
*
* @param listener the listener
* @param events the interested events
*/
@VisibleForTesting
protected void listenToPhoneState(PhoneStateListener listener, int events) {
TelephonyManager telephony = (TelephonyManager) context.getSystemService(
Context.TELEPHONY_SERVICE);
if (telephony != null) {
telephony.listen(listener, events);
}
}
/**
* Gets the announce 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);
sb.append(" ");
sb.append(seconds);
} else {
sb.append(minutes);
sb.append(" ");
sb.append(seconds);
}
return sb.toString();
}
/**
* 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;
}
}
}
@@ -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.tasks;
import android.content.Context;
/**
* A {@link PeriodicTaskFactory} for text-to-speech announcement periodic task.
*
* @author Rodrigo Damazio
*/
public class AnnouncementPeriodicTaskFactory implements PeriodicTaskFactory {
@Override
public PeriodicTask create(Context context) {
return new AnnouncementPeriodicTask(context);
}
}
@@ -0,0 +1,189 @@
/*
* Copyright 2013 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.PhotoUtils;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Matrix;
import android.media.ExifInterface;
import android.net.Uri;
import android.os.AsyncTask;
import android.util.Log;
import android.widget.ImageView;
import java.io.IOException;
import java.lang.ref.WeakReference;
/**
* A bitmap loader.
*
* @author Jimmy Shih
*/
public class BitmapLoader extends AsyncTask<Void, Void, Bitmap> {
private static final String TAG = BitmapLoader.class.getSimpleName();
private final WeakReference<ImageView> imageViewReference;
private final Uri uri;
private final int targetWidth;
private final int targetHeight;
private final boolean fitWithin;
public BitmapLoader(
ImageView imageView, Uri uri, int targetWidth, int targetHeight, boolean fitWithin) {
// Use a WeakReference to ensure the ImageView can be garbage collected
imageViewReference = new WeakReference<ImageView>(imageView);
this.uri = uri;
this.targetWidth = targetWidth;
this.targetHeight = targetHeight;
this.fitWithin = fitWithin;
}
public Uri getUri() {
return uri;
}
@Override
protected Bitmap doInBackground(Void... params) {
// Get the image dimensions
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(uri.getPath(), options);
if (options.outWidth == 0 || options.outHeight == 0) {
return null;
}
// Set imageWidth and imageHeight based on image rotation
int rotation = getRotation();
int imageWidth;
int imageHeight;
if (rotation == 0 || rotation == 180) {
imageWidth = options.outWidth;
imageHeight = options.outHeight;
} else {
imageWidth = options.outHeight;
imageHeight = options.outWidth;
}
// Get a scaled down version of the image
options.inJustDecodeBounds = false;
options.inSampleSize = getInSampleSize(imageWidth, imageHeight);
options.inPurgeable = true;
Bitmap scaledBitmap = BitmapFactory.decodeFile(uri.getPath(), options);
if (scaledBitmap == null) {
return null;
}
// Get the final bitmap after rotating the scaled down image
Bitmap bitmap;
if (rotation == 0 && fitWithin) {
bitmap = scaledBitmap;
} else {
Matrix matrix = new Matrix();
matrix.postRotate(rotation);
int offset = 0;
int height = scaledBitmap.getHeight();
if (!fitWithin && height > targetHeight) {
offset = (height - targetHeight) / 2;
height = targetHeight;
}
bitmap = Bitmap.createBitmap(
scaledBitmap, 0, offset, scaledBitmap.getWidth(), height, matrix, true);
scaledBitmap.recycle();
}
return bitmap;
}
@Override
protected void onPostExecute(Bitmap bitmap) {
if (isCancelled()) {
bitmap = null;
}
// If imageView is still around, set bitmap
if (imageViewReference != null && bitmap != null) {
ImageView imageView = imageViewReference.get();
if (imageView != null) {
BitmapLoader bitmapLoader = PhotoUtils.getBitmapLoader(imageView);
if (this == bitmapLoader) {
imageView.setImageBitmap(bitmap);
}
}
}
}
private int getRotation() {
try {
ExifInterface exifInterface = new ExifInterface(uri.getPath());
switch (exifInterface.getAttributeInt(
ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL)) {
case ExifInterface.ORIENTATION_ROTATE_90:
return 90;
case ExifInterface.ORIENTATION_ROTATE_180:
return 180;
case ExifInterface.ORIENTATION_ROTATE_270:
return 270;
default:
return 0;
}
} catch (IOException e) {
Log.e(TAG, "Unable to get photo orientation", e);
return 0;
}
}
/**
* Gets the in sample size.
*
* @param imageWidth the image width
* @param imageHeight the image height
*/
private int getInSampleSize(int imageWidth, int imageHeight) {
float widthRatio = 1;
if (imageWidth > targetWidth) {
widthRatio = (float) imageWidth / (float) targetWidth;
}
float heightRatio = 1;
if (imageHeight > targetHeight) {
heightRatio = (float) imageHeight / (float) targetHeight;
}
double size;
if (fitWithin) {
/*
* To fit within the target area, return the larger sample ratio so the
* image will not be larger than the target dimensions.
*/
size = Math.max(widthRatio, heightRatio);
} else {
/*
* To fill the target area, return the smaller ratio so the image will
* cover both dimensions.
*/
size = Math.min(widthRatio, heightRatio);
}
// Use Math.floor to not under-sample.
return (int) Math.floor(size);
}
}
@@ -0,0 +1,43 @@
/*
* 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.
*
* @param trackRecordingService the track recording service
*/
public void run(TrackRecordingService trackRecordingService);
/**
* Shuts down this task and clean up resources.
*/
public void shutdown();
}
@@ -0,0 +1,202 @@
/*
* 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.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.UnitConversions;
import android.util.Log;
/**
* Execute a periodic task on a time or distance schedule.
*
* @author Sandor Dornbush
*/
public class PeriodicTaskExecutor {
private static final String TAG = PeriodicTaskExecutor.class.getSimpleName();
private static final long MINUTE_TO_MILLISECONDS = 60000L;
private final TrackRecordingService trackRecordingService;
private final PeriodicTaskFactory periodicTaskFactory;
/**
* The task frequency. A positive value is a time frequency (minutes). A
* negative value is a distance frequency (km or mi). A zero value is to turn
* off periodic task.
*/
private int taskFrequency = PreferencesUtils.FREQUENCY_OFF;
private PeriodicTask periodicTask;
// Time periodic task executor
private TimerTaskExecutor timerTaskExecutor = null;
private boolean metricUnits;
// The next distance for the distance periodic task
private double nextTaskDistance = Double.MAX_VALUE;
public PeriodicTaskExecutor(
TrackRecordingService trackRecordingService, PeriodicTaskFactory periodicTaskFactory) {
this.trackRecordingService = trackRecordingService;
this.periodicTaskFactory = periodicTaskFactory;
}
/**
* Restores the executor.
*/
public void restore() {
if (!trackRecordingService.isRecording() || trackRecordingService.isPaused()) {
Log.d(TAG, "Not recording or paused.");
return;
}
if (!isTimeFrequency()) {
if (timerTaskExecutor != null) {
timerTaskExecutor.shutdown();
timerTaskExecutor = null;
}
}
if (taskFrequency == PreferencesUtils.FREQUENCY_OFF) {
Log.d(TAG, "Task frequency is off.");
return;
}
periodicTask = periodicTaskFactory.create(trackRecordingService);
// Returning null is ok
if (periodicTask == null) {
Log.d(TAG, "Peridoic task is null.");
return;
}
periodicTask.start();
if (isTimeFrequency()) {
if (timerTaskExecutor == null) {
timerTaskExecutor = new TimerTaskExecutor(periodicTask, trackRecordingService);
}
timerTaskExecutor.scheduleTask(taskFrequency * MINUTE_TO_MILLISECONDS);
} else {
// For distance periodic task
calculateNextTaskDistance();
}
}
/**
* Shuts down the executor.
*/
public void shutdown() {
if (periodicTask != null) {
periodicTask.shutdown();
periodicTask = null;
}
if (timerTaskExecutor != null) {
timerTaskExecutor.shutdown();
timerTaskExecutor = null;
}
}
/**
* Updates the executor.
*/
public void update() {
if (!isDistanceFrequency() || periodicTask == null) {
return;
}
TripStatistics tripStatistics = trackRecordingService.getTripStatistics();
if (tripStatistics == null) {
return;
}
double distance = tripStatistics.getTotalDistance()
* UnitConversions.M_TO_KM;
if (!metricUnits) {
distance *= UnitConversions.KM_TO_MI;
}
if (distance > nextTaskDistance) {
periodicTask.run(trackRecordingService);
calculateNextTaskDistance();
}
}
/**
* Sets task frequency.
*
* @param taskFrequency the task frequency
*/
public void setTaskFrequency(int taskFrequency) {
this.taskFrequency = taskFrequency;
restore();
}
/**
* Sets metricUnits.
*
* @param metricUnits true to use metric units
*/
public void setMetricUnits(boolean metricUnits) {
this.metricUnits = metricUnits;
calculateNextTaskDistance();
}
/**
* Calculates the next distance for the distance periodic task.
*/
private void calculateNextTaskDistance() {
if (!trackRecordingService.isRecording() || trackRecordingService.isPaused()
|| periodicTask == null) {
return;
}
TripStatistics tripStatistics = trackRecordingService.getTripStatistics();
if (tripStatistics == null) {
return;
}
if (!isDistanceFrequency()) {
nextTaskDistance = Double.MAX_VALUE;
Log.d(TAG, "SplitManager: Distance splits disabled.");
return;
}
double distance = tripStatistics.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;
}
/**
* True if time frequency.
*/
private boolean isTimeFrequency() {
return taskFrequency > 0;
}
/**
* True if distance frequency.
*/
private boolean isDistanceFrequency() {
return taskFrequency < 0;
}
}
@@ -0,0 +1,34 @@
/*
* 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 {@link PeriodicTask}.
*
* @author Sandor Dornbush
*/
public interface PeriodicTaskFactory {
/**
* Creates a {@link PeriodicTask}.
*
* @return the task, or null if the task is not supported
*/
public PeriodicTask create(Context context);
}
@@ -0,0 +1,39 @@
/*
* 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;
/**
* A simple task to insert statistics markers periodically.
*
* @author Sandor Dornbush
*/
public class SplitPeriodicTask implements PeriodicTask {
@Override
public void start() {}
@Override
public void run(TrackRecordingService trackRecordingService) {
trackRecordingService.insertWaypoint(WaypointCreationRequest.DEFAULT_STATISTICS);
}
@Override
public void shutdown() {}
}
@@ -0,0 +1,32 @@
/*
* 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.tasks;
import android.content.Context;
/**
* A {@link PeriodicTaskFactory} for {@link SplitPeriodicTask}.
*
* @author Jimmy Shih
*/
public class SplitPeriodicTaskFactory implements PeriodicTaskFactory {
@Override
public PeriodicTask create(Context context) {
return new SplitPeriodicTask();
}
}
@@ -0,0 +1,91 @@
/*
* 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;
import com.google.android.apps.mytracks.stats.TripStatistics;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;
/**
* This class will periodically perform a task.
*
* @author Sandor Dornbush
*/
public class TimerTaskExecutor {
private final PeriodicTask periodicTask;
private final TrackRecordingService trackRecordingService;
private TimerTask timerTask;
private Timer timer;
public TimerTaskExecutor(PeriodicTask periodicTask, TrackRecordingService trackRecordingService) {
this.periodicTask = periodicTask;
this.trackRecordingService = trackRecordingService;
}
/**
* Schedules the periodic task at an interval.
*
* @param interval the interval in milliseconds
*/
public void scheduleTask(long interval) {
if (interval <= 0) {
return;
}
if (!trackRecordingService.isRecording() || trackRecordingService.isPaused()) {
return;
}
TripStatistics tripStatistics = trackRecordingService.getTripStatistics();
if (tripStatistics == null) {
return;
}
shutdown();
periodicTask.start();
timerTask = new TimerTask() {
@Override
public void run() {
periodicTask.run(trackRecordingService);
}
};
timer = new Timer(TimerTaskExecutor.class.getSimpleName());
long next = System.currentTimeMillis() + interval - (tripStatistics.getTotalTime() % interval);
timer.scheduleAtFixedRate(timerTask, new Date(next), interval);
}
/**
* Shuts down.
*/
public void shutdown() {
if (timerTask != null) {
timerTask.cancel();
timerTask = null;
}
if (timer != null) {
timer.cancel();
timer.purge();
timer = null;
}
periodicTask.shutdown();
}
}