/* Copyright (C) 2023-2024 Ascense, Frank Ertl 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.withingssteelhr; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothGatt; import android.bluetooth.BluetoothGattCharacteristic; import android.bluetooth.BluetoothGattDescriptor; import android.bluetooth.BluetoothGattService; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; import android.graphics.drawable.Drawable; import android.os.Build; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.localbroadcastmanager.content.LocalBroadcastManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.Date; import java.util.List; import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.activities.SettingsActivity; import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser; import nodomain.freeyourgadget.gadgetbridge.model.Alarm; import nodomain.freeyourgadget.gadgetbridge.model.CallSpec; import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec; import nodomain.freeyourgadget.gadgetbridge.model.NotificationType; import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport; import nodomain.freeyourgadget.gadgetbridge.service.btle.GattService; import nodomain.freeyourgadget.gadgetbridge.service.btle.ServerTransactionBuilder; import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction; import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.activity.WithingsActivityType; import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.WithingsServerAction; import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.WithingsUUID; import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.conversation.ActivitySampleHandler; import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.conversation.BatteryStateHandler; import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.conversation.Conversation; import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.conversation.ConversationQueue; import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.conversation.HeartRateHandler; import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.conversation.ResponseHandler; import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.conversation.SetupFinishedHandler; import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.conversation.SimpleConversation; import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.conversation.SyncFinishedHandler; import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.conversation.WorkoutScreenListHandler; import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.ActivityTarget; import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.AlarmName; import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.AlarmSettings; import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.AlarmStatus; import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.AncsStatus; import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.DataStructureFactory; import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.EndOfTransmission; import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.GetActivitySamples; import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.ImageData; import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.ImageMetaData; import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.Locale; import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.MoveHand; import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.Probe; import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.ProbeOsVersion; import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.ScreenSettings; import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.Time; import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.TypeVersion; import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.User; import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.UserUnit; import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.UserUnitConstants; import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.WorkoutScreen; import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.ExpectedResponse; import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.Message; import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.MessageBuilder; import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.MessageFactory; import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.SimpleHexToByteMessage; import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.WithingsMessage; import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.WithingsMessageType; import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.incoming.IncomingMessageHandler; import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.incoming.IncomingMessageHandlerFactory; import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.incoming.LiveWorkoutHandler; import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.notification.GetNotificationAttributes; import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.notification.GetNotificationAttributesResponse; import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.notification.NotificationProvider; import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.notification.NotificationSource; import nodomain.freeyourgadget.gadgetbridge.util.GB; import nodomain.freeyourgadget.gadgetbridge.util.Prefs; import nodomain.freeyourgadget.gadgetbridge.util.StringUtils; import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_LANGUAGE; import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_LANGUAGE_AUTO; public class WithingsSteelHRDeviceSupport extends AbstractBTLEDeviceSupport { private static final Logger logger = LoggerFactory.getLogger(WithingsSteelHRDeviceSupport.class); public static final String LAST_ACTIVITY_SYNC = "lastActivitySync"; public static final String HANDS_CALIBRATION_CMD = "withings_hands_calibration"; public static final String START_HANDS_CALIBRATION_CMD = "start_withings_hands_calibration"; public static final String STOP_HANDS_CALIBRATION_CMD = "stop_withings_hands_calibration"; private static Prefs prefs = GBApplication.getPrefs(); private MessageBuilder messageBuilder; private LiveWorkoutHandler liveWorkoutHandler; private ActivitySampleHandler activitySampleHandler; private ConversationQueue conversationQueue; private boolean firstTimeConnect; private BluetoothGattCharacteristic notificationSourceCharacteristic; private BluetoothGattCharacteristic dataSourceCharacteristic; private BluetoothDevice device; private boolean syncInProgress; private ActivityUser activityUser; private NotificationProvider notificationProvider; private IncomingMessageHandlerFactory incomingMessageHandlerFactory; private final BroadcastReceiver commandReceiver; private int mtuSize = 115; public WithingsSteelHRDeviceSupport() { super(logger); notificationProvider = NotificationProvider.getInstance(this); messageBuilder = new MessageBuilder(this, new MessageFactory(new DataStructureFactory())); liveWorkoutHandler = new LiveWorkoutHandler(this); incomingMessageHandlerFactory = IncomingMessageHandlerFactory.getInstance(this); addSupportedService(WithingsUUID.WITHINGS_SERVICE_UUID); addSupportedService(GattService.UUID_SERVICE_GENERIC_ACCESS); addSupportedService(GattService.UUID_SERVICE_GENERIC_ATTRIBUTE); addANCSService(); activityUser = new ActivityUser(); IntentFilter commandFilter = new IntentFilter(HANDS_CALIBRATION_CMD); commandFilter.addAction(START_HANDS_CALIBRATION_CMD); commandFilter.addAction(STOP_HANDS_CALIBRATION_CMD); commandReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { if (intent.getAction() == null) { return; } switch (intent.getAction()) { case HANDS_CALIBRATION_CMD: MoveHand moveHand = new MoveHand(); moveHand.setHand(intent.getShortExtra("hand", (short)1)); moveHand.setMovement(intent.getShortExtra("movementAmount", (short)1)); sendToDevice(new WithingsMessage(WithingsMessageType.MOVE_HAND, moveHand)); break; case START_HANDS_CALIBRATION_CMD: sendToDevice(new WithingsMessage(WithingsMessageType.START_HANDS_CALIBRATION)); break; case STOP_HANDS_CALIBRATION_CMD: sendToDevice(new WithingsMessage(WithingsMessageType.STOP_HANDS_CALIBRATION)); break; } } }; LocalBroadcastManager.getInstance(GBApplication.getContext()).registerReceiver(commandReceiver, commandFilter); } @Override protected TransactionBuilder initializeDevice(TransactionBuilder builder) { logger.debug("Starting initialization..."); conversationQueue = new ConversationQueue(this); builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZING, getContext())); getDevice().setFirmwareVersion("N/A"); getDevice().setFirmwareVersion2("N/A"); BluetoothGattCharacteristic characteristic = getCharacteristic(WithingsUUID.WITHINGS_WRITE_CHARACTERISTIC_UUID); builder.notify(characteristic, true); logger.debug("Requesting change of MTU..."); builder.requestMtu(119); return builder; } @Override public boolean connectFirstTime() { firstTimeConnect = true; return connect(); } @Override public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) { logger.debug("MTU has changed to " + mtu); mtuSize = mtu; if (firstTimeConnect) { addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.INITIAL_CONNECT)); addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.SET_LOCALE, getLocale())); addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.START_HANDS_CALIBRATION)); addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.STOP_HANDS_CALIBRATION)); addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.SET_TIME, new Time())); addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.SET_USER_UNIT, new UserUnit(UserUnitConstants.DISTANCE, getUnit()))); addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.SET_USER_UNIT, new UserUnit(UserUnitConstants.CLOCK_MODE, getTimeMode()))); addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.SET_ACTIVITY_TARGET, new ActivityTarget(activityUser.getStepsGoal()))); addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.GET_ANCS_STATUS)); addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.SET_ANCS_STATUS, new AncsStatus(true))); addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.GET_BATTERY_STATUS), new BatteryStateHandler(this)); addScreenListCommands(); addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.SETUP_FINISHED), new SetupFinishedHandler(this)); } else { Message message = new WithingsMessage(WithingsMessageType.PROBE); message.addDataStructure(new Probe((short) 1, (short) 1, 5100401)); message.addDataStructure(new ProbeOsVersion((short) Build.VERSION.SDK_INT)); conversationQueue.clear(); addSimpleConversationToQueue(message, new AuthenticationHandler(this)); } if (!firstTimeConnect) { finishInitialization(); } conversationQueue.send(); } public void doSync() { activitySampleHandler = new ActivitySampleHandler(this); conversationQueue.clear(); try { if (syncInProgress || !shoudSync()) { return; } getDevice().setBusyTask("Syncing"); syncInProgress = true; addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.INITIAL_CONNECT)); addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.GET_ANCS_STATUS)); addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.GET_BATTERY_STATUS), new BatteryStateHandler(this)); addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.SET_TIME, new Time())); WithingsMessage message = new WithingsMessage(WithingsMessageType.SET_USER); message.addDataStructure(getUser()); // The UserSecret appears in the original communication with the HealthMate app. Until now GB works without the secret. // This makes the "authentication" far easier. However if it turns out that this is needed, we would need to find a way to savely store a unique generated secret. // message.addDataStructure(new UserSecret()); addSimpleConversationToQueue(message); addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.SET_ACTIVITY_TARGET, new ActivityTarget(activityUser.getStepsGoal()))); addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.SET_USER_UNIT, new UserUnit(UserUnitConstants.DISTANCE, getUnit()))); addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.SET_USER_UNIT, new UserUnit(UserUnitConstants.CLOCK_MODE, getTimeMode()))); addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.GET_ALARM_SETTINGS)); addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.GET_SCREEN_SETTINGS)); addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.GET_ALARM)); addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.GET_ALARM_ENABLED)); addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.GET_WORKOUT_SCREEN_LIST), new WorkoutScreenListHandler(this)); Calendar c = Calendar.getInstance(); c.setTimeInMillis(getLastSyncTimestamp()); message = new WithingsMessage(WithingsMessageType.GET_ACTIVITY_SAMPLES, ExpectedResponse.EOT); message.addDataStructure(new GetActivitySamples(c.getTimeInMillis() / 1000, (short) 0)); addSimpleConversationToQueue(message, activitySampleHandler); message = new WithingsMessage(WithingsMessageType.GET_MOVEMENT_SAMPLES, ExpectedResponse.EOT); message.addDataStructure(new GetActivitySamples(c.getTimeInMillis() / 1000, (short) 0)); message.addDataStructure(new TypeVersion()); addSimpleConversationToQueue(message, activitySampleHandler); message = new WithingsMessage(WithingsMessageType.GET_HEARTRATE_SAMPLES, ExpectedResponse.EOT); message.addDataStructure(new GetActivitySamples(c.getTimeInMillis() / 1000, (short) 0)); message.addDataStructure(new TypeVersion()); addSimpleConversationToQueue(message, activitySampleHandler); } catch (Exception e) { logger.error("Could not synchronize! ", e); conversationQueue.clear(); } finally { // This must be done in all cases or the watch won't respond anymore! addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.SYNC_OK), new SyncFinishedHandler(this)); } conversationQueue.send(); } @Override public boolean onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { if (super.onCharacteristicChanged(gatt, characteristic)) { return true; } byte[] data = characteristic.getValue(); boolean complete = messageBuilder.buildMessage(data); if (complete) { Message message = messageBuilder.getMessage(); if (message.isIncomingMessage()) { logger.debug("received incoming message: " + message.getType()); IncomingMessageHandler handler = incomingMessageHandlerFactory.getHandler(message); handler.handleMessage(message); } else { conversationQueue.processResponse(message); } } return true; } @Override public void onSetCallState(CallSpec callSpec) { if (callSpec.command == CallSpec.CALL_INCOMING) { NotificationSpec notificationSpec = new NotificationSpec(); notificationSpec.sourceAppId = "incoming.call"; notificationSpec.title = callSpec.number; notificationSpec.sender = callSpec.name; notificationSpec.type = NotificationType.GENERIC_PHONE; notificationProvider.notifyClient(notificationSpec); } else { logger.info("Received yet unhandled call command: " + callSpec.command); } } @Override public void onNotification(NotificationSpec notificationSpec) { notificationProvider.notifyClient(notificationSpec); } @Override public void onSetAlarms(ArrayList alarms) { if (alarms.size() > 3) { throw new IllegalArgumentException("Steel HR does only have three alarmslots!"); } if (alarms.size() == 0) { return; } boolean noAlarmsEnabled = true; conversationQueue.clear(); addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.GET_ALARM)); for (Alarm alarm : alarms) { if (alarm.getEnabled() && !alarm.getUnused()) { noAlarmsEnabled = false; addAlarm(alarm); } } if (noAlarmsEnabled) { addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.SET_ALARM_ENABLED, new AlarmStatus(false))); } conversationQueue.send(); } @Override public boolean onDescriptorWriteRequest(BluetoothDevice device, int requestId, BluetoothGattDescriptor descriptor, boolean preparedWrite, boolean responseNeeded, int offset, byte[] value) { this.device = device; return true; } @Override public boolean onCharacteristicWriteRequest(BluetoothDevice device, int requestId, BluetoothGattCharacteristic characteristic, boolean preparedWrite, boolean responseNeeded, int offset, byte[] value) { if (characteristic.getUuid().equals(WithingsUUID.CONTROL_POINT_CHARACTERISTIC_UUID)) { logger.debug("Got GetNotificationAttributesRequest: " + GB.hexdump(value)); GetNotificationAttributes request = new GetNotificationAttributes(); request.deserialize(value); notificationProvider.handleNotificationAttributeRequest(request); } return true; } @Override public void onFetchRecordedData(int dataTypes) { doSync(); } @Override public void onHeartRateTest() { conversationQueue.clear(); addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.GET_HR), new HeartRateHandler(this)); conversationQueue.send(); } @Override public void onSendConfiguration(String config) { try { switch (config) { case HuamiConst.PREF_WORKOUT_ACTIVITY_TYPES_SORTABLE: setWorkoutActivityTypes(); break; case PREF_LANGUAGE: setLanguage(); break; default: logger.debug("unknown configuration setting received: " + config); } } catch (Exception e) { GB.toast("Error setting configuration", Toast.LENGTH_LONG, GB.ERROR, e); } } @Override public void onTestNewFunction() { String hexMessage = "0105080015050900111006040102030507000000000000000000"; conversationQueue.clear(); addSimpleConversationToQueue(new SimpleHexToByteMessage(hexMessage)); conversationQueue.send(); } @Override public boolean useAutoConnect() { return false; } public void sendToDevice(Message message) { if (message == null) { return; } try { TransactionBuilder builder = createTransactionBuilder("conversation"); builder.setCallback(this); BluetoothGattCharacteristic characteristic = getCharacteristic(WithingsUUID.WITHINGS_WRITE_CHARACTERISTIC_UUID); if (characteristic == null) { logger.info("Characteristic with UUID " + WithingsUUID.WITHINGS_WRITE_CHARACTERISTIC_UUID + " not found."); return; } byte[] rawData = message.getRawData(); builder.writeChunkedData(characteristic, rawData, mtuSize - 4); builder.queue(getQueue()); } catch (Exception e) { logger.warn("Could not send message because of " + e.getMessage()); } } public void sendAncsNotificationSourceNotification(NotificationSource notificationSource) { try { ServerTransactionBuilder builder = performServer("notificationSourceNotification"); notificationSourceCharacteristic.setValue(notificationSource.serialize()); builder.add(new WithingsServerAction(device, notificationSourceCharacteristic)); builder.queue(getQueue()); } catch (IOException e) { logger.error("Could not send notification.", e); GB.toast("Could not send notification.", Toast.LENGTH_LONG, GB.ERROR, e); } } public void sendAncsDataSourceNotification(GetNotificationAttributesResponse response) { try { ServerTransactionBuilder builder = performServer("dataSourceNotification"); byte[] data = response.serialize(); dataSourceCharacteristic.setValue(response.serialize()); builder.add(new WithingsServerAction(device, dataSourceCharacteristic)); builder.queue(getQueue()); } catch (IOException e) { logger.error("Could not send notification.", e); GB.toast("Could not send notification.", Toast.LENGTH_LONG, GB.ERROR, e); } } public void finishInitialization() { TransactionBuilder builder = createTransactionBuilder("setupFinished"); builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZED, getContext())); builder.queue(getQueue()); logger.debug("Finished initialization."); } public void finishSync() { syncInProgress = false; if (getDevice().isBusy()) { getDevice().unsetBusyTask(); getDevice().sendDeviceUpdateIntent(getContext()); } activitySampleHandler.onSyncFinished(); saveLastSyncTimestamp(new Date().getTime()); } void onAuthenticationFinished() { if (!firstTimeConnect) { addScreenListCommands(); doSync(); } else { addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.SET_ANCS_STATUS, new AncsStatus(true))); addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.GET_ANCS_STATUS)); addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.GET_BATTERY_STATUS), new BatteryStateHandler(this)); conversationQueue.send(); } } private void addAlarm(Alarm alarm) { AlarmSettings alarmSettings = new AlarmSettings(); alarmSettings.setHour((short) alarm.getHour()); alarmSettings.setMinute((short) alarm.getMinute()); alarmSettings.setDayOfWeek(mapRepetitionToWithingsValue(alarm)); if (alarm.getSmartWakeup()) { // Healthmate has the possibility to change the minutecount, in GB we use a fixed value of 15 alarmSettings.setSmartWakeupMinutes((short) 15); } Message alarmMessage = new WithingsMessage(WithingsMessageType.SET_ALARM, alarmSettings); if (!StringUtils.isEmpty(alarm.getTitle())) { AlarmName alarmName = new AlarmName(alarm.getTitle()); alarmMessage.addDataStructure(alarmName); } addSimpleConversationToQueue(alarmMessage); addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.SET_ALARM_ENABLED, new AlarmStatus(true))); } private short mapRepetitionToWithingsValue(Alarm alarm) { int repetition = 0; if (alarm.getRepetition(Alarm.ALARM_MON)) { repetition += 0x02; } if (alarm.getRepetition(Alarm.ALARM_TUE)) { repetition += 0x04; } if (alarm.getRepetition(Alarm.ALARM_WED)) { repetition += 0x08; } if (alarm.getRepetition(Alarm.ALARM_THU)) { repetition += 0x10; } if (alarm.getRepetition(Alarm.ALARM_FRI)) { repetition += 0x20; } if (alarm.getRepetition(Alarm.ALARM_SAT)) { repetition += 0x40; } if (alarm.getRepetition(Alarm.ALARM_SUN)) { repetition += 0x01; } return (short)(repetition + 0x80); } private void addANCSService() { BluetoothGattService withingsGATTService = new BluetoothGattService(WithingsUUID.WITHINGS_ANCS_SERVICE_UUID, BluetoothGattService.SERVICE_TYPE_PRIMARY); notificationSourceCharacteristic = new BluetoothGattCharacteristic(WithingsUUID.NOTIFICATION_SOURCE_CHARACTERISTIC_UUID, BluetoothGattCharacteristic.PROPERTY_NOTIFY, BluetoothGattCharacteristic.PERMISSION_READ); notificationSourceCharacteristic.addDescriptor(new BluetoothGattDescriptor(WithingsUUID.CCC_DESCRIPTOR_UUID, BluetoothGattCharacteristic.PERMISSION_WRITE)); withingsGATTService.addCharacteristic(notificationSourceCharacteristic); withingsGATTService.addCharacteristic(new BluetoothGattCharacteristic(WithingsUUID.CONTROL_POINT_CHARACTERISTIC_UUID, BluetoothGattCharacteristic.PROPERTY_WRITE, BluetoothGattCharacteristic.PERMISSION_WRITE)); dataSourceCharacteristic = new BluetoothGattCharacteristic(WithingsUUID.DATA_SOURCE_CHARACTERISTIC_UUID, BluetoothGattCharacteristic.PROPERTY_NOTIFY, BluetoothGattCharacteristic.PERMISSION_READ); dataSourceCharacteristic.addDescriptor(new BluetoothGattDescriptor(WithingsUUID.CCC_DESCRIPTOR_UUID, BluetoothGattCharacteristic.PERMISSION_WRITE)); withingsGATTService.addCharacteristic(dataSourceCharacteristic); addSupportedServerService(withingsGATTService); } private void addSimpleConversationToQueue(Message message) { addSimpleConversationToQueue(message, null); } private void addSimpleConversationToQueue(Message message, ResponseHandler handler) { Conversation conversation = new SimpleConversation(handler); conversation.setRequest(message); conversationQueue.addConversation(conversation); } private void saveLastSyncTimestamp(@NonNull long timestamp) { SharedPreferences.Editor editor = GBApplication.getDeviceSpecificSharedPrefs(getDevice().getAddress()).edit(); editor.putLong(LAST_ACTIVITY_SYNC, timestamp); editor.apply(); } private long getLastSyncTimestamp() { SharedPreferences settings = GBApplication.getDeviceSpecificSharedPrefs(getDevice().getAddress()); long lastSyncTime = settings.getLong(LAST_ACTIVITY_SYNC, 0); if (lastSyncTime > 0) { return lastSyncTime; } else { Date currentDate = new Date(); Calendar c = Calendar.getInstance(); c.setTimeInMillis(currentDate.getTime()); c.add(Calendar.HOUR, - 10); return c.getTimeInMillis(); } } private boolean shoudSync() { long lastSynced = getLastSyncTimestamp(); int minuteInMillis = 60 * 1000; return new Date().getTime() - lastSynced > minuteInMillis; } private User getUser() { User user = new User(); ActivityUser activityUser = new ActivityUser(); user.setName(activityUser.getName()); user.setGender((byte) activityUser.getGender()); user.setHeight(activityUser.getHeightCm()); user.setWeight(activityUser.getWeightKg()); user.setBirthdate(activityUser.getUserBirthday()); return user; } private void addScreenListCommands() { // TODO: this needs to be more reworked, at the moment for example the notification screen is always on and this is full of magic numbers that need to be identified properly: Message message = new WithingsMessage(WithingsMessageType.SET_SCREEN_LIST); ScreenSettings settings = new ScreenSettings(); settings.setId(0xff); settings.setIdOnDevice((byte)6); message.addDataStructure(settings); settings = new ScreenSettings(); settings.setId(0x3d); settings.setIdOnDevice((byte)1); message.addDataStructure(settings); settings = new ScreenSettings(); settings.setId(0x33); settings.setIdOnDevice((byte)4); message.addDataStructure(settings); settings = new ScreenSettings(); settings.setId(0x2d); settings.setIdOnDevice((byte)2); message.addDataStructure(settings); settings = new ScreenSettings(); settings.setId(0x2a); settings.setIdOnDevice((byte)3); message.addDataStructure(settings); settings = new ScreenSettings(); settings.setId(0x26); settings.setIdOnDevice((byte)7); message.addDataStructure(settings); settings = new ScreenSettings(); settings.setId(0x39); settings.setIdOnDevice((byte)9); message.addDataStructure(settings); message.addDataStructure(new EndOfTransmission()); addSimpleConversationToQueue(message); } private void setWorkoutActivityTypes() { final SharedPreferences prefs = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()); final List allActivityTypes = Arrays.asList(getContext().getResources().getStringArray(R.array.pref_withings_steel_activity_types_values)); final List defaultActivityTypes = Arrays.asList(getContext().getResources().getStringArray(R.array.pref_withings_steel_activity_types_default)); final String activityTypesPref = prefs.getString("workout_activity_types_sortable", null); final List enabledActivityTypes; if (activityTypesPref == null || activityTypesPref.equals("")) { enabledActivityTypes = defaultActivityTypes; } else { enabledActivityTypes = Arrays.asList(activityTypesPref.split(",")); } conversationQueue.clear(); for (int i = 0; i < enabledActivityTypes.size(); i++) { String workoutType = enabledActivityTypes.get(i); try { Message message = createWorkoutScreenMessage(workoutType); if (i == enabledActivityTypes.size() - 1) { message.addDataStructure(new EndOfTransmission()); } addSimpleConversationToQueue(message); } catch (Exception e) { e.printStackTrace(); } } conversationQueue.send(); } @NonNull private Message createWorkoutScreenMessage(String workoutType) { WithingsActivityType withingsActivityType = WithingsActivityType.fromPrefValue(workoutType); int code = withingsActivityType.getCode(); Message message = new WithingsMessage(WithingsMessageType.SET_WORKOUT_SCREEN, ExpectedResponse.NONE); WorkoutScreen workoutScreen = new WorkoutScreen(); workoutScreen.setId(code); final int stringId = getContext().getResources().getIdentifier("activity_type_" + workoutType, "string", getContext().getPackageName()); workoutScreen.setName(getContext().getString(stringId)); message.addDataStructure(workoutScreen); ImageMetaData imageMetaData = new ImageMetaData(); imageMetaData.setHeight((byte)24); imageMetaData.setWidth((byte)22); message.addDataStructure(imageMetaData); ImageData imageData = new ImageData(); final int drawableId = withingsActivityType.toActivityKind().getIcon(); Drawable drawable = getContext().getDrawable(drawableId); imageData.setImageData(IconHelper.getIconBytesFromDrawable(drawable)); message.addDataStructure(imageData); return message; } protected void setLanguage() { Locale locale = getLocale(); conversationQueue.clear(); addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.SET_LOCALE, locale)); conversationQueue.send(); } private Locale getLocale() { String localeString = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()) .getString(PREF_LANGUAGE, PREF_LANGUAGE_AUTO); if (localeString.equals(PREF_LANGUAGE_AUTO)) { localeString = java.util.Locale.getDefault().getLanguage(); } String language = localeString.substring(0, 2); switch (language) { case "de": case "en": case "es": case "fr": case "it": return new Locale(language); default: return new Locale("en"); } } private short getTimeMode() { if ("24h".equals(getDevicePrefs().getTimeFormat())) { return UserUnitConstants.UNIT_24H; } else { return UserUnitConstants.UNIT_12H; } } private short getUnit() { String units = prefs.getString(SettingsActivity.PREF_MEASUREMENT_SYSTEM, GBApplication.getContext().getString(R.string.p_unit_metric)); if (units.equals(GBApplication.getContext().getString(R.string.p_unit_metric))) { return UserUnitConstants.UNIT_KM; } else { return UserUnitConstants.UNIT_MILES; } } @Override public boolean getImplicitCallbackModify() { return true; } }