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 + + + + +