1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-10-02 11:17:00 +02:00

Xiaomi: Introduce XiaomiConnectionSupport

Co-Authored-By: José Rebelo <joserebelo@outlook.com>
This commit is contained in:
MrYoranimo 2023-12-14 12:51:12 +01:00 committed by José Rebelo
parent 25dcba23c3
commit 98e8ec2329
10 changed files with 428 additions and 250 deletions

View File

@ -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");
}

View File

@ -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<UUID> 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<UUID, XiaomiBleUuids.XiaomiBleUuidSet> 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();
}
}

View File

@ -0,0 +1,5 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi;
public interface XiaomiChannelHandler {
void handle(final byte[] payload);
}

View File

@ -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;

View File

@ -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 <http://www.gnu.org/licenses/>. */
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);
}

View File

@ -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<Integer, AbstractXiaomiService> mServiceMap = new LinkedHashMap<Integer, AbstractXiaomiService>() {{
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<UUID, XiaomiBleUuids.XiaomiBleUuidSet> 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);

View File

@ -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

View File

@ -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);
}
}

View File

@ -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());
}

View File

@ -2572,4 +2572,8 @@
<string name="pref_title_fossil_hr_nav_vibrate">Vibrate on new instruction</string>
<string name="pref_summary_fossil_hr_nav_vibrate">Whether the watch should vibrate on every new or changed navigation instruction (only when the app is in the foreground)</string>
<string name="notification_channel_connection_status_name">Connection Status</string>
<string name="uploading_watchface">Uploading watchface…</string>
<string name="uploadwatchfaceoperation_in_progress">Uploading watchface</string>
<string name="uploadwatchfaceoperation_complete">Watchface installation completed</string>
<string name="uploadwatchfaceoperation_failed">Watchface installation failed</string>
</resources>