diff --git a/.idea/assetWizardSettings.xml b/.idea/assetWizardSettings.xml
new file mode 100644
index 0000000..741de99
--- /dev/null
+++ b/.idea/assetWizardSettings.xml
@@ -0,0 +1,102 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..9c4c522
--- /dev/null
+++ b/README.md
@@ -0,0 +1 @@
+Icon by [VideoPlasty](https://videoplasty.com/) Creative Commons BY-SA, trough [WikiMedia](https://commons.wikimedia.org/wiki/File:Heart_Rate_Monitor_Flat_Icon_Vector.svg)
diff --git a/app/src/main/ic_launcher-web.png b/app/src/main/ic_launcher-web.png
new file mode 100644
index 0000000..90c9170
Binary files /dev/null and b/app/src/main/ic_launcher-web.png differ
diff --git a/app/src/main/java/com/rubenvandeven/heartbeatstreamer/MainActivity.java b/app/src/main/java/com/rubenvandeven/heartbeatstreamer/MainActivity.java
index b2fed41..c469daa 100644
--- a/app/src/main/java/com/rubenvandeven/heartbeatstreamer/MainActivity.java
+++ b/app/src/main/java/com/rubenvandeven/heartbeatstreamer/MainActivity.java
@@ -8,6 +8,7 @@ import android.content.IntentFilter;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
+import android.view.View;
import android.widget.TextView;
import android.widget.Toast;
@@ -28,6 +29,8 @@ public class MainActivity extends AppCompatActivity {
private final String TAG = MainActivity.class.getSimpleName();
private TextView statusLabel;
+ private TextView serviceButton;
+ private TextView beatStatus;
@Override
protected void onCreate(Bundle savedInstanceState) {
@@ -35,6 +38,21 @@ public class MainActivity extends AppCompatActivity {
setContentView(R.layout.activity_main);
statusLabel = findViewById(R.id.status_msg);
+ serviceButton = findViewById(R.id.service_button);
+ beatStatus = findViewById(R.id.beat_status);
+
+// View.OnClickListener listener = new On;
+ serviceButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ serviceButton.setEnabled(false);
+ if(HeartRateService.isRunning()) {
+ startHeartRateMonitor();
+ } else {
+ stopHeartRateMonitor();
+ }
+ }
+ });
setup();
@@ -53,8 +71,10 @@ public class MainActivity extends AppCompatActivity {
public void setup() {
if (HeartRateService.isRunning()) {
statusLabel.setText("Running");
+ updateServiceButton(true);
} else {
statusLabel.setText("Stopped");
+ updateServiceButton(false);
}
registerBroadcastReceiver();
}
@@ -64,14 +84,25 @@ public class MainActivity extends AppCompatActivity {
Toast.makeText(this, "Already running", Toast.LENGTH_LONG);
} else {
Intent intent = new Intent(MainActivity.this, HeartRateService.class);
+ intent.setAction(HeartRateService.ACTION_START);
startService(intent);
}
}
+ public void updateServiceButton(boolean running) {
+ if(running) {
+ serviceButton.setText(R.string.service_stop);
+ } else {
+ serviceButton.setText(R.string.service_start);
+ }
+ serviceButton.setEnabled(true);
+ }
+
public void stopHeartRateMonitor() {
// stop tracking
Intent intent = new Intent(MainActivity.this, HeartRateService.class);
- stopService(intent);
+ intent.setAction(HeartRateService.ACTION_STOP);
+ startService(intent);
}
/**
@@ -86,6 +117,11 @@ public class MainActivity extends AppCompatActivity {
filter.addAction(HeartRateService.BROADCAST_MONITOR_UPDATE);
filter.addAction(HeartRateService.BROADCAST_MONITOR_UNKNOWN);
filter.addAction(HeartRateService.BROADCAST_SOCKET_SENT);
+ filter.addAction(HeartRateService.BROADCAST_SERVICE_START);
+ filter.addAction(HeartRateService.BROADCAST_SERVICE_STOPPED);
+ filter.addAction(HeartRateService.BROADCAST_BEAT);
+ filter.addAction(HeartRateService.BROADCAST_MONITOR_CONNECT_ATTEMPT);
+ filter.addAction(HeartRateService.BROADCAST_MONITOR_CONNECT_RESULT);
registerReceiver(mBroadcastReceiver, filter);
}
@@ -112,53 +148,50 @@ public class MainActivity extends AppCompatActivity {
return;
}
switch (intent.getAction()) {
+ case HeartRateService.BROADCAST_SERVICE_START:
+ statusLabel.setText("Started");
+ updateServiceButton(true);
+ break;
+ case HeartRateService.BROADCAST_SERVICE_STOPPED:
+ statusLabel.setText("Service Stopped");
+ updateServiceButton(false);
+ break;
case HeartRateService.BROADCAST_MONITOR_CONNECTED:
- statusLabel.setText("Running");
+ statusLabel.setText("Connected");
break;
case HeartRateService.BROADCAST_MONITOR_DISCONNECTED:
- statusLabel.setText("Stopped");
+ statusLabel.setText("Disconnected");
break;
case HeartRateService.BROADCAST_MONITOR_UPDATE:
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd_HHmmss");
String currentDateandTime = sdf.format(new Date());
statusLabel.setText("Last update: %s".format(currentDateandTime));
break;
-
case HeartRateService.BROADCAST_SOCKET_SENT:
String msg = intent.getStringExtra("msg");
statusLabel.setText("Last msg: %s".format(msg));
break;
+ case HeartRateService.BROADCAST_MONITOR_UNKNOWN:
+ int status = intent.getIntExtra("status", -1);
+ statusLabel.setText("Unknown state: %s".format(String.valueOf(status)));
+ break;
+ case HeartRateService.BROADCAST_BEAT:
+ String rate = intent.getStringExtra("rate");
+ String count = intent.getStringExtra("count");
+ String time = intent.getStringExtra("time");
+ Log.d(TAG, "Beat: " + rate);
+ beatStatus.setText(rate + " bpm / " + count + " beats / "+time + " s");
+ break;
+ case HeartRateService.BROADCAST_MONITOR_CONNECT_ATTEMPT:
+ beatStatus.setText("Attempting connection to monitor");
+ break;
+ case HeartRateService.BROADCAST_MONITOR_CONNECT_RESULT:
+ String connectMsg = intent.getStringExtra("msg");
+ beatStatus.setText("Connection: %s".format(connectMsg ));
+ break;
}
}
};
- /**
- * Get the token for use with the syncing.
- * @return
- */
- public String getToken() {
- String json = null;
- String token = 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;
- }
}
diff --git a/app/src/main/java/com/rubenvandeven/heartbeatstreamer/heartrate/HeartRateService.java b/app/src/main/java/com/rubenvandeven/heartbeatstreamer/heartrate/HeartRateService.java
index 735677f..4ee3c13 100644
--- a/app/src/main/java/com/rubenvandeven/heartbeatstreamer/heartrate/HeartRateService.java
+++ b/app/src/main/java/com/rubenvandeven/heartbeatstreamer/heartrate/HeartRateService.java
@@ -1,6 +1,8 @@
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;
@@ -9,6 +11,7 @@ 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;
@@ -23,11 +26,19 @@ 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 {
@@ -37,22 +48,51 @@ public class HeartRateService extends Service {
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;
- String lastMsg = "";
+ JSONObject lastMsg = null;
public static boolean isRunning = false;
+ NotificationCompat.Builder builder;
+ NotificationManager notificationManager;
+
@Override
public void onCreate() {
- isRunning = true;
+ 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.
*
@@ -63,7 +103,32 @@ public class HeartRateService extends Service {
*/
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
- Log.i(TAG, "Received start id " + startId + ": " + intent);
+ 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;
}
@@ -93,24 +158,31 @@ public class HeartRateService extends Service {
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);
}
};
@@ -138,30 +210,86 @@ public class HeartRateService extends Service {
final String textHeartBeatEventTime = df.format(heartBeatEventTime)
+ ((AntPlusHeartRatePcc.DataState.INITIAL_VALUE.equals(dataState)) ? "*" : "");
- final String msg = String.format("{\"rate\":\"%s\", \"count\":\"%s\", \"time\":\"%s\"}", textHeartRate, textHeartBeatCount, textHeartBeatEventTime);
+ Intent intent = new Intent(BROADCAST_BEAT);
+ intent.putExtra("rate", textHeartRate);
+ intent.putExtra("count", textHeartBeatCount);
+ intent.putExtra("time", textHeartBeatEventTime);
+ sendBroadcast(intent);
- if(msg.contentEquals(lastMsg)) {
- Log.i(TAG, "Skip duplicate");
- return;
+ 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();
}
- lastMsg = msg;
-
- sendResult(msg);
}
});
}
- private void sendResult(String msg) {
- Log.d(TAG, "Send: " + msg);
- final String m = msg;
+ 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: retry in 30s - ie. after internet loss
+ // TODO: store for later sync
return;
}
webSocket.send(m);
@@ -188,9 +316,20 @@ public class HeartRateService extends Service {
* wait to reconnect to dead HR-monitor
*/
protected void waitAndReconnect() {
- //TODO:
Log.d(TAG, "Wait & reconnect");
- requestAccessToPcc();
+
+ 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
}
/**
@@ -201,6 +340,7 @@ public class HeartRateService extends Service {
if(hrPcc != null) {
hrPcc.subscribeHeartRateDataEvent(null);
}
+ sendBroadcast( new Intent(BROADCAST_MONITOR_CONNECT_ATTEMPT) );
releaseHandle = AntPlusHeartRatePcc.requestAccess(this, 4818, 0, resultReceiver, stateChangeReceiver);
}
@@ -216,7 +356,7 @@ public class HeartRateService extends Service {
{
releaseHandle.close();
}
- isRunning = false;
+ setRunning(false);
super.onDestroy();
}
diff --git a/app/src/main/res/drawable-hdpi/ic_notification.png b/app/src/main/res/drawable-hdpi/ic_notification.png
new file mode 100644
index 0000000..4f5a42c
Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_notification.png differ
diff --git a/app/src/main/res/drawable-mdpi/ic_notification.png b/app/src/main/res/drawable-mdpi/ic_notification.png
new file mode 100644
index 0000000..47643ec
Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_notification.png differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_notification.png b/app/src/main/res/drawable-xhdpi/ic_notification.png
new file mode 100644
index 0000000..67add5d
Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_notification.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_notification.png b/app/src/main/res/drawable-xxhdpi/ic_notification.png
new file mode 100644
index 0000000..a39bfea
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_notification.png differ
diff --git a/app/src/main/res/drawable-xxxhdpi/ic_notification.png b/app/src/main/res/drawable-xxxhdpi/ic_notification.png
new file mode 100644
index 0000000..9e85725
Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_notification.png differ
diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml
new file mode 100644
index 0000000..1f69184
--- /dev/null
+++ b/app/src/main/res/drawable/ic_launcher_foreground.xml
@@ -0,0 +1,83 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
index 98182a1..665a21d 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -1,19 +1,46 @@
-
-
\ No newline at end of file
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
index eca70cf..ab2154c 100644
--- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -1,5 +1,4 @@
-
-
-
+`
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
index eca70cf..0d57b8f 100644
--- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -1,5 +1,4 @@
-
-
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png
index 898f3ed..2c5f394 100644
Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png
index 64ba76f..d78999a 100644
Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher.png and b/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png
index e5ed465..05b8980 100644
Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
index b0907ca..4bea081 100644
Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
index 2c18de9..30f8234 100644
Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 6ab1500..352f725 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -22,4 +22,7 @@
- Bike Speed
- Stride-based Speed and Distance
+ Start service
+ Stop service
+