package com.rubenvandeven.heartbeatstreamer.heartrate; import android.app.AlertDialog; import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Service; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.net.Uri; import android.os.Build; import android.os.Handler; import android.os.HandlerThread; import android.os.IBinder; import android.os.Looper; import android.os.VibrationEffect; import android.os.Vibrator; 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.text.SimpleDateFormat; import java.util.Date; import java.util.EnumSet; import java.util.Timer; import java.util.TimerTask; 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; SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); Timer lifelineTimer; DeviceState currentDeviceState; boolean restartService = false; @Override public void onCreate() { notificationManager = (NotificationManager) getSystemService(Service.NOTIFICATION_SERVICE); startLifelineTimer(); 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) { 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()); return START_STICKY; } protected AntPluginPcc.IDeviceStateChangeReceiver stateChangeReceiver = new AntPluginPcc.IDeviceStateChangeReceiver() { @Override public void onDeviceStateChange(final DeviceState state) { currentDeviceState = state; if(DeviceState.DEAD.equals(state) || DeviceState.CLOSED.equals(state)) { sendBroadcast(new Intent(BROADCAST_MONITOR_DISCONNECTED)); updateNotification("Disconnected"); waitAndReconnect(); } else if (DeviceState.TRACKING.equals(state)) { sendBroadcast(new Intent(BROADCAST_MONITOR_CONNECTED)); updateNotification("Connected!"); } else { Intent i = new Intent(BROADCAST_MONITOR_UNKNOWN); i.putExtra("status", state.getIntValue()); updateNotification("Unknown status: "+ String.valueOf(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"); updateNotification("Access"); connectHr(result); break; case CHANNEL_NOT_AVAILABLE: showNotification("Channel Not Available"); i.putExtra("msg", "Channel Not Available"); updateNotification("Channel not available"); break; case OTHER_FAILURE: showNotification("RequestAccess failed. See logcat for details."); i.putExtra("msg", "RequestAccess failed. See logcat for details."); updateNotification("RequestAccess failed"); break; case USER_CANCELLED: showNotification("Cancelled. Do reset."); i.putExtra("msg", "Cancelled. Do reset."); updateNotification("Cancelled, do reset"); break; case UNRECOGNIZED: default: showNotification("Unknown error. Do reset."); i.putExtra("msg", "unknown error. Do reset."); updateNotification("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) { currentDeviceState = DeviceState.TRACKING; 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); updateNotification(textHeartRate + " bpm / " + textHeartBeatCount + " beats / " + textHeartBeatEventTime ); 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.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(); } } }); } public void updateNotification(String msg) { builder.setContentText(msg); notificationManager.notify(NOTIFICATION_ID, builder.build()); } 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 { Log.d(TAG, "JSON: "+ json); 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(); } }); } protected void startLifelineTimer() { lifelineTimer = new Timer(); lifelineTimer.scheduleAtFixedRate(new TimerTask(){ @Override public void run(){ Log.d(TAG, "A Kiss every 10 seconds"); if(DeviceState.TRACKING.equals(currentDeviceState)) { Log.i(TAG, "Alive and kicking!"); return; } Log.w(TAG, "Device status different... "); String val; if(currentDeviceState == null) { val = "null"; } else { val = String.valueOf(currentDeviceState.getIntValue()); } Log.w(TAG, "Device status: '"+ val +"'"); // something is off: warn & attempt restart Vibrator v = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { v.vibrate(VibrationEffect.createOneShot(1000, VibrationEffect.DEFAULT_AMPLITUDE)); } else { //deprecated in API 26 v.vibrate(1000); } restart(); } },20000,10000); } /** * wait to reconnect to dead HR-monitor */ protected void waitAndReconnect() { Log.d(TAG, "Wait & reconnect"); if(hrPcc != null) { hrPcc.subscribeHeartRateDataEvent(null); } // triggers: "sending message to a handler on a dead thread" // new android.os.Handler().postDelayed( // new Runnable() { // public void run() { // Log.i(TAG, "Attempt reconnect"); requestAccessToPcc(); // } // }, // 20000); // 20 sec delay for connection } protected void restart(){ restartService = true; stopSelf(); } /** * Requests the ANT+ device */ protected void requestAccessToPcc() { // if(hrPcc != null) { // hrPcc.subscribeHeartRateDataEvent(null); // } Log.d(TAG, "Request Access To PCC"); 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() { Log.i(TAG, "Stop service"); stopForeground(true); lifelineTimer.cancel(); if(releaseHandle != null) { releaseHandle.close(); } setRunning(false); super.onDestroy(); if(restartService ) { startService(new Intent(this, HeartRateService.class)); } } public static boolean isRunning() { return isRunning; } }