/*
* Copyright 2015 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.github.webbluetoothcg.bletestperipheral;
import android.app.Activity;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattDescriptor;
import android.bluetooth.BluetoothGattService;
import android.os.Bundle;
import android.os.ParcelUuid;
import android.util.Log;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.view.inputmethod.EditorInfo;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemSelectedListener;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Spinner;
import android.widget.TextView;
import android.widget.TextView.OnEditorActionListener;
import android.widget.Toast;
import java.util.Arrays;
import java.util.UUID;
public class HeartRateServiceFragment extends ServiceFragment {
private static final String TAG = HeartRateServiceFragment.class.getCanonicalName();
private static final int MIN_UINT = 0;
private static final int MAX_UINT8 = (int) Math.pow(2, 8) - 1;
private static final int MAX_UINT16 = (int) Math.pow(2, 16) - 1;
/**
* See <a href="https://developer.bluetooth.org/gatt/services/Pages/ServiceViewer.aspx?u=org.bluetooth.service.heart_rate.xml">
* Heart Rate Service</a>
*/
private static final UUID HEART_RATE_SERVICE_UUID = UUID
.fromString("0000180D-0000-1000-8000-00805f9b34fb");
/**
* See <a href="https://developer.bluetooth.org/gatt/characteristics/Pages/CharacteristicViewer.aspx?u=org.bluetooth.characteristic.heart_rate_measurement.xml">
* Heart Rate Measurement</a>
*/
private static final UUID HEART_RATE_MEASUREMENT_UUID = UUID
.fromString("00002A37-0000-1000-8000-00805f9b34fb");
private static final int HEART_RATE_MEASUREMENT_VALUE_FORMAT = BluetoothGattCharacteristic.FORMAT_UINT8;
private static final int INITIAL_HEART_RATE_MEASUREMENT_VALUE = 60;
private static final int EXPENDED_ENERGY_FORMAT = BluetoothGattCharacteristic.FORMAT_UINT16;
private static final int INITIAL_EXPENDED_ENERGY = 0;
private static final String HEART_RATE_MEASUREMENT_DESCRIPTION = "Used to send a heart rate " +
"measurement";
/**
* See <a href="https://developer.bluetooth.org/gatt/characteristics/Pages/CharacteristicViewer.aspx?u=org.bluetooth.characteristic.body_sensor_location.xml">
* Body Sensor Location</a>
*/
private static final UUID BODY_SENSOR_LOCATION_UUID = UUID
.fromString("00002A38-0000-1000-8000-00805f9b34fb");
private static final int LOCATION_OTHER = 0;
/**
* See <a href="https://developer.bluetooth.org/gatt/characteristics/Pages/CharacteristicViewer.aspx?u=org.bluetooth.characteristic.heart_rate_control_point.xml">
* Heart Rate Control Point</a>
*/
private static final UUID HEART_RATE_CONTROL_POINT_UUID = UUID
.fromString("00002A39-0000-1000-8000-00805f9b34fb");
private BluetoothGattService mHeartRateService;
private BluetoothGattCharacteristic mHeartRateMeasurementCharacteristic;
private BluetoothGattCharacteristic mBodySensorLocationCharacteristic;
private BluetoothGattCharacteristic mHeartRateControlPoint;
private ServiceFragmentDelegate mDelegate;
private EditText mEditTextHeartRateMeasurement;
private final OnEditorActionListener mOnEditorActionListenerHeartRateMeasurement = new OnEditorActionListener() {
@Override
public boolean onEditorAction(TextView textView, int actionId, KeyEvent event) {
if (actionId == EditorInfo.IME_ACTION_DONE) {
String newHeartRateMeasurementValueString = textView.getText().toString();
if (isValidCharacteristicValue(newHeartRateMeasurementValueString,
HEART_RATE_MEASUREMENT_VALUE_FORMAT)) {
int newHeartRateMeasurementValue = Integer.parseInt(newHeartRateMeasurementValueString);
mHeartRateMeasurementCharacteristic.setValue(newHeartRateMeasurementValue,
HEART_RATE_MEASUREMENT_VALUE_FORMAT,
/* offset */ 1);
} else {
Toast.makeText(getActivity(), R.string.heartRateMeasurementValueInvalid,
Toast.LENGTH_SHORT).show();
}
}
return false;
}
};
private final OnEditorActionListener mOnEditorActionListenerEnergyExpended = new OnEditorActionListener() {
@Override
public boolean onEditorAction(TextView textView, int actionId, KeyEvent event) {
if (actionId == EditorInfo.IME_ACTION_DONE) {
String newEnergyExpendedString = textView.getText().toString();
if (isValidCharacteristicValue(newEnergyExpendedString,
EXPENDED_ENERGY_FORMAT)) {
int newEnergyExpended = Integer.parseInt(newEnergyExpendedString);
mHeartRateMeasurementCharacteristic.setValue(newEnergyExpended,
EXPENDED_ENERGY_FORMAT,
/* offset */ 2);
} else {
Toast.makeText(getActivity(), R.string.energyExpendedInvalid,
Toast.LENGTH_SHORT).show();
}
}
return false;
}
};
private EditText mEditTextEnergyExpended;
private Spinner mSpinnerBodySensorLocation;
private final OnItemSelectedListener mLocationSpinnerOnItemSelectedListener =
new OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
setBodySensorLocationValue(position);
}
@Override
public void onNothingSelected(AdapterView<?> parent) {
}
};
private final OnClickListener mNotifyButtonListener = new OnClickListener() {
@Override
public void onClick(View v) {
mDelegate.sendNotificationToDevices(mHeartRateMeasurementCharacteristic);
}
};
public HeartRateServiceFragment() {
mHeartRateMeasurementCharacteristic =
new BluetoothGattCharacteristic(HEART_RATE_MEASUREMENT_UUID,
BluetoothGattCharacteristic.PROPERTY_NOTIFY,
/* No permissions */ 0);
mHeartRateMeasurementCharacteristic.addDescriptor(
Peripheral.getClientCharacteristicConfigurationDescriptor());
mHeartRateMeasurementCharacteristic.addDescriptor(
Peripheral.getCharacteristicUserDescriptionDescriptor(HEART_RATE_MEASUREMENT_DESCRIPTION));
mBodySensorLocationCharacteristic =
new BluetoothGattCharacteristic(BODY_SENSOR_LOCATION_UUID,
BluetoothGattCharacteristic.PROPERTY_READ,
BluetoothGattCharacteristic.PERMISSION_READ);
mHeartRateControlPoint =
new BluetoothGattCharacteristic(HEART_RATE_CONTROL_POINT_UUID,
BluetoothGattCharacteristic.PROPERTY_WRITE,
BluetoothGattCharacteristic.PERMISSION_WRITE);
mHeartRateService = new BluetoothGattService(HEART_RATE_SERVICE_UUID,
BluetoothGattService.SERVICE_TYPE_PRIMARY);
mHeartRateService.addCharacteristic(mHeartRateMeasurementCharacteristic);
mHeartRateService.addCharacteristic(mBodySensorLocationCharacteristic);
mHeartRateService.addCharacteristic(mHeartRateControlPoint);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_heart_rate, container, false);
mSpinnerBodySensorLocation = (Spinner) view.findViewById(R.id.spinner_bodySensorLocation);
mSpinnerBodySensorLocation.setOnItemSelectedListener(mLocationSpinnerOnItemSelectedListener);
mEditTextHeartRateMeasurement = (EditText) view
.findViewById(R.id.editText_heartRateMeasurementValue);
mEditTextHeartRateMeasurement
.setOnEditorActionListener(mOnEditorActionListenerHeartRateMeasurement);
mEditTextEnergyExpended = (EditText) view
.findViewById(R.id.editText_energyExpended);
mEditTextEnergyExpended
.setOnEditorActionListener(mOnEditorActionListenerEnergyExpended);
Button notifyButton = (Button) view.findViewById(R.id.button_heartRateMeasurementNotify);
notifyButton.setOnClickListener(mNotifyButtonListener);
setHeartRateMeasurementValue(INITIAL_HEART_RATE_MEASUREMENT_VALUE,
INITIAL_EXPENDED_ENERGY);
setBodySensorLocationValue(LOCATION_OTHER);
return view;
}
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
try {
mDelegate = (ServiceFragmentDelegate) activity;
} catch (ClassCastException e) {
throw new ClassCastException(activity.toString()
+ " must implement ServiceFragmentDelegate");
}
}
@Override
public void onDetach() {
super.onDetach();
mDelegate = null;
}
@Override
public BluetoothGattService getBluetoothGattService() {
return mHeartRateService;
}
@Override
public ParcelUuid getServiceUUID() {
return new ParcelUuid(HEART_RATE_SERVICE_UUID);
}
private void setHeartRateMeasurementValue(int heartRateMeasurementValue, int expendedEnergy) {
Log.d(TAG, Arrays.toString(mHeartRateMeasurementCharacteristic.getValue()));
/* Set the org.bluetooth.characteristic.heart_rate_measurement
* characteristic to a byte array of size 4 so
* we can use setValue(value, format, offset);
*
* Flags (8bit) + Heart Rate Measurement Value (uint8) + Energy Expended (uint16) = 4 bytes
*
* Flags = 1 << 3:
* Heart Rate Format (0) -> UINT8
* Sensor Contact Status (00) -> Not Supported
* Energy Expended (1) -> Field Present
* RR-Interval (0) -> Field not pressent
* Unused (000)
*/
mHeartRateMeasurementCharacteristic.setValue(new byte[]{0b00001000, 0, 0, 0});
// Characteristic Value: [flags, 0, 0, 0]
mHeartRateMeasurementCharacteristic.setValue(heartRateMeasurementValue,
HEART_RATE_MEASUREMENT_VALUE_FORMAT,
/* offset */ 1);
// Characteristic Value: [flags, heart rate value, 0, 0]
mEditTextHeartRateMeasurement.setText(Integer.toString(heartRateMeasurementValue));
mHeartRateMeasurementCharacteristic.setValue(expendedEnergy,
EXPENDED_ENERGY_FORMAT,
/* offset */ 2);
// Characteristic Value: [flags, heart rate value, energy expended (LSB), energy expended (MSB)]
mEditTextEnergyExpended.setText(Integer.toString(expendedEnergy));
}
private void setBodySensorLocationValue(int location) {
mBodySensorLocationCharacteristic.setValue(new byte[]{(byte) location});
mSpinnerBodySensorLocation.setSelection(location);
}
private boolean isValidCharacteristicValue(String s, int format) {
try {
int value = Integer.parseInt(s);
if (format == BluetoothGattCharacteristic.FORMAT_UINT8) {
return (value >= MIN_UINT) && (value <= MAX_UINT8);
} else if (format == BluetoothGattCharacteristic.FORMAT_UINT16) {
return (value >= MIN_UINT) && (value <= MAX_UINT16);
} else {
throw new IllegalArgumentException(format + " is not a valid argument");
}
} catch (NumberFormatException e) {
return false;
}
}
@Override
public int writeCharacteristic(BluetoothGattCharacteristic characteristic, int offset, byte[] value) {
if (offset != 0) {
return BluetoothGatt.GATT_INVALID_OFFSET;
}
// Heart Rate control point is a 8bit characteristic
if (value.length != 1) {
return BluetoothGatt.GATT_INVALID_ATTRIBUTE_LENGTH;
}
if ((value[0] & 1) == 1) {
getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
mHeartRateMeasurementCharacteristic.setValue(INITIAL_EXPENDED_ENERGY,
EXPENDED_ENERGY_FORMAT, /* offset */ 2);
mEditTextEnergyExpended.setText(Integer.toString(INITIAL_EXPENDED_ENERGY));
}
});
}
return BluetoothGatt.GATT_SUCCESS;
}
@Override
public void notificationsEnabled(BluetoothGattCharacteristic characteristic, boolean indicate) {
if (characteristic.getUuid() != HEART_RATE_MEASUREMENT_UUID) {
return;
}
if (indicate) {
return;
}
getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(getActivity(), R.string.notificationsEnabled, Toast.LENGTH_SHORT)
.show();
}
});
}
@Override
public void notificationsDisabled(BluetoothGattCharacteristic characteristic) {
if (characteristic.getUuid() != HEART_RATE_MEASUREMENT_UUID) {
return;
}
getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(getActivity(), R.string.notificationsNotEnabled, Toast.LENGTH_SHORT)
.show();
}
});
}
}