From e9a68e70b585898d7b82ceef4d27e151d21d0220 Mon Sep 17 00:00:00 2001 From: cpfeiffer Date: Fri, 8 Sep 2017 23:56:05 +0200 Subject: [PATCH] Make Mi2 FetchOperation reusable --- .../devices/miband/MiBand2Service.java | 2 + .../operations/AbstractFetchOperation.java | 193 ++++++++++++++++++ .../operations/FetchActivityOperation.java | 130 ++---------- 3 files changed, 208 insertions(+), 117 deletions(-) create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2/operations/AbstractFetchOperation.java diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBand2Service.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBand2Service.java index 6fc1ce24..3c4561c4 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBand2Service.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBand2Service.java @@ -207,6 +207,8 @@ public class MiBand2Service { public static final byte SUCCESS = 0x01; public static final byte COMMAND_ACTIVITY_DATA_START_DATE = 0x01; + public static final byte COMMAND_ACTIVITY_DATA_TYPE_ACTIVTY = 0x01; + public static final byte COMMAND_ACTIVITY_DATA_TYPE_UNKNOWN_2 = 0x02; public static final byte COMMAND_ACTIVITY_DATA_XXX_DATE = 0x02; // issued on first connect, followd by COMMAND_XXXX_ACTIVITY_DATA instead of COMMAND_FETCH_ACTIVITY_DATA public static final byte COMMAND_FIRMWARE_INIT = 0x01; // to UUID_CHARACTERISTIC_FIRMWARE, followed by fw file size in bytes diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2/operations/AbstractFetchOperation.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2/operations/AbstractFetchOperation.java new file mode 100644 index 00000000..548f3670 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2/operations/AbstractFetchOperation.java @@ -0,0 +1,193 @@ +/* Copyright (C) 2016-2017 Carsten Pfeiffer + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.miband2.operations; + +import android.bluetooth.BluetoothGatt; +import android.bluetooth.BluetoothGattCharacteristic; +import android.content.SharedPreferences; +import android.support.annotation.CallSuper; +import android.support.annotation.NonNull; +import android.widget.Toast; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.text.DateFormat; +import java.util.Arrays; +import java.util.Calendar; +import java.util.GregorianCalendar; +import java.util.UUID; + +import nodomain.freeyourgadget.gadgetbridge.GBApplication; +import nodomain.freeyourgadget.gadgetbridge.Logging; +import nodomain.freeyourgadget.gadgetbridge.R; +import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBand2Service; +import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions; +import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; +import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceBusyAction; +import nodomain.freeyourgadget.gadgetbridge.service.devices.miband2.AbstractMiBand2Operation; +import nodomain.freeyourgadget.gadgetbridge.service.devices.miband2.MiBand2Support; +import nodomain.freeyourgadget.gadgetbridge.util.ArrayUtils; +import nodomain.freeyourgadget.gadgetbridge.util.GB; + +/** + * An operation that fetches activity data. For every fetch, a new operation must + * be created, i.e. an operation may not be reused for multiple fetches. + */ +public abstract class AbstractFetchOperation extends AbstractMiBand2Operation { + private static final Logger LOG = LoggerFactory.getLogger(AbstractFetchOperation.class); + + protected byte lastPacketCounter; + protected int fetchCount; + protected BluetoothGattCharacteristic characteristicActivityData; + protected BluetoothGattCharacteristic characteristicFetch; + protected Calendar startTimestamp; + + public AbstractFetchOperation(MiBand2Support support) { + super(support); + } + + @Override + protected void enableNeededNotifications(TransactionBuilder builder, boolean enable) { + if (!enable) { + // dynamically enabled, but always disabled on finish + builder.notify(getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_5_ACTIVITY_DATA), enable); + } + } + + @Override + protected void doPerform() throws IOException { + startFetching(); + } + + protected void startFetching() throws IOException { + lastPacketCounter = -1; + + TransactionBuilder builder = performInitialized("fetching activity data"); + getSupport().setLowLatency(builder); + if (fetchCount == 0) { + builder.add(new SetDeviceBusyAction(getDevice(), getContext().getString(R.string.busy_task_fetch_activity_data), getContext())); + } + fetchCount++; + + characteristicActivityData = getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_5_ACTIVITY_DATA); + builder.notify(characteristicActivityData, false); + + characteristicFetch = getCharacteristic(MiBand2Service.UUID_UNKNOWN_CHARACTERISTIC4); + builder.notify(characteristicFetch, true); + + startFetching(builder); + builder.queue(getQueue()); + } + + protected abstract void startFetching(TransactionBuilder builder); + + protected abstract String getLastSyncTimeKey(); + + @Override + public boolean onCharacteristicChanged(BluetoothGatt gatt, + BluetoothGattCharacteristic characteristic) { + UUID characteristicUUID = characteristic.getUuid(); + if (MiBand2Service.UUID_CHARACTERISTIC_5_ACTIVITY_DATA.equals(characteristicUUID)) { + handleActivityNotif(characteristic.getValue()); + return true; + } else if (MiBand2Service.UUID_UNKNOWN_CHARACTERISTIC4.equals(characteristicUUID)) { + handleActivityMetadata(characteristic.getValue()); + return true; + } else { + return super.onCharacteristicChanged(gatt, characteristic); + } + } + + @CallSuper + protected void handleActivityFetchFinish() { + operationFinished(); + unsetBusy(); + } + + /** + * Method to handle the incoming activity data. + * There are two kind of messages we currently know: + * - the first one is 11 bytes long and contains metadata (how many bytes to expect, when the data starts, etc.) + * - the second one is 20 bytes long and contains the actual activity data + *

+ * The first message type is parsed by this method, for every other length of the value param, bufferActivityData is called. + * + * @param value + */ + protected abstract void handleActivityNotif(byte[] value); + + /** + * Creates samples from the given 17-length array + * @param value + */ + protected abstract void bufferActivityData(byte[] value); + + protected void handleActivityMetadata(byte[] value) { + if (value.length == 15) { + // first two bytes are whether our request was accepted + if (ArrayUtils.equals(value, MiBand2Service.RESPONSE_ACTIVITY_DATA_START_DATE_SUCCESS, 0)) { + // the third byte (0x01 on success) = ? + // the 4th - 7th bytes probably somehow represent the number of bytes/packets to expect + + // last 8 bytes are the start date + Calendar startTimestamp = getSupport().fromTimeBytes(org.apache.commons.lang3.ArrayUtils.subarray(value, 7, value.length)); + setStartTimestamp(startTimestamp); + + GB.toast(getContext().getString(R.string.FetchActivityOperation_about_to_transfer_since, + DateFormat.getDateTimeInstance().format(startTimestamp.getTime())), Toast.LENGTH_LONG, GB.INFO); + } else { + LOG.warn("Unexpected activity metadata: " + Logging.formatBytes(value)); + handleActivityFetchFinish(); + } + } else if (value.length == 3) { + if (Arrays.equals(MiBand2Service.RESPONSE_FINISH_SUCCESS, value)) { + handleActivityFetchFinish(); + } else { + LOG.warn("Unexpected activity metadata: " + Logging.formatBytes(value)); + handleActivityFetchFinish(); + } + } else { + LOG.warn("Unexpected activity metadata: " + Logging.formatBytes(value)); + handleActivityFetchFinish(); + } + } + + protected void setStartTimestamp(Calendar startTimestamp) { + this.startTimestamp = startTimestamp; + } + + protected void saveLastSyncTimestamp(@NonNull GregorianCalendar timestamp) { + SharedPreferences.Editor editor = GBApplication.getPrefs().getPreferences().edit(); + editor.putLong(getLastSyncTimeKey(), timestamp.getTimeInMillis()); + editor.apply(); + } + + + protected GregorianCalendar getLastSuccessfulSyncTime() { + long timeStampMillis = GBApplication.getPrefs().getLong(getLastSyncTimeKey(), 0); + if (timeStampMillis != 0) { + GregorianCalendar calendar = BLETypeConversions.createCalendar(); + calendar.setTimeInMillis(timeStampMillis); + return calendar; + } + GregorianCalendar calendar = BLETypeConversions.createCalendar(); + calendar.add(Calendar.DAY_OF_MONTH, -10); + return calendar; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2/operations/FetchActivityOperation.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2/operations/FetchActivityOperation.java index 927af487..36999ace 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2/operations/FetchActivityOperation.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2/operations/FetchActivityOperation.java @@ -16,11 +16,6 @@ along with this program. If not, see . */ package nodomain.freeyourgadget.gadgetbridge.service.devices.miband2.operations; -import android.bluetooth.BluetoothGatt; -import android.bluetooth.BluetoothGattCharacteristic; -import android.content.SharedPreferences; -import android.support.annotation.NonNull; -import android.support.v4.util.TimeUtils; import android.text.format.DateUtils; import android.widget.Toast; @@ -28,18 +23,13 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; -import java.text.DateFormat; import java.util.ArrayList; -import java.util.Arrays; import java.util.Calendar; import java.util.GregorianCalendar; import java.util.List; -import java.util.UUID; import java.util.concurrent.TimeUnit; import nodomain.freeyourgadget.gadgetbridge.GBApplication; -import nodomain.freeyourgadget.gadgetbridge.Logging; -import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; import nodomain.freeyourgadget.gadgetbridge.database.DBHelper; import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider; @@ -51,11 +41,8 @@ import nodomain.freeyourgadget.gadgetbridge.entities.MiBandActivitySample; import nodomain.freeyourgadget.gadgetbridge.entities.User; import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions; import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; -import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceBusyAction; import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.WaitAction; import nodomain.freeyourgadget.gadgetbridge.service.devices.miband2.MiBand2Support; -import nodomain.freeyourgadget.gadgetbridge.service.devices.miband2.AbstractMiBand2Operation; -import nodomain.freeyourgadget.gadgetbridge.util.ArrayUtils; import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils; import nodomain.freeyourgadget.gadgetbridge.util.GB; @@ -63,95 +50,31 @@ import nodomain.freeyourgadget.gadgetbridge.util.GB; * An operation that fetches activity data. For every fetch, a new operation must * be created, i.e. an operation may not be reused for multiple fetches. */ -public class FetchActivityOperation extends AbstractMiBand2Operation { +public class FetchActivityOperation extends AbstractFetchOperation { private static final Logger LOG = LoggerFactory.getLogger(FetchActivityOperation.class); private List samples = new ArrayList<>(60*24); // 1day per default - private byte lastPacketCounter; - private Calendar startTimestamp; - private int fetchCount; - public FetchActivityOperation(MiBand2Support support) { super(support); } @Override - protected void enableNeededNotifications(TransactionBuilder builder, boolean enable) { - if (!enable) { - // dynamically enabled, but always disabled on finish - builder.notify(getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_5_ACTIVITY_DATA), enable); - } + protected void startFetching() throws IOException { + samples.clear(); + super.startFetching(); } @Override - protected void doPerform() throws IOException { - startFetching(); - } - - private void startFetching() throws IOException { - samples.clear(); - lastPacketCounter = -1; - - TransactionBuilder builder = performInitialized("fetching activity data"); - getSupport().setLowLatency(builder); - if (fetchCount == 0) { - builder.add(new SetDeviceBusyAction(getDevice(), getContext().getString(R.string.busy_task_fetch_activity_data), getContext())); - } - fetchCount++; - - BluetoothGattCharacteristic characteristicActivityData = getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_5_ACTIVITY_DATA); - builder.notify(characteristicActivityData, false); - - BluetoothGattCharacteristic characteristicFetch = getCharacteristic(MiBand2Service.UUID_UNKNOWN_CHARACTERISTIC4); - builder.notify(characteristicFetch, true); - + protected void startFetching(TransactionBuilder builder) { GregorianCalendar sinceWhen = getLastSuccessfulSyncTime(); - builder.write(characteristicFetch, BLETypeConversions.join(new byte[] { MiBand2Service.COMMAND_ACTIVITY_DATA_START_DATE, 0x01 }, getSupport().getTimeBytes(sinceWhen, TimeUnit.MINUTES))); + builder.write(characteristicFetch, BLETypeConversions.join(new byte[] { MiBand2Service.COMMAND_ACTIVITY_DATA_START_DATE, MiBand2Service.COMMAND_ACTIVITY_DATA_TYPE_ACTIVTY }, getSupport().getTimeBytes(sinceWhen, TimeUnit.MINUTES))); builder.add(new WaitAction(1000)); // TODO: actually wait for the success-reply builder.notify(characteristicActivityData, true); builder.write(characteristicFetch, new byte[] { MiBand2Service.COMMAND_FETCH_ACTIVITY_DATA }); - builder.queue(getQueue()); } - private GregorianCalendar getLastSuccessfulSyncTime() { - long timeStampMillis = GBApplication.getPrefs().getLong(getLastSyncTimeKey(), 0); - if (timeStampMillis != 0) { - GregorianCalendar calendar = BLETypeConversions.createCalendar(); - calendar.setTimeInMillis(timeStampMillis); - return calendar; - } - GregorianCalendar calendar = BLETypeConversions.createCalendar(); - calendar.add(Calendar.DAY_OF_MONTH, -10); - return calendar; - } - - private void saveLastSyncTimestamp(@NonNull GregorianCalendar timestamp) { - SharedPreferences.Editor editor = GBApplication.getPrefs().getPreferences().edit(); - editor.putLong(getLastSyncTimeKey(), timestamp.getTimeInMillis()); - editor.apply(); - } - - private String getLastSyncTimeKey() { - return getDevice().getAddress() + "_" + "lastSyncTimeMillis"; - } - - @Override - public boolean onCharacteristicChanged(BluetoothGatt gatt, - BluetoothGattCharacteristic characteristic) { - UUID characteristicUUID = characteristic.getUuid(); - if (MiBand2Service.UUID_CHARACTERISTIC_5_ACTIVITY_DATA.equals(characteristicUUID)) { - handleActivityNotif(characteristic.getValue()); - return true; - } else if (MiBand2Service.UUID_UNKNOWN_CHARACTERISTIC4.equals(characteristicUUID)) { - handleActivityMetadata(characteristic.getValue()); - return true; - } else { - return super.onCharacteristicChanged(gatt, characteristic); - } - } - - private void handleActivityFetchFinish() { + protected void handleActivityFetchFinish() { LOG.info("Fetching activity data has finished round " + fetchCount); GregorianCalendar lastSyncTimestamp = saveSamples(); if (lastSyncTimestamp != null && needsAnotherFetch(lastSyncTimestamp)) { @@ -163,8 +86,7 @@ public class FetchActivityOperation extends AbstractMiBand2Operation { } } - operationFinished(); - unsetBusy(); + super.handleActivityFetchFinish(); } private boolean needsAnotherFetch(GregorianCalendar lastSyncTimestamp) { @@ -232,7 +154,7 @@ public class FetchActivityOperation extends AbstractMiBand2Operation { * * @param value */ - private void handleActivityNotif(byte[] value) { + protected void handleActivityNotif(byte[] value) { if (!isOperationRunning()) { LOG.error("ignoring activity data notification because operation is not running. Data length: " + value.length); getSupport().logMessageContent(value); @@ -257,7 +179,7 @@ public class FetchActivityOperation extends AbstractMiBand2Operation { * Creates samples from the given 17-length array * @param value */ - private void bufferActivityData(byte[] value) { + protected void bufferActivityData(byte[] value) { int len = value.length; if (len % 4 != 1) { @@ -280,34 +202,8 @@ public class FetchActivityOperation extends AbstractMiBand2Operation { return sample; } - private void handleActivityMetadata(byte[] value) { - if (value.length == 15) { - // first two bytes are whether our request was accepted - if (ArrayUtils.equals(value, MiBand2Service.RESPONSE_ACTIVITY_DATA_START_DATE_SUCCESS, 0)) { - // the third byte (0x01 on success) = ? - // the 4th - 7th bytes probably somehow represent the number of bytes/packets to expect - - // last 8 bytes are the start date - Calendar startTimestamp = getSupport().fromTimeBytes(org.apache.commons.lang3.ArrayUtils.subarray(value, 7, value.length)); - setStartTimestamp(startTimestamp); - - GB.toast(getContext().getString(R.string.FetchActivityOperation_about_to_transfer_since, - DateFormat.getDateTimeInstance().format(startTimestamp.getTime())), Toast.LENGTH_LONG, GB.INFO); - } else { - LOG.warn("Unexpected activity metadata: " + Logging.formatBytes(value)); - handleActivityFetchFinish(); - } - } else if (value.length == 3) { - if (Arrays.equals(MiBand2Service.RESPONSE_FINISH_SUCCESS, value)) { - handleActivityFetchFinish(); - } else { - LOG.warn("Unexpected activity metadata: " + Logging.formatBytes(value)); - handleActivityFetchFinish(); - } - } - } - - private void setStartTimestamp(Calendar startTimestamp) { - this.startTimestamp = startTimestamp; + @Override + protected String getLastSyncTimeKey() { + return getDevice().getAddress() + "_" + "lastSyncTimeMillis"; } }