From cf61ab9d3821a3003314570631a466a0a510a3f7 Mon Sep 17 00:00:00 2001 From: mkusnierz <> Date: Fri, 18 Oct 2019 00:02:53 +0200 Subject: [PATCH] Fixed battery level parsing from response Added basic notification support --- app/src/main/AndroidManifest.xml | 6 + .../WatchXPlusCalibrationActivity.java | 123 ++++ .../watchxplus/WatchXPlusConstants.java | 93 +++ .../WatchXPlusDeviceCoordinator.java | 21 +- .../watchxplus/WatchXPlusPairingActivity.java | 129 ++++ .../service/DeviceSupportFactory.java | 4 + .../watchxplus/WatchXPlusDeviceSupport.java | 649 ++++++++++++++++++ .../watchxplus/operations/InitOperation.java | 94 +++ 8 files changed, 1107 insertions(+), 12 deletions(-) create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/watchxplus/WatchXPlusCalibrationActivity.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/watchxplus/WatchXPlusConstants.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/watchxplus/WatchXPlusPairingActivity.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/watchxplus/WatchXPlusDeviceSupport.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/watchxplus/operations/InitOperation.java diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index eb7b6d8de..1d5f5afc4 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -406,6 +406,12 @@ + + . */ +package nodomain.freeyourgadget.gadgetbridge.devices.watchxplus; + +import android.content.Intent; +import android.os.Bundle; +import android.os.Handler; +import android.view.View; +import android.widget.Button; +import android.widget.NumberPicker; + +import androidx.localbroadcastmanager.content.LocalBroadcastManager; +import nodomain.freeyourgadget.gadgetbridge.R; +import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBActivity; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; + +public class WatchXPlusCalibrationActivity extends AbstractGBActivity { + + private static final String STATE_DEVICE = "stateDevice"; + GBDevice device; + + NumberPicker pickerHour, pickerMinute, pickerSecond; + + Handler handler; + Runnable holdCalibration; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_watch9_calibration); + + pickerHour = findViewById(R.id.np_hour); + pickerMinute = findViewById(R.id.np_minute); + pickerSecond = findViewById(R.id.np_second); + + pickerHour.setMinValue(1); + pickerHour.setMaxValue(12); + pickerHour.setValue(12); + pickerMinute.setMinValue(0); + pickerMinute.setMaxValue(59); + pickerMinute.setValue(0); + pickerSecond.setMinValue(0); + pickerSecond.setMaxValue(59); + pickerSecond.setValue(0); + + handler = new Handler(); + holdCalibration = new Runnable() { + @Override + public void run() { + LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(new Intent(WatchXPlusConstants.ACTION_CALIBRATION_HOLD)); + handler.postDelayed(this, 10000); + } + }; + + Intent intent = getIntent(); + device = intent.getParcelableExtra(GBDevice.EXTRA_DEVICE); + if (device == null && savedInstanceState != null) { + device = savedInstanceState.getParcelable(STATE_DEVICE); + } + if (device == null) { + finish(); + } + + final Button btCalibrate = findViewById(R.id.watch9_bt_calibrate); + btCalibrate.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + btCalibrate.setEnabled(false); + handler.removeCallbacks(holdCalibration); + Intent calibrationData = new Intent(WatchXPlusConstants.ACTION_CALIBRATION_SEND); + calibrationData.putExtra(WatchXPlusConstants.VALUE_CALIBRATION_HOUR, pickerHour.getValue()); + calibrationData.putExtra(WatchXPlusConstants.VALUE_CALIBRATION_MINUTE, pickerMinute.getValue()); + calibrationData.putExtra(WatchXPlusConstants.VALUE_CALIBRATION_SECOND, pickerSecond.getValue()); + LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(calibrationData); + finish(); + } + }); + } + + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putParcelable(STATE_DEVICE, device); + } + + @Override + protected void onRestoreInstanceState(Bundle savedInstanceState) { + super.onRestoreInstanceState(savedInstanceState); + device = savedInstanceState.getParcelable(STATE_DEVICE); + } + + @Override + protected void onStart() { + super.onStart(); + Intent calibration = new Intent(WatchXPlusConstants.ACTION_CALIBRATION); + calibration.putExtra(WatchXPlusConstants.ACTION_ENABLE, true); + LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(calibration); + handler.postDelayed(holdCalibration, 1000); + } + + @Override + protected void onStop() { + super.onStop(); + Intent calibration = new Intent(WatchXPlusConstants.ACTION_CALIBRATION); + calibration.putExtra(WatchXPlusConstants.ACTION_ENABLE, false); + LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(calibration); + handler.removeCallbacks(holdCalibration); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/watchxplus/WatchXPlusConstants.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/watchxplus/WatchXPlusConstants.java new file mode 100644 index 000000000..143d0d5eb --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/watchxplus/WatchXPlusConstants.java @@ -0,0 +1,93 @@ +/* Copyright (C) 2018-2019 maxirnilian + + 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.devices.watchxplus; + +import java.util.UUID; + +public final class WatchXPlusConstants { + public static final UUID UUID_SERVICE_WATCHXPLUS = UUID.fromString("0000a800-0000-1000-8000-00805f9b34fb"); + + public static final UUID UUID_UNKNOWN_DESCRIPTOR = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"); + + public static final UUID UUID_CHARACTERISTIC_WRITE = UUID.fromString("0000a801-0000-1000-8000-00805f9b34fb"); + public static final UUID UUID_CHARACTERISTIC_UNKNOWN_2 = UUID.fromString("0000a802-0000-1000-8000-00805f9b34fb"); + public static final UUID UUID_CHARACTERISTIC_UNKNOWN_3 = UUID.fromString("0000a803-0000-1000-8000-00805f9b34fb"); + public static final UUID UUID_CHARACTERISTIC_UNKNOWN_4 = UUID.fromString("0000a804-0000-1000-8000-00805f9b34fb"); + + public static final int NOTIFICATION_CHANNEL_DEFAULT = 7; + public static final int NOTIFICATION_CHANNEL_PHONE_CALL = 1024; + + public static final byte RESPONSE = 0x13; + public static final byte REQUEST = 0x31; + + public static final byte WRITE_VALUE = 0x01; + public static final byte READ_VALUE = 0x02; + public static final byte TASK = 0x04; + public static final byte KEEP_ALIVE = -0x80; + + public static final byte[] CMD_HEADER = new byte[]{0x23, 0x01, 0x00, 0x00, 0x00}; + + // byte[] COMMAND = new byte[]{0x23, 0x01, 0x00, 0x31, 0x00, ... , 0x00} + // | | | | | | └ Checksum + // | | | | | └ Command + value + // | | | | └ Sequence number + // | | | └ Response/Request indicator + // | | └ Value length + // | | + // └-----└ Header + + public static final byte[] CMD_FIRMWARE_INFO = new byte[]{0x01, 0x02}; + public static final byte[] CMD_AUTHORIZATION_TASK = new byte[]{0x01, 0x05}; + public static final byte[] CMD_TIME_SETTINGS = new byte[]{0x01, 0x08}; + public static final byte[] CMD_ALARM_SETTINGS = new byte[]{0x01, 0x0A}; + public static final byte[] CMD_BATTERY_INFO = new byte[]{0x01, 0x14}; + + public static final byte[] CMD_NOTIFICATION_TASK = new byte[]{0x03, 0x01}; + public static final byte[] CMD_NOTIFICATION_TEXT_TASK = new byte[]{0x03, 0x06}; + public static final byte[] CMD_NOTIFICATION_SETTINGS = new byte[]{0x03, 0x02}; + public static final byte[] CMD_CALIBRATION_INIT_TASK = new byte[]{0x03, 0x31}; + public static final byte[] CMD_CALIBRATION_TASK = new byte[]{0x03, 0x33, 0x01}; + public static final byte[] CMD_CALIBRATION_KEEP_ALIVE = new byte[]{0x03, 0x34}; + public static final byte[] CMD_DO_NOT_DISTURB_SETTINGS = new byte[]{0x03, 0x61}; + + public static final byte[] CMD_FITNESS_GOAL_SETTINGS = new byte[]{0x10, 0x02}; + + public static final byte[] RESP_AUTHORIZATION_TASK = new byte[]{0x01, 0x01, 0x05}; + public static final byte[] RESP_BUTTON_INDICATOR = new byte[]{0x04, 0x03, 0x11}; + public static final byte[] RESP_ALARM_INDICATOR = new byte[]{-0x80, 0x01, 0x0A}; + + public static final byte[] RESP_FIRMWARE_INFO = new byte[]{0x08, 0x01, 0x02}; + public static final byte[] RESP_TIME_SETTINGS = new byte[]{0x08, 0x01, 0x08}; + public static final byte[] RESP_BATTERY_INFO = new byte[]{0x08, 0x01, 0x14}; + public static final byte[] RESP_NOTIFICATION_SETTINGS = new byte[]{0x08, 0x03, 0x02}; + + public static final String ACTION_ENABLE = "action.watch9.enable"; + + public static final String ACTION_CALIBRATION + = "nodomain.freeyourgadget.gadgetbridge.devices.action.watchxplus.start_calibration"; + public static final String ACTION_CALIBRATION_SEND + = "nodomain.freeyourgadget.gadgetbridge.devices.action.watchxplus.send_calibration"; + public static final String ACTION_CALIBRATION_HOLD + = "nodomain.freeyourgadget.gadgetbridge.devices.action.watchxplus.keep_calibrating"; + public static final String VALUE_CALIBRATION_HOUR + = "value.watch9.calibration_hour"; + public static final String VALUE_CALIBRATION_MINUTE + = "value.watch9.calibration_minute"; + public static final String VALUE_CALIBRATION_SECOND + = "value.watch9.calibration_second"; + +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/watchxplus/WatchXPlusDeviceCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/watchxplus/WatchXPlusDeviceCoordinator.java index 8cdb41daf..a85ba3882 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/watchxplus/WatchXPlusDeviceCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/watchxplus/WatchXPlusDeviceCoordinator.java @@ -17,8 +17,8 @@ import nodomain.freeyourgadget.gadgetbridge.GBException; import nodomain.freeyourgadget.gadgetbridge.devices.AbstractDeviceCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler; import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider; -import nodomain.freeyourgadget.gadgetbridge.devices.watch9.Watch9Constants; -import nodomain.freeyourgadget.gadgetbridge.devices.watch9.Watch9PairingActivity; +import nodomain.freeyourgadget.gadgetbridge.devices.watchxplus.WatchXPlusConstants; +import nodomain.freeyourgadget.gadgetbridge.devices.watchxplus.WatchXPlusPairingActivity; import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; import nodomain.freeyourgadget.gadgetbridge.entities.Device; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; @@ -33,9 +33,8 @@ public class WatchXPlusDeviceCoordinator extends AbstractDeviceCoordinator { @Override @TargetApi(Build.VERSION_CODES.LOLLIPOP) public Collection createBLEScanFilters() { - // TODO constants for watch x plus - //ParcelUuid watchXpService = new ParcelUuid(Watch9Constants.UUID_SERVICE_WATCH9); - ScanFilter filter = new ScanFilter.Builder().setDeviceName("Watch XPlus").build(); + ParcelUuid watchXpService = new ParcelUuid(WatchXPlusConstants.UUID_SERVICE_WATCHXPLUS); + ScanFilter filter = new ScanFilter.Builder().setServiceUuid(watchXpService).build(); return Collections.singletonList(filter); } @@ -45,21 +44,20 @@ public class WatchXPlusDeviceCoordinator extends AbstractDeviceCoordinator { } @Override - public int getBondingStyle(GBDevice device) { + public int getBondingStyle() { return BONDING_STYLE_NONE; } @NonNull @Override public DeviceType getSupportedType(GBDeviceCandidate candidate) { - // TODO constants and mac for watch x plus String macAddress = candidate.getMacAddress().toUpperCase(); String deviceName = candidate.getName().toUpperCase(); - if (candidate.supportsService(Watch9Constants.UUID_SERVICE_WATCH9)) { + if (candidate.supportsService(WatchXPlusConstants.UUID_SERVICE_WATCHXPLUS)) { return DeviceType.WATCHXPLUS; - } else if (macAddress.startsWith("1C:87:79")) { + } else if (macAddress.startsWith("DC:41:E5")) { return DeviceType.WATCHXPLUS; - } else if (deviceName.equals("WATCH XPLUS")) { + } else if (deviceName.equalsIgnoreCase("WATCH XPLUS")) { return DeviceType.WATCHXPLUS; } return DeviceType.UNKNOWN; @@ -73,8 +71,7 @@ public class WatchXPlusDeviceCoordinator extends AbstractDeviceCoordinator { @Nullable @Override public Class getPairingActivity() { - // TODO watch X plus! - return Watch9PairingActivity.class; + return WatchXPlusPairingActivity.class; } @Override diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/watchxplus/WatchXPlusPairingActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/watchxplus/WatchXPlusPairingActivity.java new file mode 100644 index 000000000..79b209c51 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/watchxplus/WatchXPlusPairingActivity.java @@ -0,0 +1,129 @@ +/* Copyright (C) 2018-2019 Daniele Gobbetti, maxirnilian + + 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.devices.watchxplus; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Bundle; +import android.widget.TextView; +import android.widget.Toast; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import androidx.localbroadcastmanager.content.LocalBroadcastManager; +import nodomain.freeyourgadget.gadgetbridge.GBApplication; +import nodomain.freeyourgadget.gadgetbridge.R; +import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBActivity; +import nodomain.freeyourgadget.gadgetbridge.activities.ControlCenterv2; +import nodomain.freeyourgadget.gadgetbridge.activities.DiscoveryActivity; +import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate; +import nodomain.freeyourgadget.gadgetbridge.util.AndroidUtils; +import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper; +import nodomain.freeyourgadget.gadgetbridge.util.GB; + +public class WatchXPlusPairingActivity extends AbstractGBActivity { + private static final Logger LOG = LoggerFactory.getLogger(WatchXPlusPairingActivity.class); + + private static final String STATE_DEVICE_CANDIDATE = "stateDeviceCandidate"; + + private TextView message; + private GBDeviceCandidate deviceCandidate; + + private final BroadcastReceiver mPairingReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (GBDevice.ACTION_DEVICE_CHANGED.equals(intent.getAction())) { + GBDevice device = intent.getParcelableExtra(GBDevice.EXTRA_DEVICE); + LOG.debug("pairing activity: device changed: " + device); + if (deviceCandidate.getMacAddress().equals(device.getAddress())) { + if (device.isInitialized()) { + pairingFinished(); + } else if (device.isConnecting() || device.isInitializing()) { + LOG.info("still connecting/initializing device..."); + } + } + } + } + }; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_watch9_pairing); + + message = findViewById(R.id.watch9_pair_message); + Intent intent = getIntent(); + deviceCandidate = intent.getParcelableExtra(DeviceCoordinator.EXTRA_DEVICE_CANDIDATE); + if (deviceCandidate == null && savedInstanceState != null) { + deviceCandidate = savedInstanceState.getParcelable(STATE_DEVICE_CANDIDATE); + } + if (deviceCandidate == null) { + Toast.makeText(this, getString(R.string.message_cannot_pair_no_mac), Toast.LENGTH_SHORT).show(); + startActivity(new Intent(this, DiscoveryActivity.class).setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)); + finish(); + return; + } + startPairing(); + } + + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putParcelable(STATE_DEVICE_CANDIDATE, deviceCandidate); + } + + @Override + protected void onRestoreInstanceState(Bundle savedInstanceState) { + super.onRestoreInstanceState(savedInstanceState); + deviceCandidate = savedInstanceState.getParcelable(STATE_DEVICE_CANDIDATE); + } + + @Override + protected void onDestroy() { + AndroidUtils.safeUnregisterBroadcastReceiver(LocalBroadcastManager.getInstance(this), mPairingReceiver); + super.onDestroy(); + } + + private void startPairing() { + message.setText(getString(R.string.pairing, deviceCandidate)); + + IntentFilter filter = new IntentFilter(GBDevice.ACTION_DEVICE_CHANGED); + LocalBroadcastManager.getInstance(this).registerReceiver(mPairingReceiver, filter); + + GBApplication.deviceService().disconnect(); + GBDevice device = DeviceHelper.getInstance().toSupportedDevice(deviceCandidate); + if (device != null) { + GBApplication.deviceService().connect(device, true); + } else { + GB.toast(this, "Unable to connect, can't recognize the device type: " + deviceCandidate, Toast.LENGTH_LONG, GB.ERROR); + } + } + + private void pairingFinished() { + AndroidUtils.safeUnregisterBroadcastReceiver(LocalBroadcastManager.getInstance(this), mPairingReceiver); + + Intent intent = new Intent(this, ControlCenterv2.class).setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + startActivity(intent); + + finish(); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceSupportFactory.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceSupportFactory.java index ce3f7a039..dba93c3aa 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceSupportFactory.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceSupportFactory.java @@ -52,6 +52,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.pebble.PebbleSupport import nodomain.freeyourgadget.gadgetbridge.service.devices.roidmi.RoidmiSupport; import nodomain.freeyourgadget.gadgetbridge.service.devices.vibratissimo.VibratissimoSupport; import nodomain.freeyourgadget.gadgetbridge.service.devices.watch9.Watch9DeviceSupport; +import nodomain.freeyourgadget.gadgetbridge.service.devices.watchxplus.WatchXPlusDeviceSupport; import nodomain.freeyourgadget.gadgetbridge.service.devices.xwatch.XWatchSupport; import nodomain.freeyourgadget.gadgetbridge.service.devices.zetime.ZeTimeDeviceSupport; import nodomain.freeyourgadget.gadgetbridge.util.GB; @@ -183,6 +184,9 @@ public class DeviceSupportFactory { case WATCH9: deviceSupport = new ServiceDeviceSupport(new Watch9DeviceSupport(), EnumSet.of(ServiceDeviceSupport.Flags.THROTTLING, ServiceDeviceSupport.Flags.BUSY_CHECKING)); break; + case WATCHXPLUS: + deviceSupport = new ServiceDeviceSupport(new WatchXPlusDeviceSupport(), EnumSet.of(ServiceDeviceSupport.Flags.THROTTLING, ServiceDeviceSupport.Flags.BUSY_CHECKING)); + break; case ROIDMI: deviceSupport = new ServiceDeviceSupport(new RoidmiSupport(), EnumSet.of(ServiceDeviceSupport.Flags.BUSY_CHECKING)); break; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/watchxplus/WatchXPlusDeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/watchxplus/WatchXPlusDeviceSupport.java new file mode 100644 index 000000000..0d4c4869d --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/watchxplus/WatchXPlusDeviceSupport.java @@ -0,0 +1,649 @@ +/* Copyright (C) 2018-2019 Andreas Shimokawa, Carsten Pfeiffer, Daniele + Gobbetti, maxirnilian, Sebastian Kranz + + 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.watchxplus; + +import android.bluetooth.BluetoothGatt; +import android.bluetooth.BluetoothGattCharacteristic; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.net.Uri; + +import androidx.annotation.IntRange; +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.GregorianCalendar; +import java.util.Locale; +import java.util.UUID; + +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo; +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventVersionInfo; +import nodomain.freeyourgadget.gadgetbridge.devices.watchxplus.WatchXPlusConstants; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser; +import nodomain.freeyourgadget.gadgetbridge.model.Alarm; +import nodomain.freeyourgadget.gadgetbridge.model.BatteryState; +import nodomain.freeyourgadget.gadgetbridge.model.CalendarEventSpec; +import nodomain.freeyourgadget.gadgetbridge.model.CallSpec; +import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec; +import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec; +import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec; +import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec; +import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec; +import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport; +import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions; +import nodomain.freeyourgadget.gadgetbridge.service.btle.GattService; +import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; +import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction; +import nodomain.freeyourgadget.gadgetbridge.service.devices.watchxplus.operations.InitOperation; +import nodomain.freeyourgadget.gadgetbridge.util.AlarmUtils; +import nodomain.freeyourgadget.gadgetbridge.util.ArrayUtils; +import nodomain.freeyourgadget.gadgetbridge.util.StringUtils; + +public class WatchXPlusDeviceSupport extends AbstractBTLEDeviceSupport { + + private boolean needsAuth; + private int sequenceNumber = 0; + private boolean isCalibrationActive = false; + + private byte ACK_CALIBRATION = 0; + + private final GBDeviceEventVersionInfo versionInfo = new GBDeviceEventVersionInfo(); + private final GBDeviceEventBatteryInfo batteryInfo = new GBDeviceEventBatteryInfo(); + + private static final Logger LOG = LoggerFactory.getLogger(WatchXPlusDeviceSupport.class); + + private final BroadcastReceiver broadcastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String broadcastAction = intent.getAction(); + switch (broadcastAction) { + case WatchXPlusConstants.ACTION_CALIBRATION: + enableCalibration(intent.getBooleanExtra(WatchXPlusConstants.ACTION_ENABLE, false)); + break; + case WatchXPlusConstants.ACTION_CALIBRATION_SEND: + int hour = intent.getIntExtra(WatchXPlusConstants.VALUE_CALIBRATION_HOUR, -1); + int minute = intent.getIntExtra(WatchXPlusConstants.VALUE_CALIBRATION_MINUTE, -1); + int second = intent.getIntExtra(WatchXPlusConstants.VALUE_CALIBRATION_SECOND, -1); + if (hour != -1 && minute != -1 && second != -1) { + sendCalibrationData(hour, minute, second); + } + break; + case WatchXPlusConstants.ACTION_CALIBRATION_HOLD: + holdCalibration(); + break; + } + } + }; + + public WatchXPlusDeviceSupport() { + super(LOG); + addSupportedService(GattService.UUID_SERVICE_GENERIC_ACCESS); + addSupportedService(GattService.UUID_SERVICE_GENERIC_ATTRIBUTE); + addSupportedService(WatchXPlusConstants.UUID_SERVICE_WATCHXPLUS); + + LocalBroadcastManager broadcastManager = LocalBroadcastManager.getInstance(getContext()); + IntentFilter intentFilter = new IntentFilter(); + intentFilter.addAction(WatchXPlusConstants.ACTION_CALIBRATION); + intentFilter.addAction(WatchXPlusConstants.ACTION_CALIBRATION_SEND); + intentFilter.addAction(WatchXPlusConstants.ACTION_CALIBRATION_HOLD); + broadcastManager.registerReceiver(broadcastReceiver, intentFilter); + } + + @Override + protected TransactionBuilder initializeDevice(TransactionBuilder builder) { + try { + boolean auth = needsAuth; + needsAuth = false; + new InitOperation(auth, this, builder).perform(); + } catch (IOException e) { + e.printStackTrace(); + } + + return builder; + } + + @Override + public boolean connectFirstTime() { + needsAuth = true; + return super.connect(); + } + + @Override + public boolean useAutoConnect() { + return true; + } + + @Override + public void onNotification(NotificationSpec notificationSpec) { + + String senderOrTitle = StringUtils.getFirstOf(notificationSpec.sender, notificationSpec.title); + + String message = StringUtils.truncate(senderOrTitle, 32) + "\0"; +// TODO: Commented out to simplify testing +// if (notificationSpec.subject != null) { +// message += StringUtils.truncate(notificationSpec.subject, 128) + "\n\n"; +// } +// if (notificationSpec.body != null) { +// message += StringUtils.truncate(notificationSpec.body, 128); +// } + + sendNotification(WatchXPlusConstants.NOTIFICATION_CHANNEL_DEFAULT, message); + } + + private void sendNotification(int notificationChannel, String notificationText) { + try { + TransactionBuilder builder = performInitialized("showNotification"); + byte[] command = WatchXPlusConstants.CMD_NOTIFICATION_TEXT_TASK; + byte[] text = notificationText.getBytes("UTF-8"); + byte[] value = new byte[text.length + 2]; + value[0] = (byte)(notificationChannel); +// TODO: Split message into 9-byte arrays and send them one by one. +// Set the message index to FF to indicate end of message + value[1] = (byte) 0xFF; + System.arraycopy(text, 0, value, 2, text.length); + + builder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE), + buildCommand(command, + WatchXPlusConstants.KEEP_ALIVE, + value)); + performImmediately(builder); + } catch (IOException e) { + LOG.warn("Unable to send notification", e); + } + } + + private WatchXPlusDeviceSupport enableNotificationChannels(TransactionBuilder builder) { + builder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE), + buildCommand(WatchXPlusConstants.CMD_NOTIFICATION_SETTINGS, + WatchXPlusConstants.WRITE_VALUE, + new byte[]{(byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF})); + + return this; + } + + public WatchXPlusDeviceSupport authorizationRequest(TransactionBuilder builder, boolean firstConnect) { + builder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE), + buildCommand(WatchXPlusConstants.CMD_AUTHORIZATION_TASK, + WatchXPlusConstants.TASK, + new byte[]{(byte) (firstConnect ? 0x00 : 0x01)})); //possibly not the correct meaning + + return this; + } + + private WatchXPlusDeviceSupport enableDoNotDisturb(TransactionBuilder builder, boolean active) { + builder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE), + buildCommand(WatchXPlusConstants.CMD_DO_NOT_DISTURB_SETTINGS, + WatchXPlusConstants.WRITE_VALUE, + new byte[]{(byte) (active ? 0x01 : 0x00)})); + + return this; + } + + private void enableCalibration(boolean enable) { + try { + TransactionBuilder builder = performInitialized("enableCalibration"); + builder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE), + buildCommand(WatchXPlusConstants.CMD_CALIBRATION_INIT_TASK, + WatchXPlusConstants.TASK, + new byte[]{(byte) (enable ? 0x01 : 0x00)})); + performImmediately(builder); + } catch (IOException e) { + LOG.warn("Unable to start/stop calibration mode", e); + } + } + + private void holdCalibration() { + try { + TransactionBuilder builder = performInitialized("holdCalibration"); + builder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE), + buildCommand(WatchXPlusConstants.CMD_CALIBRATION_KEEP_ALIVE, + WatchXPlusConstants.KEEP_ALIVE)); + performImmediately(builder); + } catch (IOException e) { + LOG.warn("Unable to keep calibration mode alive", e); + } + } + + private void sendCalibrationData(@IntRange(from=0,to=23)int hour, @IntRange(from=0,to=59)int minute, @IntRange(from=0,to=59)int second) { + try { + isCalibrationActive = true; + TransactionBuilder builder = performInitialized("calibrate"); + int handsPosition = ((hour % 12) * 60 + minute) * 60 + second; + builder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE), + buildCommand(WatchXPlusConstants.CMD_CALIBRATION_TASK, + WatchXPlusConstants.TASK, + Conversion.toByteArr16(handsPosition))); + performImmediately(builder); + } catch (IOException e) { + isCalibrationActive = false; + LOG.warn("Unable to send calibration data", e); + } + } + + private void getTime() { + try { + TransactionBuilder builder = performInitialized("getTime"); + builder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE), + buildCommand(WatchXPlusConstants.CMD_TIME_SETTINGS, + WatchXPlusConstants.READ_VALUE)); + performImmediately(builder); + } catch (IOException e) { + LOG.warn("Unable to get device time", e); + } + } + + private void handleTime(byte[] time) { + GregorianCalendar now = BLETypeConversions.createCalendar(); + GregorianCalendar nowDevice = BLETypeConversions.createCalendar(); + int year = (nowDevice.get(Calendar.YEAR) / 100) * 100 + Conversion.fromBcd8(time[8]); + nowDevice.set(year, + Conversion.fromBcd8(time[9]) - 1, + Conversion.fromBcd8(time[10]), + Conversion.fromBcd8(time[11]), + Conversion.fromBcd8(time[12]), + Conversion.fromBcd8(time[13])); + nowDevice.set(Calendar.DAY_OF_WEEK, Conversion.fromBcd8(time[16]) + 1); + + long timeDiff = (Math.abs(now.getTimeInMillis() - nowDevice.getTimeInMillis())) / 1000; + if (10 < timeDiff && timeDiff < 120) { + enableCalibration(true); + setTime(BLETypeConversions.createCalendar()); + enableCalibration(false); + } + } + + private void setTime(Calendar calendar) { + try { + TransactionBuilder builder = performInitialized("setTime"); + int timezoneOffsetMinutes = (calendar.get(Calendar.ZONE_OFFSET) + calendar.get(Calendar.DST_OFFSET)) / (60 * 1000); + int timezoneOffsetIndustrialMinutes = Math.round((Math.abs(timezoneOffsetMinutes) % 60) * 100f / 60f); + byte[] time = new byte[]{Conversion.toBcd8(calendar.get(Calendar.YEAR) % 100), + Conversion.toBcd8(calendar.get(Calendar.MONTH) + 1), + Conversion.toBcd8(calendar.get(Calendar.DAY_OF_MONTH)), + Conversion.toBcd8(calendar.get(Calendar.HOUR_OF_DAY)), + Conversion.toBcd8(calendar.get(Calendar.MINUTE)), + Conversion.toBcd8(calendar.get(Calendar.SECOND)), + (byte) (timezoneOffsetMinutes / 60), + (byte) timezoneOffsetIndustrialMinutes, + (byte) (calendar.get(Calendar.DAY_OF_WEEK) - 1) + }; + builder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE), + buildCommand(WatchXPlusConstants.CMD_TIME_SETTINGS, + WatchXPlusConstants.WRITE_VALUE, + time)); + performImmediately(builder); + } catch (IOException e) { + LOG.warn("Unable to set time", e); + } + } + + public WatchXPlusDeviceSupport getFirmwareVersion(TransactionBuilder builder) { + builder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE), + buildCommand(WatchXPlusConstants.CMD_FIRMWARE_INFO, + WatchXPlusConstants.READ_VALUE)); + + return this; + } + + private WatchXPlusDeviceSupport getBatteryState(TransactionBuilder builder) { + builder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE), + buildCommand(WatchXPlusConstants.CMD_BATTERY_INFO, + WatchXPlusConstants.READ_VALUE)); + + return this; + } + + private WatchXPlusDeviceSupport setFitnessGoal(TransactionBuilder builder) { + int fitnessGoal = new ActivityUser().getStepsGoal(); + builder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE), + buildCommand(WatchXPlusConstants.CMD_FITNESS_GOAL_SETTINGS, + WatchXPlusConstants.WRITE_VALUE, + Conversion.toByteArr16(fitnessGoal))); + + return this; + } + + public WatchXPlusDeviceSupport initialize(TransactionBuilder builder) { + getFirmwareVersion(builder) + .getBatteryState(builder) + .enableNotificationChannels(builder) + .enableDoNotDisturb(builder, false) + .setFitnessGoal(builder); + builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZED, getContext())); + builder.setGattCallback(this); + + return this; + } + + @Override + public void onDeleteNotification(int id) { + + } + + @Override + public void onSetTime() { + getTime(); + } + + @Override + public void onSetAlarms(ArrayList alarms) { + try { + TransactionBuilder builder = performInitialized("setAlarms"); + for (Alarm alarm : alarms) { + setAlarm(alarm, alarm.getPosition() + 1, builder); + } + builder.queue(getQueue()); + } catch (IOException e) { + LOG.warn("Unable to set alarms", e); + } + } + + // No useful use case at the moment, used to clear alarm slots for testing. + private void deleteAlarm(TransactionBuilder builder, int index) { + if (0 < index && index < 4) { + byte[] alarmValue = new byte[]{(byte) index, 0x00, 0x00, 0x00, 0x00, 0x00}; + builder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE), + buildCommand(WatchXPlusConstants.CMD_ALARM_SETTINGS, + WatchXPlusConstants.WRITE_VALUE, + alarmValue)); + } + } + + private void setAlarm(Alarm alarm, int index, TransactionBuilder builder) { + // Shift the GB internal repetition mask to match the device specific one. + byte repetitionMask = (byte) ((alarm.getRepetition() << 1) | (alarm.isRepetitive() ? 0x80 : 0x00)); + repetitionMask |= (alarm.getRepetition(Alarm.ALARM_SUN) ? 0x01 : 0x00); + if (0 < index && index < 4) { + byte[] alarmValue = new byte[]{(byte) index, + Conversion.toBcd8(AlarmUtils.toCalendar(alarm).get(Calendar.HOUR_OF_DAY)), + Conversion.toBcd8(AlarmUtils.toCalendar(alarm).get(Calendar.MINUTE)), + repetitionMask, + (byte) (alarm.getEnabled() ? 0x01 : 0x00), + 0x00 // TODO: Unknown + }; + builder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE), + buildCommand(WatchXPlusConstants.CMD_ALARM_SETTINGS, + WatchXPlusConstants.WRITE_VALUE, + alarmValue)); + } + } + + @Override + public void onSetCallState(CallSpec callSpec) { + switch (callSpec.command) { + case CallSpec.CALL_INCOMING: + sendNotification(WatchXPlusConstants.NOTIFICATION_CHANNEL_PHONE_CALL, callSpec.name); + break; + case CallSpec.CALL_START: + case CallSpec.CALL_END: +// sendNotification(WatchXPlusConstants.NOTIFICATION_CHANNEL_PHONE_CALL, true); +// break; + default: + break; + } + } + + @Override + public void onSetCannedMessages(CannedMessagesSpec cannedMessagesSpec) { + + } + + @Override + public void onSetMusicState(MusicStateSpec stateSpec) { + + } + + @Override + public void onSetMusicInfo(MusicSpec musicSpec) { + + } + + @Override + public void onEnableRealtimeSteps(boolean enable) { + + } + + @Override + public void onInstallApp(Uri uri) { + + } + + @Override + public void onAppInfoReq() { + + } + + @Override + public void onAppStart(UUID uuid, boolean start) { + + } + + @Override + public void onAppDelete(UUID uuid) { + + } + + @Override + public void onAppConfiguration(UUID appUuid, String config, Integer id) { + + } + + @Override + public void onAppReorder(UUID[] uuids) { + + } + + @Override + public void onFetchRecordedData(int dataTypes) { + + } + + @Override + public void onReset(int flags) { + + } + + @Override + public void onHeartRateTest() { + + } + + @Override + public void onEnableRealtimeHeartRateMeasurement(boolean enable) { + + } + + @Override + public void onFindDevice(boolean start) { + + } + + @Override + public void onSetConstantVibration(int integer) { + + } + + @Override + public void onScreenshotReq() { + + } + + @Override + public void onEnableHeartRateSleepSupport(boolean enable) { + + } + + @Override + public void onSetHeartRateMeasurementInterval(int seconds) { + + } + + @Override + public void onAddCalendarEvent(CalendarEventSpec calendarEventSpec) { + + } + + @Override + public void onDeleteCalendarEvent(byte type, long id) { + + } + + @Override + public void onSendConfiguration(String config) { + TransactionBuilder builder; + try { + builder = performInitialized("sendConfig: " + config); + switch (config) { + case ActivityUser.PREF_USER_STEPS_GOAL: + setFitnessGoal(builder); + break; + } + builder.queue(getQueue()); + } catch (IOException e) { + e.printStackTrace(); + } + } + + @Override + public void onReadConfiguration(String config) { + + } + + @Override + public void onTestNewFunction() { + + } + + @Override + public void onSendWeather(WeatherSpec weatherSpec) { + + } + + @Override + public boolean onCharacteristicChanged(BluetoothGatt gatt, + BluetoothGattCharacteristic characteristic) { + super.onCharacteristicChanged(gatt, characteristic); + + UUID characteristicUUID = characteristic.getUuid(); + if (WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE.equals(characteristicUUID)) { + byte[] value = characteristic.getValue(); + if (ArrayUtils.equals(value, WatchXPlusConstants.RESP_FIRMWARE_INFO, 5)) { + handleFirmwareInfo(value); + } else if (ArrayUtils.equals(value, WatchXPlusConstants.RESP_BATTERY_INFO, 5)) { + handleBatteryState(value); + } else if (ArrayUtils.equals(value, WatchXPlusConstants.RESP_TIME_SETTINGS, 5)) { + handleTime(value); + } else if (ArrayUtils.equals(value, WatchXPlusConstants.RESP_BUTTON_INDICATOR, 5)) { + LOG.info("Unhandled action: Button pressed"); + } else if (ArrayUtils.equals(value, WatchXPlusConstants.RESP_ALARM_INDICATOR, 5)) { + LOG.info("Alarm active: id=" + value[8]); + } else if (isCalibrationActive && value.length == 7 && value[4] == ACK_CALIBRATION) { + setTime(BLETypeConversions.createCalendar()); + isCalibrationActive = false; + } + + return true; + } else { + LOG.info("Unhandled characteristic changed: " + characteristicUUID); + logMessageContent(characteristic.getValue()); + } + + return false; + } + + private byte[] buildCommand(byte[] command, byte action) { + return buildCommand(command, action, null); + } + + private byte[] buildCommand(byte[] command, byte action, byte[] value) { + if (Arrays.equals(command, WatchXPlusConstants.CMD_CALIBRATION_TASK)) { + ACK_CALIBRATION = (byte) sequenceNumber; + } + command = BLETypeConversions.join(command, value); + byte[] result = new byte[7 + command.length]; + System.arraycopy(WatchXPlusConstants.CMD_HEADER, 0, result, 0, 5); + System.arraycopy(command, 0, result, 6, command.length); + result[2] = (byte) (command.length + 1); + result[3] = WatchXPlusConstants.REQUEST; + result[4] = (byte) sequenceNumber++; + result[5] = action; + result[result.length - 1] = calculateChecksum(result); + + return result; + } + + private byte calculateChecksum(byte[] bytes) { + byte checksum = 0x00; + for (int i = 0; i < bytes.length - 1; i++) { + checksum += (bytes[i] ^ i) & 0xFF; + } + return (byte) (checksum & 0xFF); + } + + private void handleFirmwareInfo(byte[] value) { + versionInfo.fwVersion = String.format(Locale.US,"%d.%d.%d", value[8], value[9], value[10]); + handleGBDeviceEvent(versionInfo); + } + + private void handleBatteryState(byte[] value) { + batteryInfo.state = value[8] == 1 ? BatteryState.BATTERY_NORMAL : BatteryState.BATTERY_LOW; + batteryInfo.level = value[9]; + handleGBDeviceEvent(batteryInfo); + } + + @Override + public void dispose() { + LocalBroadcastManager broadcastManager = LocalBroadcastManager.getInstance(getContext()); + broadcastManager.unregisterReceiver(broadcastReceiver); + super.dispose(); + } + + private static class Conversion { + static byte toBcd8(@IntRange(from = 0, to = 99) int value) { + int high = (value / 10) << 4; + int low = value % 10; + return (byte) (high | low); + } + + static int fromBcd8(byte value) { + int high = ((value & 0xF0) >> 4) * 10; + int low = value & 0x0F; + return high + low; + } + + static byte[] toByteArr16(int value) { + return new byte[]{(byte) (value >> 8), (byte) value}; + } + + static byte[] toByteArr32(int value) { + return new byte[]{(byte) (value >> 24), + (byte) (value >> 16), + (byte) (value >> 8), + (byte) value}; + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/watchxplus/operations/InitOperation.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/watchxplus/operations/InitOperation.java new file mode 100644 index 000000000..f0b3333a1 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/watchxplus/operations/InitOperation.java @@ -0,0 +1,94 @@ +/* Copyright (C) 2018-2019 maxirnilian + + 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.watchxplus.operations; + +import android.bluetooth.BluetoothGatt; +import android.bluetooth.BluetoothGattCharacteristic; +import android.widget.Toast; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.UUID; + +import nodomain.freeyourgadget.gadgetbridge.devices.watch9.Watch9Constants; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEOperation; +import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; +import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction; +import nodomain.freeyourgadget.gadgetbridge.service.devices.watch9.Watch9DeviceSupport; +import nodomain.freeyourgadget.gadgetbridge.service.devices.watchxplus.WatchXPlusDeviceSupport; +import nodomain.freeyourgadget.gadgetbridge.util.ArrayUtils; +import nodomain.freeyourgadget.gadgetbridge.util.GB; + +public class InitOperation extends AbstractBTLEOperation{ + + private static final Logger LOG = LoggerFactory.getLogger(InitOperation.class); + + private final TransactionBuilder builder; + private final boolean needsAuth; + private final BluetoothGattCharacteristic cmdCharacteristic = getCharacteristic(Watch9Constants.UUID_CHARACTERISTIC_WRITE); + + public InitOperation(boolean needsAuth, WatchXPlusDeviceSupport support, TransactionBuilder builder) { + super(support); + this.needsAuth = needsAuth; + this.builder = builder; + builder.setGattCallback(this); + } + + @Override + protected void doPerform() throws IOException { + builder.notify(cmdCharacteristic, true); + if (needsAuth) { + builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.AUTHENTICATING, getContext())); + getSupport().authorizationRequest(builder, needsAuth); + } else { + builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZING, getContext())); + getSupport().initialize(builder); + getSupport().performImmediately(builder); + } + } + + @Override + public boolean onCharacteristicChanged(BluetoothGatt gatt, + BluetoothGattCharacteristic characteristic) { + UUID characteristicUUID = characteristic.getUuid(); + if (Watch9Constants.UUID_CHARACTERISTIC_WRITE.equals(characteristicUUID) && needsAuth) { + try { + byte[] value = characteristic.getValue(); + getSupport().logMessageContent(value); + if (ArrayUtils.equals(value, Watch9Constants.RESP_AUTHORIZATION_TASK, 5) && value[8] == 0x01) { + TransactionBuilder builder = getSupport().createTransactionBuilder("authInit"); + builder.setGattCallback(this); + builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZING, getContext())); + getSupport().initialize(builder).performImmediately(builder); + } else { + return super.onCharacteristicChanged(gatt, characteristic); + } + } catch (Exception e) { + GB.toast(getContext(), "Error authenticating Watch 9", Toast.LENGTH_LONG, GB.ERROR, e); + } + return true; + } else { + LOG.info("Unhandled characteristic changed: " + characteristicUUID); + return super.onCharacteristicChanged(gatt, characteristic); + } + } + + +}