diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 948b1d1..5ff5cdb 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,7 +2,10 @@ - + + @@ -22,14 +25,14 @@ + android:screenOrientation="portrait" /> + android:screenOrientation="portrait" /> + \ No newline at end of file diff --git a/app/src/main/java/com/rubenvandeven/heartbeatstreamer/MainActivity.java b/app/src/main/java/com/rubenvandeven/heartbeatstreamer/MainActivity.java index 7b55d09..a2cec8c 100644 --- a/app/src/main/java/com/rubenvandeven/heartbeatstreamer/MainActivity.java +++ b/app/src/main/java/com/rubenvandeven/heartbeatstreamer/MainActivity.java @@ -1,21 +1,130 @@ package com.rubenvandeven.heartbeatstreamer; +import android.content.BroadcastReceiver; +import android.content.Context; import android.content.Intent; +import android.content.IntentFilter; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; +import android.util.Log; +import android.widget.TextView; +import android.widget.Toast; +import com.rubenvandeven.heartbeatstreamer.heartrate.Activity_AsyncScanHeartRateSampler; import com.rubenvandeven.heartbeatstreamer.heartrate.Activity_SearchUiHeartRateSampler; +import com.rubenvandeven.heartbeatstreamer.heartrate.HeartRateService; + +import java.text.SimpleDateFormat; +import java.util.Date; public class MainActivity extends AppCompatActivity { + private final String TAG = MainActivity.class.getSimpleName(); + + private TextView statusLabel; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); - Intent intent = new Intent(this, Activity_SearchUiHeartRateSampler.class); - startActivity(intent); + statusLabel = findViewById(R.id.status_msg); + + setup(); + + startHeartRateMonitor(); + } + + + @Override + protected void onResume() { + super.onResume(); + setup(); + } + + public void setup() { + if (HeartRateService.isRunning()) { + statusLabel.setText("Running"); + } else { + statusLabel.setText("Stopped"); + } + registerBroadcastReceiver(); + } + + public void startHeartRateMonitor() { + if(HeartRateService.isRunning()) { + Toast.makeText(this, "Already running", Toast.LENGTH_LONG); + } else { + Intent intent = new Intent(MainActivity.this, HeartRateService.class); + startService(intent); + } + } + + public void stopHeartRateMonitor() { + // stop tracking + Intent intent = new Intent(MainActivity.this, HeartRateService.class); + stopService(intent); + } + + /** + * Register broadcast receiver for synchronization + * and tracking status updates + */ + private void registerBroadcastReceiver() { + Log.d(TAG, "register broadcastreceiver"); + IntentFilter filter = new IntentFilter(); + filter.addAction(HeartRateService.BROADCAST_MONITOR_CONNECTED); + filter.addAction(HeartRateService.BROADCAST_MONITOR_DISCONNECTED); + filter.addAction(HeartRateService.BROADCAST_MONITOR_UPDATE); + filter.addAction(HeartRateService.BROADCAST_MONITOR_UNKNOWN); + filter.addAction(HeartRateService.BROADCAST_SOCKET_SENT); + registerReceiver(mBroadcastReceiver, filter); + } + + /** + * On pause + */ + @Override + protected void onPause() { + unregisterReceiver(mBroadcastReceiver); +// if (db != null) { +// db.close(); +// } + super.onPause(); + } + + /** + * Broadcast receiver + */ + private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + Log.d(TAG, "[broadcast received " + intent + "]"); + if (intent == null || intent.getAction() == null) { + return; + } + switch (intent.getAction()) { + case HeartRateService.BROADCAST_MONITOR_CONNECTED: + statusLabel.setText("Running"); + break; + case HeartRateService.BROADCAST_MONITOR_DISCONNECTED: + statusLabel.setText("Stopped"); + 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; + } + } + }; + } diff --git a/app/src/main/java/com/rubenvandeven/heartbeatstreamer/heartrate/Activity_AsyncScanHeartRateSampler.java b/app/src/main/java/com/rubenvandeven/heartbeatstreamer/heartrate/Activity_AsyncScanHeartRateSampler.java index b466d1c..ef9f8f2 100644 --- a/app/src/main/java/com/rubenvandeven/heartbeatstreamer/heartrate/Activity_AsyncScanHeartRateSampler.java +++ b/app/src/main/java/com/rubenvandeven/heartbeatstreamer/heartrate/Activity_AsyncScanHeartRateSampler.java @@ -10,6 +10,7 @@ All rights reserved. package com.rubenvandeven.heartbeatstreamer.heartrate; import android.os.Bundle; +import android.util.Log; import android.view.View; import android.widget.AdapterView; import android.widget.AdapterView.OnItemClickListener; @@ -98,6 +99,7 @@ public class Activity_AsyncScanHeartRateSampler extends Activity_HeartRateDispla */ protected void requestConnectToResult(final AsyncScanResultDeviceInfo asyncScanResultDeviceInfo) { + Log.i("HeartBeat", "Now go!" + asyncScanResultDeviceInfo.getDeviceDisplayName() + " " + asyncScanResultDeviceInfo.getAntDeviceNumber()); //Inform the user we are connecting runOnUiThread(new Runnable() { @@ -170,6 +172,7 @@ public class Activity_AsyncScanHeartRateSampler extends Activity_HeartRateDispla //since the user most likely wants to be aware of which device they are already using in another app if(deviceFound.isAlreadyConnected()) { + mAlreadyConnectedDeviceInfos.add(deviceFound); runOnUiThread(new Runnable() { @@ -188,16 +191,23 @@ public class Activity_AsyncScanHeartRateSampler extends Activity_HeartRateDispla } else { - mScannedDeviceInfos.add(deviceFound); - runOnUiThread(new Runnable() - { - @Override - public void run() + Log.i("HeartBeat", "Found device :-) " + deviceFound.getDeviceDisplayName()); + if(deviceFound.getDeviceDisplayName().contentEquals("Rubensheart")) { + Log.i("HeartBeat", "Attempt connect!"); + // No check or anything after first connect. Good enough for this single use implementation. + requestConnectToResult(deviceFound); + } else{ + mScannedDeviceInfos.add(deviceFound); + runOnUiThread(new Runnable() { - adapter_devNameList.add(deviceFound.getDeviceDisplayName()); - adapter_devNameList.notifyDataSetChanged(); - } - }); + @Override + public void run() + { + adapter_devNameList.add(deviceFound.getDeviceDisplayName()); + adapter_devNameList.notifyDataSetChanged(); + } + }); + } } } }); diff --git a/app/src/main/java/com/rubenvandeven/heartbeatstreamer/heartrate/Activity_HeartRateDisplayBase.java b/app/src/main/java/com/rubenvandeven/heartbeatstreamer/heartrate/Activity_HeartRateDisplayBase.java index a036df3..54ec9f1 100644 --- a/app/src/main/java/com/rubenvandeven/heartbeatstreamer/heartrate/Activity_HeartRateDisplayBase.java +++ b/app/src/main/java/com/rubenvandeven/heartbeatstreamer/heartrate/Activity_HeartRateDisplayBase.java @@ -185,19 +185,21 @@ public abstract class Activity_HeartRateDisplayBase extends AppCompatActivity public void onCompleted(Exception ex, final WebSocket webSocket) { if (ex != null) { ex.printStackTrace(); - // TODO: retry + // TODO: retry in 30s - ie. after internet loss return; } webSocket.send("Connect!"); - webSocket.setClosedCallback(new CompletedCallback() { + CompletedCallback reconnectCallback = new CompletedCallback() { @Override public void onCompleted(Exception ex) { //unsubscribe before triggering resubscription hrPcc.subscribeHeartRateDataEvent(null); subscribeToHrEvents(); } - }); + }; + webSocket.setClosedCallback(reconnectCallback); + webSocket.setEndCallback(reconnectCallback); hrPcc.subscribeHeartRateDataEvent(new IHeartRateDataReceiver() { diff --git a/app/src/main/java/com/rubenvandeven/heartbeatstreamer/heartrate/HeartRateService.java b/app/src/main/java/com/rubenvandeven/heartbeatstreamer/heartrate/HeartRateService.java new file mode 100644 index 0000000..b33c53e --- /dev/null +++ b/app/src/main/java/com/rubenvandeven/heartbeatstreamer/heartrate/HeartRateService.java @@ -0,0 +1,227 @@ +package com.rubenvandeven.heartbeatstreamer.heartrate; + +import android.app.AlertDialog; +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.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.R; + +import java.math.BigDecimal; +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; +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_SOCKET_SENT = "com.rubenvandeven.heartbeat.broadcast.socket_sent"; + + AsyncScanController hrScanCtrl; + + AntPlusHeartRatePcc hrPcc; + PccReleaseHandle releaseHandle; + String lastMsg = ""; + + public static boolean isRunning = false; + + @Override + public void onCreate() { + isRunning = true; + requestAccessToPcc(); + } + + + /** + * 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); + 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..."); + switch (resultCode) { + case SUCCESS: + connectHr(result); + break; + case CHANNEL_NOT_AVAILABLE: + showNotification("Channel Not Available"); + break; + case OTHER_FAILURE: + showNotification("RequestAccess failed. See logcat for details."); + break; + case USER_CANCELLED: + showNotification("Cancelled. Do reset."); + break; + case UNRECOGNIZED: + default: + showNotification("Unknown error. Do reset."); + break; + } + } + }; + + 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)) ? "*" : ""); + + final String msg = String.format("{\"rate\":\"%s\", \"count\":\"%s\", \"time\":\"%s\"}", textHeartRate, textHeartBeatCount, textHeartBeatEventTime); + + if(msg.contentEquals(lastMsg)) { + Log.i(TAG, "Skip duplicate"); + return; + } + lastMsg = msg; + + sendResult(msg); + } + }); + + } + + + private void sendResult(String msg) { + Log.d(TAG, "Send: " + msg); + final String m = msg; + 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 + 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() { + //TODO: + Log.d(TAG, "Wait & reconnect"); + requestAccessToPcc(); + } + + /** + * Requests the ANT+ device + */ + protected void requestAccessToPcc() + { + if(hrPcc != null) { + hrPcc.subscribeHeartRateDataEvent(null); + } + 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(); + } + isRunning = false; + super.onDestroy(); + } + + public static boolean isRunning() { + return isRunning; + } +} diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 84f1951..98182a1 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -9,7 +9,8 @@