326 lines
15 KiB
Java
326 lines
15 KiB
Java
/* Copyright (C) 2023 José Rebelo, Yoran Vulker
|
|
|
|
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 <http://www.gnu.org/licenses/>. */
|
|
package nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi;
|
|
|
|
import android.bluetooth.BluetoothAdapter;
|
|
import android.bluetooth.BluetoothGatt;
|
|
import android.bluetooth.BluetoothGattCharacteristic;
|
|
import android.content.Context;
|
|
import android.widget.Toast;
|
|
|
|
import androidx.annotation.Nullable;
|
|
|
|
import org.slf4j.Logger;
|
|
import org.slf4j.LoggerFactory;
|
|
|
|
import java.util.Map;
|
|
import java.util.Set;
|
|
import java.util.UUID;
|
|
|
|
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
|
import nodomain.freeyourgadget.gadgetbridge.proto.xiaomi.XiaomiProto;
|
|
import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport;
|
|
import nodomain.freeyourgadget.gadgetbridge.service.btle.BtLEQueue;
|
|
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
|
|
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.PlainAction;
|
|
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction;
|
|
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetProgressAction;
|
|
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
|
|
|
public class XiaomiBleSupport extends XiaomiConnectionSupport {
|
|
private static final Logger LOG = LoggerFactory.getLogger(XiaomiBleSupport.class);
|
|
|
|
private XiaomiCharacteristic characteristicCommandRead;
|
|
private XiaomiCharacteristic characteristicCommandWrite;
|
|
private XiaomiCharacteristic characteristicActivityData;
|
|
private XiaomiCharacteristic characteristicDataUpload;
|
|
|
|
private final XiaomiSupport mXiaomiSupport;
|
|
|
|
final AbstractBTLEDeviceSupport commsSupport = new AbstractBTLEDeviceSupport(LOG) {
|
|
@Override
|
|
public boolean useAutoConnect() {
|
|
return mXiaomiSupport.useAutoConnect();
|
|
}
|
|
|
|
@Override
|
|
protected Set<UUID> getSupportedServices() {
|
|
return XiaomiUuids.BLE_UUIDS.keySet();
|
|
}
|
|
|
|
@Override
|
|
protected TransactionBuilder initializeDevice(final TransactionBuilder builder) {
|
|
XiaomiUuids.XiaomiBleUuidSet uuidSet = null;
|
|
BluetoothGattCharacteristic btCharacteristicCommandRead = null;
|
|
BluetoothGattCharacteristic btCharacteristicCommandWrite = null;
|
|
BluetoothGattCharacteristic btCharacteristicActivityData = null;
|
|
BluetoothGattCharacteristic btCharacteristicDataUpload = null;
|
|
|
|
// Attempt to find a known xiaomi service
|
|
for (Map.Entry<UUID, XiaomiUuids.XiaomiBleUuidSet> xiaomiUuid : XiaomiUuids.BLE_UUIDS.entrySet()) {
|
|
if (getSupportedServices().contains(xiaomiUuid.getKey())) {
|
|
LOG.debug("Found Xiaomi service: {}", xiaomiUuid.getKey());
|
|
uuidSet = xiaomiUuid.getValue();
|
|
UUID currentChar;
|
|
|
|
if ((currentChar = uuidSet.getCharacteristicCommandRead()) == null ||
|
|
(btCharacteristicCommandRead = getCharacteristic(currentChar)) == null) {
|
|
LOG.warn("btCharacteristicCommandRead characteristicc is null");
|
|
continue;
|
|
}
|
|
|
|
if ((currentChar = uuidSet.getCharacteristicCommandWrite()) == null ||
|
|
(btCharacteristicCommandWrite = getCharacteristic(currentChar)) == null) {
|
|
LOG.warn("btCharacteristicCommandWrite characteristicc is null");
|
|
continue;
|
|
}
|
|
|
|
if ((currentChar = uuidSet.getCharacteristicActivityData()) == null ||
|
|
(btCharacteristicActivityData= getCharacteristic(currentChar)) == null) {
|
|
LOG.warn("btCharacteristicActivityData characteristicc is null");
|
|
continue;
|
|
}
|
|
|
|
if ((currentChar = uuidSet.getCharacteristicDataUpload()) == null ||
|
|
(btCharacteristicDataUpload= getCharacteristic(currentChar)) == null) {
|
|
LOG.warn("btCharacteristicDataUpload characteristicc is null");
|
|
// this characteristic may not be supported by all models
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (uuidSet == null) {
|
|
GB.toast(getContext(), "Failed to find known Xiaomi service", Toast.LENGTH_LONG, GB.ERROR);
|
|
LOG.warn("Failed to find known Xiaomi service");
|
|
builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.NOT_CONNECTED, getContext()));
|
|
return builder;
|
|
}
|
|
|
|
// FIXME unsetDynamicState unsets the fw version, which causes problems..
|
|
if (getDevice().getFirmwareVersion() == null) {
|
|
getDevice().setFirmwareVersion(mXiaomiSupport.getCachedFirmwareVersion() != null ?
|
|
mXiaomiSupport.getCachedFirmwareVersion() :
|
|
"N/A");
|
|
}
|
|
|
|
if (btCharacteristicCommandRead == null || btCharacteristicCommandWrite == null) {
|
|
LOG.warn("Characteristics are null, will attempt to reconnect");
|
|
builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.WAITING_FOR_RECONNECT, getContext()));
|
|
return builder;
|
|
}
|
|
|
|
// FIXME:
|
|
// Because the first handshake packet is sent before the actions in the builder are run,
|
|
// the maximum message size is not properly initialized if the device itself does not request
|
|
// the MTU to be upgraded. However, since we will upgrade the MTU ourselves to the highest
|
|
// possible (512) and the device will (likely) respond with something higher than 247,
|
|
// we will initialize the characteristics with that MTU.
|
|
final int expectedMtu = 247;
|
|
characteristicCommandRead = new XiaomiCharacteristic(XiaomiBleSupport.this, btCharacteristicCommandRead, mXiaomiSupport.getAuthService());
|
|
characteristicCommandRead.setEncrypted(uuidSet.isEncrypted());
|
|
characteristicCommandRead.setChannelHandler(mXiaomiSupport::handleCommandBytes);
|
|
characteristicCommandRead.setMtu(expectedMtu);
|
|
characteristicCommandWrite = new XiaomiCharacteristic(XiaomiBleSupport.this, btCharacteristicCommandWrite, mXiaomiSupport.getAuthService());
|
|
characteristicCommandWrite.setEncrypted(uuidSet.isEncrypted());
|
|
characteristicCommandWrite.setMtu(expectedMtu);
|
|
characteristicActivityData = new XiaomiCharacteristic(XiaomiBleSupport.this, btCharacteristicActivityData, mXiaomiSupport.getAuthService());
|
|
characteristicActivityData.setChannelHandler(mXiaomiSupport.getHealthService().getActivityFetcher()::addChunk);
|
|
characteristicActivityData.setEncrypted(uuidSet.isEncrypted());
|
|
characteristicActivityData.setMtu(expectedMtu);
|
|
characteristicDataUpload = new XiaomiCharacteristic(XiaomiBleSupport.this, btCharacteristicDataUpload, mXiaomiSupport.getAuthService());
|
|
characteristicDataUpload.setEncrypted(uuidSet.isEncrypted());
|
|
characteristicDataUpload.setIncrementNonce(false);
|
|
characteristicDataUpload.setMtu(expectedMtu);
|
|
|
|
// request highest possible MTU; device should response with the highest supported MTU anyway
|
|
builder.requestMtu(512);
|
|
builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZING, getContext()));
|
|
builder.notify(btCharacteristicCommandWrite, true);
|
|
builder.notify(btCharacteristicCommandRead, true);
|
|
builder.notify(btCharacteristicActivityData, true);
|
|
builder.notify(btCharacteristicDataUpload, true);
|
|
builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.AUTHENTICATING, getContext()));
|
|
|
|
if (uuidSet.isEncrypted()) {
|
|
builder.add(new PlainAction() {
|
|
@Override
|
|
public boolean run(BluetoothGatt gatt) {
|
|
mXiaomiSupport.getAuthService().startEncryptedHandshake();
|
|
return true;
|
|
}
|
|
});
|
|
} else {
|
|
builder.add(new PlainAction() {
|
|
@Override
|
|
public boolean run(BluetoothGatt gatt) {
|
|
mXiaomiSupport.getAuthService().startClearTextHandshake();
|
|
return true;
|
|
}
|
|
});
|
|
}
|
|
|
|
return builder;
|
|
}
|
|
|
|
@Override
|
|
public boolean onCharacteristicChanged(final BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic) {
|
|
if (super.onCharacteristicChanged(gatt, characteristic)) {
|
|
return true;
|
|
}
|
|
|
|
final UUID characteristicUUID = characteristic.getUuid();
|
|
final byte[] value = characteristic.getValue();
|
|
|
|
if (characteristicCommandRead.getCharacteristicUUID().equals(characteristicUUID)) {
|
|
characteristicCommandRead.onCharacteristicChanged(value);
|
|
return true;
|
|
} else if (characteristicCommandWrite.getCharacteristicUUID().equals(characteristicUUID)) {
|
|
characteristicCommandWrite.onCharacteristicChanged(value);
|
|
return true;
|
|
} else if (characteristicActivityData.getCharacteristicUUID().equals(characteristicUUID)) {
|
|
characteristicActivityData.onCharacteristicChanged(value);
|
|
return true;
|
|
} else if (characteristicDataUpload.getCharacteristicUUID().equals(characteristicUUID)) {
|
|
characteristicDataUpload.onCharacteristicChanged(value);
|
|
return true;
|
|
}
|
|
|
|
LOG.warn("Unhandled characteristic changed: {} {}", characteristicUUID, GB.hexdump(value));
|
|
return false;
|
|
}
|
|
|
|
@Override
|
|
public boolean getImplicitCallbackModify() {
|
|
return mXiaomiSupport.getImplicitCallbackModify();
|
|
}
|
|
|
|
@Override
|
|
public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) {
|
|
super.onMtuChanged(gatt, mtu, status);
|
|
|
|
if (characteristicCommandRead != null)
|
|
characteristicCommandRead.setMtu(mtu);
|
|
if (characteristicCommandWrite != null)
|
|
characteristicCommandWrite.setMtu(mtu);
|
|
if (characteristicDataUpload != null)
|
|
characteristicDataUpload.setMtu(mtu);
|
|
if (characteristicActivityData != null)
|
|
characteristicActivityData.setMtu(mtu);
|
|
}
|
|
};
|
|
|
|
public XiaomiBleSupport(final XiaomiSupport xiaomiSupport) {
|
|
this.mXiaomiSupport = xiaomiSupport;
|
|
}
|
|
|
|
public void onAuthSuccess() {
|
|
characteristicCommandRead.reset();
|
|
characteristicCommandWrite.reset();
|
|
characteristicActivityData.reset();
|
|
characteristicDataUpload.reset();
|
|
}
|
|
|
|
@Override
|
|
public void setContext(GBDevice device, BluetoothAdapter adapter, Context context) {
|
|
this.commsSupport.setContext(device, adapter, context);
|
|
}
|
|
|
|
public void sendCommand(final String taskName, final XiaomiProto.Command command) {
|
|
if (this.characteristicCommandWrite == null) {
|
|
// Can sometimes happen in race conditions when connecting + receiving calendar event or weather updates
|
|
LOG.warn("characteristicCommandWrite is null!");
|
|
return;
|
|
}
|
|
|
|
this.characteristicCommandWrite.write(taskName, command.toByteArray());
|
|
}
|
|
|
|
@Override
|
|
public void sendDataChunk(String taskName, byte[] chunk, @Nullable XiaomiCharacteristic.SendCallback callback) {
|
|
if (this.characteristicDataUpload == null) {
|
|
LOG.warn("characteristicDataUpload is null!");
|
|
return;
|
|
}
|
|
|
|
this.characteristicDataUpload.write(taskName, chunk, callback);
|
|
}
|
|
|
|
/**
|
|
* Realistically, this function should only be used during auth, as we must schedule the command after
|
|
* notifications were enabled on the characteristics, and for that we need the builder to guarantee the
|
|
* order.
|
|
*/
|
|
public void sendCommand(final TransactionBuilder builder, final XiaomiProto.Command command) {
|
|
if (this.characteristicCommandWrite == null) {
|
|
// Can sometimes happen in race conditions when connecting + receiving calendar event or weather updates
|
|
LOG.warn("characteristicCommandWrite is null!");
|
|
return;
|
|
}
|
|
|
|
this.characteristicCommandWrite.write(builder, command.toByteArray());
|
|
}
|
|
|
|
public TransactionBuilder createTransactionBuilder(String taskName) {
|
|
return commsSupport.createTransactionBuilder(taskName);
|
|
}
|
|
|
|
public BtLEQueue getQueue() {
|
|
return commsSupport.getQueue();
|
|
}
|
|
|
|
@Override
|
|
public void onUploadProgress(int textRsrc, int progressPercent, boolean ongoing) {
|
|
try {
|
|
final TransactionBuilder builder = commsSupport.createTransactionBuilder("send data upload progress");
|
|
builder.add(new SetProgressAction(
|
|
commsSupport.getContext().getString(textRsrc),
|
|
ongoing,
|
|
progressPercent,
|
|
commsSupport.getContext()
|
|
));
|
|
builder.queue(commsSupport.getQueue());
|
|
} catch (final Exception e) {
|
|
LOG.error("Failed to update progress notification", e);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public boolean connect() {
|
|
return commsSupport.connect();
|
|
}
|
|
|
|
@Override
|
|
public void runOnQueue(String taskName, Runnable runnable) {
|
|
final TransactionBuilder b = commsSupport.createTransactionBuilder("run task " + taskName + " on queue");
|
|
b.add(new PlainAction() {
|
|
@Override
|
|
public boolean run(BluetoothGatt gatt) {
|
|
runnable.run();
|
|
return true;
|
|
}
|
|
});
|
|
b.queue(commsSupport.getQueue());
|
|
}
|
|
|
|
@Override
|
|
public void dispose() {
|
|
commsSupport.dispose();
|
|
}
|
|
}
|