package com.rubenvandeven.heartbeatstreamer.heartrate; import android.app.AlertDialog; import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Service; import android.content.DialogInterface; import android.content.Intent; import android.net.Uri; import android.os.Handler; import android.os.HandlerThread; import android.os.IBinder; import android.os.Looper; import android.support.v4.app.NotificationCompat; import android.util.Log; import android.view.View; import android.widget.Toast; import com.dsi.ant.plugins.antplus.pcc.AntPlusHeartRatePcc; 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; import com.dsi.ant.plugins.antplus.pccbase.AsyncScanController; import com.dsi.ant.plugins.antplus.pccbase.PccReleaseHandle; import com.koushikdutta.async.future.Future; import com.koushikdutta.async.http.AsyncHttpClient; import com.koushikdutta.async.http.WebSocket; import com.rubenvandeven.heartbeatstreamer.MainActivity; import com.rubenvandeven.heartbeatstreamer.R; import org.json.JSONException; import org.json.JSONObject; import java.io.IOException; import java.io.InputStream; import java.math.BigDecimal; import java.text.DateFormat; import java.text.DecimalFormat; import java.text.DecimalFormatSymbols; import java.util.Date; import java.util.EnumSet; public class HeartRateService extends Service { private static final String TAG = HeartRateService.class.getSimpleName(); public static final String BROADCAST_MONITOR_CONNECTED = "com.rubenvandeven.heartbeat.broadcast.monitor_started"; public static final String BROADCAST_MONITOR_DISCONNECTED = "com.rubenvandeven.heartbeat.broadcast.monitor_stopped"; public static final String BROADCAST_MONITOR_UNKNOWN = "com.rubenvandeven.heartbeat.broadcast.monitor_unknown"; public static final String BROADCAST_MONITOR_UPDATE = "com.rubenvandeven.heartbeat.broadcast.monitor_update"; public static final String BROADCAST_MONITOR_CONNECT_ATTEMPT = "com.rubenvandeven.heartbeat.broadcast.connect_attempt"; public static final String BROADCAST_MONITOR_CONNECT_RESULT = "com.rubenvandeven.heartbeat.broadcast.connect_reslt"; public static final String BROADCAST_SOCKET_SENT = "com.rubenvandeven.heartbeat.broadcast.socket_sent"; public static final String BROADCAST_SERVICE_START = "com.rubenvandeven.heartbeat.broadcast.service_start"; public static final String BROADCAST_SERVICE_STOPPED = "com.rubenvandeven.heartbeat.broadcast.service_stopped"; public static final String BROADCAST_BEAT = "com.rubenvandeven.heartbeat.broadcast.beat"; public static final String ACTION_START = "com.rubenvandeven.heartbeat.service_start"; public static final String ACTION_STOP = "com.rubenvandeven.heartbeat.service_stop"; public static final int NOTIFICATION_ID = 101; AsyncScanController hrScanCtrl; AntPlusHeartRatePcc hrPcc; PccReleaseHandle releaseHandle; JSONObject lastMsg = null; public static boolean isRunning = false; NotificationCompat.Builder builder; NotificationManager notificationManager; @Override public void onCreate() { notificationManager = (NotificationManager) getSystemService(Service.NOTIFICATION_SERVICE); setRunning(true); requestAccessToPcc(); } public void setRunning(boolean running) { if(running == isRunning) { return; } isRunning = running; Intent i; if(running) { i = new Intent(BROADCAST_SERVICE_START); } else { i = new Intent(BROADCAST_SERVICE_STOPPED); } sendBroadcast(i); } /** * Start main thread, request location updates, start synchronization. * * @param intent Intent * @param flags Flags * @param startId Unique id * @return Always returns START_STICKY */ @Override public int onStartCommand(Intent intent, int flags, int startId) { if (intent.getAction().equals(ACTION_START)) { Log.i(TAG, "Received start id " + startId + ": " + intent); Intent notificationIntent = new Intent(this, MainActivity.class); notificationIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, 0); builder = new NotificationCompat.Builder(this) .setContentText("Starting") .setContentTitle("Heartbeat Streamer") .setSmallIcon(R.drawable.ic_notification) .setAutoCancel(false) .setOngoing(true) .setOnlyAlertOnce(false) .setContentIntent(pendingIntent) ; startForeground(NOTIFICATION_ID, builder.build()); } else if (intent.getAction().equals(ACTION_STOP)) { Log.i(TAG, "Received stop id " + startId + ": " + intent); stopForeground(true); stopSelf(); } return START_STICKY; } protected AntPluginPcc.IDeviceStateChangeReceiver stateChangeReceiver = new AntPluginPcc.IDeviceStateChangeReceiver() { @Override public void onDeviceStateChange(final DeviceState state) { if(DeviceState.DEAD.equals(state) || DeviceState.CLOSED.equals(state)) { sendBroadcast(new Intent(BROADCAST_MONITOR_DISCONNECTED)); waitAndReconnect(); } else if (DeviceState.TRACKING.equals(state)) { sendBroadcast(new Intent(BROADCAST_MONITOR_CONNECTED)); } else { Intent i = new Intent(BROADCAST_MONITOR_UNKNOWN); i.putExtra("status", state.getIntValue()); sendBroadcast(i); } } }; protected AntPluginPcc.IPluginAccessResultReceiver resultReceiver = new AntPluginPcc.IPluginAccessResultReceiver() { //Handle the result, connecting to events on success or reporting failure to user. @Override public void onResultReceived(AntPlusHeartRatePcc result, RequestAccessResult resultCode, DeviceState initialDeviceState) { // showDataDisplay("Connecting..."); Intent i = new Intent(BROADCAST_MONITOR_CONNECT_RESULT); switch (resultCode) { case SUCCESS: i.putExtra("msg", "Success"); connectHr(result); break; case CHANNEL_NOT_AVAILABLE: showNotification("Channel Not Available"); i.putExtra("msg", "Channel Not Available"); break; case OTHER_FAILURE: showNotification("RequestAccess failed. See logcat for details."); i.putExtra("msg", "RequestAccess failed. See logcat for details."); break; case USER_CANCELLED: showNotification("Cancelled. Do reset."); i.putExtra("msg", "Cancelled. Do reset."); break; case UNRECOGNIZED: default: showNotification("Unknown error. Do reset."); i.putExtra("msg", "unknown error. Do reset."); break; } sendBroadcast(i); } }; private void connectHr(AntPlusHeartRatePcc result) { hrPcc = result; // keep track of current connection hrPcc.subscribeHeartRateDataEvent(new AntPlusHeartRatePcc.IHeartRateDataReceiver() { @Override public void onNewHeartRateData(final long estTimestamp, EnumSet eventFlags, final int computedHeartRate, final long heartBeatCount, final BigDecimal heartBeatEventTime, final AntPlusHeartRatePcc.DataState dataState) { DecimalFormatSymbols otherSymbols = new DecimalFormatSymbols(); otherSymbols.setDecimalSeparator('.'); DecimalFormat df = new DecimalFormat("#.##", otherSymbols); // Mark heart rate with asterisk if zero detected final String textHeartRate = String.valueOf(computedHeartRate) + ((AntPlusHeartRatePcc.DataState.ZERO_DETECTED.equals(dataState)) ? "*" : ""); // Mark heart beat count and heart beat event time with asterisk if initial value final String textHeartBeatCount = String.valueOf(heartBeatCount) + ((AntPlusHeartRatePcc.DataState.INITIAL_VALUE.equals(dataState)) ? "*" : ""); final String textHeartBeatEventTime = df.format(heartBeatEventTime) + ((AntPlusHeartRatePcc.DataState.INITIAL_VALUE.equals(dataState)) ? "*" : ""); Intent intent = new Intent(BROADCAST_BEAT); intent.putExtra("rate", textHeartRate); intent.putExtra("count", textHeartBeatCount); intent.putExtra("time", textHeartBeatEventTime); sendBroadcast(intent); builder.setContentText(textHeartRate + " bpm"); notificationManager.notify(NOTIFICATION_ID, builder.build()); JSONObject jObjectData = new JSONObject(); // final String msg = String.format("{\"rate\":\"%s\", \"count\":\"%s\", \"time\":\"%s\"}", textHeartRate, textHeartBeatCount, textHeartBeatEventTime); final String msg; try { jObjectData.put("rate", textHeartRate); jObjectData.put("count", textHeartBeatCount); jObjectData.put("time", textHeartBeatEventTime); jObjectData.put("timestamp", DateFormat.getDateTimeInstance().format(new Date())); jObjectData.put("token", getToken()); msg = jObjectData.toString(); if(lastMsg != null && lastMsg.getString("time").equals(jObjectData.getString("time"))) { Log.i(TAG, "Skip duplicate"); return; } lastMsg = jObjectData; sendResult(jObjectData.toString()); } catch (JSONException e) { e.printStackTrace(); } } }); } String token = null; /** * Get the token for use with the syncing. * @return */ public String getToken() { if(token != null) { return token; } String json = null; try { InputStream is = getAssets().open("token.json"); int size = is.available(); byte[] buffer = new byte[size]; is.read(buffer); is.close(); json = new String(buffer, "UTF-8"); } catch (IOException ex) { ex.printStackTrace(); return null; } try { JSONObject jsonObject = new JSONObject(json); token = jsonObject.getString("token'"); } catch (JSONException e) { e.printStackTrace(); return null; } return token; } private void sendResult(final String m) { Log.d(TAG, "Send: " + m); Future fws = AsyncHttpClient.getDefaultInstance().websocket("ws://heartbeat.rubenvandeven.com:8888/ws", "my-protocol", new AsyncHttpClient.WebSocketConnectCallback() { @Override public void onCompleted(Exception ex, final WebSocket webSocket) { if (ex != null) { ex.printStackTrace(); // TODO: store for later sync return; } webSocket.send(m); Intent intent = new Intent(BROADCAST_SOCKET_SENT); intent.putExtra("msg", m); sendBroadcast(intent); webSocket.close(); } }); } private void showNotification(String msg) { final String m = msg; Handler handler = new Handler(Looper.getMainLooper()); handler.post(new Runnable() { @Override public void run() { Toast.makeText(HeartRateService.this.getApplicationContext(),m,Toast.LENGTH_SHORT).show(); } }); } /** * wait to reconnect to dead HR-monitor */ protected void waitAndReconnect() { Log.d(TAG, "Wait & reconnect"); if(hrPcc != null) { hrPcc.subscribeHeartRateDataEvent(null); } new android.os.Handler().postDelayed( new Runnable() { public void run() { Log.i(TAG, "Attempt reconnect"); requestAccessToPcc(); } }, 20000); // 20 sec delay for connection } /** * Requests the ANT+ device */ protected void requestAccessToPcc() { if(hrPcc != null) { hrPcc.subscribeHeartRateDataEvent(null); } sendBroadcast( new Intent(BROADCAST_MONITOR_CONNECT_ATTEMPT) ); releaseHandle = AntPlusHeartRatePcc.requestAccess(this, 4818, 0, resultReceiver, stateChangeReceiver); } @Override public IBinder onBind(Intent intent) { // TODO: Return the communication channel to the service. throw new UnsupportedOperationException("Not yet implemented"); } @Override public void onDestroy() { if(releaseHandle != null) { releaseHandle.close(); } setRunning(false); super.onDestroy(); } public static boolean isRunning() { return isRunning; } }