/* * Copyright (C) 2014 jonas.oreland@gmail.com * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.runnerup.hr; import android.annotation.TargetApi; import android.app.Activity; import android.content.Context; import android.os.Build; import android.os.Handler; import com.dsi.ant.plugins.antplus.pcc.AntPlusHeartRatePcc; import com.dsi.ant.plugins.antplus.pcc.AntPlusHeartRatePcc.IHeartRateDataReceiver; import com.dsi.ant.plugins.antplus.pcc.defines.DeviceState; import com.dsi.ant.plugins.antplus.pcc.defines.EventFlag; import com.dsi.ant.plugins.antplus.pcc.defines.RequestAccessResult; import com.dsi.ant.plugins.antplus.pccbase.AntPluginPcc.IDeviceStateChangeReceiver; import com.dsi.ant.plugins.antplus.pccbase.AntPluginPcc.IPluginAccessResultReceiver; import com.dsi.ant.plugins.antplus.pccbase.AsyncScanController; import com.dsi.ant.plugins.antplus.pccbase.AsyncScanController.AsyncScanResultDeviceInfo; import com.dsi.ant.plugins.antplus.pccbase.AsyncScanController.IAsyncScanResultReceiver; import com.dsi.ant.plugins.antplus.pccbase.PccReleaseHandle; import java.math.BigDecimal; import java.util.EnumSet; import java.util.HashSet; /** * Provides connectivity to ANT+ modules * * @author jonas */ @TargetApi(Build.VERSION_CODES.FROYO) public class AntPlus extends BtHRBase { static final String NAME = "AntPlus"; static final String DISPLAY_NAME = "ANT+"; final Context context; int hrValue; long hrTimestamp; HRDeviceRef connectRef = null; AntPlusHeartRatePcc hrPcc = null; AsyncScanController<AntPlusHeartRatePcc> hrScanCtrl = null; private boolean mIsScanning = false; private boolean mIsConnected = false; private boolean mIsConnecting = false; private PccReleaseHandle<AntPlusHeartRatePcc> releaseHandle; public static boolean checkLibrary(Context ctx) { try { Class.forName("com.dsi.ant.plugins.antplus.pcc.AntPlusHeartRatePcc"); Class.forName("com.dsi.ant.plugins.antplus.pcc.defines.DeviceState"); Class.forName("com.dsi.ant.plugins.antplus.pcc.defines.RequestAccessResult"); Class.forName("com.dsi.ant.plugins.antplus.pccbase.AsyncScanController"); return true; } catch (Exception e) { } return false; } public AntPlus(Context ctx) { this.context = ctx; } @Override public String getName() { return DISPLAY_NAME; } @Override public String getProviderName() { return NAME; } @Override public void open(Handler handler, HRClient hrClient) { log("open()"); this.hrClientHandler = handler; this.hrClient = hrClient; hrClient.onOpenResult(true); } @Override public void close() { HRClient client = hrClient; hrClient = null; stopScan(); disconnect(); if (client != null) { client.onCloseResult(true); } } @Override public boolean isBondingDevice() { return false; } @Override public boolean isScanning() { return mIsScanning; } @Override public boolean isConnected() { return mIsConnected; } @Override public boolean isConnecting() { return mIsConnecting; } @Override public void startScan() { stopScan(); log("startScan()"); mIsScanning = true; mScanDevices.clear(); hrScanCtrl = AntPlusHeartRatePcc.requestAsyncScanController(context, 0, scanReceiver); } @Override public void stopScan() { if (mIsScanning || hrScanCtrl != null) log("stopScan()"); mIsScanning = false; if (hrScanCtrl != null) { hrScanCtrl.closeScanController(); hrScanCtrl = null; } } final HashSet<String> mScanDevices = new HashSet<String>(); final IAsyncScanResultReceiver scanReceiver = new IAsyncScanResultReceiver() { @Override public void onSearchResult(final AsyncScanResultDeviceInfo arg0) { log("onSearchResult(" + arg0 + ")"); if (hrClient == null) return; if (hrClientHandler == null) return; final HRDeviceRef ref = HRDeviceRef.create(NAME, arg0.getDeviceDisplayName(), Integer.toString(arg0.getAntDeviceNumber())); if ((mIsConnecting || mIsConnected) && ref.deviceAddress.equals(connectRef.deviceAddress) && ref.deviceName.equals(connectRef.deviceName)) { stopScan(); releaseHandle = AntPlusHeartRatePcc.requestAccess(context, arg0.getAntDeviceNumber(), 0, resultReceiver, stateReceiver); return; } if (mScanDevices.contains(ref.deviceAddress)) return; mScanDevices.add(ref.deviceAddress); hrClientHandler.post(new Runnable() { @Override public void run() { if (mIsScanning) { // NOTE: mIsScanning in user-thread hrClient.onScanResult(HRDeviceRef.create(NAME, arg0.getDeviceDisplayName(), Integer.toString(arg0.getAntDeviceNumber()))); } } }); } @Override public void onSearchStopped(RequestAccessResult arg0) { log("onSearchStopped(" + arg0 + ")"); } }; @Override public void connect(HRDeviceRef ref) { stopScan(); disconnectImpl(); log("connect(" + Integer.parseInt(ref.deviceAddress) + ")"); connectRef = ref; mIsConnecting = true; releaseHandle = AntPlusHeartRatePcc.requestAccess(context, Integer.parseInt(ref.deviceAddress), 0, resultReceiver, stateReceiver); } AntPlusHeartRatePcc antDevice = null; final IPluginAccessResultReceiver<AntPlusHeartRatePcc> resultReceiver = new IPluginAccessResultReceiver<AntPlusHeartRatePcc>() { @Override public void onResultReceived(AntPlusHeartRatePcc arg0, RequestAccessResult arg1, DeviceState arg2) { log("onResultReceived(" + arg0 + ", " + arg1 + ", " + arg2 + ")"); antDevice = arg0; switch (arg1) { case ALREADY_SUBSCRIBED: case CHANNEL_NOT_AVAILABLE: case DEPENDENCY_NOT_INSTALLED: case DEVICE_ALREADY_IN_USE: case OTHER_FAILURE: case SEARCH_TIMEOUT: case UNRECOGNIZED: case USER_CANCELLED: reportConnectFailed(arg1); return; case SUCCESS: break; } switch (arg2) { case UNRECOGNIZED: case CLOSED: case DEAD: case PROCESSING_REQUEST: case SEARCHING: reportConnectFailed(arg1); return; case TRACKING: break; default: // ??? break; } antDevice.subscribeHeartRateDataEvent(heartRateDataReceiver); } }; final IHeartRateDataReceiver heartRateDataReceiver = new IHeartRateDataReceiver() { @Override public void onNewHeartRateData(long arg0, EnumSet<EventFlag> arg1, int arg2, long arg3, BigDecimal bigDecimal, AntPlusHeartRatePcc.DataState dataState) { switch (dataState) { case LIVE_DATA: break; case INITIAL_VALUE: break; case ZERO_DETECTED: break; case UNRECOGNIZED: break; } if (arg2 == 0) { log("got hrValue == 0 => aborting"); if (mIsConnecting) reportConnected(false); else if (mIsConnected) reportDisconnected(true); return; } hrValue = arg2; hrTimestamp = System.currentTimeMillis(); if (mIsConnecting) { reportConnected(true); } } }; final IDeviceStateChangeReceiver stateReceiver = new IDeviceStateChangeReceiver() { @Override public void onDeviceStateChange(DeviceState arg0) { log("onDeviceStateChange(" + arg0 + ")"); switch (arg0) { case CLOSED: break; case DEAD: if (mIsConnected) { /** don't silent reconnect, let upper lay handle that */ reportDisconnected(true); return; } if (mIsConnecting) { reportConnectFailed(null); return; } break; case PROCESSING_REQUEST: break; case SEARCHING: break; case TRACKING: break; case UNRECOGNIZED: break; } } }; private void reportConnectFailed(RequestAccessResult arg1) { disconnectImpl(); if (hrClientHandler != null) { hrClientHandler.post(new Runnable() { @Override public void run() { if (hrClient != null) { hrClient.onConnectResult(false); } } }); } } protected void reportConnected(final boolean b) { hrClientHandler.post(new Runnable() { @Override public void run() { if (hrClient == null) { disconnectImpl(); return; } else if (mIsConnecting) { mIsConnected = b; mIsConnecting = false; hrClient.onConnectResult(b); } } }); } protected void reportDisconnected(final boolean b) { disconnectImpl(); hrClientHandler.post(new Runnable() { @Override public void run() { if (hrClient != null) hrClient.onDisconnectResult(b); } }); } @Override public void disconnect() { disconnectImpl(); if (hrClientHandler != null) { hrClientHandler.post(new Runnable() { @Override public void run() { if (hrClient != null) { hrClient.onDisconnectResult(true); } } }); } } private void disconnectImpl() { log("disconnectImpl"); stopScan(); if (antDevice != null) { antDevice.releaseAccess(); antDevice = null; } if (releaseHandle != null) { releaseHandle.close(); releaseHandle = null; } mIsConnecting = false; mIsConnected = false; } @Override public int getHRValue() { return hrValue; } @Override public long getHRValueTimestamp() { return hrTimestamp; } @Override public HRData getHRData() { if (hrValue <= 0) { return null; } return new HRData().setHeartRate(hrValue).setTimestampEstimate(hrTimestamp); } @Override public int getBatteryLevel() { return -1; } /** it seems ANT+ requires Bluetooth too */ @Override public boolean isEnabled() { return Bt20Base.isEnabledImpl(); } @Override public boolean startEnableIntent(Activity activity, int requestCode) { return Bt20Base.startEnableIntentImpl(activity, requestCode); } }