From add6bd2aeb38b231f981bf278761405ed8311288 Mon Sep 17 00:00:00 2001 From: Aleksander Nowakowski Date: Mon, 5 Sep 2016 18:41:02 +0200 Subject: [PATCH] CGMS profile added, ver 1.18.0 + minor fixes --- README.md | 1 + app/build.gradle | 4 +- app/src/main/AndroidManifest.xml | 15 +- .../android/nrftoolbox/cgms/CGMSActivity.java | 292 +++++++++++++ .../android/nrftoolbox/cgms/CGMSManager.java | 383 ++++++++++++++++++ .../nrftoolbox/cgms/CGMSManagerCallbacks.java | 54 +++ .../android/nrftoolbox/cgms/CGMSRecord.java | 56 +++ .../nrftoolbox/cgms/CGMSRecordsAdapter.java | 87 ++++ .../android/nrftoolbox/cgms/CGMService.java | 267 ++++++++++++ .../profile/BleProfileActivity.java | 1 + .../res/drawable-hdpi/ic_cgms_feature.png | Bin 0 -> 2926 bytes .../res/drawable-hdpi/ic_stat_notify_cgms.png | Bin 0 -> 659 bytes .../res/drawable-xhdpi/ic_cgms_feature.png | Bin 0 -> 4650 bytes .../drawable-xhdpi/ic_hts_feature_black.png | Bin 0 -> 368 bytes .../drawable-xhdpi/ic_stat_notify_cgms.png | Bin 0 -> 844 bytes .../res/layout-land/activity_feature_bpm.xml | 2 +- .../res/layout-land/activity_feature_cgms.xml | 227 +++++++++++ .../res/layout-land/activity_feature_csc.xml | 2 +- .../res/layout-land/activity_feature_dfu.xml | 2 +- .../res/layout-land/activity_feature_hrs.xml | 2 +- .../res/layout-land/activity_feature_hts.xml | 2 +- .../activity_feature_proximity.xml | 2 +- .../res/layout-land/activity_feature_rsc.xml | 2 +- .../res/layout-land/activity_feature_uart.xml | 2 +- .../activity_feature_bpm.xml | 2 +- .../activity_feature_csc.xml | 2 +- .../activity_feature_dfu.xml | 2 +- .../activity_feature_hrs.xml | 2 +- .../activity_feature_rsc.xml | 2 +- .../main/res/layout/activity_feature_bpm.xml | 2 +- .../main/res/layout/activity_feature_cgms.xml | 225 ++++++++++ .../res/layout/activity_feature_cgms_item.xml | 71 ++++ .../main/res/layout/activity_feature_csc.xml | 2 +- .../main/res/layout/activity_feature_dfu.xml | 2 +- .../main/res/layout/activity_feature_hrs.xml | 2 +- .../main/res/layout/activity_feature_hts.xml | 2 +- .../res/layout/activity_feature_proximity.xml | 2 +- .../main/res/layout/activity_feature_rsc.xml | 2 +- .../res/layout/activity_feature_template.xml | 2 +- .../main/res/layout/activity_feature_uart.xml | 2 +- app/src/main/res/layout/activity_features.xml | 14 +- app/src/main/res/values-land/strings_cgms.xml | 29 ++ app/src/main/res/values/strings.xml | 2 +- app/src/main/res/values/strings_cgms.xml | 37 ++ wear/build.gradle | 4 +- 45 files changed, 1778 insertions(+), 35 deletions(-) create mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/cgms/CGMSActivity.java create mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/cgms/CGMSManager.java create mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/cgms/CGMSManagerCallbacks.java create mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/cgms/CGMSRecord.java create mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/cgms/CGMSRecordsAdapter.java create mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/cgms/CGMService.java create mode 100644 app/src/main/res/drawable-hdpi/ic_cgms_feature.png create mode 100644 app/src/main/res/drawable-hdpi/ic_stat_notify_cgms.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_cgms_feature.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_hts_feature_black.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_stat_notify_cgms.png create mode 100644 app/src/main/res/layout-land/activity_feature_cgms.xml create mode 100644 app/src/main/res/layout/activity_feature_cgms.xml create mode 100644 app/src/main/res/layout/activity_feature_cgms_item.xml create mode 100644 app/src/main/res/values-land/strings_cgms.xml create mode 100644 app/src/main/res/values/strings_cgms.xml diff --git a/README.md b/README.md index 0315f489..b79d7f9b 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ It contains applications demonstrating Bluetooth Smart profiles: * **Blood Pressure Monitor**, * **Health Thermometer Monitor**, * **Glucose Monitor**, +* **Continuous Glucose Monitor**, * **Proximity Monitor**. Since version 1.10.0 the *nRF Toolbox* also supports the **Nordic UART Service** which may be used for bidirectional text communication between devices. diff --git a/app/build.gradle b/app/build.gradle index 751da1d6..1d837ea4 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -8,8 +8,8 @@ android { applicationId "no.nordicsemi.android.nrftoolbox" minSdkVersion 18 targetSdkVersion 24 - versionCode 46 - versionName "1.17.0" + versionCode 47 + versionName "1.18.0" } buildTypes { release { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1cf1e40e..ba0b5038 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -19,7 +19,7 @@ ~ LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ~ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE ~ USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - --> +--> @@ -201,6 +201,16 @@ + + + + + + + + diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/cgms/CGMSActivity.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/cgms/CGMSActivity.java new file mode 100644 index 00000000..3788f6ca --- /dev/null +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/cgms/CGMSActivity.java @@ -0,0 +1,292 @@ +/* + * Copyright (c) 2016, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package no.nordicsemi.android.nrftoolbox.cgms; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Bundle; +import android.support.v4.content.LocalBroadcastManager; +import android.util.SparseArray; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.widget.ListView; +import android.widget.PopupMenu; +import android.widget.TextView; + +import java.util.UUID; + +import no.nordicsemi.android.nrftoolbox.R; +import no.nordicsemi.android.nrftoolbox.profile.BleProfileService; +import no.nordicsemi.android.nrftoolbox.profile.BleProfileServiceReadyActivity; + +public class CGMSActivity extends BleProfileServiceReadyActivity implements PopupMenu.OnMenuItemClickListener { + + private View mControlPanelStd; + private View mControlPanelAbort; + private TextView mUnitView; + private ListView mRecordsListView; + private CGMSRecordsAdapter mCgmsRecordsAdapter; + + private CGMService.CGMSBinder mBinder; + + @Override + protected void onCreateView(Bundle savedInstanceState) { + setContentView(R.layout.activity_feature_cgms); + setGUI(); + } + + @Override + protected void onInitialize(Bundle savedInstanceState) { + LocalBroadcastManager.getInstance(this).registerReceiver(mBroadcastReceiver, makeIntentFilter()); + } + + private void setGUI() { + mRecordsListView = (ListView) findViewById(R.id.list); + mUnitView = (TextView) findViewById(R.id.unit); + mControlPanelStd = findViewById(R.id.cgms_control_std); + mControlPanelAbort = findViewById(R.id.cgms_control_abort); + + findViewById(R.id.action_last).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + clearRecords(); + if(mBinder != null) { + mBinder.clear(); + mBinder.getLastRecord(); + } + } + }); + findViewById(R.id.action_all).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + clearRecords(); + if(mBinder != null){ + clearRecords(); + mBinder.getAllRecords(); + } + } + }); + findViewById(R.id.action_abort).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if(mBinder != null){ + mBinder.abort(); + } + } + }); + + // create popup menu attached to the button More + findViewById(R.id.action_more).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + PopupMenu menu = new PopupMenu(CGMSActivity.this, v); + menu.setOnMenuItemClickListener(CGMSActivity.this); + MenuInflater inflater = menu.getMenuInflater(); + inflater.inflate(R.menu.gls_more, menu.getMenu()); + menu.show(); + } + }); + } + + private void loadAdapter(SparseArray records){ + for(int i = 0; i < records.size(); i++){ + mCgmsRecordsAdapter.addItem(records.get(i)); + } + mCgmsRecordsAdapter.notifyDataSetChanged(); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + LocalBroadcastManager.getInstance(this).unregisterReceiver(mBroadcastReceiver); + } + + @Override + protected void onServiceBinded(CGMService.CGMSBinder binder) { + mBinder = binder; + SparseArray cgmsRecords = binder.getCgmsRecords(); + if(cgmsRecords != null && cgmsRecords.size() > 0){ + if(mCgmsRecordsAdapter == null) { + mCgmsRecordsAdapter = new CGMSRecordsAdapter(CGMSActivity.this); + mRecordsListView.setAdapter(mCgmsRecordsAdapter); + } + loadAdapter(cgmsRecords); + } + } + + @Override + protected void onServiceUnbinded() { + mBinder = null; + } + + @Override + protected Class getServiceClass() { + return CGMService.class; + } + + @Override + protected int getLoggerProfileTitle() { + return R.string.cgms_feature_title; + } + + @Override + protected int getAboutTextId() { + return R.string.cgms_about_text; + } + + @Override + protected int getDefaultDeviceName() { + return R.string.cgms_default_name; + } + + @Override + protected UUID getFilterUUID() { + return CGMSManager.CGMS_UUID; + } + + @Override + public void onServicesDiscovered(final boolean optionalServicesFound) { + // this may notify user or show some views + } + + @Override + public void onDeviceReady() { + + } + + private void setOperationInProgress(final boolean progress) { + runOnUiThread(new Runnable() { + @Override + public void run() { + // setSupportProgressBarIndeterminateVisibility(progress); + mControlPanelStd.setVisibility(!progress ? View.VISIBLE : View.GONE); + mControlPanelAbort.setVisibility(progress ? View.VISIBLE : View.GONE); + } + }); + } + + @Override + public void onDeviceDisconnected() { + super.onDeviceDisconnected(); + setOperationInProgress(false); + clearRecords(); + } + + @Override + protected void setDefaultUI() { + + } + + @Override + public boolean onMenuItemClick(MenuItem menuItem) { + switch (menuItem.getItemId()) { + case R.id.action_refresh: + /*if(mBinder != null) + mBinder.refreshRecords();*/ + break; + case R.id.action_first: + clearRecords(); + if(mBinder != null) + mBinder.getFirstRecord(); + break; + case R.id.action_clear: + clearRecords(); + if(mBinder != null) + mBinder.clear(); + break; + case R.id.action_delete_all: + clearRecords(); + if(mBinder != null) + mBinder.deleteAllRecords(); + break; + } + return true; + } + + private void clearRecords(){ + runOnUiThread(new Runnable() { + @Override + public void run() { + if(mCgmsRecordsAdapter != null){ + mCgmsRecordsAdapter.clear(); + mCgmsRecordsAdapter.notifyDataSetChanged(); + } + } + }); + } + + private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(final Context context, final Intent intent) { + final String action = intent.getAction(); + if (CGMService.BROADCAST_CGMS_VALUES.equals(action)) { + CGMSRecord cgmsRecord = intent.getExtras().getParcelable(CGMService.EXTRA_CGMS_RECORD); + if(mCgmsRecordsAdapter == null){ + mCgmsRecordsAdapter = new CGMSRecordsAdapter(CGMSActivity.this); + mRecordsListView.setAdapter(mCgmsRecordsAdapter); + } + mCgmsRecordsAdapter.addItem(cgmsRecord); + mCgmsRecordsAdapter.notifyDataSetChanged(); + + } else if (CGMService.BROADCAST_DATA_SET_CHANGED.equals(action)) { + // Update GUI + clearRecords(); + } else if (CGMService.OPERATION_STARTED.equals(action)) { + // Update GUI + setOperationInProgress(true); + } else if (CGMService.OPERATION_COMPLETED.equals(action)) { + // Update GUI + setOperationInProgress(false); + } else if (CGMService.OPERATION_SUPPORTED.equals(action)) { + // Update GUI + setOperationInProgress(false); + } else if (CGMService.OPERATION_NOT_SUPPORTED.equals(action)) { + // Update GUI + setOperationInProgress(false); + } else if (CGMService.OPERATION_ABORTED.equals(action)) { + // Update GUI + setOperationInProgress(false); + } else if (CGMService.OPERATION_FAILED.equals(action)) { + // Update GUI + setOperationInProgress(false); + showToast(R.string.gls_operation_failed); + } + } + }; + + private static IntentFilter makeIntentFilter() { + final IntentFilter intentFilter = new IntentFilter(); + intentFilter.addAction(CGMService.BROADCAST_CGMS_VALUES); + intentFilter.addAction(CGMService.BROADCAST_DATA_SET_CHANGED); + intentFilter.addAction(CGMService.OPERATION_STARTED); + intentFilter.addAction(CGMService.OPERATION_COMPLETED); + intentFilter.addAction(CGMService.OPERATION_SUPPORTED); + intentFilter.addAction(CGMService.OPERATION_NOT_SUPPORTED); + intentFilter.addAction(CGMService.OPERATION_ABORTED); + intentFilter.addAction(CGMService.OPERATION_FAILED); + return intentFilter; + } +} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/cgms/CGMSManager.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/cgms/CGMSManager.java new file mode 100644 index 00000000..e1f1c863 --- /dev/null +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/cgms/CGMSManager.java @@ -0,0 +1,383 @@ +/* + * Copyright (c) 2016, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package no.nordicsemi.android.nrftoolbox.cgms; + +import android.bluetooth.BluetoothGatt; +import android.bluetooth.BluetoothGattCharacteristic; +import android.bluetooth.BluetoothGattService; +import android.content.Context; +import android.nfc.Tag; +import android.util.Log; +import android.util.SparseArray; + +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.LinkedList; +import java.util.Queue; +import java.util.UUID; + +import no.nordicsemi.android.log.Logger; +import no.nordicsemi.android.nrftoolbox.parser.HeartRateMeasurementParser; +import no.nordicsemi.android.nrftoolbox.parser.RecordAccessControlPointParser; +import no.nordicsemi.android.nrftoolbox.profile.BleManager; +import no.nordicsemi.android.nrftoolbox.utility.DebugLogger; + +/** + * Created by rora on 10.05.2016. + */ +public class CGMSManager extends BleManager { + private static final String TAG = "CGMSManager"; + + /** Cycling Speed and Cadence service UUID */ + public final static UUID CGMS_UUID = UUID.fromString("0000181F-0000-1000-8000-00805f9b34fb"); + private static final UUID CGM_MEASUREMENT_UUID = UUID.fromString("00002AA7-0000-1000-8000-00805f9b34fb"); + private static final UUID CGM_OPS_CONTROL_POINT_UUID = UUID.fromString("00002AAC-0000-1000-8000-00805f9b34fb"); + /** Record Access Control Point characteristic UUID */ + private final static UUID RACP_UUID = UUID.fromString("00002A52-0000-1000-8000-00805f9b34fb"); + + private final static int OP_CODE_REPORT_STORED_RECORDS = 1; + private final static int OP_CODE_DELETE_STORED_RECORDS = 2; + private final static int OP_CODE_ABORT_OPERATION = 3; + private final static int OP_CODE_REPORT_NUMBER_OF_RECORDS = 4; + private final static int OP_CODE_NUMBER_OF_STORED_RECORDS_RESPONSE = 5; + private final static int OP_CODE_RESPONSE_CODE = 6; + + private final static int OPERATOR_NULL = 0; + private final static int OPERATOR_ALL_RECORDS = 1; + private final static int OPERATOR_LESS_THEN_OR_EQUAL = 2; + private final static int OPERATOR_GREATER_THEN_OR_EQUAL = 3; + private final static int OPERATOR_WITHING_RANGE = 4; + private final static int OPERATOR_FIRST_RECORD = 5; + private final static int OPERATOR_LAST_RECORD = 6; + + /** + * The filter type is used for range operators ({@link #OPERATOR_LESS_THEN_OR_EQUAL}, {@link #OPERATOR_GREATER_THEN_OR_EQUAL}, {@link #OPERATOR_WITHING_RANGE}.
+ * The syntax of the operand is: [Filter Type][Minimum][Maximum].
+ * This filter selects the records by the sequence number. + */ + private final static int FILTER_TYPE_SEQUENCE_NUMBER = 1; + /** + * The filter type is used for range operators ({@link #OPERATOR_LESS_THEN_OR_EQUAL}, {@link #OPERATOR_GREATER_THEN_OR_EQUAL}, {@link #OPERATOR_WITHING_RANGE}.
+ * The syntax of the operand is: [Filter Type][Minimum][Maximum].
+ * This filter selects the records by the user facing time (base time + offset time). + */ + private final static int FILTER_TYPE_USER_FACING_TIME = 2; + private final static int RESPONSE_SUCCESS = 1; + private final static int RESPONSE_OP_CODE_NOT_SUPPORTED = 2; + private final static int RESPONSE_INVALID_OPERATOR = 3; + private final static int RESPONSE_OPERATOR_NOT_SUPPORTED = 4; + private final static int RESPONSE_INVALID_OPERAND = 5; + private final static int RESPONSE_NO_RECORDS_FOUND = 6; + private final static int RESPONSE_ABORT_UNSUCCESSFUL = 7; + private final static int RESPONSE_PROCEDURE_NOT_COMPLETED = 8; + private final static int RESPONSE_OPERAND_NOT_SUPPORTED = 9; + + private final static SimpleDateFormat mTimeFormat= new SimpleDateFormat("dd MM YYYY HH:mm:ss"); + + private BluetoothGattCharacteristic mCGMMEasurementCharacteristic; + private BluetoothGattCharacteristic mCGMOpsControlPointCharacteristic; + private BluetoothGattCharacteristic mRecordAccessControlPointCharacteristic; + + private static CGMSManager managerInstance = null; + private SparseArray mRecords = new SparseArray<>(); + private boolean mAbort; + private long mSessionStartTime; + + /** + * singleton implementation of HRSManager class + */ + public static synchronized CGMSManager getInstance(final Context context) { + if (managerInstance == null) { + managerInstance = new CGMSManager(context); + } + return managerInstance; + } + public CGMSManager(Context context) { + super(context); + } + + @Override + protected BleManagerGattCallback getGattCallback() { + return mGattCallback; + } + + /** + * BluetoothGatt callbacks for connection/disconnection, service discovery, receiving notification, etc + */ + private final BleManagerGattCallback mGattCallback = new BleManagerGattCallback() { + + @Override + protected Queue initGatt(final BluetoothGatt gatt) { + final LinkedList requests = new LinkedList<>(); + requests.push(Request.newEnableNotificationsRequest(mCGMMEasurementCharacteristic)); + if (mCGMOpsControlPointCharacteristic != null) { + mSessionStartTime = System.currentTimeMillis(); + requests.push(Request.newWriteRequest(mCGMOpsControlPointCharacteristic, new byte[]{26} /*start session value*/)); + } + requests.push(Request.newEnableIndicationsRequest(mRecordAccessControlPointCharacteristic)); + return requests; + } + + @Override + protected boolean isRequiredServiceSupported(final BluetoothGatt gatt) { + final BluetoothGattService service = gatt.getService(CGMS_UUID); + if (service != null) { + mCGMMEasurementCharacteristic = service.getCharacteristic(CGM_MEASUREMENT_UUID); + mCGMOpsControlPointCharacteristic = service.getCharacteristic(CGM_OPS_CONTROL_POINT_UUID); + mRecordAccessControlPointCharacteristic = service.getCharacteristic(RACP_UUID); + } + return mCGMMEasurementCharacteristic != null && mCGMOpsControlPointCharacteristic != null && mRecordAccessControlPointCharacteristic != null; + } + + @Override + protected boolean isOptionalServiceSupported(final BluetoothGatt gatt) { + final BluetoothGattService service = gatt.getService(CGMS_UUID); + if (service != null) { + mCGMOpsControlPointCharacteristic = service.getCharacteristic(CGM_OPS_CONTROL_POINT_UUID); + } + return mCGMOpsControlPointCharacteristic != null; + } + + @Override + public void onCharacteristicRead(final BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic) { + } + + @Override + protected void onDeviceDisconnected() { + mCGMOpsControlPointCharacteristic = null; + mCGMMEasurementCharacteristic = null; + //mRecordAccessControlPointCharacteristic = null; + } + + @Override + public void onCharacteristicNotified(final BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic) { + final UUID uuid = characteristic.getUuid(); + if(CGM_MEASUREMENT_UUID.equals(uuid)) { + if (mLogSession != null) + Logger.a(mLogSession, HeartRateMeasurementParser.parse(characteristic)); + byte [] data = characteristic.getValue(); + int cgmSize = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, 0); + float cgmValue; + int timeOffset; + if (cgmSize > 0) { + cgmValue = characteristic.getFloatValue(BluetoothGattCharacteristic.FORMAT_SFLOAT, 2); + timeOffset = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, 4); + Date date = new Date(mSessionStartTime + (timeOffset * 3600)); + final String timeStamp = mTimeFormat.format(date); + //This will send callback to CGMSActivity when new concentration value is received from CGMS device + mCallbacks.onCGMValueReceived(cgmValue, timeStamp); + } + } else if (CGM_OPS_CONTROL_POINT_UUID.equals(uuid)){ + Log.v(TAG, "CGM Ops control"); + } else if (RACP_UUID.equals(uuid)){ + Log.v(TAG, "RACP Ops control"); + } + } + + @Override + protected void onCharacteristicIndicated(final BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic) { + if (mLogSession != null) + Logger.a(mLogSession, RecordAccessControlPointParser.parse(characteristic)); + + // Record Access Control Point characteristic + int offset = 0; + final int opCode = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, offset); + offset += 2; // skip the operator + + if (opCode == OP_CODE_NUMBER_OF_STORED_RECORDS_RESPONSE) { + // We've obtained the number of all records + final int number = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, offset); + + mCallbacks.onNumberOfRecordsRequested(number); + + // Request the records + if (number > 0) { + final BluetoothGattCharacteristic racpCharacteristic = mRecordAccessControlPointCharacteristic; + setOpCode(racpCharacteristic, OP_CODE_REPORT_STORED_RECORDS, OPERATOR_ALL_RECORDS); + writeCharacteristic(racpCharacteristic); + } else { + mCallbacks.onOperationCompleted(); + } + } else if (opCode == OP_CODE_RESPONSE_CODE) { + final int requestedOpCode = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, offset); + final int responseCode = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, offset + 1); + DebugLogger.d(TAG, "Response result for: " + requestedOpCode + " is: " + responseCode); + + switch (responseCode) { + case RESPONSE_SUCCESS: + if (!mAbort) + mCallbacks.onOperationCompleted(); + else + mCallbacks.onOperationAborted(); + break; + case RESPONSE_NO_RECORDS_FOUND: + mCallbacks.onOperationCompleted(); + break; + case RESPONSE_OP_CODE_NOT_SUPPORTED: + mCallbacks.onOperationNotSupported(); + break; + case RESPONSE_PROCEDURE_NOT_COMPLETED: + case RESPONSE_ABORT_UNSUCCESSFUL: + default: + mCallbacks.onOperationFailed(); + break; + } + mAbort = false; + } + } + + }; + + /** + * Writes given operation parameters to the characteristic + * + * @param characteristic + * the characteristic to write. This must be the Record Access Control Point characteristic + * @param opCode + * the operation code + * @param operator + * the operator (see {@link #OPERATOR_NULL} and others + * @param params + * optional parameters (one for >=, <=, two for the range, none for other operators) + */ + private void setOpCode(final BluetoothGattCharacteristic characteristic, final int opCode, final int operator, final Integer... params) { + final int size = 2 + ((params.length > 0) ? 1 : 0) + params.length * 2; // 1 byte for opCode, 1 for operator, 1 for filter type (if parameters exists) and 2 for each parameter + characteristic.setValue(new byte[size]); + + // write the operation code + int offset = 0; + characteristic.setValue(opCode, BluetoothGattCharacteristic.FORMAT_UINT8, offset); + offset += 1; + + // write the operator. This is always present but may be equal to OPERATOR_NULL + characteristic.setValue(operator, BluetoothGattCharacteristic.FORMAT_UINT8, offset); + offset += 1; + + // if parameters exists, append them. Parameters should be sorted from minimum to maximum. Currently only one or two params are allowed + if (params.length > 0) { + // our implementation use only sequence number as a filer type + characteristic.setValue(FILTER_TYPE_SEQUENCE_NUMBER, BluetoothGattCharacteristic.FORMAT_UINT8, offset); + offset += 1; + + for (final Integer i : params) { + characteristic.setValue(i, BluetoothGattCharacteristic.FORMAT_UINT16, offset); + offset += 2; + } + } + } + + /** + * Sends the request to obtain the last (most recent) record from glucose device. The data will be returned to Glucose Measurement characteristic as a notification followed by Record Access + * Control Point indication with status code ({@link #RESPONSE_SUCCESS} or other in case of error. + */ + public void getLastRecord() { + if (mRecordAccessControlPointCharacteristic == null) + return; + + clear(); + mCallbacks.onOperationStarted(); + + final BluetoothGattCharacteristic characteristic = mRecordAccessControlPointCharacteristic; + setOpCode(characteristic, OP_CODE_REPORT_STORED_RECORDS, OPERATOR_LAST_RECORD); + writeCharacteristic(characteristic); + } + + /** + * Returns all records as a sparse array where sequence number is the key. + * + * @return the records list + */ + public SparseArray getRecords() { + return mRecords; + } + + /** + * Clears the records list locally + */ + public void clear() { + mRecords.clear(); + mCallbacks.onDatasetChanged(); + } + + /** + * Sends abort operation signal to the device + */ + public void abort() { + if (mRecordAccessControlPointCharacteristic == null) + return; + + mAbort = true; + final BluetoothGattCharacteristic characteristic = mRecordAccessControlPointCharacteristic; + setOpCode(characteristic, OP_CODE_ABORT_OPERATION, OPERATOR_NULL); + writeCharacteristic(characteristic); + } + + /** + * Sends the request to obtain the first (oldest) record from glucose device. The data will be returned to Glucose Measurement characteristic as a notification followed by Record Access Control + * Point indication with status code ({@link #RESPONSE_SUCCESS} or other in case of error. + */ + public void getFirstRecord() { + if (mRecordAccessControlPointCharacteristic == null) + return; + + clear(); + mCallbacks.onOperationStarted(); + + final BluetoothGattCharacteristic characteristic = mRecordAccessControlPointCharacteristic; + setOpCode(characteristic, OP_CODE_REPORT_STORED_RECORDS, OPERATOR_FIRST_RECORD); + final LinkedList requests = new LinkedList<>(); + writeCharacteristic(characteristic); + } + + /** + * Sends the request to obtain all records from glucose device. Initially we want to notify him/her about the number of the records so the {@link #OP_CODE_REPORT_NUMBER_OF_RECORDS} is send. The + * data will be returned to Glucose Measurement characteristic as a notification followed by Record Access Control Point indication with status code ({@link #RESPONSE_SUCCESS} or other in case of + * error. + */ + public void getAllRecords() { + if (mRecordAccessControlPointCharacteristic == null) + return; + + clear(); + mCallbacks.onOperationStarted(); + + final BluetoothGattCharacteristic characteristic = mRecordAccessControlPointCharacteristic; + setOpCode(characteristic, OP_CODE_REPORT_NUMBER_OF_RECORDS, OPERATOR_ALL_RECORDS); + writeCharacteristic(characteristic); + } + + public void deleteAllRecords() { + if (mRecordAccessControlPointCharacteristic == null) + return; + + clear(); + mCallbacks.onOperationStarted(); + + final BluetoothGattCharacteristic characteristic = mRecordAccessControlPointCharacteristic; + setOpCode(characteristic, OP_CODE_DELETE_STORED_RECORDS, OPERATOR_ALL_RECORDS); + writeCharacteristic(characteristic); + } + +} + diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/cgms/CGMSManagerCallbacks.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/cgms/CGMSManagerCallbacks.java new file mode 100644 index 00000000..040f7415 --- /dev/null +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/cgms/CGMSManagerCallbacks.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2016, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package no.nordicsemi.android.nrftoolbox.cgms; + +import no.nordicsemi.android.nrftoolbox.profile.BleManagerCallbacks; + +/** + * Created by rora on 10.05.2016. + */ +public interface CGMSManagerCallbacks extends BleManagerCallbacks { + /** + * Called when new CGM value has been obtained from the sensor + * + * @param value + * the new value + */ + public void onCGMValueReceived(float value, String timeStamp); + + + public void onOperationStarted(); + + public void onOperationCompleted(); + + public void onOperationFailed(); + + public void onOperationAborted(); + + public void onOperationNotSupported(); + + public void onDatasetChanged(); + + public void onNumberOfRecordsRequested(final int value); + +} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/cgms/CGMSRecord.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/cgms/CGMSRecord.java new file mode 100644 index 00000000..3568a9f1 --- /dev/null +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/cgms/CGMSRecord.java @@ -0,0 +1,56 @@ +package no.nordicsemi.android.nrftoolbox.cgms; + +import android.os.Parcel; +import android.os.Parcelable; + +import java.util.Calendar; + +/** + * Created by rora on 02.09.2016. + */ +public class CGMSRecord implements Parcelable{ + + /** Record sequence number */ + protected int sequenceNumber; + /** The base time of the measurement */ + protected Calendar time; + /** Time offset of the record */ + protected String timeStamp; + protected float reading; + + protected CGMSRecord(float cgmsValue, String timeStamp) { + sequenceNumber = 0; + this.timeStamp = timeStamp; + this.reading = cgmsValue; + } + + protected CGMSRecord(Parcel in) { + sequenceNumber = in.readInt(); + timeStamp = in.readString(); + reading = in.readFloat(); + } + + public static final Creator CREATOR = new Creator() { + @Override + public CGMSRecord createFromParcel(Parcel in) { + return new CGMSRecord(in); + } + + @Override + public CGMSRecord[] newArray(int size) { + return new CGMSRecord[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel parcel, int i) { + parcel.writeInt(sequenceNumber); + parcel.writeString(timeStamp); + parcel.writeFloat(reading); + } +} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/cgms/CGMSRecordsAdapter.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/cgms/CGMSRecordsAdapter.java new file mode 100644 index 00000000..e773bb82 --- /dev/null +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/cgms/CGMSRecordsAdapter.java @@ -0,0 +1,87 @@ +package no.nordicsemi.android.nrftoolbox.cgms; + +import android.content.Context; +import android.util.SparseArray; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.TextView; + +import no.nordicsemi.android.nrftoolbox.R; + +/** + * Created by rora on 02.09.2016. + */ +public class CGMSRecordsAdapter extends BaseAdapter { + + private SparseArray mRecords; + private LayoutInflater mInflator; + private Context context; + + public CGMSRecordsAdapter(Context context) { + super(); + mRecords = new SparseArray<>(); + this.context = context; + mInflator = (LayoutInflater) this.context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + } + + @Override + public int getCount() { + return mRecords.size(); + } + + @Override + public Object getItem(int i) { + return null; + } + + @Override + public long getItemId(int i) { + return i; + } + + @Override + public View getView(final int position, View convertView, ViewGroup parent) { + + ViewHolder viewHolder; + if (convertView == null) { + convertView = mInflator.inflate(R.layout.activity_feature_cgms_item, null); + viewHolder = new ViewHolder(); + viewHolder.concentration = (TextView) convertView.findViewById(R.id.cgms_concentration); + viewHolder.time = (TextView) convertView.findViewById(R.id.time); + viewHolder.details = (TextView) convertView.findViewById(R.id.details); + convertView.setTag(viewHolder); + } else { + viewHolder = (ViewHolder) convertView.getTag(); + } + + final CGMSRecord cgmsRecord = mRecords.get(position); + final String concentration = String.valueOf(cgmsRecord.reading); + + if (concentration != null && concentration.length() > 0) { + viewHolder.concentration.setText(concentration /*+ " " + context.getString(R.string.cgms_value_unit*/); + viewHolder.time.setText(cgmsRecord.timeStamp); + } + + return convertView; + } + + public void addItem(CGMSRecord record) { + mRecords.put(mRecords.size(), record); + } + + public SparseArray getValues() { + return mRecords; + } + + public void clear() { + mRecords.clear(); + } + + static class ViewHolder { + TextView time; + TextView details; + TextView concentration; + } +} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/cgms/CGMService.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/cgms/CGMService.java new file mode 100644 index 00000000..3e91712d --- /dev/null +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/cgms/CGMService.java @@ -0,0 +1,267 @@ +package no.nordicsemi.android.nrftoolbox.cgms; + +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.support.v4.content.LocalBroadcastManager; +import android.support.v7.app.NotificationCompat; +import android.util.SparseArray; + +import no.nordicsemi.android.log.Logger; +import no.nordicsemi.android.nrftoolbox.FeaturesActivity; +import no.nordicsemi.android.nrftoolbox.R; +import no.nordicsemi.android.nrftoolbox.profile.BleManager; +import no.nordicsemi.android.nrftoolbox.profile.BleProfileService; + +/** + * Created by rora on 05.09.2016. + */ +public class CGMService extends BleProfileService implements CGMSManagerCallbacks { + private static final String ACTION_DISCONNECT = "no.nordicsemi.android.nrftoolbox.cgms.ACTION_DISCONNECT"; + public static final String BROADCAST_CGMS_VALUES = "no.nordicsemi.android.nrftoolbox.cgms.BROADCAST_CGMS_VALUES"; + public static final String OPERATION_STARTED = "no.nordicsemi.android.nrftoolbox.cgms.OPERATION_STARTED"; + public static final String OPERATION_COMPLETED = "no.nordicsemi.android.nrftoolbox.cgms.OPERATION_COMPLETED"; + public static final String OPERATION_SUPPORTED = "no.nordicsemi.android.nrftoolbox.cgms.OPERATION_SUPPORTED"; + public static final String OPERATION_NOT_SUPPORTED = "no.nordicsemi.android.nrftoolbox.cgms.OPERATION_NOT_SUPPORTED"; + public static final String OPERATION_FAILED = "no.nordicsemi.android.nrftoolbox.cgms.OPERATION_FAILED"; + public static final String OPERATION_ABORTED = "no.nordicsemi.android.nrftoolbox.cgms.OPERATION_ABORTED"; + public static final String EXTRA_CGMS_RECORD = "no.nordicsemi.android.nrftoolbox.cgms.EXTRA_CGMS_RECORD"; + public static final String BROADCAST_DATA_SET_CHANGED = "no.nordicsemi.android.nrftoolbox.cgms.BROADCAST_DATA_SET_CHANGED"; + public static final String EXTRA_DATA = "no.nordicsemi.android.nrftoolbox.cgms.EXTRA_DATA"; + + private final static int NOTIFICATION_ID = 229; + private final static int OPEN_ACTIVITY_REQ = 0; + private final static int DISCONNECT_REQ = 1; + + private CGMSManager mManager; + private final LocalBinder mBinder = new CGMSBinder(); + private SparseArray mRecords = new SparseArray<>(); + + /** + * This local binder is an interface for the bonded activity to operate with the RSC sensor + */ + + public class CGMSBinder extends LocalBinder { + public SparseArray getCgmsRecords() { + return mRecords; + } + + /** + * Sends the request to obtain the last (most recent) record from glucose device. The data will be returned to Glucose Measurement characteristic as a notification followed by Record Access + * Control Point indication with status code ({@link CGMSManager#RESPONSE_SUCCESS} or other in case of error. + */ + public void getLastRecord() { + clear(); + if(mManager != null) + mManager.getLastRecord(); + } + + /** + * Returns all records as a sparse array where sequence number is the key. + * + * @return the records list + */ + public SparseArray getRecords() { + return mRecords; + } + + /** + * Clears the records list locally + */ + public void clear() { + mRecords.clear(); + } + + /** + * Sends abort operation signal to the device + */ + public void abort() { + + if(mManager != null) + mManager.abort(); + } + + /** + * Sends the request to obtain the first (oldest) record from glucose device. The data will be returned to Glucose Measurement characteristic as a notification followed by Record Access Control + * Point indication with status code ({@link CGMSManager# RESPONSE_SUCCESS} or other in case of error. + */ + public void getFirstRecord() { + if(mManager != null) + mManager.getFirstRecord(); + } + + /** + * Sends the request to obtain all records from glucose device. Initially we want to notify him/her about the number of the records so the {@link CGMSManager#OP_CODE_REPORT_NUMBER_OF_RECORDS} is send. The + * data will be returned to Glucose Measurement characteristic as a notification followed by Record Access Control Point indication with status code ({@link CGMSManager#RESPONSE_SUCCESS} or other in case of + * error. + */ + public void getAllRecords() { + clear(); + if(mManager != null) + mManager.getAllRecords(); + } + + public void deleteAllRecords() { + + if(mManager != null) + mManager.deleteAllRecords(); + } + } + + @Override + protected LocalBinder getBinder() { + return mBinder; + } + + @Override + protected BleManager initializeManager() { + return mManager = new CGMSManager(this); + } + + + @Override + public void onCreate() { + super.onCreate(); + final IntentFilter filter = new IntentFilter(); + filter.addAction(ACTION_DISCONNECT); + registerReceiver(mDisconnectActionBroadcastReceiver, filter); + } + + @Override + public void onDestroy() { + // when user has disconnected from the sensor, we have to cancel the notification that we've created some milliseconds before using unbindService + cancelNotification(); + unregisterReceiver(mDisconnectActionBroadcastReceiver); + + super.onDestroy(); + } + + @Override + protected void onRebind() { + // when the activity rebinds to the service, remove the notification + cancelNotification(); + } + + @Override + protected void onUnbind() { + // when the activity closes we need to show the notification that user is connected to the sensor + createNotification(R.string.csc_notification_connected_message, 0); + } + + @Override + protected void onServiceStarted() { + // logger is now available. Assign it to the manager + mManager.setLogger(getLogSession()); + } + + /** + * Creates the notification + * + * @param messageResId + * the message resource id. The message must have one String parameter,
+ * f.e. <string name="name">%s is connected</string> + * @param defaults + * signals that will be used to notify the user + */ + private void createNotification(final int messageResId, final int defaults) { + final Intent parentIntent = new Intent(this, FeaturesActivity.class); + parentIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + final Intent targetIntent = new Intent(this, CGMSActivity.class); + + final Intent disconnect = new Intent(ACTION_DISCONNECT); + final PendingIntent disconnectAction = PendingIntent.getBroadcast(this, DISCONNECT_REQ, disconnect, PendingIntent.FLAG_UPDATE_CURRENT); + + // both activities above have launchMode="singleTask" in the AndroidManifest.xml file, so if the task is already running, it will be resumed + final PendingIntent pendingIntent = PendingIntent.getActivities(this, OPEN_ACTIVITY_REQ, new Intent[] { parentIntent, targetIntent }, PendingIntent.FLAG_UPDATE_CURRENT); + final NotificationCompat.Builder builder = new NotificationCompat.Builder(this); + builder.setContentIntent(pendingIntent); + builder.setContentTitle(getString(R.string.app_name)).setContentText(getString(messageResId, getDeviceName())); + builder.setSmallIcon(R.drawable.ic_stat_notify_cgms); + builder.setShowWhen(defaults != 0).setDefaults(defaults).setAutoCancel(true).setOngoing(true); + builder.addAction(new NotificationCompat.Action(R.drawable.ic_action_bluetooth, getString(R.string.csc_notification_action_disconnect), disconnectAction)); + + final Notification notification = builder.build(); + final NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + nm.notify(NOTIFICATION_ID, notification); + } + + /** + * Cancels the existing notification. If there is no active notification this method does nothing + */ + private void cancelNotification() { + final NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); + nm.cancel(NOTIFICATION_ID); + } + + /** + * This broadcast receiver listens for {@link #ACTION_DISCONNECT} that may be fired by pressing Disconnect action button on the notification. + */ + private final BroadcastReceiver mDisconnectActionBroadcastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(final Context context, final Intent intent) { + Logger.i(getLogSession(), "[Notification] Disconnect action pressed"); + if (isConnected()) + getBinder().disconnect(); + else + stopSelf(); + } + }; + + @Override + public void onCGMValueReceived(float value, String timeStamp) { + final Intent broadcast = new Intent(BROADCAST_CGMS_VALUES); + CGMSRecord cgmsRecord = new CGMSRecord(value, timeStamp); + broadcast.putExtra(EXTRA_CGMS_RECORD, cgmsRecord); + LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); + mRecords.put(mRecords.size(), cgmsRecord); + } + + @Override + public void onOperationStarted() { + final Intent broadcast = new Intent(OPERATION_STARTED); + broadcast.putExtra(EXTRA_DATA, true); + LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); + } + + @Override + public void onOperationCompleted() { + final Intent broadcast = new Intent(OPERATION_COMPLETED); + broadcast.putExtra(EXTRA_DATA, true); + LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); + } + + @Override + public void onOperationFailed() { + final Intent broadcast = new Intent(OPERATION_FAILED); + broadcast.putExtra(EXTRA_DATA, true); + LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); + } + + @Override + public void onOperationAborted() { + final Intent broadcast = new Intent(OPERATION_ABORTED); + broadcast.putExtra(EXTRA_DATA, true); + LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); + } + + @Override + public void onOperationNotSupported() { + final Intent broadcast = new Intent(OPERATION_NOT_SUPPORTED); + broadcast.putExtra(EXTRA_DATA, false); + LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); + } + + @Override + public void onDatasetChanged() { + + } + + @Override + public void onNumberOfRecordsRequested(int value) { + showToast(getString(R.string.gls_progress, value)); + } +} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/profile/BleProfileActivity.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/profile/BleProfileActivity.java index aaec5229..bdb0d8ae 100644 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/profile/BleProfileActivity.java +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/profile/BleProfileActivity.java @@ -265,6 +265,7 @@ public abstract class BleProfileActivity extends AppCompatActivity implements Bl mConnectButton.setText(R.string.action_connect); mDeviceNameView.setText(getDefaultDeviceName()); mBatteryLevelView.setText(R.string.not_available); + mConnectButton.setEnabled(true); } }); } diff --git a/app/src/main/res/drawable-hdpi/ic_cgms_feature.png b/app/src/main/res/drawable-hdpi/ic_cgms_feature.png new file mode 100644 index 0000000000000000000000000000000000000000..6e05b926b6ee59bbf0861cf19522140eeb1ff141 GIT binary patch literal 2926 zcmai$`#aN*`^R4+h8%O4m|>Pf4sVBYKE)g+IaHKW6wN8hDI4A_MHc3iGgA&ZlS9qf z8pU!Zp~x^+isl$9KHmSp_j}#f^L|{{q@0WHd%al&{Bkd3heOrIm z(Ax;T>ojk~Am(>6YC2a)Q#H8;8sc`9SI=fzm0kY7QHH#1`6Pnkc3wMh9tp_2$`g9# zSIh@Qfyu}%l9ezQEHx^duCbcN?7C>R_);c<#fN271un6AnnQqkGzTKv0S|#BkSe7a z&O(#IX@P!#m=6-5%KF`3prm;3?H*kM#iTKf9vnZxo4#bJSt^iwISfj@fv|NMo_4!t zqrH@vhM1R|8D0%bqW_6__!VLX-B{Lr$nZ6L|$EwsJF%It*P7ip6#;i!Gk1^f@kfX|)IfIo?Bk0Bm6%BqT1A+`lCCb6P#8xn4 zO8uvFBMqz88|R)Ir`OlE`>(YvB*G}wGMEd;BW>f$GI|a9DN0fbv40BZtB^k-XdH5$ zmB7QRy;+mW1dL^Hh3M2FZ;XgGzy~O8-*3*N6U8V$^;2X

1V{hPoSXI8>ra?~XmD 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-hdpi/ic_stat_notify_cgms.png b/app/src/main/res/drawable-hdpi/ic_stat_notify_cgms.png new file mode 100644 index 0000000000000000000000000000000000000000..2b783a8c532423e3817a6eab2275133435390e3f GIT binary patch literal 659 zcmV;E0&M+>P)}7OL(mi=8U#)9Sa=r3%e{MNHhY8MzRm7j&c5^9bI#1{NImuRzX=e~ z1O}Jm;h(F4jopB{2}~^G_a}f^{?LDH0&H6%TZ@R*l07FMkYXgb2|O{R&DN?I_pC!Vb# zNmoidnvxdHth9$Vl+^ua24)*D>EqoV)9>R`1s=yzH?*MwqXnShAOdQ@y-ZUNXU0w7 zSKu+)>6X6+7zgeHcY(9M0>jB3Wmj1+UfbHYJnPfIRFwzsZD6xQUd#b%05hJ&e&AX1 z`IP`O3Ox4jbzmOY(kA|8E4x*|90~g2EwHL+lWdh0*xGd%f5X8{lP>|^fbAZ$%bySU z_$qL?3K#)>K6#OjXI9w=aK6eWephnF0i5&LJyox$@q`0&5_nqKigM$R2|%C2y(&eP z0F>fhmo!&;M&+oM5c@SU?qH`dsU^D}QL57`c{l3=zPDLr*H-$%Xuvw)RSwLhZp~w< tnexA>J0t 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

^+#g( znu_nJe&CLIUZKUd<6usR%^~x5g;PKG>TeK_{r=HTc7IEJ_*2at4C(^6fdRqb>FVdQ I&MBb@0B7EhsQ>@~ literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/ic_stat_notify_cgms.png b/app/src/main/res/drawable-xhdpi/ic_stat_notify_cgms.png new file mode 100644 index 0000000000000000000000000000000000000000..4ff0268acfa2f19920e0b80407548eaf086e947f GIT binary patch literal 844 zcmV-S1GD^zP))C|U-I!61P&8f+{C6)a4VMo}<|g(nda zv9Jjedn1Tw1VKbZ5P$IGQixte3k~>7OprV_b1*Kudpolm_X7Jice}Uq&inn$%=g`j zWXO;q!;Awk2bj?YT$o1gMj!_?8TS&MU@r+WmZ^XPz-Ru&PoOsuBaDf@Xz~*YiXSE% zZ-7}z7}4(`Zi-6^3=$2`VtESK;X_)C6JDSPJ9JskrN!3+e*#D^JBn|`9*_ z%rkbH^5+P!(70X-d<5D&bF9-jKIP%PmsGKJBt-lZz1@1pJ^;4*@%iv8U@q|9;C(k8 zrF1uN5x54N0&)Su@@ATZ@GSx!0&5Lp?gEqApv0SvD9>~O1IBfGPzMKeXm1}h?tL;1 z`k!5z0PBH*alKs&90(b=!1(?c=uD%H9X&xdTWQ@}4^g?@I4Hfom^KVO&D^=XOk zwm7NVe8!IftJ2zWHab+4y#~i~Kcj4+Vi++dB8`qU{kVvflu~_ZZJF)F!%$LsYOF)1 zl!(ag7;_esQhSoh`x?h_iyg;(6Om0o*%7H3FzPt2MR#P9S_M2KftQ WBxC=td^AA-0000UAo literal 0 HcmV?d00001 diff --git a/app/src/main/res/layout-land/activity_feature_bpm.xml b/app/src/main/res/layout-land/activity_feature_bpm.xml index 6d6e34bf..55e980f9 100644 --- a/app/src/main/res/layout-land/activity_feature_bpm.xml +++ b/app/src/main/res/layout-land/activity_feature_bpm.xml @@ -24,7 +24,7 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" - tools:context=".BPMActivity"> + tools:context=".bpm.BPMActivity"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +