Now with Service :-)
This commit is contained in:
parent
9af67a8d65
commit
0a47e53ffb
6 changed files with 376 additions and 20 deletions
|
@ -2,7 +2,10 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.rubenvandeven.heartbeatstreamer">
|
||||
|
||||
<uses-feature android:name="android.hardware.bluetooth_le" android:required="true" />
|
||||
<uses-feature
|
||||
android:name="android.hardware.bluetooth_le"
|
||||
android:required="true" />
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
|
||||
|
@ -22,14 +25,14 @@
|
|||
</activity>
|
||||
<activity
|
||||
android:name=".heartrate.Activity_SearchUiHeartRateSampler"
|
||||
android:configChanges="orientation"
|
||||
android:label="Heart Rate Plugin Sampler"
|
||||
android:screenOrientation="portrait"
|
||||
android:configChanges="orientation"/>
|
||||
android:screenOrientation="portrait" />
|
||||
<activity
|
||||
android:name=".heartrate.Activity_AsyncScanHeartRateSampler"
|
||||
android:configChanges="orientation"
|
||||
android:label="Async Scan Demo Sampler"
|
||||
android:screenOrientation="portrait"
|
||||
android:configChanges="orientation"/>
|
||||
android:screenOrientation="portrait" />
|
||||
<activity
|
||||
android:name=".multidevicesearch.Activity_MultiDeviceFilter"
|
||||
android:label="Multi-Device Search Plugin Sampler"
|
||||
|
@ -39,6 +42,10 @@
|
|||
android:label="Multi-Device Search Plugin Sampler"
|
||||
android:screenOrientation="portrait" />
|
||||
|
||||
<service
|
||||
android:name=".heartrate.HeartRateService"
|
||||
android:enabled="true"
|
||||
android:exported="false" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
@ -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,6 +191,12 @@ public class Activity_AsyncScanHeartRateSampler extends Activity_HeartRateDispla
|
|||
}
|
||||
else
|
||||
{
|
||||
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()
|
||||
{
|
||||
|
@ -200,6 +209,7 @@ public class Activity_AsyncScanHeartRateSampler extends Activity_HeartRateDispla
|
|||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
{
|
||||
|
|
|
@ -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<AntPlusHeartRatePcc> hrScanCtrl;
|
||||
|
||||
AntPlusHeartRatePcc hrPcc;
|
||||
PccReleaseHandle<AntPlusHeartRatePcc> 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<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...");
|
||||
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<EventFlag> 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<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: 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;
|
||||
}
|
||||
}
|
|
@ -9,7 +9,8 @@
|
|||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Hello World!"
|
||||
android:id="@+id/status_msg"
|
||||
android:text="..."
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
|
|
Loading…
Reference in a new issue