/*
* Copyright 2017 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.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.EditorInfo;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.TextView.OnEditorActionListener;
import android.widget.Toast;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Timer;
import java.util.TimerTask;
import java.util.UUID;
public class HealthThermometerServiceFragment extends ServiceFragment {
/**
* See <a href="https://developer.bluetooth.org/gatt/services/Pages/ServiceViewer.aspx?u=org.bluetooth.service.health_thermometer.xml">
* Health Thermometer Service</a>
* This service exposes two characteristics with descriptors:
* - Measurement Interval Characteristic:
* - Listen to notifications to from which you can subscribe to notifications
* - CCCD Descriptor:
* - Read/Write to get/set notifications.
* - User Description Descriptor:
* - Read/Write to get/set the description of the Characteristic.
* - Temperature Measurement Characteristic:
* - Read value to get the current interval of the temperature measurement timer.
* - Write value resets the temperature measurement timer with the new value. This timer
* is responsible for triggering value changed events every "Measurement Interval" value.
* - CCCD Descriptor:
* - Read/Write to get/set notifications.
* - User Description Descriptor:
* - Read/Write to get/set the description of the Characteristic.
*/
private static final UUID HEALTH_THERMOMETER_SERVICE_UUID = UUID
.fromString("00001809-0000-1000-8000-00805f9b34fb");
/**
* See <a href="https://developer.bluetooth.org/gatt/characteristics/Pages/CharacteristicViewer.aspx?u=org.bluetooth.characteristic.temperature_measurement.xml">
* Temperature Measurement</a>
*/
private static final UUID TEMPERATURE_MEASUREMENT_UUID = UUID
.fromString("00002A1C-0000-1000-8000-00805f9b34fb");
private static final int TEMPERATURE_MEASUREMENT_VALUE_FORMAT = BluetoothGattCharacteristic.FORMAT_FLOAT;
private static final float INITIAL_TEMPERATURE_MEASUREMENT_VALUE = 37.0f;
private static final int EXPONENT_MASK = 0x7f800000;
private static final int EXPONENT_SHIFT = 23;
private static final int MANTISSA_MASK = 0x007fffff;
private static final int MANTISSA_SHIFT = 0;
private static final String TEMPERATURE_MEASUREMENT_DESCRIPTION = "This characteristic is used " +
"to send a temperature measurement.";
/**
* See <a href="https://developer.bluetooth.org/gatt/characteristics/Pages/CharacteristicViewer.aspx?u=org.bluetooth.characteristic.measurement_interval.xml">
* Measurement Interval</a>
*/
private static final UUID MEASUREMENT_INTERVAL_UUID = UUID
.fromString("00002A21-0000-1000-8000-00805f9b34fb");
private static final int MEASUREMENT_INTERVAL_FORMAT = BluetoothGattCharacteristic.FORMAT_UINT16;
private static final int INITIAL_MEASUREMENT_INTERVAL = 1;
private static final int MIN_MEASUREMENT_INTERVAL = 1;
private static final int MAX_MEASUREMENT_INTERVAL = (int) Math.pow(2, 16) - 1;
private static final String MEASUREMENT_INTERVAL_DESCRIPTION = "This characteristic is used " +
"to enable and control the interval between consecutive temperature measurements.";
private BluetoothGattService mHealthThermometerService;
private BluetoothGattCharacteristic mTemperatureMeasurementCharacteristic;
private BluetoothGattCharacteristic mMeasurementIntervalCharacteristic;
private BluetoothGattDescriptor mMeasurementIntervalCCCDescriptor;
private ServiceFragmentDelegate mDelegate;
private Timer mTimer;
private EditText mEditTextTemperatureMeasurement;
private final OnEditorActionListener mOnEditorActionListenerTemperatureMeasurement = new OnEditorActionListener() {
@Override
public boolean onEditorAction(TextView textView, int actionId, KeyEvent event) {
if (actionId == EditorInfo.IME_ACTION_DONE) {
String newTemperatureMeasurementValueString = textView.getText().toString();
if (isValidTemperatureMeasurementValue(newTemperatureMeasurementValueString)) {
float newTemperatureMeasurementValue = Float.valueOf(newTemperatureMeasurementValueString);
setTemperatureMeasurementValue(newTemperatureMeasurementValue);
} else {
Toast.makeText(getActivity(), R.string.temperatureMeasurementValueInvalid,
Toast.LENGTH_SHORT).show();
}
}
return false;
}
};
private final OnEditorActionListener mOnEditorActionListenerMeasurementInterval = new OnEditorActionListener() {
@Override
public boolean onEditorAction(TextView textView, int actionId, KeyEvent event) {
if (actionId == EditorInfo.IME_ACTION_DONE) {
int newMeasurementInterval = Integer.parseInt(textView.getText().toString());
if (isValidMeasurementIntervalValue(newMeasurementInterval)) {
mMeasurementIntervalCharacteristic.setValue(newMeasurementInterval,
MEASUREMENT_INTERVAL_FORMAT,
/* offset */ 0);
resetTimer(newMeasurementInterval);
} else {
Toast.makeText(getActivity(), R.string.measurementIntervalInvalid,
Toast.LENGTH_SHORT).show();
}
}
return false;
}
};
private EditText mEditTextMeasurementInterval;
private TextView mTextViewNotifications;
public HealthThermometerServiceFragment() {
mTemperatureMeasurementCharacteristic =
new BluetoothGattCharacteristic(TEMPERATURE_MEASUREMENT_UUID,
BluetoothGattCharacteristic.PROPERTY_INDICATE,
/* No permissions */ 0);
mTemperatureMeasurementCharacteristic.addDescriptor(
Peripheral.getClientCharacteristicConfigurationDescriptor());
mTemperatureMeasurementCharacteristic.addDescriptor(
Peripheral.getCharacteristicUserDescriptionDescriptor(TEMPERATURE_MEASUREMENT_DESCRIPTION));
mMeasurementIntervalCharacteristic =
new BluetoothGattCharacteristic(
MEASUREMENT_INTERVAL_UUID,
(BluetoothGattCharacteristic.PROPERTY_READ |
BluetoothGattCharacteristic.PROPERTY_WRITE |
BluetoothGattCharacteristic.PROPERTY_INDICATE),
(BluetoothGattCharacteristic.PERMISSION_READ |
BluetoothGattCharacteristic.PERMISSION_WRITE));
mMeasurementIntervalCCCDescriptor = Peripheral.getClientCharacteristicConfigurationDescriptor();
mMeasurementIntervalCharacteristic.addDescriptor(mMeasurementIntervalCCCDescriptor);
mMeasurementIntervalCharacteristic.addDescriptor(
Peripheral.getCharacteristicUserDescriptionDescriptor(MEASUREMENT_INTERVAL_DESCRIPTION));
mHealthThermometerService = new BluetoothGattService(HEALTH_THERMOMETER_SERVICE_UUID,
BluetoothGattService.SERVICE_TYPE_PRIMARY);
mHealthThermometerService.addCharacteristic(mTemperatureMeasurementCharacteristic);
mHealthThermometerService.addCharacteristic(mMeasurementIntervalCharacteristic);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_health_thermometer, container, false);
mEditTextTemperatureMeasurement = (EditText) view
.findViewById(R.id.editText_temperatureMeasurementValue);
mEditTextTemperatureMeasurement
.setOnEditorActionListener(mOnEditorActionListenerTemperatureMeasurement);
mEditTextMeasurementInterval = (EditText) view
.findViewById(R.id.editText_measurementIntervalValue);
mEditTextMeasurementInterval
.setOnEditorActionListener(mOnEditorActionListenerMeasurementInterval);
mEditTextTemperatureMeasurement.setText(Float.toString(INITIAL_TEMPERATURE_MEASUREMENT_VALUE));
setTemperatureMeasurementValue(INITIAL_TEMPERATURE_MEASUREMENT_VALUE);
mMeasurementIntervalCharacteristic.setValue(INITIAL_MEASUREMENT_INTERVAL,
MEASUREMENT_INTERVAL_FORMAT,
/* offset */ 0);
mEditTextMeasurementInterval.setText(Integer.toString(INITIAL_MEASUREMENT_INTERVAL));
mTextViewNotifications = (TextView) view.findViewById(R.id.textView_notifications);
mTextViewNotifications.setText(R.string.notificationsNotEnabled);
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 void onStop() {
super.onStop();
cancelTimer();
}
@Override
public BluetoothGattService getBluetoothGattService() {
return mHealthThermometerService;
}
@Override
public ParcelUuid getServiceUUID() {
return new ParcelUuid(HEALTH_THERMOMETER_SERVICE_UUID);
}
private void setTemperatureMeasurementValue(float temperatureMeasurementValue) {
/* Set the org.bluetooth.characteristic.temperature_measurement
* characteristic to a byte array of size 5 so
* we can use setValue(value, format, offset);
*
* Flags (8bit) + Temperature Measurement Value (float) = 5 bytes
*
* Flags:
* Temperature Units Flag (0) -> Celsius
* Time Stamp Flag (0) -> Time Stamp field not present
* Temperature Type Flag (0) -> Temperature Type field not present
* Unused (00000)
*/
mTemperatureMeasurementCharacteristic.setValue(new byte[]{0b00000000, 0, 0, 0, 0});
// Characteristic Value: [flags, 0, 0, 0, 0]
int bits = Float.floatToIntBits(temperatureMeasurementValue);
int exponent = (bits & EXPONENT_MASK) >>> EXPONENT_SHIFT;
int mantissa = (bits & MANTISSA_MASK) >>> MANTISSA_SHIFT;
mTemperatureMeasurementCharacteristic.setValue(mantissa, exponent,
TEMPERATURE_MEASUREMENT_VALUE_FORMAT,
/* offset */ 1);
// Characteristic Value: [flags, temperature measurement value]
}
private void setTemperatureMeasurementTimerInterval(int measurementIntervalValueSeconds) {
mTimer = new Timer();
mTimer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
mDelegate.sendNotificationToDevices(mTemperatureMeasurementCharacteristic);
}
});
}
}, 0 /* delay */, measurementIntervalValueSeconds * 1000);
}
private void cancelTimer() {
if (mTimer != null) {
mTimer.cancel();
}
}
private void resetTimer(int measurementIntervalValue) {
cancelTimer();
setTemperatureMeasurementTimerInterval(measurementIntervalValue);
}
private boolean isValidTemperatureMeasurementValue(String s) {
try {
float value = Float.valueOf(s);
return true;
} catch (NumberFormatException e) {
return false;
}
}
private boolean isValidMeasurementIntervalValue(int value) {
return (value >= MIN_MEASUREMENT_INTERVAL) && (value <= MAX_MEASUREMENT_INTERVAL);
}
@Override
public int writeCharacteristic(BluetoothGattCharacteristic characteristic, int offset, byte[] value) {
if (offset != 0) {
return BluetoothGatt.GATT_INVALID_OFFSET;
}
// Measurement Interval is a 16bit characteristic
if (value.length != 2) {
return BluetoothGatt.GATT_INVALID_ATTRIBUTE_LENGTH;
}
ByteBuffer byteBuffer = ByteBuffer.wrap(value);
byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
final int newMeasurementIntervalValue = byteBuffer.getShort();
if (!isValidMeasurementIntervalValue(newMeasurementIntervalValue)) {
return BluetoothGatt.GATT_FAILURE;
}
getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
mMeasurementIntervalCharacteristic.setValue(newMeasurementIntervalValue,
MEASUREMENT_INTERVAL_FORMAT,
/* offset */ 0);
if (mMeasurementIntervalCCCDescriptor.getValue() == BluetoothGattDescriptor.ENABLE_INDICATION_VALUE) {
resetTimer(newMeasurementIntervalValue);
mTextViewNotifications.setText(R.string.notificationsEnabled);
}
}
});
return BluetoothGatt.GATT_SUCCESS;
}
@Override
public void notificationsDisabled(BluetoothGattCharacteristic characteristic) {
if (characteristic.getUuid() != TEMPERATURE_MEASUREMENT_UUID) {
return;
}
cancelTimer();
getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
mTextViewNotifications.setText(R.string.notificationsNotEnabled);
}
});
}
@Override
public void notificationsEnabled(BluetoothGattCharacteristic characteristic, boolean indicate) {
if (characteristic.getUuid() != TEMPERATURE_MEASUREMENT_UUID) {
return;
}
if (!indicate) {
return;
}
getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
int newMeasurementInterval = Integer.parseInt(mEditTextMeasurementInterval.getText()
.toString());
if (isValidMeasurementIntervalValue(newMeasurementInterval)) {
mMeasurementIntervalCharacteristic.setValue(newMeasurementInterval,
MEASUREMENT_INTERVAL_FORMAT,
/* offset */ 0);
resetTimer(newMeasurementInterval);
mTextViewNotifications.setText(R.string.notificationsEnabled);
}
}
});
}
}