441 lines
17 KiB
Java
441 lines
17 KiB
Java
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<AntPlusHeartRatePcc> hrScanCtrl;
|
|
|
|
AntPlusHeartRatePcc hrPcc;
|
|
PccReleaseHandle<AntPlusHeartRatePcc> 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<AntPlusHeartRatePcc> resultReceiver =
|
|
new AntPluginPcc.IPluginAccessResultReceiver<AntPlusHeartRatePcc>() {
|
|
//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<EventFlag> 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<WebSocket> 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;
|
|
}
|
|
}
|