diff --git a/app/src/main/.classpath b/app/src/main/.classpath new file mode 100644 index 000000000..60ab2ffa5 --- /dev/null +++ b/app/src/main/.classpath @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/app/src/main/.project b/app/src/main/.project new file mode 100644 index 000000000..f8b13a0fd --- /dev/null +++ b/app/src/main/.project @@ -0,0 +1,33 @@ + + + Gadgetbridge + + + + + + com.android.ide.eclipse.adt.ResourceManagerBuilder + + + + + com.android.ide.eclipse.adt.PreCompilerBuilder + + + + + org.eclipse.jdt.core.javabuilder + + + + + com.android.ide.eclipse.adt.ApkBuilder + + + + + + com.android.ide.eclipse.adt.AndroidNature + org.eclipse.jdt.core.javanature + + diff --git a/app/src/main/.settings/org.eclipse.jdt.core.prefs b/app/src/main/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 000000000..d17b6724d --- /dev/null +++ b/app/src/main/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,12 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.methodParameters=do not generate +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.7 +org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve +org.eclipse.jdt.core.compiler.compliance=1.7 +org.eclipse.jdt.core.compiler.debug.lineNumber=generate +org.eclipse.jdt.core.compiler.debug.localVariable=generate +org.eclipse.jdt.core.compiler.debug.sourceFile=generate +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.source=1.7 diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 583561c3c..382f27b7f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -10,6 +10,7 @@ + = State.CONNECTED.ordinal(); + } + + public boolean isInitialized() { + return state.ordinal() >= State.INITIALIZED.ordinal(); + } + public State getState() { return state; } @@ -65,6 +73,7 @@ public class GBDevice { return type; } + // TODO: this doesn't really belong here public void sendDeviceUpdateIntent(Context context) { Intent deviceUpdateIntent = new Intent(ControlCenter.ACTION_REFRESH_DEVICELIST); deviceUpdateIntent.putExtra("device_address", getAddress()); @@ -75,9 +84,11 @@ public class GBDevice { } public enum State { + // Note: the order is important! NOT_CONNECTED, CONNECTING, - CONNECTED + CONNECTED, + INITIALIZED } public enum Type { @@ -85,5 +96,4 @@ public class GBDevice { PEBBLE, MIBAND } - } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/btle/AbstractBTLEDeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/btle/AbstractBTLEDeviceSupport.java new file mode 100644 index 000000000..5f2cf2052 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/btle/AbstractBTLEDeviceSupport.java @@ -0,0 +1,172 @@ +package nodomain.freeyourgadget.gadgetbridge.btle; + +import java.io.IOException; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.UUID; + +import nodomain.freeyourgadget.gadgetbridge.AbstractDeviceSupport; +import android.bluetooth.BluetoothGatt; +import android.bluetooth.BluetoothGattCharacteristic; +import android.bluetooth.BluetoothGattService; +import android.util.Log; + +/** + * + * @see TransactionBuilder + * @see BtLEQueue + */ +public abstract class AbstractBTLEDeviceSupport extends AbstractDeviceSupport implements GattCallback { + private static final String TAG = "AbstractBTLEDeviceSupport"; + + private BtLEQueue mQueue; + private HashMap mAvailableCharacteristics; + private Set mSupportedServices = new HashSet<>(4); + + @Override + public boolean connect() { + if (mQueue == null) { + mQueue = new BtLEQueue(getBluetoothAdapter(), getDevice(), this, getContext()); + } + return mQueue.connect(); + } + + /** + * Subclasses should populate the given builder to initialize the device (if necessary). + * @param builder + * @return the same builder as passed as the argument + */ + protected TransactionBuilder initializeDevice(TransactionBuilder builder) { + return builder; + } + + @Override + public void dispose() { + if (mQueue != null) { + mQueue.dispose(); + } + } + + /** + * Send commands like this to the device: + *

+ * perform("sms notification").write(someCharacteristic, someByteArray).queue(getQueue()); + *

+ * TODO: support orchestration of multiple reads and writes depending on returned values + * @see #performConnected(Transaction) + * @see #initializeDevice(TransactionBuilder) + */ + protected TransactionBuilder performInitialized(String taskName) throws IOException { + if (!isConnected()) { + if (!connect()) { + throw new IOException("1: Unable to connect to device: " + getDevice()); + } + } + if (!isInitialized()) { + // first, add a transaction that performs device initialization + TransactionBuilder builder = new TransactionBuilder("Initialize device"); + initializeDevice(builder).queue(getQueue()); + } + return new TransactionBuilder(taskName); + } + + /** + * + * @param transaction + * @throws IOException + * @see {@link #performInitialized(String)} + */ + protected void performConnected(Transaction transaction) throws IOException { + if (!isConnected()) { + if (!connect()) { + throw new IOException("2: Unable to connect to device: " + getDevice()); + } + } + getQueue().add(transaction); + } + + public BtLEQueue getQueue() { + return mQueue; + } + + /** + * Subclasses should call this method to add services they support. + * Only supported services will be queried for characteristics. + * @param aSupportedService + * @see #getCharacteristic(UUID) + */ + protected void addSupportedService(UUID aSupportedService) { + mSupportedServices.add(aSupportedService); + } + + /** + * Returns the characteristic matching the given UUID. Only characteristics + * are returned whose service is marked as supported. + * @param uuid + * @return the characteristic for the given UUID or null + * @see #addSupportedService(UUID) + */ + protected BluetoothGattCharacteristic getCharacteristic(UUID uuid) { + if (mAvailableCharacteristics == null) { + return null; + } + return mAvailableCharacteristics.get(uuid); + } + + private void gattServicesDiscovered(List discoveredGattServices) { + mAvailableCharacteristics = null; + + if (discoveredGattServices == null) { + return; + } + Set supportedServices = getSupportedServices(); + + for (BluetoothGattService service : discoveredGattServices) { + if (supportedServices.contains(service.getUuid())) { + List characteristics = service.getCharacteristics(); + if (characteristics == null || characteristics.isEmpty()) { + Log.w(TAG, "Supported LE service " + service.getUuid() + "did not return any characteristics"); + continue; + } + mAvailableCharacteristics = new HashMap<>(characteristics.size()); + for (BluetoothGattCharacteristic characteristic : characteristics) { + mAvailableCharacteristics.put(characteristic.getUuid(), characteristic); + } + } + } + } + + protected Set getSupportedServices() { + return mSupportedServices; + } + + // default implementations of event handler methods (gatt callbacks) + @Override + public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) { + } + + @Override + public void onServicesDiscovered(BluetoothGatt gatt) { + gattServicesDiscovered(getQueue().getSupportedGattServices()); + } + + @Override + public void onCharacteristicRead(BluetoothGatt gatt, + BluetoothGattCharacteristic characteristic, int status) { + } + + @Override + public void onCharacteristicWrite(BluetoothGatt gatt, + BluetoothGattCharacteristic characteristic, int status) { + } + + @Override + public void onCharacteristicChanged(BluetoothGatt gatt, + BluetoothGattCharacteristic characteristic) { + } + + public void onReadRemoteRssi(BluetoothGatt gatt, int rssi, int status) { + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/btle/BtLEAction.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/btle/BtLEAction.java new file mode 100644 index 000000000..2e261e314 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/btle/BtLEAction.java @@ -0,0 +1,39 @@ +package nodomain.freeyourgadget.gadgetbridge.btle; + +import android.bluetooth.BluetoothGatt; +import android.bluetooth.BluetoothGattCharacteristic; + +/** + * The Bluedroid implementation only allows performing one GATT request at a time. + * As they are asynchronous anyway, we encapsulate every GATT request (read and write) + * inside a runnable action. + * + * These actions are then executed one after another, ensuring that every action's result + * has been posted before invoking the next action. + */ +public abstract class BtLEAction { + private final BluetoothGattCharacteristic characteristic; + + public BtLEAction() { + this(null); + } + + public BtLEAction(BluetoothGattCharacteristic characteristic) { + this.characteristic = characteristic; + } + + public abstract boolean run(BluetoothGatt gatt); + /** + * Returns the GATT characteristic being read/written/... + * @return the GATT characteristic, or null + */ + public BluetoothGattCharacteristic getCharacteristic() { + return characteristic; + } + + public String toString() { + BluetoothGattCharacteristic characteristic = getCharacteristic(); + String uuid = characteristic == null ? "(null)" : characteristic.getUuid().toString(); + return getClass().getSimpleName() + " on characteristic: " + uuid; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/btle/BtLEQueue.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/btle/BtLEQueue.java new file mode 100644 index 000000000..bbb42b81f --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/btle/BtLEQueue.java @@ -0,0 +1,273 @@ +package nodomain.freeyourgadget.gadgetbridge.btle; + +import java.util.Collections; +import java.util.List; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.LinkedBlockingQueue; + +import nodomain.freeyourgadget.gadgetbridge.DeviceSupport; +import nodomain.freeyourgadget.gadgetbridge.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.GBDevice.State; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothGatt; +import android.bluetooth.BluetoothGattCallback; +import android.bluetooth.BluetoothGattCharacteristic; +import android.bluetooth.BluetoothGattService; +import android.bluetooth.BluetoothProfile; +import android.content.Context; +import android.util.Log; + +/** + * One queue/thread per connectable device. + */ +public final class BtLEQueue { + private static final String TAG = BtLEQueue.class.getSimpleName(); + + private GBDevice mGbDevice; + private BluetoothAdapter mBluetoothAdapter; + private BluetoothGatt mBluetoothGatt; + private volatile BlockingQueue mTransactions = new LinkedBlockingQueue(); + private volatile boolean mDisposed; + private volatile boolean mAbortTransaction; + + private Context mContext; + private CountDownLatch mWaitForActionResultLatch; + private CountDownLatch mConnectionLatch; + private BluetoothGattCharacteristic mWaitCharacteristic; + private GattCallback mExternalGattCallback; + + private Thread dispatchThread = new Thread("Bluetooth GATT Dispatcher") { + + public void run() { + while (!mDisposed) { + try { + Transaction transaction = mTransactions.take(); + if (!isConnected()) { + // TODO: request connection and initialization from the outside and wait until finished + + // wait until the connection succeeds before running the actions + // Note that no automatic connection is performed. This has to be triggered + // on the outside typically by the DeviceSupport. The reason is that + // devices have different kinds of initializations and this class has no + // idea about them. + mConnectionLatch = new CountDownLatch(1); + mConnectionLatch.await(); + mConnectionLatch = null; + } + + mAbortTransaction = false; + // Run all actions of the transaction until one doesn't succeed + for (BtLEAction action : transaction.getActions()) { + mWaitCharacteristic = action.getCharacteristic(); + if (action.run(mBluetoothGatt)) { + mWaitForActionResultLatch = new CountDownLatch(1); + mWaitForActionResultLatch.await(); + mWaitForActionResultLatch = null; + if (mAbortTransaction) { + break; + } + } else { + Log.e(TAG, "Action returned false: " + action); + break; // abort the transaction + } + } + } catch (InterruptedException ignored) { + mWaitForActionResultLatch = null; + mConnectionLatch = null; + } finally { + mWaitCharacteristic = null; + } + } + Log.i(TAG, "Queue Dispatch Thread terminated."); + } + }; + + public BtLEQueue(BluetoothAdapter bluetoothAdapter, GBDevice gbDevice, GattCallback externalGattCallback, Context context) { + mBluetoothAdapter = bluetoothAdapter; + mGbDevice = gbDevice; + mExternalGattCallback = externalGattCallback; + mContext = context; + + dispatchThread.start(); + } + + protected boolean isConnected() { + return mGbDevice.isConnected(); + } + + /** + * Connects to the given remote device. Note that this does not perform any device + * specific initialization. This should be done in the specific {@link DeviceSupport} + * class. + * + * @return true whether the connection attempt was successfully triggered + */ + public boolean connect() { + if (mBluetoothGatt != null) { + disconnect(); + } + BluetoothDevice remoteDevice = mBluetoothAdapter.getRemoteDevice(mGbDevice.getAddress()); + mBluetoothGatt = remoteDevice.connectGatt(mContext, false, internalGattCallback); + boolean result = mBluetoothGatt.connect(); + setDeviceConnectionState(result ? State.CONNECTING : State.NOT_CONNECTED); + return result; + } + + private void setDeviceConnectionState(State newState) { + mGbDevice.setState(newState); + mGbDevice.sendDeviceUpdateIntent(mContext); + if (mConnectionLatch != null) { + mConnectionLatch.countDown(); + } + } + + public void disconnect() { + if (mBluetoothGatt != null) { + Log.i(TAG, "Disconnecting BtLEQueue from GATT device"); + mBluetoothGatt.disconnect(); + mBluetoothGatt.close(); + mBluetoothGatt = null; + } + } + + private void handleDisconnected() { + mTransactions.clear(); + if (mWaitForActionResultLatch != null) { + mWaitForActionResultLatch.countDown(); + } + setDeviceConnectionState(State.NOT_CONNECTED); + } + + public void dispose() { + if (mDisposed) { + return; + } + mDisposed = true; +// try { + disconnect(); + dispatchThread.interrupt(); + dispatchThread = null; +// dispatchThread.join(); +// } catch (InterruptedException ex) { +// Log.e(TAG, "Exception while disposing BtLEQueue", ex); +// } + } + + /** + * Adds a transaction to the end of the queue. + * @param transaction + */ + public void add(Transaction transaction) { + if (!transaction.isEmpty()) { + mTransactions.add(transaction); + } + } + + public void clear() { + mTransactions.clear(); + } + + /** + * Retrieves a list of supported GATT services on the connected device. This should be + * invoked only after {@code BluetoothGatt#discoverServices()} completes successfully. + * + * @return A {@code List} of supported services. + */ + public List getSupportedGattServices() { + if (mBluetoothGatt == null) { + return Collections.emptyList(); + } + return mBluetoothGatt.getServices(); + } + + // Implements callback methods for GATT events that the app cares about. For example, + // connection change and services discovered. + private final BluetoothGattCallback internalGattCallback = new BluetoothGattCallback() { + @Override + public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) { + switch (newState) { + case BluetoothProfile.STATE_CONNECTED: + Log.i(TAG, "Connected to GATT server."); + setDeviceConnectionState(State.CONNECTED); + // Attempts to discover services after successful connection. + Log.i(TAG, "Attempting to start service discovery:" + + mBluetoothGatt.discoverServices()); + break; + case BluetoothProfile.STATE_DISCONNECTED: + Log.i(TAG, "Disconnected from GATT server."); + handleDisconnected(); + break; + case BluetoothProfile.STATE_CONNECTING: + Log.i(TAG, "Connecting to GATT server..."); + setDeviceConnectionState(State.CONNECTING); + break; + } + } + + @Override + public void onServicesDiscovered(BluetoothGatt gatt, int status) { + if (status == BluetoothGatt.GATT_SUCCESS) { + if (mExternalGattCallback != null) { + // only propagate the successful event + mExternalGattCallback.onServicesDiscovered(gatt); + } + } else { + Log.w(TAG, "onServicesDiscovered received: " + status); + } + } + + @Override + public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { + if (status == BluetoothGatt.GATT_SUCCESS) { + Log.e(TAG, "Writing characteristic " + characteristic.getUuid() + " succeeded."); + } else { + Log.e(TAG, "Writing characteristic " + characteristic.getUuid() + " failed: "+ status); + } + if (mExternalGattCallback != null) { + mExternalGattCallback.onCharacteristicWrite(gatt, characteristic, status); + } + checkWaitingCharacteristic(characteristic, status); + } + + @Override + public void onCharacteristicRead(BluetoothGatt gatt, + BluetoothGattCharacteristic characteristic, + int status) { + if (status != BluetoothGatt.GATT_SUCCESS) { + Log.e(TAG, "Reading characteristic " + characteristic.getUuid() + " failed: " + status); + } + if (mExternalGattCallback != null) { + mExternalGattCallback.onCharacteristicRead(gatt, characteristic, status); + } + checkWaitingCharacteristic(characteristic, status); + } + + @Override + public void onCharacteristicChanged(BluetoothGatt gatt, + BluetoothGattCharacteristic characteristic) { + if (mExternalGattCallback != null) { + mExternalGattCallback.onCharacteristicChanged(gatt, characteristic); + } + } + + @Override + public void onReadRemoteRssi(BluetoothGatt gatt, int rssi, int status) { + if (mExternalGattCallback != null) { + mExternalGattCallback.onReadRemoteRssi(gatt, rssi, status); + } + } + + private void checkWaitingCharacteristic(BluetoothGattCharacteristic characteristic, int status) { + if (status != BluetoothGatt.GATT_SUCCESS) { + mAbortTransaction = true; + } + if (characteristic != null && BtLEQueue.this.mWaitCharacteristic != null && characteristic.getUuid().equals(BtLEQueue.this.mWaitCharacteristic.getUuid())) { + if (mWaitForActionResultLatch != null) { + mWaitForActionResultLatch.countDown(); + } + } + } + }; +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/btle/GattCallback.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/btle/GattCallback.java new file mode 100644 index 000000000..ce20371d5 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/btle/GattCallback.java @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * 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 nodomain.freeyourgadget.gadgetbridge.btle; + +import android.bluetooth.BluetoothGatt; +import android.bluetooth.BluetoothGattCallback; +import android.bluetooth.BluetoothGattCharacteristic; + +/** + * Callback interface handling gatt events. + * Pretty much the same as {@link BluetoothGattCallback}, except it's an interface + * instead of an abstract class. Some handlers commented out, because not used (yet). + */ +public interface GattCallback { + + /** + * @see BluetoothGattCallback#onConnectionStateChange(BluetoothGatt, int, int) + * @param gatt + * @param status + * @param newState + */ + public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState); + + /** + * @see BluetoothGattCallback#onServicesDiscovered(BluetoothGatt, int) + * @param gatt + */ + public void onServicesDiscovered(BluetoothGatt gatt); + + /** + * @see BluetoothGattCallback#onCharacteristicRead(BluetoothGatt, BluetoothGattCharacteristic, int) + * @param gatt + * @param characteristic + * @param status + */ + public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status); + + /** + * @see BluetoothGattCallback#onCharacteristicWrite(BluetoothGatt, BluetoothGattCharacteristic, int) + * @param gatt + * @param characteristic + * @param status + */ + public void onCharacteristicWrite(BluetoothGatt gatt, + BluetoothGattCharacteristic characteristic, int status); + + /** + * @see BluetoothGattCallback#onCharacteristicChanged(BluetoothGatt, BluetoothGattCharacteristic) + * @param gatt + * @param characteristic + */ + public void onCharacteristicChanged(BluetoothGatt gatt, + BluetoothGattCharacteristic characteristic); + +// /** +// * @see BluetoothGattCallback#onDescriptorRead(BluetoothGatt, BluetoothGattDescriptor, int) +// * @param gatt +// * @param descriptor +// * @param status +// */ +// public void onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, +// int status); +// +// /** +// * @see BluetoothGattCallback#onDescriptorWrite(BluetoothGatt, BluetoothGattDescriptor, int) +// * @param gatt +// * @param descriptor +// * @param status +// */ +// public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, +// int status); +// +// /** +// * @see BluetoothGattCallback#onReliableWriteCompleted(BluetoothGatt, int) +// * @param gatt +// * @param status +// */ +// public void onReliableWriteCompleted(BluetoothGatt gatt, int status); + + /** + * @see BluetoothGattCallback#onReadRemoteRssi(BluetoothGatt, int, int) + * @param gatt + * @param rssi + * @param status + */ + public void onReadRemoteRssi(BluetoothGatt gatt, int rssi, int status); + +// /** +// * @see BluetoothGattCallback#onMtuChanged(BluetoothGatt, int, int) +// * @param gatt +// * @param mtu +// * @param status +// */ +// public void onMtuChanged(BluetoothGatt gatt, int mtu, int status); +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/btle/ReadAction.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/btle/ReadAction.java new file mode 100644 index 000000000..598bbb4b7 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/btle/ReadAction.java @@ -0,0 +1,22 @@ +package nodomain.freeyourgadget.gadgetbridge.btle; + +import android.bluetooth.BluetoothGatt; +import android.bluetooth.BluetoothGattCallback; +import android.bluetooth.BluetoothGattCharacteristic; + +/** + * Invokes a read operation on a given GATT characteristic. + * The result will be made available asynchronously through the + * {@link BluetoothGattCallback} + */ +public class ReadAction extends BtLEAction { + + public ReadAction(BluetoothGattCharacteristic characteristic) { + super(characteristic); + } + + @Override + public boolean run(BluetoothGatt gatt) { + return gatt.readCharacteristic(getCharacteristic()); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/btle/Transaction.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/btle/Transaction.java new file mode 100644 index 000000000..8c4c4ce63 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/btle/Transaction.java @@ -0,0 +1,42 @@ +package nodomain.freeyourgadget.gadgetbridge.btle; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Locale; + +/** + * Groups a bunch of {@link BtLEAction actions} together, making sure + * that upon failure of one action, all subsequent actions are discarded. + * + * @author TREND + */ +public class Transaction { + private String mName; + private List mActions = new ArrayList<>(4); + + public Transaction(String taskName) { + this.mName = taskName; + } + + public String getTaskName() { + return mName; + } + + public void add(BtLEAction action) { + mActions.add(action); + } + + public List getActions() { + return Collections.unmodifiableList(mActions); + } + + public boolean isEmpty() { + return mActions.isEmpty(); + } + + @Override + public String toString() { + return String.format(Locale.US, "Transaction task: %s with %d actions", getTaskName(), mActions.size()); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/btle/TransactionBuilder.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/btle/TransactionBuilder.java new file mode 100644 index 000000000..40ae8e0e6 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/btle/TransactionBuilder.java @@ -0,0 +1,54 @@ +package nodomain.freeyourgadget.gadgetbridge.btle; + +import android.bluetooth.BluetoothGattCharacteristic; +import android.util.Log; + +public class TransactionBuilder { + private static final String TAG = TransactionBuilder.class.getSimpleName(); + + private Transaction mTransaction; + + public TransactionBuilder(String taskName) { + mTransaction = new Transaction(taskName); + } + + public TransactionBuilder read(BluetoothGattCharacteristic characteristic) { + if (characteristic == null) { + Log.w(TAG, "Unable to read characteristic: null"); + return this; + } + ReadAction action = new ReadAction(characteristic); + return add(action); + } + + public TransactionBuilder write(BluetoothGattCharacteristic characteristic, byte[] data) { + if (characteristic == null) { + Log.w(TAG, "Unable to write characteristic: null"); + return this; + } + WriteAction action = new WriteAction(characteristic, data); + return add(action); + } + + public TransactionBuilder wait(int millis) { + WaitAction action = new WaitAction(millis); + return add(action); + } + + public TransactionBuilder add(BtLEAction action) { + mTransaction.add(action); + return this; + } + + /** + * To be used as the final step to execute the transaction by the given queue. + * @param queue + */ + public void queue(BtLEQueue queue) { + queue.add(mTransaction); + } + + public Transaction getTransaction() { + return mTransaction; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/btle/WaitAction.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/btle/WaitAction.java new file mode 100644 index 000000000..b0cf424dd --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/btle/WaitAction.java @@ -0,0 +1,22 @@ +package nodomain.freeyourgadget.gadgetbridge.btle; + +import android.bluetooth.BluetoothGatt; + +public class WaitAction extends BtLEAction { + + private int mMillis; + + public WaitAction(int millis) { + mMillis = millis; + } + + @Override + public boolean run(BluetoothGatt gatt) { + try { + Thread.sleep(mMillis); + return true; + } catch (InterruptedException e) { + return false; + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/btle/WriteAction.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/btle/WriteAction.java new file mode 100644 index 000000000..4925a8ace --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/btle/WriteAction.java @@ -0,0 +1,28 @@ +package nodomain.freeyourgadget.gadgetbridge.btle; + +import android.bluetooth.BluetoothGatt; +import android.bluetooth.BluetoothGattCallback; +import android.bluetooth.BluetoothGattCharacteristic; + +/** + * Invokes a write operation on a given GATT characteristic. + * The result status will be made available asynchronously through the + * {@link BluetoothGattCallback} + */ +public class WriteAction extends BtLEAction { + + private byte[] value; + + public WriteAction(BluetoothGattCharacteristic characteristic, byte[] value) { + super(characteristic); + this.value = value; + } + + @Override + public boolean run(BluetoothGatt gatt) { + if (getCharacteristic().setValue(value)) { + return gatt.writeCharacteristic(getCharacteristic()); + } + return false; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/miband/MiBandService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/miband/MiBandService.java new file mode 100644 index 000000000..6d11bcfdd --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/miband/MiBandService.java @@ -0,0 +1,271 @@ +package nodomain.freeyourgadget.gadgetbridge.miband; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +public class MiBandService { + + public static final String MAC_ADDRESS_FILTER = "88:0F:10"; + + public static final String BASE_UUID = "0000%s-0000-1000-8000-00805f9b34fb"; + + public static final UUID UUID_SERVICE_MIBAND_SERVICE = UUID.fromString(String.format(BASE_UUID, "FEE0")); + + public static final UUID UUID_CHARACTERISTIC_DEVICE_INFO = UUID.fromString(String.format(BASE_UUID, "FF01")); + + public static final UUID UUID_CHARACTERISTIC_DEVICE_NAME = UUID.fromString(String.format(BASE_UUID, "FF02")); + + public static final UUID UUID_CHARACTERISTIC_NOTIFICATION = UUID.fromString(String.format(BASE_UUID, "FF03")); + + public static final UUID UUID_CHARACTERISTIC_USER_INFO = UUID.fromString(String.format(BASE_UUID, "FF04")); + + public static final UUID UUID_CHARACTERISTIC_CONTROL_POINT = UUID.fromString(String.format(BASE_UUID, "FF05")); + + public static final UUID UUID_CHARACTERISTIC_REALTIME_STEPS = UUID.fromString(String.format(BASE_UUID, "FF06")); + + public static final UUID UUID_CHARACTERISTIC_ACTIVITY_DATA = UUID.fromString(String.format(BASE_UUID, "FF07")); + + public static final UUID UUID_CHARACTERISTIC_FIRMWARE_DATA = UUID.fromString(String.format(BASE_UUID, "FF08")); + + public static final UUID UUID_CHARACTERISTIC_LE_PARAMS = UUID.fromString(String.format(BASE_UUID, "FF09")); + + public static final UUID UUID_CHARACTERISTIC_DATE_TIME = UUID.fromString(String.format(BASE_UUID, "FF0A")); + + public static final UUID UUID_CHARACTERISTIC_STATISTICS = UUID.fromString(String.format(BASE_UUID, "FF0B")); + + public static final UUID UUID_CHARACTERISTIC_BATTERY = UUID.fromString(String.format(BASE_UUID, "FF0C")); + + public static final UUID UUID_CHARACTERISTIC_TEST = UUID.fromString(String.format(BASE_UUID, "FF0D")); + + public static final UUID UUID_CHARACTERISTIC_SENSOR_DATA = UUID.fromString(String.format(BASE_UUID, "FF0E")); + + public static final UUID UUID_CHARACTERISTIC_PAIR = UUID.fromString(String.format(BASE_UUID, "FF0F")); + + public static final byte ALIAS_LEN = 0xa; + + public static final byte NOTIFY_AUTHENTICATION_FAILED = 0x6; + + public static final byte NOTIFY_AUTHENTICATION_SUCCESS = 0x5; + + public static final byte NOTIFY_CONN_PARAM_UPDATE_FAILED = 0x3; + + public static final byte NOTIFY_CONN_PARAM_UPDATE_SUCCESS = 0x4; + + public static final int NOTIFY_DEVICE_MALFUNCTION = 0xff; + + public static final byte NOTIFY_FIRMWARE_UPDATE_FAILED = 0x1; + + public static final byte NOTIFY_FIRMWARE_UPDATE_SUCCESS = 0x2; + + public static final byte NOTIFY_FITNESS_GOAL_ACHIEVED = 0x7; + + public static final byte NOTIFY_FW_CHECK_FAILED = 0xb; + + public static final byte NOTIFY_FW_CHECK_SUCCESS = 0xc; + + public static final byte NOTIFY_NORMAL = 0x0; + + public static final int NOTIFY_PAIR_CANCEL = 0xef; + + public static final byte NOTIFY_RESET_AUTHENTICATION_FAILED = 0x9; + + public static final byte NOTIFY_RESET_AUTHENTICATION_SUCCESS = 0xa; + + public static final byte NOTIFY_SET_LATENCY_SUCCESS = 0x8; + + public static final byte NOTIFY_STATUS_MOTOR_ALARM = 0x11; + + public static final byte NOTIFY_STATUS_MOTOR_AUTH = 0x13; + + public static final byte NOTIFY_STATUS_MOTOR_AUTH_SUCCESS = 0x15; + + public static final byte NOTIFY_STATUS_MOTOR_CALL = 0xe; + + public static final byte NOTIFY_STATUS_MOTOR_DISCONNECT = 0xf; + + public static final byte NOTIFY_STATUS_MOTOR_GOAL = 0x12; + + public static final byte NOTIFY_STATUS_MOTOR_NOTIFY = 0xd; + + public static final byte NOTIFY_STATUS_MOTOR_SHUTDOWN = 0x14; + + public static final byte NOTIFY_STATUS_MOTOR_SMART_ALARM = 0x10; + + public static final byte NOTIFY_STATUS_MOTOR_TEST = 0x16; + + public static final byte NOTIFY_UNKNOWN = -0x1; + + public static final String UUID_CHARACTERISTIC_FEATURE = "2A9E"; + + public static final String UUID_CHARACTERISTIC_MEASUREMENT = "2A9D"; + + public static final String UUID_SERVICE_WEIGHT_SCALE_SERVICE = "181D"; + + public static final String UUID_SERVICE_WEIGHT_SERVICE = "00001530-0000-3512-2118-0009af100700"; + + public static final byte MSG_CONNECTED = 0x0; + + public static final byte MSG_DISCONNECTED = 0x1; + + public static final byte MSG_CONNECTION_FAILED = 0x2; + + public static final byte MSG_INITIALIZATION_FAILED = 0x3; + + public static final byte MSG_INITIALIZATION_SUCCESS = 0x4; + + public static final byte MSG_STEPS_CHANGED = 0x5; + + public static final byte MSG_DEVICE_STATUS_CHANGED = 0x6; + + public static final byte MSG_BATTERY_STATUS_CHANGED = 0x7; + + /* + + public static final COMMAND_CONFIRM_ACTIVITY_DATA_TRANSFER_COMPLETE = 0xat; + + public static final byte COMMAND_FACTORY_RESET = 0x9t; + + public static final byte COMMAND_FETCH_DATA = 0x6t; + + public static final byte COMMAND_GET_SENSOR_DATA = 0x12t + + public static final byte COMMAND_REBOOT = 0xct + + public static final byte COMMAND_SEND_FIRMWARE_INFO = 0x7t + + public static final COMMAND_SEND_NOTIFICATION = 0x8t + + public static final int COMMAND_SET_COLOR_THEME = et; + + public static final COMMAND_SET_FITNESS_GOAL = 0x5t + + public static final COMMAND_SET_REALTIME_STEP = 0x10t + + public static final COMMAND_SET_REALTIME_STEPS_NOTIFICATION = 0x3t + + public static final COMMAND_SET_TIMER = 0x4t + + public static final COMMAND_SET_WEAR_LOCATION = 0xft + + public static final COMMAND_STOP_MOTOR_VIBRATE = 0x13t + + public static final COMMAND_STOP_SYNC_DATA = 0x11t + + public static final COMMAND_SYNC = 0xbt + + public static final CONNECTION_LATENCY_LEVEL_HIGH = 0x2t; + + public static final CONNECTION_LATENCY_LEVEL_LOW = 0x0t; + + public static final CONNECTION_LATENCY_LEVEL_MEDIUM = 0x1t; + + public static final MODE_REGULAR_DATA_LEN_BYTE = 0x0t; + + public static final MODE_REGULAR_DATA_LEN_MINITE = 0x1t + + public static final PROFILE_STATE_AUTHENTICATION_FAILED:I = 0x4 + + public static final PROFILE_STATE_AUTHENTICATION_SUCCESS:I = 0x3 + + public static final PROFILE_STATE_INITIALIZATION_FAILED:I = 0x2 + + public static final PROFILE_STATE_INITIALIZATION_SUCCESS:I = 0x1 + + public static final PROFILE_STATE_UNKNOWN:I = 0x0 + + public static final TEST_DISCONNECTED_REMINDER = 0x5t + + public static final TEST_NOTIFICATION = 0x3t + + public static final TEST_REMOTE_DISCONNECT = 0x1t + + public static final TEST_SELFTEST = 0x2t + + */ + + private static Map MIBAND_DEBUG; + + static { + MIBAND_DEBUG = new HashMap<>(); + MIBAND_DEBUG.put(UUID_SERVICE_MIBAND_SERVICE, "MiBand Service"); + + MIBAND_DEBUG.put(UUID_CHARACTERISTIC_DEVICE_INFO, "Device Info"); + MIBAND_DEBUG.put(UUID_CHARACTERISTIC_DEVICE_NAME, "Device Name"); + MIBAND_DEBUG.put(UUID_CHARACTERISTIC_NOTIFICATION, "Notification"); + MIBAND_DEBUG.put(UUID_CHARACTERISTIC_USER_INFO, "User Info"); + MIBAND_DEBUG.put(UUID_CHARACTERISTIC_CONTROL_POINT, "Control Point"); + MIBAND_DEBUG.put(UUID_CHARACTERISTIC_REALTIME_STEPS, "Realtime Steps"); + MIBAND_DEBUG.put(UUID_CHARACTERISTIC_ACTIVITY_DATA, "Activity Data"); + MIBAND_DEBUG.put(UUID_CHARACTERISTIC_FIRMWARE_DATA, "Firmware Data"); + MIBAND_DEBUG.put(UUID_CHARACTERISTIC_LE_PARAMS, "LE Params"); + MIBAND_DEBUG.put(UUID_CHARACTERISTIC_DATE_TIME, "Date/Time"); + MIBAND_DEBUG.put(UUID_CHARACTERISTIC_STATISTICS, "Statistics"); + MIBAND_DEBUG.put(UUID_CHARACTERISTIC_BATTERY, "Battery"); + MIBAND_DEBUG.put(UUID_CHARACTERISTIC_TEST, "Test"); + MIBAND_DEBUG.put(UUID_CHARACTERISTIC_SENSOR_DATA, "Sensor Data"); + MIBAND_DEBUG.put(UUID_CHARACTERISTIC_PAIR, "Pair"); + + // extra: + MIBAND_DEBUG.put(UUID.fromString("00001800-0000-1000-8000-00805f9b34fb"), "Generic Access Service"); + MIBAND_DEBUG.put(UUID.fromString("00001801-0000-1000-8000-00805f9b34fb"), "Generic Attribute Service"); + MIBAND_DEBUG.put(UUID.fromString("00002a43-0000-1000-8000-00805f9b34fb"), "Alert Category ID"); + MIBAND_DEBUG.put(UUID.fromString("00002a42-0000-1000-8000-00805f9b34fb"), "Alert Category ID Bit Mask"); + MIBAND_DEBUG.put(UUID.fromString("00002a06-0000-1000-8000-00805f9b34fb"), "Alert Level"); + MIBAND_DEBUG.put(UUID.fromString("00002a44-0000-1000-8000-00805f9b34fb"), "Alert Notification Control Point"); + MIBAND_DEBUG.put(UUID.fromString("00002a3f-0000-1000-8000-00805f9b34fb"), "Alert Status"); + MIBAND_DEBUG.put(UUID.fromString("00002a01-0000-1000-8000-00805f9b34fb"), "Appearance"); + MIBAND_DEBUG.put(UUID.fromString("00002a49-0000-1000-8000-00805f9b34fb"), "Blood Pressure Feature"); + MIBAND_DEBUG.put(UUID.fromString("00002a35-0000-1000-8000-00805f9b34fb"), "Blood Pressure Measurement"); + MIBAND_DEBUG.put(UUID.fromString("00002a38-0000-1000-8000-00805f9b34fb"), "Body Sensor Location"); + MIBAND_DEBUG.put(UUID.fromString("00002a2b-0000-1000-8000-00805f9b34fb"), "Current Time"); + MIBAND_DEBUG.put(UUID.fromString("00002a08-0000-1000-8000-00805f9b34fb"), "Date Time"); + MIBAND_DEBUG.put(UUID.fromString("00002a0a-0000-1000-8000-00805f9b34fb"), "Day Date Time"); + MIBAND_DEBUG.put(UUID.fromString("00002a09-0000-1000-8000-00805f9b34fb"), "Day of Week"); + MIBAND_DEBUG.put(UUID.fromString("00002a00-0000-1000-8000-00805f9b34fb"), "Device Name"); + MIBAND_DEBUG.put(UUID.fromString("00002a0d-0000-1000-8000-00805f9b34fb"), "DST Offset"); + MIBAND_DEBUG.put(UUID.fromString("00002a0c-0000-1000-8000-00805f9b34fb"), "Exact Time 256"); + MIBAND_DEBUG.put(UUID.fromString("00002a26-0000-1000-8000-00805f9b34fb"), "Firmware Revision String"); + MIBAND_DEBUG.put(UUID.fromString("00002a27-0000-1000-8000-00805f9b34fb"), "Hardware Revision String"); + MIBAND_DEBUG.put(UUID.fromString("00002a39-0000-1000-8000-00805f9b34fb"), "Heart Rate Control Point"); + MIBAND_DEBUG.put(UUID.fromString("00002a37-0000-1000-8000-00805f9b34fb"), "Heart Rate Measurement"); + MIBAND_DEBUG.put(UUID.fromString("00002a2a-0000-1000-8000-00805f9b34fb"), "IEEE 11073-20601 Regulatory"); + MIBAND_DEBUG.put(UUID.fromString("00002a36-0000-1000-8000-00805f9b34fb"), "Intermediate Cuff Pressure"); + MIBAND_DEBUG.put(UUID.fromString("00002a1e-0000-1000-8000-00805f9b34fb"), "Intermediate Temperature"); + MIBAND_DEBUG.put(UUID.fromString("00002a0f-0000-1000-8000-00805f9b34fb"), "Local Time Information"); + MIBAND_DEBUG.put(UUID.fromString("00002a29-0000-1000-8000-00805f9b34fb"), "Manufacturer Name String"); + MIBAND_DEBUG.put(UUID.fromString("00002a21-0000-1000-8000-00805f9b34fb"), "Measurement Interval"); + MIBAND_DEBUG.put(UUID.fromString("00002a24-0000-1000-8000-00805f9b34fb"), "Model Number String"); + MIBAND_DEBUG.put(UUID.fromString("00002a46-0000-1000-8000-00805f9b34fb"), "New Alert"); + MIBAND_DEBUG.put(UUID.fromString("00002a04-0000-1000-8000-00805f9b34fb"), "Peripheral Preferred Connection Parameters"); + MIBAND_DEBUG.put(UUID.fromString("00002a02-0000-1000-8000-00805f9b34fb"), "Peripheral Privacy Flag"); + MIBAND_DEBUG.put(UUID.fromString("00002a03-0000-1000-8000-00805f9b34fb"), "Reconnection Address"); + MIBAND_DEBUG.put(UUID.fromString("00002a14-0000-1000-8000-00805f9b34fb"), "Reference Time Information"); + MIBAND_DEBUG.put(UUID.fromString("00002a40-0000-1000-8000-00805f9b34fb"), "Ringer Control Point"); + MIBAND_DEBUG.put(UUID.fromString("00002a41-0000-1000-8000-00805f9b34fb"), "Ringer Setting"); + MIBAND_DEBUG.put(UUID.fromString("00002a25-0000-1000-8000-00805f9b34fb"), "Serial Number String"); + MIBAND_DEBUG.put(UUID.fromString("00002a05-0000-1000-8000-00805f9b34fb"), "Service Changed"); + MIBAND_DEBUG.put(UUID.fromString("00002a28-0000-1000-8000-00805f9b34fb"), "Software Revision String"); + MIBAND_DEBUG.put(UUID.fromString("00002a47-0000-1000-8000-00805f9b34fb"), "Supported New Alert Category"); + MIBAND_DEBUG.put(UUID.fromString("00002a48-0000-1000-8000-00805f9b34fb"), "Supported Unread Alert Category"); + MIBAND_DEBUG.put(UUID.fromString("00002a23-0000-1000-8000-00805f9b34fb"), "System ID"); + MIBAND_DEBUG.put(UUID.fromString("00002a1c-0000-1000-8000-00805f9b34fb"), "Temperature Measurement"); + MIBAND_DEBUG.put(UUID.fromString("00002a1d-0000-1000-8000-00805f9b34fb"), "Temperature Type"); + MIBAND_DEBUG.put(UUID.fromString("00002a12-0000-1000-8000-00805f9b34fb"), "Time Accuracy"); + MIBAND_DEBUG.put(UUID.fromString("00002a13-0000-1000-8000-00805f9b34fb"), "Time Source"); + MIBAND_DEBUG.put(UUID.fromString("00002a16-0000-1000-8000-00805f9b34fb"), "Time Update Control Point"); + MIBAND_DEBUG.put(UUID.fromString("00002a17-0000-1000-8000-00805f9b34fb"), "Time Update State"); + MIBAND_DEBUG.put(UUID.fromString("00002a11-0000-1000-8000-00805f9b34fb"), "Time with DST"); + MIBAND_DEBUG.put(UUID.fromString("00002a0e-0000-1000-8000-00805f9b34fb"), "Time Zone"); + MIBAND_DEBUG.put(UUID.fromString("00002a07-0000-1000-8000-00805f9b34fb"), "Tx Power Level"); + MIBAND_DEBUG.put(UUID.fromString("00002a45-0000-1000-8000-00805f9b34fb"), "Unread Alert Status"); + } + + public static String lookup(UUID uuid, String fallback) { + String name = MIBAND_DEBUG.get(uuid); + if (name == null) { + name = fallback; + } + return name; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/miband/MiBandSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/miband/MiBandSupport.java index 12c6661d9..f652d64ac 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/miband/MiBandSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/miband/MiBandSupport.java @@ -1,32 +1,120 @@ package nodomain.freeyourgadget.gadgetbridge.miband; -import nodomain.freeyourgadget.gadgetbridge.AbstractBTLEDeviceSupport; +import java.io.IOException; +import java.util.UUID; + import nodomain.freeyourgadget.gadgetbridge.GBCommand; +import nodomain.freeyourgadget.gadgetbridge.btle.AbstractBTLEDeviceSupport; +import nodomain.freeyourgadget.gadgetbridge.btle.TransactionBuilder; +import android.bluetooth.BluetoothGatt; +import android.bluetooth.BluetoothGattCharacteristic; +import android.util.Log; public class MiBandSupport extends AbstractBTLEDeviceSupport { - @Override - public void dispose() { - // TODO Auto-generated method stub + private static final String TAG = MiBandSupport.class.getSimpleName(); + public MiBandSupport() { + addSupportedService(MiBandService.UUID_SERVICE_MIBAND_SERVICE); } @Override - public boolean connect() { - // TODO Auto-generated method stub - return false; + protected TransactionBuilder initializeDevice(TransactionBuilder builder) { + pair(builder).sendUserInfo(builder); + return builder; + } + + @Override + public boolean useAutoConnect() { + return true; + } + + private byte[] getDefaultNotification() { + final int vibrateTimes = 1; + final long vibrateDuration = 250l; + final int flashTimes = 1; + final int flashColour = 0xFFFFFFFF; + final int originalColour = 0xFFFFFFFF; + final long flashDuration = 250l; + + return getNotification(vibrateDuration, vibrateTimes, flashTimes, flashColour, originalColour, flashDuration); + } + + private void sendDefaultNotification(TransactionBuilder builder) { + BluetoothGattCharacteristic characteristic = getCharacteristic(MiBandService.UUID_CHARACTERISTIC_CONTROL_POINT); + Log.i(TAG, "Sending notification to MiBand: " + characteristic); + builder.write(characteristic, getDefaultNotification()).queue(getQueue()); + } + + private byte[] getNotification(long vibrateDuration, int vibrateTimes, int flashTimes, int flashColour, int originalColour, long flashDuration) { + byte[] vibrate = new byte[]{ (byte) 8, (byte) 1 }; + byte r = 6; + byte g = 0; + byte b = 6; + boolean display = true; + // byte[] flashColor = new byte[]{ 14, r, g, b, display ? (byte) 1 : (byte) 0 }; + return vibrate; + } + + private UserInfo getUserInfo() { + // SharedPreferences mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(mContext.getApplicationContext()); + // UserInfo mInfo = new UserInfo( + // mSharedPreferences.getString(MiBandConstants.PREFERENCE_MAC_ADDRESS, ""), + // "1550050550", + // (mSharedPreferences.getString(MiBandConstants.PREFERENCE_GENDER, "Male") == "Male") ? 1 : 0, + // Integer.parseInt(mSharedPreferences.getString(MiBandConstants.PREFERENCE_AGE, "25")), + // Integer.parseInt(mSharedPreferences.getString(MiBandConstants.PREFERENCE_HEIGHT, "175")), + // Integer.parseInt(mSharedPreferences.getString(MiBandConstants.PREFERENCE_WEIGHT, "60")), + // 0 + // ); + return UserInfo.getDefault(getDevice().getAddress()); + } + + /** + * Part of device initialization process. Do not call manually. + * @param builder + * @return + */ + private MiBandSupport sendUserInfo(TransactionBuilder builder) { + Log.d(TAG, "Writing User Info!"); + BluetoothGattCharacteristic characteristic = getCharacteristic(MiBandService.UUID_CHARACTERISTIC_USER_INFO); + builder.write(characteristic, getUserInfo().getData()); + return this; + } + + /** + * Part of device initialization process. Do not call manually. + * @param builder + * @return + */ + private MiBandSupport pair(TransactionBuilder transaction) { + Log.i(TAG, "Attempting to pair MI device..."); + BluetoothGattCharacteristic characteristic = getCharacteristic(MiBandService.UUID_CHARACTERISTIC_PAIR); + if (characteristic != null) { + transaction.write(characteristic, new byte[]{2}); + } else { + Log.i(TAG, "Unable to pair MI device -- characteristic not available"); + } + return this; + } + + private void performDefaultNotification(String task) { + try { + TransactionBuilder builder = performInitialized(task); + sendDefaultNotification(builder); + } catch (IOException ex) { + Log.e(TAG, "Unable to send notification to MI device", ex); + } } @Override public void onSMS(String from, String body) { - // TODO Auto-generated method stub - + performDefaultNotification("sms received"); } @Override public void onEmail(String from, String subject, String body) { - // TODO Auto-generated method stub - + performDefaultNotification("email received"); } @Override @@ -70,4 +158,39 @@ public class MiBandSupport extends AbstractBTLEDeviceSupport { // TODO Auto-generated method stub } + + @Override + public void onCharacteristicWrite(BluetoothGatt gatt, + BluetoothGattCharacteristic characteristic, int status) { + UUID characteristicUUID = characteristic.getUuid(); + if (MiBandService.UUID_CHARACTERISTIC_PAIR.equals(characteristicUUID)) { + handlePairResult(characteristic.getValue(), status); + } + } + + private void handlePairResult(byte[] pairResult, int status) { + if (status != BluetoothGatt.GATT_SUCCESS) { + Log.i(TAG, "Pairing MI device failed: " + status); + return; + } + + Object value = null; + if (pairResult != null) { + if (pairResult.length == 1) { + try { + byte b = pairResult[0]; + Integer intValue = Integer.valueOf((int) b); + if (intValue.intValue() == 2) { + Log.i(TAG, "Successfully paired MI device"); + return; + } + } catch (Exception ex) { + Log.w(TAG, "Error identifying pairing result", ex); + return; + } + } + value = pairResult.toString(); + } + Log.i(TAG, "MI Band pairing result: " + value); + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/miband/UserInfo.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/miband/UserInfo.java new file mode 100644 index 000000000..dbca48040 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/miband/UserInfo.java @@ -0,0 +1,91 @@ +package nodomain.freeyourgadget.gadgetbridge.miband; + +/** + * Created by UgoRaffaele on 30/01/2015. + */ +public class UserInfo { + + private String btAddress; + private String alias; + private int gender; + private int age; + private int height; + private int weight; + private int type; + + private byte[] data = new byte[20]; + + /** + * Creates a default user info. + * @param btAddress the address of the MI Band to connect to. + */ + public static UserInfo getDefault(String btAddress) { + return new UserInfo(btAddress, "1550050550", 0, 25, 175, 70, 0); + } + + /** + * Creates a user info with the given data + * @param address the address of the MI Band to connect to. + */ + public UserInfo(String address, String alias, int gender, int age, int height, int weight, int type) { + + this.btAddress = address; + this.alias = alias; + this.gender = gender; + this.age = age; + this.height = height; + this.weight = weight; + this.type = type; + + byte[] sequence = new byte[20]; + + int uid = Integer.parseInt(alias); + + sequence[0] = (byte) uid; + sequence[1] = (byte) (uid >>> 8); + sequence[2] = (byte) (uid >>> 16); + sequence[3] = (byte) (uid >>> 24); + + sequence[4] = (byte) (gender & 0xff); + sequence[5] = (byte) (age & 0xff); + sequence[6] = (byte) (height & 0xff); + sequence[7] = (byte) (weight & 0xff); + sequence[8] = (byte) (type & 0xff); + + for (int u = 9; u < 19; u++) + sequence[u] = alias.getBytes()[u-9]; + + byte[] crcSequence = new byte[19]; + for (int u = 0; u < crcSequence.length; u++) + crcSequence[u] = sequence[u]; + + sequence[19] = (byte) ((getCRC8(crcSequence) ^ Integer.parseInt(address.substring(address.length() - 2), 16)) & 0xff); + + this.data = sequence; + } + + public byte[] getData() { + return this.data; + } + + protected int getCRC8(byte[] seq) { + int len = seq.length; + int i = 0; + byte crc = 0x00; + + while (len-- > 0) { + byte extract = seq[i++]; + for (byte tempI = 8; tempI != 0; tempI--) { + byte sum = (byte) ((crc & 0xff) ^ (extract & 0xff)); + sum = (byte) ((sum & 0xff) & 0x01); + crc = (byte) ((crc & 0xff) >>> 1); + if (sum != 0) { + crc = (byte)((crc & 0xff) ^ 0x8c); + } + extract = (byte) ((extract & 0xff) >>> 1); + } + } + return (crc & 0xff); + } + +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/pebble/PebbleSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/pebble/PebbleSupport.java index d2ad502ff..c49e7ea44 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/pebble/PebbleSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/pebble/PebbleSupport.java @@ -25,4 +25,9 @@ public class PebbleSupport extends AbstractBTDeviceSupport { protected GBDeviceIoThread createDeviceIOThread() { return new PebbleIoThread(getDevice(), getDeviceProtocol(), getBluetoothAdapter(), getContext()); } + + @Override + public boolean useAutoConnect() { + return false; + } } diff --git a/app/src/main/lint.xml b/app/src/main/lint.xml new file mode 100644 index 000000000..ee0eead5b --- /dev/null +++ b/app/src/main/lint.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/app/src/main/project.properties b/app/src/main/project.properties new file mode 100644 index 000000000..7ad4d39c9 --- /dev/null +++ b/app/src/main/project.properties @@ -0,0 +1,15 @@ +# This file is automatically generated by Android Tools. +# Do not modify this file -- YOUR CHANGES WILL BE ERASED! +# +# This file must be checked in Version Control Systems. +# +# To customize properties used by the Ant build system edit +# "ant.properties", and override values to adapt the script to your +# project structure. +# +# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home): +#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt + +# Project target. +target=android-21 +android.library.reference.1=../../../../android-sdk/extras/android/support/v7/appcompat