From 98e8ec23293ccd20e0130647b19b315e89d3826c Mon Sep 17 00:00:00 2001 From: MrYoranimo Date: Thu, 14 Dec 2023 12:51:12 +0100 Subject: [PATCH] Xiaomi: Introduce XiaomiConnectionSupport MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: José Rebelo --- .../devices/xiaomi/XiaomiAuthService.java | 35 +-- .../devices/xiaomi/XiaomiBleSupport.java | 242 ++++++++++++++++ .../devices/xiaomi/XiaomiChannelHandler.java | 5 + .../devices/xiaomi/XiaomiCharacteristic.java | 31 +- .../xiaomi/XiaomiConnectionSupport.java | 33 +++ .../service/devices/xiaomi/XiaomiSupport.java | 272 +++++++----------- .../services/XiaomiNotificationService.java | 6 +- .../xiaomi/services/XiaomiSystemService.java | 25 +- .../services/XiaomiWatchfaceService.java | 25 +- app/src/main/res/values/strings.xml | 4 + 10 files changed, 428 insertions(+), 250 deletions(-) create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiBleSupport.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiChannelHandler.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiConnectionSupport.java diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiAuthService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiAuthService.java index 9ab5e16da..3ef9f5f11 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiAuthService.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiAuthService.java @@ -57,12 +57,10 @@ import nodomain.freeyourgadget.gadgetbridge.util.GB; public class XiaomiAuthService extends AbstractXiaomiService { private static final Logger LOG = LoggerFactory.getLogger(XiaomiAuthService.class); - public static final byte[] PAYLOAD_HEADER_AUTH = new byte[]{0, 0, 2, 2}; public static final int COMMAND_TYPE = 1; public static final int CMD_SEND_USERID = 5; - public static final int CMD_NONCE = 26; public static final int CMD_AUTH = 27; @@ -83,7 +81,8 @@ public class XiaomiAuthService extends AbstractXiaomiService { return encryptionInitialized; } - protected void startEncryptedHandshake(final TransactionBuilder builder) { + // TODO also implement for spp + protected void startEncryptedHandshake(final XiaomiBleSupport support, final TransactionBuilder builder) { encryptionInitialized = false; builder.add(new SetDeviceStateAction(getSupport().getDevice(), GBDevice.State.AUTHENTICATING, getSupport().getContext())); @@ -91,10 +90,10 @@ public class XiaomiAuthService extends AbstractXiaomiService { System.arraycopy(getSecretKey(getSupport().getDevice()), 0, secretKey, 0, 16); new SecureRandom().nextBytes(nonce); - getSupport().sendCommand(builder, buildNonceCommand(nonce)); + support.sendCommand(builder, buildNonceCommand(nonce)); } - protected void startClearTextHandshake(final TransactionBuilder builder) { + protected void startClearTextHandshake(final XiaomiBleSupport support, final TransactionBuilder builder) { builder.add(new SetDeviceStateAction(getSupport().getDevice(), GBDevice.State.AUTHENTICATING, getSupport().getContext())); final XiaomiProto.Auth auth = XiaomiProto.Auth.newBuilder() @@ -107,7 +106,7 @@ public class XiaomiAuthService extends AbstractXiaomiService { .setAuth(auth) .build(); - getSupport().sendCommand(builder, command); + support.sendCommand(builder, command); } @Override @@ -121,33 +120,27 @@ public class XiaomiAuthService extends AbstractXiaomiService { LOG.debug("Got watch nonce"); // Watch nonce - final XiaomiProto.Command reply = handleWatchNonce(cmd.getAuth().getWatchNonce()); - if (reply == null) { + final XiaomiProto.Command command = handleWatchNonce(cmd.getAuth().getWatchNonce()); + if (command == null) { getSupport().disconnect(); return; } - final TransactionBuilder builder = getSupport().createTransactionBuilder("auth step 2"); - // TODO use sendCommand - builder.write( - getSupport().getCharacteristic(getSupport().characteristicCommandWrite.getCharacteristicUUID()), - ArrayUtils.addAll(PAYLOAD_HEADER_AUTH, reply.toByteArray()) - ); - builder.queue(getSupport().getQueue()); + getSupport().sendCommand("auth step 2", command); break; } case CMD_AUTH: case CMD_SEND_USERID: { if (cmd.getSubtype() == CMD_AUTH || cmd.getAuth().getStatus() == 1) { - LOG.info("Authenticated!"); - encryptionInitialized = cmd.getSubtype() == CMD_AUTH; - final TransactionBuilder builder = getSupport().createTransactionBuilder("phase 2 initialize"); - builder.add(new SetDeviceStateAction(getSupport().getDevice(), GBDevice.State.INITIALIZED, getSupport().getContext())); - getSupport().phase2Initialize(); - builder.queue(getSupport().getQueue()); + LOG.info("Authenticated, further communications are {}", encryptionInitialized ? "encrypted" : "in plaintext"); + + getSupport().getDevice().setState(GBDevice.State.INITIALIZED); + getSupport().getDevice().sendDeviceUpdateIntent(getSupport().getContext(), GBDevice.DeviceUpdateSubject.DEVICE_STATE); + + getSupport().onAuthSuccess(); } else { LOG.warn("could not authenticate"); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiBleSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiBleSupport.java new file mode 100644 index 000000000..138c5a7c2 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiBleSupport.java @@ -0,0 +1,242 @@ +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 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.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 getSupportedServices() { + return XiaomiBleUuids.UUIDS.keySet(); + } + + @Override + protected final TransactionBuilder initializeDevice(final TransactionBuilder builder) { + XiaomiBleUuids.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 xiaomiUuid : XiaomiBleUuids.UUIDS.entrySet()) { + if (getSupportedServices().contains(xiaomiUuid.getKey())) { + LOG.debug("Found Xiaomi service: {}", xiaomiUuid.getKey()); + uuidSet = xiaomiUuid.getValue(); + + btCharacteristicCommandRead = getCharacteristic(uuidSet.getCharacteristicCommandRead()); + btCharacteristicCommandWrite = getCharacteristic(uuidSet.getCharacteristicCommandWrite()); + btCharacteristicActivityData = getCharacteristic(uuidSet.getCharacteristicActivityData()); + btCharacteristicDataUpload = getCharacteristic(uuidSet.getCharacteristicDataUpload()); + if (btCharacteristicCommandRead == null) { + LOG.warn("btCharacteristicCommandRead characteristicc is null"); + continue; + } else if (btCharacteristicCommandWrite == null) { + LOG.warn("btCharacteristicCommandWrite characteristicc is null"); + continue; + } else if (btCharacteristicActivityData == null) { + LOG.warn("btCharacteristicActivityData characteristicc is null"); + continue; + } else if (btCharacteristicDataUpload == null) { + LOG.warn("btCharacteristicDataUpload characteristicc is null"); + continue; + } + + 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 && mXiaomiSupport.getCachedFirmwareVersion() != null) { + getDevice().setFirmwareVersion(mXiaomiSupport.getCachedFirmwareVersion()); + } + + 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; + } + + XiaomiBleSupport.this.characteristicCommandRead = new XiaomiCharacteristic(XiaomiBleSupport.this, btCharacteristicCommandRead, mXiaomiSupport.getAuthService()); + XiaomiBleSupport.this.characteristicCommandRead.setEncrypted(uuidSet.isEncrypted()); + XiaomiBleSupport.this.characteristicCommandRead.setChannelHandler(mXiaomiSupport::handleCommandBytes); + XiaomiBleSupport.this.characteristicCommandWrite = new XiaomiCharacteristic(XiaomiBleSupport.this, btCharacteristicCommandWrite, mXiaomiSupport.getAuthService()); + XiaomiBleSupport.this.characteristicCommandWrite.setEncrypted(uuidSet.isEncrypted()); + XiaomiBleSupport.this.characteristicActivityData = new XiaomiCharacteristic(XiaomiBleSupport.this, btCharacteristicActivityData, mXiaomiSupport.getAuthService()); + XiaomiBleSupport.this.characteristicActivityData.setChannelHandler(mXiaomiSupport.getHealthService().getActivityFetcher()::addChunk); + XiaomiBleSupport.this.characteristicActivityData.setEncrypted(uuidSet.isEncrypted()); + XiaomiBleSupport.this.characteristicDataUpload = new XiaomiCharacteristic(XiaomiBleSupport.this, btCharacteristicDataUpload, mXiaomiSupport.getAuthService()); + XiaomiBleSupport.this.characteristicDataUpload.setEncrypted(uuidSet.isEncrypted()); + XiaomiBleSupport.this.characteristicDataUpload.setIncrementNonce(false); + + mXiaomiSupport.getDataUploadService().setDataUploadCharacteristic(XiaomiBleSupport.this.characteristicDataUpload); + + builder.requestMtu(247); + 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); + + if (uuidSet.isEncrypted()) { + mXiaomiSupport.getAuthService().startEncryptedHandshake(XiaomiBleSupport.this, builder); + } else { + mXiaomiSupport.getAuthService().startClearTextHandshake(XiaomiBleSupport.this, builder); + } + + 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(); + } + }; + + 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); + } + + @Override + public void disconnect() { + this.commsSupport.disconnect(); + } + + 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()); + } + + /** + * 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) { + try { + final TransactionBuilder builder = commsSupport.createTransactionBuilder("send data upload progress"); + builder.add(new SetProgressAction( + commsSupport.getContext().getString(textRsrc), + true, + 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 dispose() { + commsSupport.dispose(); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiChannelHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiChannelHandler.java new file mode 100644 index 000000000..eee5a20e8 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiChannelHandler.java @@ -0,0 +1,5 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi; + +public interface XiaomiChannelHandler { + void handle(final byte[] payload); +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiCharacteristic.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiCharacteristic.java index 8ad1dcf28..e66520c76 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiCharacteristic.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiCharacteristic.java @@ -45,7 +45,7 @@ public class XiaomiCharacteristic { // max chunk size, including headers public static final int MAX_WRITE_SIZE = 242; - private final XiaomiSupport mSupport; + private final XiaomiBleSupport mSupport; private final BluetoothGattCharacteristic bluetoothGattCharacteristic; private final UUID characteristicUUID; @@ -68,11 +68,11 @@ public class XiaomiCharacteristic { private boolean sendingChunked = false; private Payload currentPayload = null; - private Handler handler = null; + private XiaomiChannelHandler channelHandler = null; private SendCallback callback; - public XiaomiCharacteristic(final XiaomiSupport support, + public XiaomiCharacteristic(final XiaomiBleSupport support, final BluetoothGattCharacteristic bluetoothGattCharacteristic, @Nullable final XiaomiAuthService authService) { this.mSupport = support; @@ -86,8 +86,8 @@ public class XiaomiCharacteristic { return characteristicUUID; } - public void setHandler(final Handler handler) { - this.handler = handler; + public void setChannelHandler(final XiaomiChannelHandler handler) { + this.channelHandler = handler; } public void setCallback(final SendCallback callback) { @@ -162,11 +162,15 @@ public class XiaomiCharacteristic { if (chunk == numChunks) { sendChunkEndAck(); - if (isEncrypted) { - // chunks are always encrypted if an auth service is available - handler.handle(authService.decrypt(chunkBuffer.toByteArray())); + if (channelHandler != null) { + if (isEncrypted) { + // chunks are always encrypted if an auth service is available + channelHandler.handle(authService.decrypt(chunkBuffer.toByteArray())); + } else { + channelHandler.handle(chunkBuffer.toByteArray()); + } } else { - handler.handle(chunkBuffer.toByteArray()); + LOG.warn("Channel handler for char {} is null!", characteristicUUID); } currentChunk = 0; @@ -249,7 +253,10 @@ public class XiaomiCharacteristic { buf.get(plainValue); } - handler.handle(plainValue); + if (channelHandler != null) + channelHandler.handle(plainValue); + else + LOG.warn("Channel handler for char {} is null!", characteristicUUID); return; case 3: @@ -362,10 +369,6 @@ public class XiaomiCharacteristic { builder.queue(mSupport.getQueue()); } - public interface Handler { - void handle(final byte[] payload); - } - private static class Payload { private final String taskName; private final byte[] bytes; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiConnectionSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiConnectionSupport.java new file mode 100644 index 000000000..df47cf4c9 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiConnectionSupport.java @@ -0,0 +1,33 @@ +/* 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 . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi; + +import android.bluetooth.BluetoothAdapter; +import android.content.Context; + +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.proto.xiaomi.XiaomiProto; + +public abstract class XiaomiConnectionSupport { + public abstract boolean connect(); + public abstract void onAuthSuccess(); + public abstract void onUploadProgress(int textRsrc, int progressPercent); + public abstract void dispose(); + public abstract void setContext(final GBDevice device, final BluetoothAdapter adapter, final Context context); + public abstract void disconnect(); + public abstract void sendCommand(final String taskName, final XiaomiProto.Command command); +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiSupport.java index 154bf6059..5fd6aaa41 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiSupport.java @@ -18,12 +18,9 @@ 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.location.Location; import android.net.Uri; -import android.widget.Toast; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; @@ -42,6 +39,7 @@ import java.util.UUID; import nodomain.freeyourgadget.gadgetbridge.BuildConfig; import nodomain.freeyourgadget.gadgetbridge.GBApplication; +import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.xiaomi.XiaomiCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.xiaomi.XiaomiFWHelper; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; @@ -57,9 +55,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.Reminder; import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec; import nodomain.freeyourgadget.gadgetbridge.model.WorldClock; import nodomain.freeyourgadget.gadgetbridge.proto.xiaomi.XiaomiProto; -import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport; -import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; -import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction; +import nodomain.freeyourgadget.gadgetbridge.service.AbstractDeviceSupport; import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.activity.XiaomiActivityFileId; import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.activity.XiaomiActivityParser; import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.services.AbstractXiaomiService; @@ -77,27 +73,23 @@ import nodomain.freeyourgadget.gadgetbridge.util.FileUtils; import nodomain.freeyourgadget.gadgetbridge.util.GB; import nodomain.freeyourgadget.gadgetbridge.util.Prefs; -public class XiaomiSupport extends AbstractBTLEDeviceSupport { +public class XiaomiSupport extends AbstractDeviceSupport { private static final Logger LOG = LoggerFactory.getLogger(XiaomiSupport.class); - protected XiaomiCharacteristic characteristicCommandRead; - protected XiaomiCharacteristic characteristicCommandWrite; - protected XiaomiCharacteristic characteristicActivityData; - protected XiaomiCharacteristic characteristicDataUpload; + private final XiaomiAuthService authService = new XiaomiAuthService(this); + private final XiaomiMusicService musicService = new XiaomiMusicService(this); + private final XiaomiHealthService healthService = new XiaomiHealthService(this); + private final XiaomiNotificationService notificationService = new XiaomiNotificationService(this); + private final XiaomiScheduleService scheduleService = new XiaomiScheduleService(this); + private final XiaomiWeatherService weatherService = new XiaomiWeatherService(this); + private final XiaomiSystemService systemService = new XiaomiSystemService(this); + private final XiaomiCalendarService calendarService = new XiaomiCalendarService(this); + private final XiaomiWatchfaceService watchfaceService = new XiaomiWatchfaceService(this); + private final XiaomiDataUploadService dataUploadService = new XiaomiDataUploadService(this); + private final XiaomiPhonebookService phonebookService = new XiaomiPhonebookService(this); - protected final XiaomiAuthService authService = new XiaomiAuthService(this); - protected final XiaomiMusicService musicService = new XiaomiMusicService(this); - protected final XiaomiHealthService healthService = new XiaomiHealthService(this); - protected final XiaomiNotificationService notificationService = new XiaomiNotificationService(this); - protected final XiaomiScheduleService scheduleService = new XiaomiScheduleService(this); - protected final XiaomiWeatherService weatherService = new XiaomiWeatherService(this); - protected final XiaomiSystemService systemService = new XiaomiSystemService(this); - protected final XiaomiCalendarService calendarService = new XiaomiCalendarService(this); - protected final XiaomiWatchfaceService watchfaceService = new XiaomiWatchfaceService(this); - protected final XiaomiDataUploadService dataUploadService = new XiaomiDataUploadService(this); - protected final XiaomiPhonebookService phonebookService = new XiaomiPhonebookService(this); - - private String mFirmwareVersion = null; + private String cachedFirmwareVersion = null; + private XiaomiConnectionSupport connectionSupport = null; private final Map mServiceMap = new LinkedHashMap() {{ put(XiaomiAuthService.COMMAND_TYPE, authService); @@ -113,98 +105,6 @@ public class XiaomiSupport extends AbstractBTLEDeviceSupport { put(XiaomiPhonebookService.COMMAND_TYPE, phonebookService); }}; - public XiaomiSupport() { - super(LOG); - for (final UUID uuid : XiaomiBleUuids.UUIDS.keySet()) { - addSupportedService(uuid); - } - } - - @Override - protected final TransactionBuilder initializeDevice(final TransactionBuilder builder) { - XiaomiBleUuids.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 xiaomiUuid : XiaomiBleUuids.UUIDS.entrySet()) { - if (getSupportedServices().contains(xiaomiUuid.getKey())) { - LOG.debug("Found Xiaomi service: {}", xiaomiUuid.getKey()); - uuidSet = xiaomiUuid.getValue(); - - btCharacteristicCommandRead = getCharacteristic(uuidSet.getCharacteristicCommandRead()); - btCharacteristicCommandWrite = getCharacteristic(uuidSet.getCharacteristicCommandWrite()); - btCharacteristicActivityData = getCharacteristic(uuidSet.getCharacteristicActivityData()); - btCharacteristicDataUpload = getCharacteristic(uuidSet.getCharacteristicDataUpload()); - if (btCharacteristicCommandRead == null) { - LOG.warn("btCharacteristicCommandRead characteristicc is null"); - continue; - } else if (btCharacteristicCommandWrite == null) { - LOG.warn("btCharacteristicCommandWrite characteristicc is null"); - continue; - } else if (btCharacteristicActivityData == null) { - LOG.warn("btCharacteristicActivityData characteristicc is null"); - continue; - } else if (btCharacteristicDataUpload == null) { - LOG.warn("btCharacteristicDataUpload characteristicc is null"); - continue; - } - - 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 && mFirmwareVersion != null) { - getDevice().setFirmwareVersion(mFirmwareVersion); - } - - 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; - } - - this.characteristicCommandRead = new XiaomiCharacteristic(this, btCharacteristicCommandRead, authService); - this.characteristicCommandRead.setEncrypted(uuidSet.isEncrypted()); - this.characteristicCommandRead.setHandler(this::handleCommandBytes); - this.characteristicCommandWrite = new XiaomiCharacteristic(this, btCharacteristicCommandWrite, authService); - this.characteristicCommandWrite.setEncrypted(uuidSet.isEncrypted()); - this.characteristicActivityData = new XiaomiCharacteristic(this, btCharacteristicActivityData, authService); - this.characteristicActivityData.setHandler(healthService.getActivityFetcher()::addChunk); - this.characteristicActivityData.setEncrypted(uuidSet.isEncrypted()); - this.characteristicDataUpload = new XiaomiCharacteristic(this, btCharacteristicDataUpload, authService); - this.characteristicDataUpload.setEncrypted(uuidSet.isEncrypted()); - this.characteristicDataUpload.setIncrementNonce(false); - this.dataUploadService.setDataUploadCharacteristic(this.characteristicDataUpload); - - builder.requestMtu(247); - - 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); - - if (uuidSet.isEncrypted()) { - authService.startEncryptedHandshake(builder); - } else { - authService.startClearTextHandshake(builder); - } - - return builder; - } - @Override public boolean useAutoConnect() { return true; @@ -215,46 +115,84 @@ public class XiaomiSupport extends AbstractBTLEDeviceSupport { return false; } - @Override - public void setContext(final GBDevice gbDevice, final BluetoothAdapter btAdapter, final Context context) { - // FIXME unsetDynamicState unsets the fw version, which causes problems.. - if (mFirmwareVersion == null && gbDevice.getFirmwareVersion() != null) { - mFirmwareVersion = gbDevice.getFirmwareVersion(); + private XiaomiConnectionSupport createConnectionSpecificSupport() { + DeviceCoordinator.ConnectionType connType = getCoordinator().getConnectionType(); + + switch (connType) { + case BLE: + case BOTH: + return new XiaomiBleSupport(this); } - super.setContext(gbDevice, btAdapter, context); - for (final AbstractXiaomiService service : mServiceMap.values()) { - service.setContext(context); + LOG.error("Cannot create connection-specific support, unhanded {} connection type", connType); + return null; + } + + public XiaomiConnectionSupport getConnectionSpecificSupport() { + if (connectionSupport == null) { + connectionSupport = createConnectionSpecificSupport(); } + + return connectionSupport; } @Override - public boolean onCharacteristicChanged(final BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic) { - if (super.onCharacteristicChanged(gatt, characteristic)) { - return true; - } + public boolean connect() { + if (getConnectionSpecificSupport() != null) + return getConnectionSpecificSupport().connect(); - 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)); + LOG.error("getConnectionSpecificSupport returned null, could not connect"); return false; } + public void onUploadProgress(int textRsrc, int progressPercent) { + if (getConnectionSpecificSupport() == null) { + LOG.error("onUploadProgress called but connection specific unavailable"); + return; + } + + getConnectionSpecificSupport().onUploadProgress(textRsrc, progressPercent); + } + + @Override + public void dispose() { + if (getConnectionSpecificSupport() != null) { + getConnectionSpecificSupport().dispose(); + connectionSupport = null; + } + } + + public void setContext(final GBDevice device, final BluetoothAdapter adapter, final Context context) { + // FIXME unsetDynamicState unsets the fw version, which causes problems.. + if (getCachedFirmwareVersion() == null && device.getFirmwareVersion() != null) { + setCachedFirmwareVersion(device.getFirmwareVersion()); + } + + super.setContext(device, adapter, context); + + for (AbstractXiaomiService service : mServiceMap.values()) { + service.setContext(context); + } + + if (getConnectionSpecificSupport() != null) { + getConnectionSpecificSupport().setContext(device, adapter, context); + } + } + + public String getCachedFirmwareVersion() { + return this.cachedFirmwareVersion; + } + + public void setCachedFirmwareVersion(String version) { + this.cachedFirmwareVersion = version; + } + + public void disconnect() { + if (getConnectionSpecificSupport() != null) { + getConnectionSpecificSupport().disconnect(); + } + } + public void handleCommandBytes(final byte[] plainValue) { LOG.debug("Got command: {}", GB.hexdump(plainValue)); @@ -459,13 +397,10 @@ public class XiaomiSupport extends AbstractBTLEDeviceSupport { return (XiaomiCoordinator) gbDevice.getDeviceCoordinator(); } - protected void phase2Initialize() { - LOG.info("phase2Initialize"); + protected void onAuthSuccess() { + LOG.info("onAuthSuccess"); - characteristicCommandRead.reset(); - characteristicCommandWrite.reset(); - characteristicActivityData.reset(); - characteristicDataUpload.reset(); + getConnectionSpecificSupport().onAuthSuccess(); if (GBApplication.getPrefs().getBoolean("datetime_synconconnect", true)) { systemService.setCurrentTime(); @@ -477,28 +412,7 @@ public class XiaomiSupport extends AbstractBTLEDeviceSupport { } 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()); - } - - /** - * 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()); + getConnectionSpecificSupport().sendCommand(taskName, command); } public void sendCommand(final String taskName, final int type, final int subtype) { @@ -511,10 +425,18 @@ public class XiaomiSupport extends AbstractBTLEDeviceSupport { ); } - public XiaomiDataUploadService getDataUploader() { + public XiaomiAuthService getAuthService() { + return this.authService; + } + + public XiaomiDataUploadService getDataUploadService() { return this.dataUploadService; } + public XiaomiHealthService getHealthService() { + return this.healthService; + } + @Override public String customStringFilter(final String inputString) { return StringUtils.replaceEach(inputString, EMOJI_SOURCE, EMOJI_TARGET); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/services/XiaomiNotificationService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/services/XiaomiNotificationService.java index 4c9d5dbf1..18cb79859 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/services/XiaomiNotificationService.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/services/XiaomiNotificationService.java @@ -538,15 +538,15 @@ public class XiaomiNotificationService extends AbstractXiaomiService implements } } - getSupport().getDataUploader().setCallback(this); - getSupport().getDataUploader().requestUpload(XiaomiDataUploadService.TYPE_NOTIFICATION_ICON, buf.array()); + getSupport().getDataUploadService().setCallback(this); + getSupport().getDataUploadService().requestUpload(XiaomiDataUploadService.TYPE_NOTIFICATION_ICON, buf.array()); } @Override public void onUploadFinish(final boolean success) { LOG.debug("Notification icon upload finished: {}", success); - getSupport().getDataUploader().setCallback(null); + getSupport().getDataUploadService().setCallback(null); } @Override diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/services/XiaomiSystemService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/services/XiaomiSystemService.java index 79608e770..b83edd108 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/services/XiaomiSystemService.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/services/XiaomiSystemService.java @@ -53,8 +53,6 @@ import nodomain.freeyourgadget.gadgetbridge.model.BatteryState; import nodomain.freeyourgadget.gadgetbridge.model.SleepState; import nodomain.freeyourgadget.gadgetbridge.model.WearingState; import nodomain.freeyourgadget.gadgetbridge.proto.xiaomi.XiaomiProto; -import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; -import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetProgressAction; import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.XiaomiPreferences; import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.XiaomiSupport; import nodomain.freeyourgadget.gadgetbridge.util.CheckSums; @@ -143,8 +141,8 @@ public class XiaomiSystemService extends AbstractXiaomiService implements Xiaomi LOG.debug("Firmware install status 0, uploading"); setDeviceBusy(); - getSupport().getDataUploader().setCallback(this); - getSupport().getDataUploader().requestUpload(XiaomiDataUploadService.TYPE_FIRMWARE, fwHelper.getBytes()); + getSupport().getDataUploadService().setCallback(this); + getSupport().getDataUploadService().requestUpload(XiaomiDataUploadService.TYPE_FIRMWARE, fwHelper.getBytes()); return; case CMD_PASSWORD_GET: handlePassword(cmd.getSystem().getPassword()); @@ -315,9 +313,9 @@ public class XiaomiSystemService extends AbstractXiaomiService implements Xiaomi gbDeviceEventVersionInfo.fwVersion = deviceInfo.getFirmware(); //gbDeviceEventVersionInfo.fwVersion2 = "N/A"; gbDeviceEventVersionInfo.hwVersion = deviceInfo.getModel(); - final GBDeviceEventUpdateDeviceInfo gbDeviceEventUpdateDeviceInfo = new GBDeviceEventUpdateDeviceInfo("SERIAL: ", deviceInfo.getSerialNumber()); - getSupport().evaluateGBDeviceEvent(gbDeviceEventVersionInfo); + + final GBDeviceEventUpdateDeviceInfo gbDeviceEventUpdateDeviceInfo = new GBDeviceEventUpdateDeviceInfo("SERIAL: ", deviceInfo.getSerialNumber()); getSupport().evaluateGBDeviceEvent(gbDeviceEventUpdateDeviceInfo); } @@ -937,7 +935,7 @@ public class XiaomiSystemService extends AbstractXiaomiService implements Xiaomi public void onUploadFinish(final boolean success) { LOG.debug("Firmware upload finished: {}", success); - getSupport().getDataUploader().setCallback(null); + getSupport().getDataUploadService().setCallback(null); final String notificationMessage = success ? getSupport().getContext().getString(R.string.updatefirmwareoperation_update_complete) : @@ -952,17 +950,6 @@ public class XiaomiSystemService extends AbstractXiaomiService implements Xiaomi @Override public void onUploadProgress(final int progressPercent) { - try { - final TransactionBuilder builder = getSupport().createTransactionBuilder("send data upload progress"); - builder.add(new SetProgressAction( - getSupport().getContext().getString(R.string.updatefirmwareoperation_update_in_progress), - true, - progressPercent, - getSupport().getContext() - )); - builder.queue(getSupport().getQueue()); - } catch (final Exception e) { - LOG.error("Failed to update progress notification", e); - } + getSupport().onUploadProgress(R.string.updatefirmwareoperation_update_in_progress, progressPercent); } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/services/XiaomiWatchfaceService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/services/XiaomiWatchfaceService.java index 7480eb0c8..02bec40d1 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/services/XiaomiWatchfaceService.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/services/XiaomiWatchfaceService.java @@ -80,8 +80,8 @@ public class XiaomiWatchfaceService extends AbstractXiaomiService implements Xia LOG.debug("Watchface install status 0, uploading"); setDeviceBusy(); - getSupport().getDataUploader().setCallback(this); - getSupport().getDataUploader().requestUpload(XiaomiDataUploadService.TYPE_WATCHFACE, fwHelper.getBytes()); + getSupport().getDataUploadService().setCallback(this); + getSupport().getDataUploadService().requestUpload(XiaomiDataUploadService.TYPE_WATCHFACE, fwHelper.getBytes()); return; } @@ -230,11 +230,11 @@ public class XiaomiWatchfaceService extends AbstractXiaomiService implements Xia public void onUploadFinish(final boolean success) { LOG.debug("Watchface upload finished: {}", success); - getSupport().getDataUploader().setCallback(null); + getSupport().getDataUploadService().setCallback(null); final String notificationMessage = success ? - getSupport().getContext().getString(R.string.updatefirmwareoperation_update_complete) : - getSupport().getContext().getString(R.string.updatefirmwareoperation_write_failed); + getSupport().getContext().getString(R.string.uploadwatchfaceoperation_complete) : + getSupport().getContext().getString(R.string.uploadwatchfaceoperation_failed); GB.updateInstallNotification(notificationMessage, false, 100, getSupport().getContext()); @@ -250,23 +250,12 @@ public class XiaomiWatchfaceService extends AbstractXiaomiService implements Xia @Override public void onUploadProgress(final int progressPercent) { - try { - final TransactionBuilder builder = getSupport().createTransactionBuilder("send data upload progress"); - builder.add(new SetProgressAction( - getSupport().getContext().getString(R.string.updatefirmwareoperation_update_in_progress), - true, - progressPercent, - getSupport().getContext() - )); - builder.queue(getSupport().getQueue()); - } catch (final Exception e) { - LOG.error("Failed to update progress notification", e); - } + getSupport().onUploadProgress(R.string.uploadwatchfaceoperation_in_progress, progressPercent); } private void setDeviceBusy() { final GBDevice device = getSupport().getDevice(); - device.setBusyTask(getSupport().getContext().getString(R.string.updating_firmware)); + device.setBusyTask(getSupport().getContext().getString(R.string.uploading_watchface)); device.sendDeviceUpdateIntent(getSupport().getContext()); } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 14c6722d4..596d51c85 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2572,4 +2572,8 @@ Vibrate on new instruction Whether the watch should vibrate on every new or changed navigation instruction (only when the app is in the foreground) Connection Status + Uploading watchface… + Uploading watchface + Watchface installation completed + Watchface installation failed