package com.betomaluje.miband.bluetooth;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattDescriptor;
import android.content.Context;
import android.util.Log;
import android.widget.Toast;
import com.betomaluje.miband.ActionCallback;
import com.betomaluje.miband.NotifyListener;
import com.betomaluje.miband.model.Profile;
import com.betomaluje.miband.model.Protocol;
import com.betomaluje.miband.models.ActivityData;
import com.betomaluje.miband.sqlite.ActivitySQLite;
import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.List;
import java.util.UUID;
public class BTCommandManager {
private static final String TAG = BTCommandManager.class.getSimpleName();
private ActionCallback currentCallback;
private QueueConsumer mQueueConsumer;
public HashMap<UUID, NotifyListener> notifyListeners = new HashMap<UUID, NotifyListener>();
private Context context;
private BluetoothGatt gatt;
public BTCommandManager(Context context, BluetoothGatt gatt) {
this.context = context;
this.gatt = gatt;
mQueueConsumer = new QueueConsumer(context, this);
Thread t = new Thread(mQueueConsumer);
t.start();
}
public void queueTask(final BLETask task) {
mQueueConsumer.add(task);
}
public QueueConsumer getmQueueConsumer() {
return mQueueConsumer;
}
public void writeAndRead(final UUID uuid, byte[] valueToWrite, final ActionCallback callback) {
ActionCallback readCallback = new ActionCallback() {
@Override
public void onSuccess(Object characteristic) {
BTCommandManager.this.readCharacteristic(uuid, callback);
}
@Override
public void onFail(int errorCode, String msg) {
callback.onFail(errorCode, msg);
}
};
this.writeCharacteristic(uuid, valueToWrite, readCallback);
}
/**
* Sends a command to the Mi Band
*
* @param uuid the {@link Profile} used
* @param value the values to send
* @param callback
*/
public void writeCharacteristic(UUID uuid, byte[] value, ActionCallback callback) {
try {
this.currentCallback = callback;
BluetoothGattCharacteristic chara = gatt.getService(Profile.UUID_SERVICE_MILI).getCharacteristic(uuid);
if (null == chara) {
this.onFail(-1, "BluetoothGattCharacteristic " + uuid + " doesn't exist");
return;
}
chara.setValue(value);
if (!this.gatt.writeCharacteristic(chara)) {
this.onFail(-1, "gatt.writeCharacteristic() return false");
} else {
onSuccess(chara);
}
} catch (Throwable tr) {
Log.e(TAG, "writeCharacteristic", tr);
this.onFail(-1, tr.getMessage());
}
}
public boolean writeCharacteristicWithResponse(UUID service, UUID uuid, byte[] value, ActionCallback callback) {
try {
this.currentCallback = callback;
BluetoothGattCharacteristic chara = gatt.getService(service).getCharacteristic(uuid);
if (null == chara) {
return false;
}
chara.setValue(value);
return this.gatt.writeCharacteristic(chara);
} catch (Throwable tr) {
return false;
}
}
public boolean writeCharacteristicWithResponse(UUID uuid, byte[] value, ActionCallback callback) {
try {
this.currentCallback = callback;
BluetoothGattCharacteristic chara = gatt.getService(Profile.UUID_SERVICE_MILI).getCharacteristic(uuid);
if (null == chara) {
return false;
}
chara.setValue(value);
return this.gatt.writeCharacteristic(chara);
} catch (Throwable tr) {
return false;
}
}
/**
* Reads a command from the Mi Band
*
* @param uuid the {@link Profile} used
* @param callback
*/
public void readCharacteristic(UUID uuid, ActionCallback callback) {
try {
this.currentCallback = callback;
BluetoothGattCharacteristic chara = gatt.getService(Profile.UUID_SERVICE_MILI).getCharacteristic(uuid);
if (null == chara) {
this.onFail(-1, "BluetoothGattCharacteristic " + uuid + " doesn't exist");
return;
}
if (!this.gatt.readCharacteristic(chara)) {
this.onFail(-1, "gatt.readCharacteristic() return false");
}
} catch (Throwable tr) {
Log.e(TAG, "readCharacteristic", tr);
this.onFail(-1, tr.getMessage());
}
}
public boolean readCharacteristicWithResponse(UUID uuid, ActionCallback callback) {
try {
this.currentCallback = callback;
BluetoothGattCharacteristic chara = gatt.getService(Profile.UUID_SERVICE_MILI).getCharacteristic(uuid);
if (null == chara) {
return false;
}
return this.gatt.readCharacteristic(chara);
} catch (Throwable tr) {
Log.e(TAG, "readCharacteristic", tr);
return false;
}
}
/**
* Reads the bluetooth's received signal strength indication
*
* @param callback
*/
public void readRssi(ActionCallback callback) {
try {
this.currentCallback = callback;
this.gatt.readRemoteRssi();
} catch (Throwable tr) {
Log.e(TAG, "readRssi", tr);
this.onFail(-1, tr.getMessage());
}
}
public void setNotifyListener(UUID characteristicId, NotifyListener listener) {
if (this.notifyListeners.containsKey(characteristicId))
return;
BluetoothGattCharacteristic chara = gatt.getService(Profile.UUID_SERVICE_MILI).getCharacteristic(characteristicId);
if (chara == null)
return;
this.gatt.setCharacteristicNotification(chara, true);
BluetoothGattDescriptor descriptor = chara.getDescriptor(Profile.UUID_DESCRIPTOR_UPDATE_NOTIFICATION);
descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
this.gatt.writeDescriptor(descriptor);
this.notifyListeners.put(characteristicId, listener);
}
public void setHighLatency() {
writeCharacteristic(Profile.UUID_CHAR_LE_PARAMS, Protocol.HIGH_LATENCY_LEPARAMS, null);
}
public void onSuccess(Object data) {
if (this.currentCallback != null) {
ActionCallback callback = this.currentCallback;
this.currentCallback = null;
callback.onSuccess(data);
}
}
public void onFail(int errorCode, String msg) {
if (this.currentCallback != null) {
ActionCallback callback = this.currentCallback;
this.currentCallback = null;
callback.onFail(errorCode, msg);
}
}
public void handleControlPointResult(byte[] value) {
if (value != null) {
for (byte b : value) {
Log.i(TAG, "handleControlPoint GOT DATA:" + String.format("0x%8x", b));
}
} else {
Log.e(TAG, "handleControlPoint GOT null");
}
}
//ACTIVITY DATA
//temporary buffer, size is a multiple of 60 because we want to store complete minutes (1 minute = 3 bytes)
private static final int activityDataHolderSize = 3 * 60 * 4; // 8h
private static class ActivityStruct {
public byte[] activityDataHolder = new byte[activityDataHolderSize];
//index of the buffer above
public int activityDataHolderProgress = 0;
//number of bytes we will get in a single data transfer, used as counter
public int activityDataRemainingBytes = 0;
//same as above, but remains untouched for the ack message
public int activityDataUntilNextHeader = 0;
//timestamp of the single data transfer, incremented to store each minute's data
public GregorianCalendar activityDataTimestampProgress = null;
//same as above, but remains untouched for the ack message
public GregorianCalendar activityDataTimestampToAck = null;
}
private ActivityStruct activityStruct;
public void handleActivityNotif(byte[] value) {
boolean firstChunk = activityStruct == null;
if (firstChunk) {
activityStruct = new ActivityStruct();
}
if (value.length == 11) {
// byte 0 is the data type: 1 means that each minute is represented by a triplet of bytes
int dataType = value[0];
// byte 1 to 6 represent a timestamp
GregorianCalendar timestamp = parseTimestamp(value, 1);
// counter of all data held by the band
int totalDataToRead = (value[7] & 0xff) | ((value[8] & 0xff) << 8);
totalDataToRead *= (dataType == 1) ? 3 : 1;
// counter of this data block
int dataUntilNextHeader = (value[9] & 0xff) | ((value[10] & 0xff) << 8);
dataUntilNextHeader *= (dataType == 1) ? 3 : 1;
// there is a total of totalDataToRead that will come in chunks (3 bytes per minute if dataType == 1),
// these chunks are usually 20 bytes long and grouped in blocks
// after dataUntilNextHeader bytes we will get a new packet of 11 bytes that should be parsed
// as we just did
if (firstChunk && dataUntilNextHeader != 0) {
String message = String.format("About to transfer %1$s of data starting from %2$s",
(totalDataToRead / 3),
DateFormat.getDateTimeInstance().format(timestamp.getTime()));
Toast.makeText(context, message, Toast.LENGTH_LONG).show();
}
Log.i(TAG, "total data to read: " + totalDataToRead + " len: " + (totalDataToRead / 3) + " minute(s)");
Log.i(TAG, "data to read until next header: " + dataUntilNextHeader + " len: " + (dataUntilNextHeader / 3) + " minute(s)");
Log.i(TAG, "TIMESTAMP: " + DateFormat.getDateTimeInstance().format(timestamp.getTime()) + " magic byte: " + dataUntilNextHeader);
activityStruct.activityDataRemainingBytes = activityStruct.activityDataUntilNextHeader = dataUntilNextHeader;
activityStruct.activityDataTimestampToAck = (GregorianCalendar) timestamp.clone();
activityStruct.activityDataTimestampProgress = timestamp;
} else {
bufferActivityData(value);
}
Log.e(TAG, "activity data: length: " + value.length + ", remaining bytes: " + activityStruct.activityDataRemainingBytes);
if (activityStruct.activityDataRemainingBytes == 0) {
sendAckDataTransfer(activityStruct.activityDataTimestampToAck, activityStruct.activityDataUntilNextHeader);
}
}
private GregorianCalendar parseTimestamp(byte[] value, int offset) {
GregorianCalendar timestamp = new GregorianCalendar(
value[offset] + 2000,
value[offset + 1],
value[offset + 2],
value[offset + 3],
value[offset + 4],
value[offset + 5]);
return timestamp;
}
private void bufferActivityData(byte[] value) {
if (activityStruct.activityDataRemainingBytes >= value.length) {
//I don't like this clause, but until we figure out why we get different data sometimes this should work
if (value.length == 20 || value.length == activityStruct.activityDataRemainingBytes) {
System.arraycopy(value, 0, activityStruct.activityDataHolder, activityStruct.activityDataHolderProgress, value.length);
activityStruct.activityDataHolderProgress += value.length;
activityStruct.activityDataRemainingBytes -= value.length;
if (this.activityDataHolderSize == activityStruct.activityDataHolderProgress) {
flushActivityDataHolder();
}
} else {
// the length of the chunk is not what we expect. We need to make sense of this data
Log.w(TAG, "GOT UNEXPECTED ACTIVITY DATA WITH LENGTH: " + value.length + ", EXPECTED LENGTH: " + activityStruct.activityDataRemainingBytes);
for (byte b : value) {
Log.w(TAG, "DATA: " + String.format("0x%8x", b));
}
}
} else {
Log.e(TAG, "error buffering activity data: remaining bytes: " + activityStruct.activityDataRemainingBytes + ", received: " + value.length);
}
}
private void flushActivityDataHolder() {
if (activityStruct == null) {
Log.d(TAG, "nothing to flush, struct is already null");
return;
}
byte category, intensity, steps;
ActivitySQLite dbHandler = ActivitySQLite.getInstance(context);
for (int i = 0; i < activityStruct.activityDataHolderProgress; i += 3) { //TODO: check if multiple of 3, if not something is wrong
category = activityStruct.activityDataHolder[i];
intensity = activityStruct.activityDataHolder[i + 1];
steps = activityStruct.activityDataHolder[i + 2];
dbHandler.saveActivity((int) (activityStruct.activityDataTimestampProgress.getTimeInMillis() / 1000),
ActivityData.PROVIDER_MIBAND,
intensity,
steps,
category);
activityStruct.activityDataTimestampProgress.add(Calendar.MINUTE, 1);
}
activityStruct.activityDataHolderProgress = 0;
}
private void sendAckDataTransfer(Calendar time, int bytesTransferred) {
byte[] ack = new byte[]{
Protocol.COMMAND_CONFIRM_ACTIVITY_DATA_TRANSFER_COMPLETE,
(byte) (time.get(Calendar.YEAR) - 2000),
(byte) time.get(Calendar.MONTH),
(byte) time.get(Calendar.DATE),
(byte) time.get(Calendar.HOUR_OF_DAY),
(byte) time.get(Calendar.MINUTE),
(byte) time.get(Calendar.SECOND),
(byte) (bytesTransferred & 0xff),
(byte) (0xff & (bytesTransferred >> 8))
};
final List<BLEAction> list = new ArrayList<>();
list.add(new WriteAction(Profile.UUID_CHAR_CONTROL_POINT, ack));
BLETask task = new BLETask(list);
try {
queueTask(task);
} catch (NullPointerException e) {
} finally {
// flush to the DB after sending the ACK
flushActivityDataHolder();
//The last data chunk sent by the miband has always length 0.
//When we ack this chunk, the transfer is done.
if (bytesTransferred == 0) {
activityStruct = null;
onSuccess("sync complete");
}
}
}
}