/*
* 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;
import java.util.Locale;
/**
* 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(Locale.US,
"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;
}
}
}