diff --git a/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java b/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java
index 13868d787..a8ab19315 100644
--- a/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java
+++ b/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java
@@ -72,6 +72,8 @@ public class GBDaoGenerator {
addXWatchActivitySample(schema, user, device);
addZeTimeActivitySample(schema, user, device);
addID115ActivitySample(schema, user, device);
+ addWatchXPlusHealthActivitySample(schema, user, device);
+ addWatchXPlusHealthActivityKindOverlay(schema, user, device);
addCalendarSyncState(schema, device);
addAlarms(schema, user, device);
@@ -330,6 +332,36 @@ public class GBDaoGenerator {
return activitySample;
}
+ private static Entity addWatchXPlusHealthActivitySample(Schema schema, Entity user, Entity device) {
+ Entity activitySample = addEntity(schema, "WatchXPlusActivitySample");
+ activitySample.implementsSerializable();
+ addCommonActivitySampleProperties("AbstractActivitySample", activitySample, user, device);
+ activitySample.addByteArrayProperty("rawWatchXPlusHealthData");
+ activitySample.addIntProperty(SAMPLE_RAW_KIND).notNull().primaryKey();
+ activitySample.addIntProperty(SAMPLE_RAW_INTENSITY).notNull().codeBeforeGetterAndSetter(OVERRIDE);
+ activitySample.addIntProperty(SAMPLE_STEPS).notNull().codeBeforeGetterAndSetter(OVERRIDE);
+ addHeartRateProperties(activitySample);
+ activitySample.addIntProperty("distance");
+ activitySample.addIntProperty("calories");
+ return activitySample;
+ }
+
+ private static Entity addWatchXPlusHealthActivityKindOverlay(Schema schema, Entity user, Entity device) {
+ Entity activityOverlay = addEntity(schema, "WatchXPlusHealthActivityOverlay");
+
+ activityOverlay.addIntProperty(TIMESTAMP_FROM).notNull().primaryKey();
+ activityOverlay.addIntProperty(TIMESTAMP_TO).notNull().primaryKey();
+ activityOverlay.addIntProperty(SAMPLE_RAW_KIND).notNull().primaryKey();
+ Property deviceId = activityOverlay.addLongProperty("deviceId").primaryKey().notNull().getProperty();
+ activityOverlay.addToOne(device, deviceId);
+
+ Property userId = activityOverlay.addLongProperty("userId").notNull().getProperty();
+ activityOverlay.addToOne(user, userId);
+ activityOverlay.addByteArrayProperty("rawWatchXPlusHealthData");
+
+ return activityOverlay;
+ }
+
private static void addCommonActivitySampleProperties(String superClass, Entity activitySample, Entity user, Entity device) {
activitySample.setSuperclass(superClass);
activitySample.addImport(MAIN_PACKAGE + ".devices.SampleProvider");
diff --git a/README.md b/README.md
index da40517f6..c832a886c 100644
--- a/README.md
+++ b/README.md
@@ -40,6 +40,7 @@ vendor's servers.
* HPlus Devices (e.g. ZeBand) [Wiki](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/HPlus)
* ID115
* Lenovo Watch 9
+* Lenovo Watch X Plus
* Liveview
* Makibes HR3
* Mi Band, Mi Band 1A, Mi Band 1S [Wiki](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Mi-Band)
@@ -75,6 +76,7 @@ Please see [FEATURES.md](https://codeberg.org/Freeyourgadget/Gadgetbridge/src/ma
* Sebastian Kranz (ZeTime)
* Vadim Kaushan (ID115)
* "maxirnilian" (Lenovo Watch 9)
+* "ksiwczynski", "mkusnierz", "mamutcho" (Lenovo Watch X Plus)
* Andreas Böhler (Casio GB-6900B)
* Jean-François Greffier (Mi Scale 2)
* Johannes Schmitt (BFH-16)
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index e7585523d..a02fe6631 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -73,6 +73,10 @@
android:name=".devices.zetime.ZeTimePreferenceActivity"
android:label="@string/zetime_title_settings"
android:parentActivityName=".activities.SettingsActivity" />
+
+
+
. */
+package nodomain.freeyourgadget.gadgetbridge.devices.lenovo;
+
+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 LenovoWatchCalibrationActivity 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_watchxplus_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(LenovoWatchConstants.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(LenovoWatchConstants.ACTION_CALIBRATION_SEND);
+ calibrationData.putExtra(LenovoWatchConstants.VALUE_CALIBRATION_HOUR, pickerHour.getValue());
+ calibrationData.putExtra(LenovoWatchConstants.VALUE_CALIBRATION_MINUTE, pickerMinute.getValue());
+ calibrationData.putExtra(LenovoWatchConstants.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(LenovoWatchConstants.ACTION_CALIBRATION);
+ calibration.putExtra(LenovoWatchConstants.ACTION_ENABLE, true);
+ LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(calibration);
+ handler.postDelayed(holdCalibration, 1000);
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ Intent calibration = new Intent(LenovoWatchConstants.ACTION_CALIBRATION);
+ calibration.putExtra(LenovoWatchConstants.ACTION_ENABLE, false);
+ LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(calibration);
+ handler.removeCallbacks(holdCalibration);
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lenovo/LenovoWatchConstants.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lenovo/LenovoWatchConstants.java
new file mode 100644
index 000000000..81365035c
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lenovo/LenovoWatchConstants.java
@@ -0,0 +1,79 @@
+/* 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.lenovo;
+
+public class LenovoWatchConstants {
+
+ 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_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[]{0x01, 0x03, 0x02};
+
+ public static final String ACTION_ENABLE = "action.watch9.enable";
+
+ public static final String ACTION_CALIBRATION
+ = "nodomain.freeyourgadget.gadgetbridge.devices.action.lenovowatch.start_calibration";
+ public static final String ACTION_CALIBRATION_SEND
+ = "nodomain.freeyourgadget.gadgetbridge.devices.action.lenovowatch.send_calibration";
+ public static final String ACTION_CALIBRATION_HOLD
+ = "nodomain.freeyourgadget.gadgetbridge.devices.action.lenovowatch.keep_calibrating";
+ public static final String VALUE_CALIBRATION_HOUR
+ = "value.lenovowatch.calibration_hour";
+ public static final String VALUE_CALIBRATION_MINUTE
+ = "value.lenovowatch.calibration_minute";
+ public static final String VALUE_CALIBRATION_SECOND
+ = "value.lenovowatch.calibration_second";
+
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lenovo/LenovoWatchPairingActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lenovo/LenovoWatchPairingActivity.java
new file mode 100644
index 000000000..2b6f795ae
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lenovo/LenovoWatchPairingActivity.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.lenovo;
+
+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 LenovoWatchPairingActivity extends AbstractGBActivity {
+ private static final Logger LOG = LoggerFactory.getLogger(LenovoWatchPairingActivity.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/devices/lenovo/watchxplus/WatchXPlusConstants.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lenovo/watchxplus/WatchXPlusConstants.java
new file mode 100644
index 000000000..adf14f598
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lenovo/watchxplus/WatchXPlusConstants.java
@@ -0,0 +1,114 @@
+/* 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.lenovo.watchxplus;
+
+import java.util.UUID;
+
+import nodomain.freeyourgadget.gadgetbridge.devices.lenovo.LenovoWatchConstants;
+
+public final class WatchXPlusConstants extends LenovoWatchConstants {
+ 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_DATABASE_READ = 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 String PREF_ACTIVATE_DISPLAY = "activate_display_on_lift_wrist";
+ public static final String PREF_DISCONNECT_REMIND = "disconnect_notification";
+ public static final String PREF_FIND_PHONE = "prefs_find_phone";
+ public static final String PREF_FIND_PHONE_DURATION = "prefs_find_phone_duration";
+ public static final String PREF_ALTITUDE = "pref_watchxplus_altitude";
+ public static final String PREF_REPEAT = "watchxplus_repeat";
+ public static final String PREF_CONTINIOUS = "watchxplus_continious";
+ public static final String PREF_MISSED_CALL = "watchxplus_missed";
+ public static final String PREF_MISSED_CALL_REPEAT = "watchxplus_repeat_missedcall";
+ public static final String PREF_IS_BP_CALIBRATED = "watchxplus_is_bp_calibrated";
+ public static final String PREF_BUTTON_REJECT = "watchxplus_button_reject";
+ public static final String PREF_SHAKE_REJECT = "watchxplus_shake_reject";
+ public static final String PREF_BP_CAL_LOW = "pref_wxp_bp_calibration_low";
+ public static final String PREF_BP_CAL_HIGH = "pref_wxp_bp_calibration_high";
+ public static final String PREF_BP_CAL_SWITCH = "wxp_button_BP_calibration_list";
+ public static final String PREF_DO_NOT_DISTURB = "do_not_disturb_no_auto";
+ public static final String PREF_DO_NOT_DISTURB_START = "do_not_disturb_no_auto_start";
+ public static final String PREF_DO_NOT_DISTURB_END = "do_not_disturb_no_auto_end";
+ public static final String PREF_LONGSIT_SWITCH = "pref_watchxplus_longsit_switch";
+ public static final String PREF_LONGSIT_PERIOD = "pref_watchxplus_longsit_period";
+ public static final String PREF_WXP_LANGUAGE = "pref_wxp_language";
+ public static final String PREF_POWER_MODE = "pref_wxp_power_mode";
+
+
+ // time format constants
+ public static final byte ARG_SET_TIMEMODE_24H = 0x00;
+ public static final byte ARG_SET_TIMEMODE_12H = 0x01;
+
+ public static final int NOTIFICATION_CHANNEL_DEFAULT = 0;
+ public static final int NOTIFICATION_CHANNEL_PHONE_CALL = 10;
+
+ public static final byte[] CMD_WEATHER_SET = new byte[]{0x01, 0x10};
+ public static final byte[] CMD_RETRIEVE_DATA_COUNT = new byte[]{(byte)0xF0, 0x10};
+ public static final byte[] CMD_RETRIEVE_DATA_DETAILS = new byte[]{(byte)0xF0, 0x11};
+ public static final byte[] CMD_RETRIEVE_DATA_CONTENT = new byte[]{(byte)0xF0, 0x12};
+ public static final byte[] CMD_REMOVE_DATA_CONTENT = new byte[]{(byte)0xF0, 0x32};
+ public static final byte[] CMD_BLOOD_PRESSURE_MEASURE = new byte[]{0x05, 0x0D};
+ public static final byte[] CMD_HEART_RATE_MEASURE = new byte[]{0x03, 0x23};
+ public static final byte[] CMD_IS_BP_CALIBRATED = new byte[]{0x05, 0x0B};
+ public static final byte[] CMD_BP_CALIBRATION = new byte[]{0x05, 0x0C};
+
+ public static final byte[] CMD_NOTIFICATION_TEXT_TASK = new byte[]{0x03, 0x06};
+ public static final byte[] CMD_NOTIFICATION_CANCEL = new byte[]{0x03, 0x04};
+ public static final byte[] CMD_NOTIFICATION_SETTINGS = new byte[]{0x03, 0x02};
+ public static final byte[] CMD_DO_NOT_DISTURB_SETTINGS = new byte[]{0x03, 0x61};
+ public static final byte[] CMD_POWER_MODE = new byte[]{0x03, -0x7F};
+ public static final byte[] CMD_SET_QUITE_HOURS_TIME = new byte[]{0x03, 0x62};
+ public static final byte[] CMD_SET_QUITE_HOURS_SWITCH = new byte[]{0x03, 0x61};
+ public static final byte[] CMD_SET_PERSONAL_INFO = new byte[]{0x01, 0x0E};
+ public static final byte[] CMD_INACTIVITY_REMINDER_SWITCH = new byte[]{0x03, 0x51};
+ public static final byte[] CMD_INACTIVITY_REMINDER_SET = new byte[]{0x03, 0x52};
+ public static final byte[] CMD_SET_UNITS = new byte[]{0x03, -0x6D};
+
+ public static final byte[] CMD_FITNESS_GOAL_SETTINGS = new byte[]{0x10, 0x02};
+ public static final byte[] CMD_DAY_STEPS_INFO = new byte[]{0x10, 0x03};
+
+ public static final byte[] CMD_SHAKE_SWITCH = new byte[]{0x03, -0x6E};
+ public static final byte[] CMD_DISCONNECT_REMIND = new byte[]{0x00, 0x11};
+ public static final byte[] CMD_TIME_LANGUAGE = new byte[]{0x03, -0x6F};
+ public static final byte[] CMD_ALTITUDE = new byte[]{0x05, 0x0A};
+
+ public static final byte[] RESP_SHAKE_SWITCH = new byte[]{0x08, 0x03, -0x6E};
+ public static final byte[] RESP_DISCONNECT_REMIND = new byte[]{0x08, 0x00, 0x11};
+ public static final byte[] RESP_IS_BP_CALIBRATED = new byte[]{0x08, 0x05, 0x0B};
+ public static final byte[] RESP_BUTTON_WHILE_RING = new byte[]{0x04, 0x03, 0x03};
+ public static final byte[] RESP_BP_CALIBRATION = new byte[]{0x08, 0x05, 0x0C};
+ public static final byte[] RESP_SET_PERSONAL_INFO = new byte[]{0x08, 0x01, 0x0E};
+ public static final byte[] RESP_GOAL_AIM_STATUS = new byte[]{0x08, 0x10, 0x02};
+ public static final byte[] RESP_INACTIVITY_REMINDER_SWITCH = new byte[]{0x08, 0x03, 0x51};
+ public static final byte[] RESP_INACTIVITY_REMINDER_SET = new byte[]{0x08, 0x03, 0x52};
+
+ public static final byte[] RESP_AUTHORIZATION_TASK = new byte[]{0x01, 0x01, 0x05};
+ public static final byte[] RESP_DAY_STEPS_INDICATOR = new byte[]{0x08, 0x10, 0x03};
+ public static final byte[] RESP_HEARTRATE = new byte[]{(byte) 0x80, 0x15, 0x03};
+
+ public static final byte[] RESP_DATA_COUNT = new byte[]{0x08, (byte)0xF0, 0x10};
+ public static final byte[] RESP_DATA_DETAILS = new byte[]{0x08, (byte)0xF0, 0x11};
+ public static final byte[] RESP_DATA_CONTENT = new byte[]{0x08, (byte)0xF0, 0x12};
+ public static final byte[] RESP_DATA_CONTENT_REMOVE = new byte[]{-0x80, (byte)0xF0, 0x32};
+ public static final byte[] RESP_BP_MEASURE_STARTED = new byte[]{0x08, 0x05, 0x0D};
+
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lenovo/watchxplus/WatchXPlusDeviceCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lenovo/watchxplus/WatchXPlusDeviceCoordinator.java
new file mode 100644
index 000000000..aff51c4bc
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lenovo/watchxplus/WatchXPlusDeviceCoordinator.java
@@ -0,0 +1,352 @@
+package nodomain.freeyourgadget.gadgetbridge.devices.lenovo.watchxplus;
+
+import android.annotation.TargetApi;
+import android.app.Activity;
+import android.bluetooth.le.ScanFilter;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.net.Uri;
+import android.os.Build;
+import android.os.ParcelUuid;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Collection;
+import java.util.Collections;
+
+import nodomain.freeyourgadget.gadgetbridge.GBApplication;
+import nodomain.freeyourgadget.gadgetbridge.GBException;
+import nodomain.freeyourgadget.gadgetbridge.R;
+import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst;
+import nodomain.freeyourgadget.gadgetbridge.devices.AbstractDeviceCoordinator;
+import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
+import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
+import nodomain.freeyourgadget.gadgetbridge.devices.lenovo.LenovoWatchPairingActivity;
+import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
+import nodomain.freeyourgadget.gadgetbridge.entities.Device;
+import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
+import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
+import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
+import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.lenovo.watchxplus.WatchXPlusDeviceSupport;
+import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
+
+import static nodomain.freeyourgadget.gadgetbridge.GBApplication.getContext;
+
+
+public class WatchXPlusDeviceCoordinator extends AbstractDeviceCoordinator {
+ private static final Logger LOG = LoggerFactory.getLogger(WatchXPlusDeviceSupport.class);
+ private static final int FindPhone_ON = -1;
+ public static final int FindPhone_OFF = 0;
+ public static boolean isBPCalibrated = false;
+
+ private static Prefs prefs = GBApplication.getPrefs();
+
+ @NonNull
+ @Override
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ public Collection extends ScanFilter> createBLEScanFilters() {
+ ParcelUuid watchXpService = new ParcelUuid(WatchXPlusConstants.UUID_SERVICE_WATCHXPLUS);
+ ScanFilter filter = new ScanFilter.Builder().setServiceUuid(watchXpService).build();
+ return Collections.singletonList(filter);
+ }
+
+ @Override
+ protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) {
+
+ }
+
+ @Override
+ public int getBondingStyle() {
+ return BONDING_STYLE_NONE;
+ }
+
+ @NonNull
+ @Override
+ public DeviceType getSupportedType(GBDeviceCandidate candidate) {
+ String macAddress = candidate.getMacAddress().toUpperCase();
+ String deviceName = candidate.getName().toUpperCase();
+ if (candidate.supportsService(WatchXPlusConstants.UUID_SERVICE_WATCHXPLUS)) {
+ return DeviceType.WATCHXPLUS;
+ } else if (macAddress.startsWith("DC:41:E5")) {
+ return DeviceType.WATCHXPLUS;
+ } else if (deviceName.equalsIgnoreCase("WATCH XPLUS")) {
+ return DeviceType.WATCHXPLUS;
+ // add initial support for Watch X non-plus (forces Watch X to be recognized as Watch XPlus)
+ // Watch X non-plus have same MAC address as Watch 9 (starts with "1C:87:79")
+ } else if (deviceName.equalsIgnoreCase("WATCH X")) {
+ return DeviceType.WATCHXPLUS;
+ }
+ return DeviceType.UNKNOWN;
+ }
+
+ @Override
+ public DeviceType getDeviceType() {
+ return DeviceType.WATCHXPLUS;
+ }
+
+ @Nullable
+ @Override
+ public Class extends Activity> getPairingActivity() {
+ return LenovoWatchPairingActivity.class;
+ }
+
+ @Override
+ public boolean supportsActivityDataFetching() {
+ return true;
+ }
+
+ @Override
+ public boolean supportsActivityTracking() {
+ return true;
+ }
+
+ @Override
+ public SampleProvider extends ActivitySample> getSampleProvider(GBDevice device, DaoSession session) {
+ return new WatchXPlusSampleProvider(device, session);
+ }
+
+ @Override
+ public InstallHandler findInstallHandler(Uri uri, Context context) {
+ return null;
+ }
+
+ @Override
+ public boolean supportsScreenshots() {
+ return false;
+ }
+
+ @Override
+ public int getAlarmSlotCount() {
+ return 3;
+ }
+
+ @Override
+ public boolean supportsSmartWakeup(GBDevice device) {
+ return false;
+ }
+
+ @Override
+ public boolean supportsHeartRateMeasurement(GBDevice device) {
+ return true;
+ }
+
+ @Override
+ public String getManufacturer() {
+ return "Lenovo";
+ }
+
+ @Override
+ public boolean supportsAppsManagement() {
+ return false;
+ }
+
+ @Override
+ public Class extends Activity> getAppsManagementActivity() {
+ return null;
+ }
+
+ @Override
+ public boolean supportsCalendarEvents() {
+ return false;
+ }
+
+ @Override
+ public boolean supportsRealtimeData() { return false; }
+ @Override
+ public boolean supportsWeather() {
+ return true;
+ }
+
+ @Override
+ public boolean supportsFindDevice() { return false; }
+
+ @Override
+ public int[] getSupportedDeviceSpecificSettings(GBDevice device) {
+ return new int[]{
+ R.xml.devicesettings_liftwrist_display,
+ R.xml.devicesettings_disconnectnotification,
+ R.xml.devicesettings_find_phone,
+ R.xml.devicesettings_timeformat,
+ R.xml.devicesettings_donotdisturb_no_auto
+ };
+ }
+
+/*
+Prefs from device settings on main page
+ */
+// return time format pref
+ public static byte getTimeMode(SharedPreferences sharedPrefs) {
+ String timeMode = sharedPrefs.getString(DeviceSettingsPreferenceConst.PREF_TIMEFORMAT, getContext().getString(R.string.p_timeformat_24h));
+ assert timeMode != null;
+ if (timeMode.equals(getContext().getString(R.string.p_timeformat_24h))) {
+ return WatchXPlusConstants.ARG_SET_TIMEMODE_24H;
+ } else {
+ return WatchXPlusConstants.ARG_SET_TIMEMODE_12H;
+ }
+ }
+
+// return watch language pref
+ public static byte getLanguage(SharedPreferences sharedPrefs) {
+ int settingRead = prefs.getInt(WatchXPlusConstants.PREF_WXP_LANGUAGE, 1);
+ return (byte) settingRead;
+ }
+
+// check if it is needed to toggle Lift Wrist to Sreen on
+ public static boolean shouldEnableHeadsUpScreen(SharedPreferences sharedPrefs) {
+ String liftMode = sharedPrefs.getString(WatchXPlusConstants.PREF_ACTIVATE_DISPLAY, getContext().getString(R.string.p_on));
+ // WatchXPlus doesn't support scheduled intervals. Treat it as "on".
+ assert liftMode != null;
+ return !liftMode.equals(getContext().getString(R.string.p_off));
+ }
+
+// check if it is needed to toggle Disconnect reminder
+ public static boolean shouldEnableDisconnectReminder(SharedPreferences sharedPrefs) {
+ String lostReminder = sharedPrefs.getString(WatchXPlusConstants.PREF_DISCONNECT_REMIND, getContext().getString(R.string.p_on));
+ // WatchXPlus doesn't support scheduled intervals. Treat it as "on".
+ assert lostReminder != null;
+ return !lostReminder.equals(getContext().getString(R.string.p_off));
+ }
+
+// find phone settings
+ /**
+ * @return {@link #FindPhone_OFF}, {@link #FindPhone_ON}, or the duration
+ */
+ public static int getFindPhone(SharedPreferences sharedPrefs) {
+ String findPhone = sharedPrefs.getString(WatchXPlusConstants.PREF_FIND_PHONE, getContext().getString(R.string.p_off));
+
+ assert findPhone != null;
+ if (findPhone.equals(getContext().getString(R.string.p_off))) {
+ return FindPhone_OFF;
+ } else if (findPhone.equals(getContext().getString(R.string.p_on))) {
+ return FindPhone_ON;
+ } else { // Duration
+ String duration = sharedPrefs.getString(WatchXPlusConstants.PREF_FIND_PHONE_DURATION, "0");
+
+ try {
+ int iDuration;
+
+ try {
+ assert duration != null;
+ iDuration = Integer.valueOf(duration);
+ } catch (Exception ex) {
+ iDuration = 60;
+ }
+
+ return iDuration;
+ } catch (Exception e) {
+ return FindPhone_ON;
+ }
+ }
+ }
+
+ /**
+ * @param startOut out Only hour/minute are used.
+ * @param endOut out Only hour/minute are used.
+ * @return True if quite hours are enabled.
+ */
+ public static boolean getQuiteHours(SharedPreferences sharedPrefs, Calendar startOut, Calendar endOut) {
+ String doNotDisturb = sharedPrefs.getString(WatchXPlusConstants.PREF_DO_NOT_DISTURB, getContext().getString(R.string.p_off));
+
+ assert doNotDisturb != null;
+ if (doNotDisturb.equals(getContext().getString(R.string.p_off))) {
+ LOG.info(" DND is disabled ");
+ return false;
+ } else {
+ String start = sharedPrefs.getString(WatchXPlusConstants.PREF_DO_NOT_DISTURB_START, "00:00");
+ String end = sharedPrefs.getString(WatchXPlusConstants.PREF_DO_NOT_DISTURB_END, "00:00");
+
+ DateFormat df = new SimpleDateFormat("HH:mm");
+
+ try {
+ startOut.setTime(df.parse(start));
+ endOut.setTime(df.parse(end));
+
+ return true;
+ } catch (Exception e) {
+ return false;
+ }
+ }
+ }
+
+ /**
+ * @param startOut out Only hour/minute are used.
+ * @param endOut out Only hour/minute are used.
+ * @return True if quite hours are enabled.
+ */
+ public static boolean getLongSitHours(SharedPreferences sharedPrefs, Calendar startOut, Calendar endOut) {
+ boolean enabled = prefs.getBoolean(WatchXPlusConstants.PREF_LONGSIT_SWITCH, false);
+
+ if (!enabled) {
+ LOG.info(" DND is disabled ");
+ return false;
+ } else {
+ String start = sharedPrefs.getString(WatchXPlusConstants.PREF_DO_NOT_DISTURB_START, "00:00");
+ String end = sharedPrefs.getString(WatchXPlusConstants.PREF_DO_NOT_DISTURB_END, "00:00");
+
+ DateFormat df = new SimpleDateFormat("HH:mm");
+
+ try {
+ startOut.setTime(df.parse(start));
+ endOut.setTime(df.parse(end));
+
+ return true;
+ } catch (Exception e) {
+ return false;
+ }
+ }
+ }
+
+/*
+Values from device specific settings page
+ */
+// read altitude from preferences
+ public static int getAltitude(String address) {
+ return prefs.getInt(WatchXPlusConstants.PREF_ALTITUDE, 200);
+ }
+
+// read repeat call notification
+ public static int getRepeatOnCall(String address) {
+ return prefs.getInt(WatchXPlusConstants.PREF_REPEAT, 1);
+ }
+
+//read continious call notification
+ public static boolean getContiniousVibrationOnCall(String address) {
+ return prefs.getBoolean(WatchXPlusConstants.PREF_CONTINIOUS, false);
+ }
+
+//read missed call notification
+ public static boolean getMissedCallReminder(String address) {
+ return prefs.getBoolean(WatchXPlusConstants.PREF_MISSED_CALL, false);
+ }
+
+//read missed call notification
+ public static int getMissedCallRepeat(String address) {
+ return prefs.getInt(WatchXPlusConstants.PREF_MISSED_CALL_REPEAT, 0);
+ }
+
+
+//read button reject call settings
+ public static boolean getButtonReject(String address) {
+ return prefs.getBoolean(WatchXPlusConstants.PREF_BUTTON_REJECT, false);
+ }
+
+//read shake wrist reject call settings
+ public static boolean getShakeReject(String address) {
+ return prefs.getBoolean(WatchXPlusConstants.PREF_SHAKE_REJECT, false);
+ }
+
+/*
+Other saved preferences
+ */
+
+
+
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lenovo/watchxplus/WatchXPlusPreferenceActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lenovo/watchxplus/WatchXPlusPreferenceActivity.java
new file mode 100644
index 000000000..46ef967e6
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lenovo/watchxplus/WatchXPlusPreferenceActivity.java
@@ -0,0 +1,64 @@
+/* Copyright (C) 2018-2019 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.devices.lenovo.watchxplus;
+
+import android.os.Bundle;
+import android.preference.Preference;
+
+import nodomain.freeyourgadget.gadgetbridge.GBApplication;
+import nodomain.freeyourgadget.gadgetbridge.R;
+import nodomain.freeyourgadget.gadgetbridge.activities.AbstractSettingsActivity;
+
+public class WatchXPlusPreferenceActivity extends AbstractSettingsActivity {
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ addPreferencesFromResource(R.xml.watchxplus_preferences);
+
+ // notifications
+ //addPreferenceHandlerFor(WatchXPlusConstants.PREF_REPEAT);
+ //addPreferenceHandlerFor(WatchXPlusConstants.PREF_CONTINIOUS);
+ //addPreferenceHandlerFor(WatchXPlusConstants.PREF_MISSED_CALL);
+ //addPreferenceHandlerFor(WatchXPlusConstants.PREF_MISSED_CALL_REPEAT);
+ //addPreferenceHandlerFor(WatchXPlusConstants.PREF_BUTTON_REJECT);
+ //addPreferenceHandlerFor(WatchXPlusConstants.PREF_SHAKE_REJECT);
+
+ // settings
+ addPreferenceHandlerFor(WatchXPlusConstants.PREF_POWER_MODE);
+ addPreferenceHandlerFor(WatchXPlusConstants.PREF_WXP_LANGUAGE);
+ addPreferenceHandlerFor(WatchXPlusConstants.PREF_LONGSIT_PERIOD);
+ addPreferenceHandlerFor(WatchXPlusConstants.PREF_LONGSIT_SWITCH);
+ // calibration
+ addPreferenceHandlerFor(WatchXPlusConstants.PREF_ALTITUDE);
+ addPreferenceHandlerFor(WatchXPlusConstants.PREF_BP_CAL_LOW);
+ addPreferenceHandlerFor(WatchXPlusConstants.PREF_BP_CAL_HIGH);
+ addPreferenceHandlerFor(WatchXPlusConstants.PREF_BP_CAL_SWITCH);
+
+ }
+
+ private void addPreferenceHandlerFor(final String preferenceKey) {
+ Preference pref = findPreference(preferenceKey);
+ pref.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
+ @Override public boolean onPreferenceChange(Preference preference, Object newVal) {
+ GBApplication.deviceService().onSendConfiguration(preferenceKey);
+ return true;
+ }
+ });
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lenovo/watchxplus/WatchXPlusSampleProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lenovo/watchxplus/WatchXPlusSampleProvider.java
new file mode 100644
index 000000000..cb9bfeba1
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lenovo/watchxplus/WatchXPlusSampleProvider.java
@@ -0,0 +1,63 @@
+package nodomain.freeyourgadget.gadgetbridge.devices.lenovo.watchxplus;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import de.greenrobot.dao.AbstractDao;
+import de.greenrobot.dao.Property;
+import nodomain.freeyourgadget.gadgetbridge.devices.AbstractSampleProvider;
+import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
+import nodomain.freeyourgadget.gadgetbridge.entities.WatchXPlusActivitySample;
+import nodomain.freeyourgadget.gadgetbridge.entities.WatchXPlusActivitySampleDao;
+import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
+
+public class WatchXPlusSampleProvider extends AbstractSampleProvider {
+
+ public WatchXPlusSampleProvider(GBDevice device, DaoSession session) {
+ super(device, session);
+
+ }
+
+ @Override
+ public int normalizeType(int rawType) {
+ return rawType;
+ }
+
+ @Override
+ public int toRawActivityKind(int activityKind) {
+ return activityKind;
+ }
+
+ @Override
+ public float normalizeIntensity(int rawIntensity) {
+ return rawIntensity;
+ }
+
+ @Override
+ public WatchXPlusActivitySample createActivitySample() {
+ return new WatchXPlusActivitySample();
+ }
+
+ @Override
+ public AbstractDao getSampleDao() {
+ return getSession().getWatchXPlusActivitySampleDao();
+ }
+
+ @Nullable
+ @Override
+ protected Property getRawKindSampleProperty() {
+ return WatchXPlusActivitySampleDao.Properties.RawKind;
+ }
+
+ @NonNull
+ @Override
+ protected Property getTimestampSampleProperty() {
+ return WatchXPlusActivitySampleDao.Properties.Timestamp;
+ }
+
+ @NonNull
+ @Override
+ protected Property getDeviceIdentifierSampleProperty() {
+ return WatchXPlusActivitySampleDao.Properties.DeviceId;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lenovo/watchxplus/WorkProgress b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lenovo/watchxplus/WorkProgress
new file mode 100644
index 000000000..f93f95330
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lenovo/watchxplus/WorkProgress
@@ -0,0 +1,82 @@
+NEED TO BE DONE
+ Watch settings
+ - Implement temperature alarm on watch //tried to implement with no luck
+ - Implement continuous blood pressure measurement (on, off, scheduled)
+
+ Add feature to initiate button press event on watch
+ - Send command to watch
+ - Get trigger on button press
+
+ Schedulers:
+ - Screen on scheduler (inApp, not supported by watch)
+ - Disconnect reminder scheduler (inApp, not supported by watch)
+ - Continuous blood pressure measurement (supported by watch, there are command for that, but not tested)
+
+ Refine get activity data
+ - Fix get sleep data
+
+ Measurements
+ - Blood pressure measurement
+ - Show blood pressure measurement (view)
+ - Implement heart rate measurement //tried to implement with no luck
+ - Implement temperature measurement //tried to implement with no luck
+ - Implement UV index measurement //tried to implement with no luck
+
+
+
+WORK PROGRESS
+ Bump Gadgetbridge version to 0.39 (19.11.2019)
+
+ Send notification to watch
+ - On incoming call
+ - add function to cancel notification on watch (04.11.2019)
+ - cancel notification on change phone state (end call, reject call etc.) (06.11.2019)
+ - settings for repeat notification [0-10 times] (05.11.2019)
+ - settings for continious notification while phone ring [on, off] (06.11.2019)
+ - settings for send once notification for missed call [on, off] (06.11.2019)
+ * send missed call notification every minute for X times (17.11.2019)
+ - On text message, or other application
+ - On triger phone alarm (05.11.2019)
+
+ Call handling
+ - Setting for ignore/reject call with watch button [on->reject call, off->ignore call] (06.11.2019)
+ - Setting for ignore/reject call with shake device - duplicates button action [on, off] (06.11.2019)
+ - On watch - show small phone icon near bluetooth icon when there are missed call (06.11.2019)
+
+ Calibrations
+ - Time calibration
+ * send current date/time to watch
+ - Set watch alarms
+ - Altitude calibration [altitude (meters)] (04.11.2019)
+ * it's used in Climb activity
+ - Status of blood pressure calibration (06.11.2019)
+ * it's used in blood pressure measurement
+ - Blood pressure calibration (09.11.2019)
+
+ Device settings
+ - Lift wrist to screen on [on, off,TODO scheduled] (02.11.2019)
+ - Change time format 12/24h (02.11.2019)
+ - Disconnect reminder [on, off,TODO scheduled] (02.11.2019)
+ - Find my phone [on, off, ring duration] (02.11.2019)
+ - Set watch modes (energy saving) (10.11.2019) (Need testing)
+ - Normal -> the watch work normally
+ - Power-saving mode -> the app turn off the bluetooth on the watch
+ - Trad-watch mode -> the watch only works as an analog one
+ - Do not disturb [on, off, scheduled] (10.11.2019) (need reconnect to apply)
+ - Send User details to watch [height, weight, age, gender] (10.11.2019) (need more testing)
+ - Implemented long sit reminder (inactivity reminder)[on, off, period] (17.11.2019)
+ - Set watch language [English, Chinese] (17.11.2019)
+ - Set watch units (metric/imperial) (17.11.2019)
+ - Redesign Device Settings (19.11.2019)
+
+ Activity data
+ - get steps per day
+ - get heart rate measurements
+ - get sleep data
+ - set user goal for steps
+
+ Send weather
+ - Send weather icon (17.11.2019)
+ Changed in app device icon (02.11.2019)
+ Get blood pressure measurement result (work only if blood pressure is calibrated)
+ Pairing activity
\ No newline at end of file
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/watch9/Watch9DeviceCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/watch9/Watch9DeviceCoordinator.java
index 18b0a8844..08d8d1d04 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/watch9/Watch9DeviceCoordinator.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/watch9/Watch9DeviceCoordinator.java
@@ -63,7 +63,8 @@ public class Watch9DeviceCoordinator extends AbstractDeviceCoordinator {
String deviceName = candidate.getName().toUpperCase();
if (candidate.supportsService(Watch9Constants.UUID_SERVICE_WATCH9)) {
return DeviceType.WATCH9;
- } else if (macAddress.startsWith("1C:87:79")) {
+ // add support for Watch X non-plus (same MAC address)
+ } else if ((macAddress.startsWith("1C:87:79")) && (!deviceName.equalsIgnoreCase("WATCH X"))) {
return DeviceType.WATCH9;
} else if (deviceName.equals("WATCH 9")) {
return DeviceType.WATCH9;
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/NotificationListener.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/NotificationListener.java
index 97d123b03..f8232e0e2 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/NotificationListener.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/NotificationListener.java
@@ -71,6 +71,7 @@ import nodomain.freeyourgadget.gadgetbridge.entities.NotificationFilterEntry;
import nodomain.freeyourgadget.gadgetbridge.entities.NotificationFilterEntryDao;
import nodomain.freeyourgadget.gadgetbridge.model.AppNotificationType;
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
+import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec;
import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec;
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
@@ -255,9 +256,12 @@ public class NotificationListener extends NotificationListenerService {
return;
}
}
+
if (shouldIgnore(sbn)) {
- LOG.info("Ignore notification");
- return;
+ if (!"com.sec.android.app.clockpackage".equals(sbn.getPackageName())) { // workaround to allow phone alarm notification
+ LOG.info("Ignore notification: " + sbn.getPackageName()); // need to fix
+ return;
+ }
}
switch (GBApplication.getGrantedInterruptionFilter()) {
@@ -682,7 +686,6 @@ public class NotificationListener extends NotificationListenerService {
if (!isServiceRunning() || sbn == null) {
return true;
}
-
return shouldIgnoreSource(sbn.getPackageName()) || shouldIgnoreNotification(
sbn.getNotification(), sbn.getPackageName());
@@ -726,8 +729,9 @@ public class NotificationListener extends NotificationListenerService {
MediaSessionCompat.Token mediaSession = getMediaSession(notification);
//try to handle media session notifications
- if (mediaSession != null && handleMediaSessionNotification(mediaSession))
+ if (mediaSession != null && handleMediaSessionNotification(mediaSession)) {
return true;
+ }
NotificationType type = AppNotificationType.getInstance().get(source);
//ignore notifications marked as LocalOnly https://developer.android.com/reference/android/app/Notification.html#FLAG_LOCAL_ONLY
@@ -748,7 +752,6 @@ public class NotificationListener extends NotificationListenerService {
return true;
}
}
-
return (notification.flags & Notification.FLAG_ONGOING_EVENT) == Notification.FLAG_ONGOING_EVENT;
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java
index 82508f0e1..ea1569f63 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java
@@ -56,6 +56,8 @@ public enum DeviceType {
ZETIME(80, R.drawable.ic_device_zetime, R.drawable.ic_device_zetime_disabled, R.string.devicetype_mykronoz_zetime),
ID115(90, R.drawable.ic_device_h30_h10, R.drawable.ic_device_h30_h10_disabled, R.string.devicetype_id115),
WATCH9(100, R.drawable.ic_device_default, R.drawable.ic_device_default_disabled, R.string.devicetype_watch9),
+ WATCHX(101, R.drawable.ic_device_watchxplus, R.drawable.ic_device_watchxplus_disabled, R.string.devicetype_watchx),
+ WATCHXPLUS(102, R.drawable.ic_device_watchxplus, R.drawable.ic_device_watchxplus_disabled, R.string.devicetype_watchxplus),
ROIDMI(110, R.drawable.ic_device_roidmi, R.drawable.ic_device_roidmi_disabled, R.string.devicetype_roidmi),
ROIDMI3(112, R.drawable.ic_device_roidmi, R.drawable.ic_device_roidmi_disabled, R.string.devicetype_roidmi3),
CASIOGB6900(120, R.drawable.ic_device_default, R.drawable.ic_device_default_disabled, R.string.devicetype_casiogb6900),
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 d0519fdcd..45d3c9e4f 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceSupportFactory.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceSupportFactory.java
@@ -57,6 +57,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.QHybridSuppo
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.lenovo.watchxplus.WatchXPlusDeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.xwatch.XWatchSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.zetime.ZeTimeDeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
@@ -197,6 +198,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/lenovo/operations/InitOperation.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lenovo/operations/InitOperation.java
new file mode 100644
index 000000000..f5f366f5b
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lenovo/operations/InitOperation.java
@@ -0,0 +1,92 @@
+/* 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.lenovo.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.lenovo.watchxplus.WatchXPlusConstants;
+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.lenovo.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(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE);
+ private final BluetoothGattCharacteristic dbCharacteristic = getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_DATABASE_READ);
+
+ 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).notify(dbCharacteristic, true);
+ builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.AUTHENTICATING, getContext()));
+ getSupport().authorizationRequest(builder, needsAuth);
+ 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 X Plus", Toast.LENGTH_LONG, GB.ERROR, e);
+ }
+ return true;
+ } else {
+ LOG.info("Unhandled characteristic changed: " + characteristicUUID);
+ return super.onCharacteristicChanged(gatt, characteristic);
+ }
+ }
+
+
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lenovo/watchxplus/WatchXPlusDeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lenovo/watchxplus/WatchXPlusDeviceSupport.java
new file mode 100644
index 000000000..33d882e9c
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lenovo/watchxplus/WatchXPlusDeviceSupport.java
@@ -0,0 +1,2097 @@
+/* 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.lenovo.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.content.SharedPreferences;
+import android.net.Uri;
+import android.os.Handler;
+import android.widget.Toast;
+
+import androidx.annotation.IntRange;
+import androidx.localbroadcastmanager.content.LocalBroadcastManager;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.GregorianCalendar;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.TimeZone;
+import java.util.UUID;
+
+import nodomain.freeyourgadget.gadgetbridge.GBApplication;
+import nodomain.freeyourgadget.gadgetbridge.R;
+import nodomain.freeyourgadget.gadgetbridge.activities.SettingsActivity;
+import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst;
+import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
+import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
+import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo;
+import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventCallControl;
+import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventVersionInfo;
+import nodomain.freeyourgadget.gadgetbridge.devices.lenovo.DataType;
+import nodomain.freeyourgadget.gadgetbridge.devices.lenovo.watchxplus.WatchXPlusConstants;
+import nodomain.freeyourgadget.gadgetbridge.devices.lenovo.watchxplus.WatchXPlusDeviceCoordinator;
+import nodomain.freeyourgadget.gadgetbridge.devices.lenovo.watchxplus.WatchXPlusSampleProvider;
+import nodomain.freeyourgadget.gadgetbridge.entities.WatchXPlusActivitySample;
+import nodomain.freeyourgadget.gadgetbridge.entities.WatchXPlusHealthActivityOverlay;
+import nodomain.freeyourgadget.gadgetbridge.entities.WatchXPlusHealthActivityOverlayDao;
+import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventFindPhone;
+import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
+import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
+import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
+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.lenovo.operations.InitOperation;
+import nodomain.freeyourgadget.gadgetbridge.util.AlarmUtils;
+import nodomain.freeyourgadget.gadgetbridge.util.ArrayUtils;
+import nodomain.freeyourgadget.gadgetbridge.util.GB;
+import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
+import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
+
+
+public class WatchXPlusDeviceSupport extends AbstractBTLEDeviceSupport {
+ private static Prefs prefs = GBApplication.getPrefs();
+
+ private boolean needsAuth;
+ private int sequenceNumber = 0;
+ private boolean isCalibrationActive = false;
+
+ private final Map dataToFetch = new LinkedHashMap<>();
+ private int requestedDataTimestamp;
+ private int dataSlots = 0;
+ private DataType currentDataType;
+
+ 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();
+ assert broadcastAction != null;
+ 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, 14) + "\0";
+ if (notificationSpec.subject != null) {
+ message += StringUtils.truncate(notificationSpec.subject, 20) + ": ";
+ }
+ if (notificationSpec.body != null) {
+ message += StringUtils.truncate(notificationSpec.body, 64);
+ }
+
+ sendNotification(WatchXPlusConstants.NOTIFICATION_CHANNEL_DEFAULT, message);
+ }
+
+ /** Cancel notification
+ * cancel watch notification - stop vibration and turn off screen
+ * on watch - clear phone icon near bluetooth
+ */
+ private void cancelNotification() {
+ try {
+ getQueue().clear();
+ TransactionBuilder builder = performInitialized("cancelNotification");
+ byte[] bArr;
+ int mPosition = 1024; // all positions
+ int mMessageId = 0xFF; // all messages
+ bArr = new byte[6];
+ bArr[0] = (byte) ((int) (mPosition >> 24));
+ bArr[1] = (byte) ((int) (mPosition >> 16));
+ bArr[2] = (byte) ((int) (mPosition >> 8));
+ bArr[3] = (byte) ((int) mPosition);
+ bArr[4] = (byte) (mMessageId >> 8);
+ bArr[5] = (byte) mMessageId;
+ builder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE),
+ buildCommand(WatchXPlusConstants.CMD_NOTIFICATION_CANCEL,
+ WatchXPlusConstants.WRITE_VALUE,
+ bArr));
+ builder.queue(getQueue());
+ } catch (IOException e) {
+ LOG.warn("Unable to cancel notification", e);
+ }
+ }
+
+ /** Format text and send it to watch
+ * @param notificationChannel - text or call
+ * @param notificationText - text to show
+ */
+ private void sendNotification(int notificationChannel, String notificationText) {
+ try {
+ TransactionBuilder builder = performInitialized("showNotification");
+ byte[] command = WatchXPlusConstants.CMD_NOTIFICATION_TEXT_TASK;
+ byte[] text = notificationText.getBytes(StandardCharsets.UTF_8);
+ byte[] messagePart;
+
+ int messageLength = text.length;
+ int parts = messageLength / 9;
+ int remainder = messageLength % 9;
+
+// Increment parts quantity if message length is not multiple of 9
+ if (remainder != 0) {
+ parts++;
+ }
+ for (int messageIndex = 0; messageIndex < parts; messageIndex++) {
+ if (messageIndex + 1 != parts || remainder == 0) {
+ messagePart = new byte[11];
+ } else {
+ messagePart = new byte[remainder + 2];
+ }
+
+ System.arraycopy(text, messageIndex * 9, messagePart, 2, messagePart.length - 2);
+
+ if (messageIndex + 1 == parts) {
+ messageIndex = 0xFF;
+ }
+ messagePart[0] = (byte) notificationChannel;
+ messagePart[1] = (byte) messageIndex;
+ builder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE),
+ buildCommand(command,
+ WatchXPlusConstants.KEEP_ALIVE,
+ messagePart));
+ }
+ builder.queue(getQueue());
+ } catch (IOException e) {
+ LOG.warn("Unable to send notification", e);
+ }
+ }
+
+ /** enable notification channels on watch
+ * @param builder
+ * enable all notification channels
+ * TODO add settings to choose notification channels
+ */
+ 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 void 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
+
+ }
+
+ 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));
+ builder.queue(getQueue());
+ } 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));
+ builder.queue(getQueue());
+ } catch (IOException e) {
+ LOG.warn("Unable to set time", e);
+ }
+ }
+
+ /** send command to request watch firmware version
+ * @param builder - transaction builder
+ */
+ private WatchXPlusDeviceSupport getFirmwareVersion(TransactionBuilder builder) {
+ builder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE),
+ buildCommand(WatchXPlusConstants.CMD_FIRMWARE_INFO,
+ WatchXPlusConstants.READ_VALUE));
+
+ return this;
+ }
+
+ /** send command to request watch battery state
+ * @param builder - transaction builder
+ */
+ private WatchXPlusDeviceSupport getBatteryState(TransactionBuilder builder) {
+ builder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE),
+ buildCommand(WatchXPlusConstants.CMD_BATTERY_INFO,
+ WatchXPlusConstants.READ_VALUE));
+
+ return this;
+ }
+
+ /** initialize device on connect
+ * @param builder - transaction builder
+ */
+ public WatchXPlusDeviceSupport initialize(TransactionBuilder builder) {
+ getFirmwareVersion(builder)
+ .getBatteryState(builder)
+ .enableNotificationChannels(builder)
+ .setFitnessGoal(builder) // set steps per day
+ .getBloodPressureCalibrationStatus(builder) // request blood pressure calibration
+ //.setUnitsSettings() // set metric/imperial units
+ .syncPreferences(builder); // read preferences from app and set them to watch
+ builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZED, getContext()));
+ builder.setGattCallback(this);
+ return this;
+ }
+
+ @Override
+ public void onDeleteNotification(int id) {
+ isMissedCall = false;
+ cancelNotification();
+ }
+
+ @Override
+ public void onSetTime() {
+ getTime();
+ }
+
+ @Override
+ public void onSetAlarms(ArrayList extends Alarm> 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));
+ }
+ }
+
+ private boolean isRinging = false; // store ringing state
+ private boolean outCall = false; // store outgoing call state
+ private boolean isMissedCall = false; // missed call state
+ private int remainingRepeats = 0; // initialize call notification reminds
+ private int remainingMissedRepeats = 0; // initialize missed call notification reminds
+
+ /** send notification on watch when phone rings
+ * @param callSpec - phone state
+ * send notification on incoming call, cancel notification when call is answered, ignored or rejected
+ * send missed call notification (if enabled from settings) when phone state changed from ringing to end call
+ * TODO add missed call reminder (send notification to watch at desired period)
+ */
+ // variables to handle ring notifications
+
+ @Override
+ public void onSetCallState(final CallSpec callSpec) {
+ final int repeatDelay = 5000; // repeat delay of 5 sec (watch show call notifications for about 5 sec.)
+ final int repeatMissedDelay = 60000; // repeat missed call delay of 60 sec
+ // get settings for continuous vibration while phone rings
+ final boolean continuousRing = WatchXPlusDeviceCoordinator.getContiniousVibrationOnCall(getDevice().getAddress());
+ // set settings for missed call
+ boolean missedCall = WatchXPlusDeviceCoordinator.getMissedCallReminder(getDevice().getAddress());
+ int repeatCount = WatchXPlusDeviceCoordinator.getRepeatOnCall(getDevice().getAddress());
+ int repeatCountMissed = WatchXPlusDeviceCoordinator.getMissedCallRepeat(getDevice().getAddress());
+ // check if repeatCount is in boundaries min=0, max=10
+ if (repeatCount < 0) repeatCount = 0;
+ if (repeatCount > 10) repeatCount = 10; // limit repeats to 10
+ // check if repeatCountMissed is in boundaries min=0, max=10
+ if (repeatCountMissed < 0) repeatCountMissed = 0;
+ if (repeatCountMissed > 10) repeatCountMissed = 10; // limit repeats to 10
+
+ switch (callSpec.command) {
+ case CallSpec.CALL_INCOMING:
+ isRinging = true;
+ isMissedCall = false;
+ remainingRepeats = repeatCount;
+ LOG.info(" Incomming call ");
+ if (("Phone".equals(callSpec.name)) || (callSpec.name.contains("ropusn")) || (callSpec.name.contains("issed"))) {
+ // do nothing for notifications without caller name, e.g. system call event
+ } else {
+ // send first notification
+ sendNotification(WatchXPlusConstants.NOTIFICATION_CHANNEL_PHONE_CALL, callSpec.name);
+ // init repeat handler
+ final Handler handler = new Handler();
+ handler.postDelayed(new Runnable() {
+ public void run() {
+ // Actions to do after repeatDelay seconds
+ if (((isRinging) && (remainingRepeats > 0)) || ((isRinging) && (continuousRing))) {
+ remainingRepeats = remainingRepeats - 1;
+ sendNotification(WatchXPlusConstants.NOTIFICATION_CHANNEL_PHONE_CALL, callSpec.name);
+ // re-run handler
+ handler.postDelayed(this, repeatDelay);
+ } else {
+ remainingRepeats = 0;
+ // stop handler
+ handler.removeCallbacks(this);
+ cancelNotification();
+ }
+ }
+ }, repeatDelay);
+ }
+ break;
+ case CallSpec.CALL_START:
+ isRinging = false;
+ outCall = false;
+ isMissedCall = false;
+ cancelNotification();
+ LOG.info(" Call start ");
+ break;
+ case CallSpec.CALL_REJECT:
+ isRinging = false;
+ outCall = false;
+ isMissedCall = false;
+ cancelNotification();
+ LOG.info(" Call reject ");
+ break;
+ case CallSpec.CALL_ACCEPT:
+ isRinging = false;
+ outCall = false;
+ isMissedCall = false;
+ cancelNotification();
+ LOG.info(" Call accept ");
+ break;
+ case CallSpec.CALL_OUTGOING:
+ outCall = true;
+ isRinging = false;
+ isMissedCall = false;
+ cancelNotification();
+ LOG.info(" Outgoing call ");
+ break;
+ case CallSpec.CALL_END:
+ if ((isRinging) && (!outCall)) {
+ LOG.info(" End call ");
+ // it's a missed call, don't clear notification to preserve small icon near bluetooth
+ isRinging = false;
+ outCall = false;
+ isMissedCall = true;
+ remainingMissedRepeats = repeatCountMissed;
+ // send missed call notification if enabled in settings
+ if (missedCall) {
+ LOG.info(" Missed call ");
+ sendNotification(WatchXPlusConstants.NOTIFICATION_CHANNEL_PHONE_CALL, "Missed call");
+ // repeat missed call notification
+ final Handler handler = new Handler();
+ handler.postDelayed(new Runnable() {
+ public void run() {
+ // Actions to do after repeatDelay seconds
+ if ((isMissedCall) && (remainingMissedRepeats > 0)) {
+ remainingMissedRepeats = remainingMissedRepeats - 1;
+ sendNotification(WatchXPlusConstants.NOTIFICATION_CHANNEL_PHONE_CALL, "Missed call");
+ // re-run handler
+ handler.postDelayed(this, repeatMissedDelay);
+ } else {
+ remainingMissedRepeats = 0;
+ isMissedCall = false;
+ // stop handler
+ handler.removeCallbacks(this);
+ cancelNotification();
+ }
+ }
+ }, repeatMissedDelay);
+ }
+ } else {
+ isRinging = false;
+ outCall = false;
+ isMissedCall = false;
+ cancelNotification();
+ LOG.info(" Outgoing call end ");
+ }
+ break;
+ default:
+ isRinging = false;
+ isMissedCall = false;
+ cancelNotification();
+ LOG.info(" Call default ");
+ break;
+ }
+ }
+
+ /** handle button press while ringing
+ * @param value - reply from watch
+ * while phone rings choose what to do when watch button is pressed
+ */
+ private void handleButtonWhenRing(byte[] value) {
+ GBDeviceEventCallControl callCmd = new GBDeviceEventCallControl();
+ // get saved settings if true - reject call, otherwise ignore call
+ boolean buttonReject = WatchXPlusDeviceCoordinator.getButtonReject(getDevice().getAddress());
+ if (buttonReject) {
+ LOG.info(" call rejected ");
+ isRinging = false;
+ remainingRepeats = 0;
+ isMissedCall = false;
+ callCmd.event = GBDeviceEventCallControl.Event.REJECT;
+ evaluateGBDeviceEvent(callCmd);
+ cancelNotification();
+ } else {
+ LOG.info(" call ignored ");
+ isRinging = false;
+ remainingRepeats = 0;
+ isMissedCall = false;
+ callCmd.event = GBDeviceEventCallControl.Event.IGNORE;
+ evaluateGBDeviceEvent(callCmd);
+ cancelNotification();
+ }
+ }
+
+ 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;
+ }
+
+ /** set personal info - read it from About me
+ * @param builder - transaction builder
+ * @param height - user height in meters
+ * @param weight - user weight in kg
+ * @param age - user age
+ * @param gender - user age
+ */
+ private void setPersonalInformation(TransactionBuilder builder, int height, int weight, int age, int gender) {
+ LOG.warn(" Setting Personal Information... height:"+height+" weight:"+weight+" age:"+age+" gender:"+gender);
+ byte[] command = WatchXPlusConstants.CMD_SET_PERSONAL_INFO;
+
+ byte[] bArr = new byte[4];
+ bArr[0] = (byte) height; // byte[08]
+ bArr[1] = (byte) weight; // byte[09]
+ bArr[2] = (byte) age; // byte[10]
+ bArr[3] = (byte) gender; // byte[11]
+
+ builder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE),
+ buildCommand(command,
+ WatchXPlusConstants.WRITE_VALUE,
+ bArr));
+ }
+
+ /** handle get/set personal info
+ * @param value - reply from watch
+ * actual do nothing (for test purposes only)
+ */
+ private void handlePersonalInfo(byte[] value) {
+ int height = Conversion.fromByteArr16(value[8]);
+ int weight = Conversion.fromByteArr16(value[9]);
+ int age = Conversion.fromByteArr16(value[10]);
+ int gender = Conversion.fromByteArr16(value[11]);
+ LOG.info(" Personal info - height:" + height + ", weight:" + weight + ", age:" + age + ", gender:" + gender);
+ }
+
+ @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) {
+
+ TransactionBuilder builder;
+ try {
+ builder = performInitialized("fetchData");
+
+ builder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE),
+ buildCommand(WatchXPlusConstants.CMD_DAY_STEPS_INFO,
+ WatchXPlusConstants.READ_VALUE));
+
+// Fetch heart rate data samples count
+ requestDataCount(DataType.HEART_RATE);
+
+ builder.queue(getQueue());
+ } catch (IOException e) {
+ LOG.warn("Unable to retrieve recorded data", e);
+ }
+ }
+
+ @Override
+ public void onReset(int flags) {
+ // testNewCommands();
+ }
+
+ @Override
+ public void onHeartRateTest() {
+ //requestHeartRateMeasurement();
+ }
+
+ @Override
+ public void onEnableRealtimeHeartRateMeasurement(boolean enable) {
+
+ }
+
+ @Override
+ public void onFindDevice(boolean start) {
+ }
+
+ @Override
+ public void onSetConstantVibration(int integer) {
+
+ }
+
+ @Override
+ public void onScreenshotReq() {
+ sendBloodPressureCalibration();
+ }
+
+ @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;
+ SharedPreferences sharedPreferences = GBApplication.getDeviceSpecificSharedPrefs(this.getDevice().getAddress());
+ try {
+ builder = performInitialized("sendConfig: " + config);
+ switch (config) {
+ // settings from App Settings
+ case SettingsActivity.PREF_MEASUREMENT_SYSTEM:
+ setUnitsSettings();
+ break;
+ case ActivityUser.PREF_USER_STEPS_GOAL:
+ setFitnessGoal(builder);
+ break;
+ // settings from App Settings -> WatchXPlus settings
+ case WatchXPlusConstants.PREF_POWER_MODE:
+ setPowerMode();
+ break;
+ case WatchXPlusConstants.PREF_WXP_LANGUAGE:
+ setLanguageAndTimeFormat(builder, sharedPreferences);
+ break;
+ case WatchXPlusConstants.PREF_LONGSIT_PERIOD:
+ case WatchXPlusConstants.PREF_LONGSIT_SWITCH:
+ setLongSitHours(builder, sharedPreferences);
+ break;
+ // calibrations
+ case WatchXPlusConstants.PREF_ALTITUDE:
+ setAltitude(builder);
+ break;
+ case WatchXPlusConstants.PREF_BP_CAL_SWITCH:
+ sendBloodPressureCalibration();
+ break;
+ // settings from device card
+ case WatchXPlusConstants.PREF_ACTIVATE_DISPLAY:
+ setHeadsUpScreen(builder, sharedPreferences);
+ getShakeStatus(builder);
+ break;
+ case WatchXPlusConstants.PREF_DISCONNECT_REMIND:
+ setDisconnectReminder(builder, sharedPreferences);
+ getDisconnectReminderStatus(builder);
+ break;
+ case DeviceSettingsPreferenceConst.PREF_TIMEFORMAT:
+ setLanguageAndTimeFormat(builder, sharedPreferences);
+ break;
+ case WatchXPlusConstants.PREF_DO_NOT_DISTURB:
+ case WatchXPlusConstants.PREF_DO_NOT_DISTURB_START:
+ case WatchXPlusConstants.PREF_DO_NOT_DISTURB_END:
+ setQuiteHours(builder, sharedPreferences);
+ break;
+ }
+ builder.queue(getQueue());
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ @Override
+ public void onReadConfiguration(String config) {
+
+ }
+
+ @Override
+ public void onTestNewFunction() {
+ requestBloodPressureMeasurement();
+ }
+
+
+ /** set long sit reminder time
+ * @param builder - transaction builder
+ * @param enable - state (true - enabled or false - disabled)
+ * @param hourStart - begin hour
+ * @param minuteStart - begin minute
+ * @param hourEnd - end hour
+ * @param minuteEnd - end minute
+ * set long sit reminder (inactivity reminder) on watch
+ */
+ private WatchXPlusDeviceSupport setLongSitHours(TransactionBuilder builder, boolean enable, int hourStart, int minuteStart, int hourEnd, int minuteEnd, int period) {
+ LOG.warn(" Setting Long sit reminder... Enabled:"+enable+" Period:"+period);
+ LOG.warn(" Setting Long sit time... Hs:"+hourEnd+" Ms:"+minuteEnd+" He:"+hourStart+" Me:"+minuteStart);
+ LOG.warn(" Setting Long sit DND time... Hs:"+hourStart+" Ms:"+minuteStart+" He:"+hourEnd+" Me:"+minuteEnd);
+ // set Long Sit reminder time
+ byte[] command = WatchXPlusConstants.CMD_INACTIVITY_REMINDER_SET;
+
+ byte[] bArr = new byte[10];
+ bArr[0] = (byte) hourEnd; // byte[08]
+ bArr[1] = (byte) minuteEnd; // byte[09]
+ bArr[2] = (byte) hourStart; // byte[10]
+ bArr[3] = (byte) minuteStart; // byte[11]
+ bArr[4] = (byte) hourStart; // byte[12]
+ bArr[5] = (byte) minuteStart; // byte[13]
+ bArr[6] = (byte) hourEnd; // byte[14]
+ bArr[7] = (byte) minuteEnd; // byte[15]
+ bArr[8] = (byte) (period >> 8); // byte[16]
+ bArr[9] = (byte) period; // byte[17]
+
+ builder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE),
+ buildCommand(command, WatchXPlusConstants.WRITE_VALUE, bArr));
+ // set long sit reminder state (enabled, disabled)
+ setLongSitSwitch(builder, enable);
+ return this;
+ }
+
+ /** get Long sit settings from app, and send it to watch
+ * @param builder - transaction builder
+ * @param sharedPreferences - shared preferences
+ */
+ private void setLongSitHours(TransactionBuilder builder, SharedPreferences sharedPreferences) {
+ Calendar start = new GregorianCalendar();
+ Calendar end = new GregorianCalendar();
+ boolean enable = WatchXPlusDeviceCoordinator.getLongSitHours(sharedPreferences, start, end);
+ if (enable) {
+ int period = prefs.getInt(WatchXPlusConstants.PREF_LONGSIT_PERIOD, 60);
+ this.setLongSitHours(builder, enable,
+ start.get(Calendar.HOUR_OF_DAY), start.get(Calendar.MINUTE),
+ end.get(Calendar.HOUR_OF_DAY), end.get(Calendar.MINUTE),
+ period);
+ } else {
+ // disable Long sit reminder
+ LOG.info(" Long sit reminder are disabled");
+ this.setLongSitSwitch(builder, enable);
+ }
+ }
+
+ /** set long sit reminder switch
+ * @param tbuilder - transaction builder
+ * @param enable - true or false
+ * enabled or disables long sit reminder (inactivity reminder) on watch
+ */
+ private WatchXPlusDeviceSupport setLongSitSwitch(TransactionBuilder tbuilder, boolean enable) {
+ LOG.warn("Setting Long sit reminder switch to" + enable);
+ tbuilder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE),
+ buildCommand(WatchXPlusConstants.CMD_INACTIVITY_REMINDER_SWITCH,
+ WatchXPlusConstants.WRITE_VALUE,
+ new byte[]{(byte) (enable ? 0x01 : 0x00)}));
+ return this;
+ }
+
+
+
+ /** set do not disturb time
+ * @param builder - transaction builder
+ * @param enable - state (true - enabled or false - disabled)
+ * @param hourStart - begin hour
+ * @param minuteStart - begin minute
+ * @param hourEnd - end hour
+ * @param minuteEnd - end minute
+ * set do not disturb on watch
+ */
+ private WatchXPlusDeviceSupport setQuiteHours(TransactionBuilder builder, boolean enable, int hourStart, int minuteStart, int hourEnd, int minuteEnd) {
+ LOG.warn(" Setting DND time... Hs:"+hourStart+" Ms:"+minuteStart+" He:"+hourEnd+" Me:"+minuteEnd);
+ // set DND time
+ byte[] command = WatchXPlusConstants.CMD_SET_QUITE_HOURS_TIME;
+
+ byte[] bArr = new byte[4];
+ bArr[0] = (byte) hourStart; // byte[08]
+ bArr[1] = (byte) minuteStart; // byte[09]
+ bArr[2] = (byte) hourEnd; // byte[10]
+ bArr[3] = (byte) minuteEnd; // byte[11]
+ builder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE),
+ buildCommand(command, WatchXPlusConstants.WRITE_VALUE, bArr));
+ // set DND state (enabled, disabled)
+ setQuiteHoursSwitch(builder, enable);
+ return this;
+ }
+
+ /** set do not disturb switch
+ * @param tbuilder - transaction builder
+ * @param enable - true or false
+ * enabled or disables DND on watch
+ */
+ private WatchXPlusDeviceSupport setQuiteHoursSwitch(TransactionBuilder tbuilder, boolean enable) {
+ LOG.warn("Setting DND switch to" + enable);
+ tbuilder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE),
+ buildCommand(WatchXPlusConstants.CMD_SET_QUITE_HOURS_SWITCH,
+ WatchXPlusConstants.WRITE_VALUE,
+ new byte[]{(byte) (enable ? 0x01 : 0x00)}));
+ return this;
+ }
+
+ /** get DND settings from app, and send it to watch
+ * @param builder - transaction builder
+ * @param sharedPreferences - shared preferences
+ */
+ private void setQuiteHours(TransactionBuilder builder, SharedPreferences sharedPreferences) {
+ Calendar start = new GregorianCalendar();
+ Calendar end = new GregorianCalendar();
+ boolean enable = WatchXPlusDeviceCoordinator.getQuiteHours(sharedPreferences, start, end);
+ if (enable) {
+ this.setQuiteHours(builder, enable,
+ start.get(Calendar.HOUR_OF_DAY), start.get(Calendar.MINUTE),
+ end.get(Calendar.HOUR_OF_DAY), end.get(Calendar.MINUTE));
+ } else {
+ // disable DND
+ LOG.info(" Quiet hours are disabled");
+ this.setQuiteHoursSwitch(builder, enable);
+ }
+ }
+
+ /** set watch power
+ * switch watch power mode
+ * modes (0- normal, 1- energysaving, 2- only watch)
+ */
+ private void setPowerMode() {
+ int settingRead = prefs.getInt(WatchXPlusConstants.PREF_POWER_MODE, 0);
+ byte[] bArr = new byte[1];
+ bArr[0] = (byte) settingRead;
+ LOG.info(" setting power mode to: " + settingRead);
+ try {
+ TransactionBuilder builder = performInitialized("setPowerMode");
+ builder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE),
+ buildCommand(WatchXPlusConstants.CMD_POWER_MODE,
+ WatchXPlusConstants.TASK,
+ bArr));
+ builder.queue(getQueue());
+ } catch (IOException e) {
+ LOG.warn("Unable to set power mode", e);
+ }
+ }
+
+ /** request watch units
+ * for testing purposes only
+ */
+ private WatchXPlusDeviceSupport getUnitsSettings() {
+ LOG.info(" Get units from watch... ");
+ try {
+ TransactionBuilder builder = performInitialized("getUnits");
+ builder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE),
+ buildCommand(WatchXPlusConstants.CMD_SET_UNITS,
+ WatchXPlusConstants.READ_VALUE));
+ builder.queue(getQueue());
+ } catch (IOException e) {
+ LOG.warn("Unable to get units", e);
+ }
+ return this;
+ }
+
+ /** set watch units
+ *
+ */
+ private void setUnitsSettings() {
+ int units = 0;
+ if (getContext().getString(R.string.p_unit_metric).equals(units)) {
+ LOG.info(" Changed units: metric");
+ } else {
+ LOG.info(" Changed units: imperial");
+ }
+ byte[] bArr = new byte[3];
+ bArr[0] = (byte) units; // metric - 0/imperial - 1
+ bArr[1] = (byte) 0x00; //time unit 12/24h (there are separate command for this)
+ bArr[2] = (byte) 0x00; // temperature unit (do nothing)
+ try {
+ TransactionBuilder builder = performInitialized("setUnits");
+ builder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE),
+ buildCommand(WatchXPlusConstants.CMD_SET_UNITS,
+ WatchXPlusConstants.WRITE_VALUE,
+ bArr));
+ builder.queue(getQueue());
+ } catch (IOException e) {
+ LOG.warn("Unable to set units", e);
+ }
+ }
+
+ /** request status of blood pressure calibration
+ * @param builder - transaction builder
+ */
+ private WatchXPlusDeviceSupport getBloodPressureCalibrationStatus(TransactionBuilder builder) {
+ builder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE),
+ buildCommand(WatchXPlusConstants.CMD_IS_BP_CALIBRATED,
+ WatchXPlusConstants.READ_VALUE));
+
+ return this;
+ }
+
+ /** send blood pressure calibration to watch
+ * TODO add better error handling if blood pressure calibration is failed
+ */
+ private void sendBloodPressureCalibration() {
+ try {
+ int beginCalibration = prefs.getInt(WatchXPlusConstants.PREF_BP_CAL_SWITCH, 0);
+ if (beginCalibration == 1) {
+ LOG.warn(" Calibrating BP - cancel " + beginCalibration);
+ return;
+ }
+ int mLowP = prefs.getInt(WatchXPlusConstants.PREF_BP_CAL_LOW, 80);
+ int mHighP = prefs.getInt(WatchXPlusConstants.PREF_BP_CAL_HIGH, 130);
+ LOG.warn(" Calibrating BP ... LowP=" + mLowP + " HighP="+mHighP);
+ GB.toast("Calibrating BP...", Toast.LENGTH_LONG, GB.INFO);
+
+ TransactionBuilder builder = performInitialized("bpCalibrate");
+
+ byte[] command = WatchXPlusConstants.CMD_BP_CALIBRATION;
+ byte mStart = 0x01; // initiate calibration
+
+ byte[] bArr = new byte[5];
+ bArr[0] = mStart; // byte[08]
+ bArr[1] = (byte) (mHighP >> 8); // byte[09]
+ bArr[2] = (byte) mHighP; // byte[10]
+ bArr[3] = (byte) (mLowP >> 8); // byte[11]
+ bArr[4] = (byte) mLowP; // byte[12]
+
+ builder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE),
+ buildCommand(command,
+ WatchXPlusConstants.TASK,
+ bArr));
+ builder.queue(getQueue());
+ } catch (IOException e) {
+ LOG.warn("Unable to send BP Calibration", e);
+ }
+ }
+
+ /** handle watch response if blood pressure is calibrated
+ * @param value - watch response
+ * save result to global variable (uses for BP measurement)
+ */
+ private void handleBloodPressureCalibrationStatus(byte[] value) {
+ WatchXPlusDeviceCoordinator.isBPCalibrated = Conversion.fromByteArr16(value[8]) == 0;
+ }
+
+ /** handle watch response for result of blood pressure calibration
+ * @param value - watch response
+ */
+ private void handleBloodPressureCalibrationResult(byte[] value) {
+ if (Conversion.fromByteArr16(value[8]) != 0x00) {
+ WatchXPlusDeviceCoordinator.isBPCalibrated = false;
+ GB.toast("Calibrating BP fail", Toast.LENGTH_LONG, GB.ERROR);
+ } else {
+ WatchXPlusDeviceCoordinator.isBPCalibrated = true;
+ int high = Conversion.fromByteArr16(value[9], value[10]);
+ int low = Conversion.fromByteArr16(value[11], value[12]);
+ GB.toast("OK. Measured Low:"+low+" high:"+high, Toast.LENGTH_LONG, GB.INFO);
+ }
+ }
+
+ /** request blood pressure measurement
+ * first check if blood pressure is calibrated
+ */
+ private void requestBloodPressureMeasurement() {
+ if (!WatchXPlusDeviceCoordinator.isBPCalibrated) {
+ LOG.warn("BP is NOT calibrated");
+ GB.toast("BP is not calibrated", Toast.LENGTH_LONG, GB.WARN);
+ return;
+ }
+ try {
+ TransactionBuilder builder = performInitialized("bpMeasure");
+
+ byte[] command = WatchXPlusConstants.CMD_BLOOD_PRESSURE_MEASURE;
+
+ builder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE),
+ buildCommand(command,
+ WatchXPlusConstants.TASK, new byte[]{0x01}));
+ builder.queue(getQueue());
+ } catch (IOException e) {
+ LOG.warn("Unable to request BP Measure", e);
+ }
+ }
+
+
+
+ // not working!!!
+ private void testNewCommands() {
+ try {
+ TransactionBuilder builder = performInitialized("test");
+
+ int first = prefs.getInt("wxp_newcmd_first", 0);
+ int second = prefs.getInt("wxp_newcmd_second", 0);
+ byte[] command = new byte[]{(byte) first, (byte) second};
+
+ LOG.info("testing new command " + Arrays.toString(command));
+ builder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE),
+ buildCommand(command,
+ WatchXPlusConstants.READ_VALUE));
+
+ builder.queue(getQueue());
+ } catch (IOException e) {
+ LOG.warn("Unable to request HR Measure", e);
+ }
+ }
+
+
+ @Override
+ public void onSendWeather(WeatherSpec weatherSpec) {
+ try {
+ TransactionBuilder builder = performInitialized("setWeather");
+ int currentTemp;
+ int todayMinTemp;
+ int todayMaxTemp;
+ byte[] command = WatchXPlusConstants.CMD_WEATHER_SET;
+ byte[] weatherInfo = new byte[5];
+ int currentCondition = weatherSpec.currentConditionCode;
+// set weather icon
+ int currentConditionCode = 0; // 0 is sunny
+ switch (currentCondition) {
+//Group 2xx: Thunderstorm
+ case 200: //thunderstorm with light rain: //11d
+ case 201: //thunderstorm with rain: //11d
+ case 202: //thunderstorm with heavy rain: //11d
+ currentConditionCode = 1024;
+ break;
+ case 210: //light thunderstorm:: //11d
+ case 211: //thunderstorm: //11d
+ case 212: //heavy thunderstorm: //11d
+ case 221: //ragged thunderstorm: //11d
+ case 230: //thunderstorm with light drizzle: //11d
+ case 231: //thunderstorm with drizzle: //11d
+ case 232: //thunderstorm with heavy drizzle: //11d
+ currentConditionCode = 1025;
+ break;
+//Group 3xx: Drizzle
+ case 300: //light intensity drizzle: //09d
+ case 301: //drizzle: //09d
+ case 302: //heavy intensity drizzle: //09d
+ case 310: //light intensity drizzle rain: //09d
+ case 500: //light rain: //10d
+ currentConditionCode = 256;
+ break;
+ case 311: //drizzle rain: //09d
+ case 312: //heavy intensity drizzle rain: //09d
+ case 313: //shower rain and drizzle: //09d
+ case 314: //heavy shower rain and drizzle: //09d
+ case 321: //shower drizzle: //09d
+ case 501: //moderate rain: //10d
+ currentConditionCode = 1280;
+ break;
+//Group 5xx: Rain
+ case 511: //freezing rain: //13d
+ case 520: //light intensity shower rain: //09d
+ case 521: //shower rain: //09d
+ case 502: //heavy intensity rain: //10d
+ case 503: //very heavy rain: //10d
+ case 504: //extreme rain: //10d
+ case 522: //heavy intensity shower rain: //09d
+ case 531: //ragged shower rain: //09d
+ currentConditionCode = 258;
+ break;
+//Group 6xx: Snow
+ case 600: //light snow:
+ case 601: //snow: //[[file:13d.png]]
+ currentConditionCode = 513;
+ break;
+ case 620: //light shower snow: //[[file:13d.png]]
+ currentConditionCode = 514;
+ break;
+ case 602: //heavy snow: //[[file:13d.png]]
+ case 621: //shower snow: //[[file:13d.png]]
+ case 622: //heavy shower snow: //[[file:13d.png]]
+ currentConditionCode = 515;
+ break;
+ case 611: //sleet: //[[file:13d.png]]
+ case 612: //shower sleet: //[[file:13d.png]]
+ currentConditionCode = 1026;
+ break;
+ case 615: //light rain and snow: //[[file:13d.png]]
+ case 616: //rain and snow: //[[file:13d.png]]
+ currentConditionCode = 4;
+ break;
+//Group 7xx: Atmosphere
+ case 741: //fog: //[[file:50d.png]]
+ case 701: //mist: //[[file:50d.png]]
+ case 711: //smoke: //[[file:50d.png]]
+ currentConditionCode = 5;
+ break;
+ case 721: //haze: //[[file:50d.png]]
+ currentConditionCode = 3;
+ break;
+ case 731: //sandcase dust whirls: //[[file:50d.png]]
+ currentConditionCode = 771;
+ break;
+ case 751: //sand: //[[file:50d.png]]
+ case 761: //dust: //[[file:50d.png]]
+ case 762: //volcanic ash: //[[file:50d.png]]
+ case 771: //squalls: //[[file:50d.png]]
+ currentConditionCode = 769;
+ break;
+ case 781: //tornado: //[[file:50d.png]]
+ case 900: //tornado
+ currentConditionCode = 1283;
+ break;
+//Group 800: Clear
+ case 800: //clear sky
+ currentConditionCode = 0;
+ break;
+//Group 80x: Clouds
+ case 801: //few clouds: //[[file:02d.png]] [[file:02n.png]]
+ case 802: //scattered clouds: //[[file:03d.png]] [[file:03d.png]]
+ case 803: //broken clouds: //[[file:04d.png]] [[file:03d.png]]
+ currentConditionCode = 1;
+ break;
+ case 804: //overcast clouds: //[[file:04d.png]] [[file:04d.png]]
+ currentConditionCode = 2;
+ break;
+//Group 90x: Extreme
+ case 901: //tropical storm
+ case 903: //cold
+ case 904: //hot
+ case 905: //windy
+ case 906: //hail
+ currentConditionCode = 1027;
+ break;
+//Group 9xx: Additional
+ case 951: //calm
+ case 952: //light breeze
+ case 953: //gentle breeze
+ case 954: //moderate breeze
+ case 955: //fresh breeze
+ case 956: //strong breeze
+ case 957: //high windcase near gale
+ case 958: //gale
+ case 959: //severe gale
+ case 960: //storm
+ case 961: //violent storm
+ case 902: //hurricane
+ case 962: //hurricane
+ currentConditionCode = 261;
+ break;
+ }
+ LOG.info( "Weather cond: " + currentCondition + " icon: " + currentConditionCode);
+// calculate for temps under 0
+ currentTemp = (Math.abs(weatherSpec.currentTemp)) - 273;
+ if (currentTemp < 0) {
+ currentTemp = (Math.abs(currentTemp) ^ 255) + 1;
+ }
+ todayMinTemp = (Math.abs(weatherSpec.todayMinTemp)) - 273;
+ if (todayMinTemp < 0) {
+ todayMinTemp = (Math.abs(todayMinTemp) ^ 255) + 1;
+ }
+ todayMaxTemp = (Math.abs(weatherSpec.todayMaxTemp)) - 273;
+ if (todayMaxTemp < 0) {
+ todayMaxTemp = (Math.abs(todayMaxTemp) ^ 255) + 1;
+ }
+ LOG.warn(" Set weather min: " + todayMinTemp + " max: " + todayMaxTemp + " current: " + currentTemp + " icon: " + currentCondition);
+// First two bytes are controlling the icon
+ weatherInfo[0] = (byte )(currentConditionCode >> 8);
+ weatherInfo[1] = (byte )currentConditionCode;
+ weatherInfo[2] = (byte) todayMinTemp;
+ weatherInfo[3] = (byte) todayMaxTemp;
+ weatherInfo[4] = (byte) currentTemp;
+ builder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE),
+ buildCommand(command,
+ WatchXPlusConstants.KEEP_ALIVE,
+ weatherInfo));
+ builder.queue(getQueue());
+ } catch (IOException e) {
+ LOG.warn("Unable to set weather", e);
+ }
+ }
+
+ @Override
+ public boolean onCharacteristicChanged(BluetoothGatt gatt,
+ BluetoothGattCharacteristic characteristic) {
+ super.onCharacteristicChanged(gatt, characteristic);
+
+ UUID characteristicUUID = characteristic.getUuid();
+ byte[] value = characteristic.getValue();
+ if (WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE.equals(characteristicUUID)) {
+ if (ArrayUtils.equals(value, WatchXPlusConstants.RESP_FIRMWARE_INFO, 5)) {
+ handleFirmwareInfo(value);
+ } else if (ArrayUtils.equals(value, WatchXPlusConstants.RESP_SHAKE_SWITCH, 5)) {
+ handleShakeState(value);
+ } else if (ArrayUtils.equals(value, WatchXPlusConstants.RESP_SET_PERSONAL_INFO, 5)) {
+ handlePersonalInfo(value);
+ } else if (ArrayUtils.equals(value, WatchXPlusConstants.RESP_BUTTON_WHILE_RING, 5)) {
+ handleButtonWhenRing(value);
+ } else if (ArrayUtils.equals(value, WatchXPlusConstants.RESP_DISCONNECT_REMIND, 5)) {
+ handleDisconnectReminderState(value);
+ } else if (ArrayUtils.equals(value, WatchXPlusConstants.RESP_BATTERY_INFO, 5)) {
+ handleBatteryState(value);
+ } else if (ArrayUtils.equals(value, WatchXPlusConstants.RESP_GOAL_AIM_STATUS, 5)) {
+ handleSportAimStatus(value);
+ } else if (ArrayUtils.equals(value, WatchXPlusConstants.RESP_TIME_SETTINGS, 5)) {
+ handleTime(value);
+ } else if (ArrayUtils.equals(value, WatchXPlusConstants.RESP_IS_BP_CALIBRATED, 5)) {
+ handleBloodPressureCalibrationStatus(value);
+ } else if (ArrayUtils.equals(value, WatchXPlusConstants.RESP_BP_CALIBRATION, 5)) {
+ handleBloodPressureCalibrationResult(value);
+ } else if (ArrayUtils.equals(value, WatchXPlusConstants.RESP_BUTTON_INDICATOR, 5)) {
+ this.onReverseFindDevice(true);
+// It looks like WatchXPlus doesn't send this action
+// WRONG: WatchXPlus send this on find phone
+ 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;
+ } else if (ArrayUtils.equals(value, WatchXPlusConstants.RESP_DAY_STEPS_INDICATOR, 5)) {
+ handleStepsInfo(value);
+ } else if (ArrayUtils.equals(value, WatchXPlusConstants.RESP_DATA_COUNT, 5)) {
+ LOG.info(" Received data count");
+ handleDataCount(value);
+ } else if (ArrayUtils.equals(value, WatchXPlusConstants.RESP_DATA_DETAILS, 5)) {
+ LOG.info(" Received data details");
+ handleDataDetails(value);
+ } else if (ArrayUtils.equals(value, WatchXPlusConstants.RESP_DATA_CONTENT, 5)) {
+ LOG.info(" Received data content");
+ handleDataContentAck(value);
+ } else if (ArrayUtils.equals(value, WatchXPlusConstants.RESP_BP_MEASURE_STARTED, 5)) {
+ handleBpMeasureResult(value);
+ } else if (ArrayUtils.equals(value, WatchXPlusConstants.RESP_DATA_CONTENT_REMOVE, 5)) {
+ handleDataContentRemove(value);
+ } else if (value.length == 7 && value[5] == 0) {
+ LOG.info(" Received ACK");
+// Not sure if that's necessary. There is no response for ACK in original app logs
+// handleAck();
+ } else if (ArrayUtils.equals(value, WatchXPlusConstants.RESP_NOTIFICATION_SETTINGS, 5)) {
+ LOG.info(" Received notification settings status");
+ } else {
+ LOG.info(" Unhandled value change for characteristic: " + characteristicUUID);
+ logMessageContent(characteristic.getValue());
+ }
+
+ return true;
+ } else if (WatchXPlusConstants.UUID_CHARACTERISTIC_DATABASE_READ.equals(characteristicUUID)) {
+ LOG.info(" Value change for characteristic DATABASE: " + characteristicUUID + " value " + Arrays.toString(value));
+ handleContentDataChunk(value);
+ return true;
+ } else {
+ LOG.info(" Unhandled characteristic changed: " + characteristicUUID + " value " + Arrays.toString(value));
+ logMessageContent(characteristic.getValue());
+ }
+
+ return false;
+ }
+
+ private void handleDataContentRemove(byte[] value) {
+ int dataType = Conversion.fromByteArr16(value[8], value[9]);
+ int timestamp = Conversion.fromByteArr16(value[10], value[11], value[12], value[13]);
+ int removed = value[14];
+ DataType type = DataType.getType(dataType);
+ if( removed == 0) {
+ LOG.info(" Removed " + type + " data for timestamp " + timestamp);
+ } else {
+ LOG.info(" Unsuccessful removal of " + type + " data for timestamp " + timestamp);
+ }
+ }
+
+ /**
+ * Heart rate history retrieve flow:
+ * 1. Request for heart rate data slots count. CMD_RETRIEVE_DATA_COUNT, {@link WatchXPlusDeviceSupport#requestDataCount}
+ * 2. Extract data count from response. RESP_DATA_COUNT, {@link WatchXPlusDeviceSupport#handleDataCount}
+ * 3. Request for N data slot details. CMD_RETRIEVE_DATA_DETAILS, {@link WatchXPlusDeviceSupport#requestDataDetails}
+ * 4. Timestamp of slot is returned, save it for later use. RESP_DATA_DETAILS, {@link WatchXPlusDeviceSupport#handleDataDetails}
+ * 5. Repeat step 3-4 until all slots details retrieved.
+ * 6. Request for M data content by timestamp. CMD_RETRIEVE_DATA_CONTENT, {@link WatchXPlusDeviceSupport#requestDataContentForTimestamp}
+ * 7. Receive kind of pre-flight response. RESP_DATA_CONTENT, {@link WatchXPlusDeviceSupport#handleDataContentAck}
+ * 8. Receive frames with content. They are different than other frames, {@link WatchXPlusDeviceSupport#handleContentDataChunk}
+ * ie. 0000000255-4F4C48-434241434444454648474747, 0001000247-474645-434240FFFFFFFFFFFFFFFFFF
+ */
+ private void requestDataCount(DataType dataType) {
+
+ TransactionBuilder builder;
+ try {
+ builder = performInitialized("requestDataCount");
+
+ builder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE),
+ buildCommand(WatchXPlusConstants.CMD_RETRIEVE_DATA_COUNT,
+ WatchXPlusConstants.READ_VALUE,
+ dataType.getValue()));
+
+ builder.queue(getQueue());
+ } catch (IOException e) {
+ LOG.warn("Unable to send request to retrieve recorded data", e);
+ }
+ }
+
+ private void handleDataCount(byte[] value) {
+
+ int dataType = Conversion.fromByteArr16(value[8], value[9]);
+ int dataCount = Conversion.fromByteArr16(value[10], value[11]);
+
+ DataType type = DataType.getType(dataType);
+ LOG.info("Watch contains " + dataCount + " " + type + " entries");
+ dataSlots = dataCount;
+ dataToFetch.clear();
+ if (dataCount != 0) {
+ requestDataDetails(dataToFetch.size(), type);
+ }
+ }
+
+ private void requestDataDetails(int i, DataType dataType) {
+ LOG.info(" Requesting " + dataType + " details");
+ try {
+ TransactionBuilder builder = performInitialized("requestDataDetails");
+
+ byte[] index = Conversion.toByteArr16(i);
+ byte[] req = BLETypeConversions.join(dataType.getValue(), index);
+ currentDataType = dataType;
+ builder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE),
+ buildCommand(WatchXPlusConstants.CMD_RETRIEVE_DATA_DETAILS,
+ WatchXPlusConstants.READ_VALUE,
+ req));
+
+ builder.queue(getQueue());
+ } catch (IOException e) {
+ LOG.warn("Unable to request data details", e);
+ }
+ }
+
+ private void handleDataDetails(byte[] value) {
+ LOG.info("Got data details");
+ int timestamp = Conversion.fromByteArr16(value[8], value[9], value[10], value[11]);
+ int dataLength = Conversion.fromByteArr16(value[12], value[13]);
+ int samplingInterval = (int) onSamplingInterval(value[14] >> 4, Conversion.fromByteArr16((byte) (value[14] & 15), value[15]));
+ int mtu = Conversion.fromByteArr16(value[16]);
+ int parts = dataLength / 16;
+ if (dataLength % 16 > 0) {
+ parts++;
+ }
+
+ LOG.info("timestamp (UTC): " + timestamp);
+ LOG.info("timestamp (UTC): " + new Date((long) timestamp * 1000));
+ LOG.info("dataLength (data length): " + dataLength);
+ LOG.info("samplingInterval (per time): " + samplingInterval);
+ LOG.info("mtu (mtu): " + mtu);
+ LOG.info("parts: " + parts);
+
+ dataToFetch.put(timestamp, parts);
+
+ if (dataToFetch.size() == dataSlots) {
+ Map.Entry currentValue = dataToFetch.entrySet().iterator().next();
+ requestedDataTimestamp = currentValue.getKey();
+ requestDataContentForTimestamp(requestedDataTimestamp, currentDataType);
+ } else {
+ requestDataDetails(dataToFetch.size(), currentDataType);
+ }
+ }
+
+ private void requestDataContentForTimestamp(int timestamp, DataType dataType) {
+ byte[] command = WatchXPlusConstants.CMD_RETRIEVE_DATA_CONTENT;
+
+ try {
+ TransactionBuilder builder = performInitialized("requestDataContentForTimestamp");
+ byte[] ts = Conversion.toByteArr32(timestamp);
+ byte[] req = BLETypeConversions.join(dataType.getValue(), ts);
+ req = BLETypeConversions.join(req, Conversion.toByteArr16(0));
+ requestedDataTimestamp = timestamp;
+ builder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE),
+ buildCommand(command,
+ WatchXPlusConstants.READ_VALUE,
+ req));
+ builder.queue(getQueue());
+ } catch (IOException e) {
+ LOG.warn("Unable to request data content", e);
+ }
+ }
+
+ private void removeDataContentForTimestamp(int timestamp, DataType dataType) {
+ byte[] command = WatchXPlusConstants.CMD_REMOVE_DATA_CONTENT;
+
+ try {
+ TransactionBuilder builder = performInitialized("removeDataContentForTimestamp");
+ byte[] ts = Conversion.toByteArr32(timestamp);
+ byte[] req = BLETypeConversions.join(dataType.getValue(), ts);
+ builder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE),
+ buildCommand(command,
+ WatchXPlusConstants.TASK,
+ req));
+ builder.queue(getQueue());
+ } catch (IOException e) {
+ LOG.warn("Unable to remove data content", e);
+ }
+ }
+
+ private void handleDataContentAck(byte[] value) {
+ LOG.info(" Received data content start");
+// To verify: Chunks are sent if value[8] == 0, if value[8] == 1 they are not sent by watch
+ }
+
+ private void handleContentDataChunk(byte[] value) {
+ int chunkNo = Conversion.fromByteArr16(value[0], value[1]);
+ int dataType = Conversion.fromByteArr16(value[2], value[3]);
+ int timezoneOffset = TimeZone.getDefault().getOffset(System.currentTimeMillis())/1000;
+ DataType type = DataType.getType(dataType);
+ try (DBHandler dbHandler = GBApplication.acquireDB()) {
+ WatchXPlusSampleProvider provider = new WatchXPlusSampleProvider(getDevice(), dbHandler.getDaoSession());
+ List samples = new ArrayList<>();
+
+ if (DataType.SLEEP.equals(type)) {
+ WatchXPlusHealthActivityOverlayDao overlayDao = dbHandler.getDaoSession().getWatchXPlusHealthActivityOverlayDao();
+ List overlayList = new ArrayList<>();
+
+ for (int i = 4; i < value.length; i+= 2) {
+
+ int val = Conversion.fromByteArr16(value[i], value[i+1]);
+ if (65535 == val) {
+ break;
+ }
+
+ int tsWithOffset = requestedDataTimestamp + (((((chunkNo * 16) / 2) + ((i - 4) / 2)) *5) * 60) - timezoneOffset;
+ LOG.debug(" requested timestamp " + requestedDataTimestamp + " chunkNo " + chunkNo + " Got data: " + new Date((long) tsWithOffset * 1000) + ", value: " + val);
+ WatchXPlusActivitySample sample = createSample(dbHandler, tsWithOffset);
+ sample.setTimestamp(tsWithOffset);
+ sample.setProvider(provider);
+ sample.setRawIntensity(val);
+ sample.setRawKind(val == 0 ? ActivityKind.TYPE_DEEP_SLEEP : ActivityKind.TYPE_LIGHT_SLEEP);
+ samples.add(sample);
+ overlayList.add(new WatchXPlusHealthActivityOverlay(sample.getTimestamp(), sample.getTimestamp()+300, sample.getRawKind(), sample.getDeviceId(), sample.getUserId(), sample.getRawWatchXPlusHealthData()));
+ }
+ overlayDao.insertOrReplaceInTx(overlayList);
+ provider.addGBActivitySamples(samples.toArray(new WatchXPlusActivitySample[0]));
+
+ handleEndOfDataChunks(chunkNo, type);
+ } else if (DataType.HEART_RATE.equals(type)) {
+
+ for (int i = 4; i < value.length; i++) {
+
+ int val = Conversion.fromByteArr16(value[i]);
+ if (255 == val) {
+ break;
+ }
+ int tsWithOffset = requestedDataTimestamp + (((((chunkNo * 16) + i) - 4) * 2) * 60) - timezoneOffset;
+// LOG.debug(" requested timestamp " + requestedDataTimestamp + " chunkNo " + chunkNo + " Got data: " + new Date((long) tsWithOffset * 1000) + ", value: " + val);
+ WatchXPlusActivitySample sample = createSample(dbHandler, tsWithOffset);
+ sample.setTimestamp(tsWithOffset);
+ sample.setHeartRate(val);
+ sample.setProvider(provider);
+ sample.setRawKind(ActivityKind.TYPE_ACTIVITY);
+ samples.add(sample);
+ }
+ provider.addGBActivitySamples(samples.toArray(new WatchXPlusActivitySample[0]));
+
+ handleEndOfDataChunks(chunkNo, type);
+ } else {
+ LOG.warn(" Got unsupported data package type: " + type);
+ }
+ } catch (Exception ex) {
+ LOG.info((ex.getMessage()));
+ }
+
+ }
+
+ private void handleEndOfDataChunks(int chunkNo, DataType type) {
+ if(!dataToFetch.isEmpty() && chunkNo == dataToFetch.get(requestedDataTimestamp) - 1) {
+ dataToFetch.remove(requestedDataTimestamp);
+ removeDataContentForTimestamp(requestedDataTimestamp, currentDataType);
+ if (!dataToFetch.isEmpty()) {
+ Map.Entry currentValue = dataToFetch.entrySet().iterator().next();
+ requestedDataTimestamp = currentValue.getKey();
+ requestDataContentForTimestamp(requestedDataTimestamp, type);
+ } else {
+ dataSlots = 0;
+ if(type.equals(DataType.HEART_RATE)) {
+ currentDataType = DataType.SLEEP;
+ requestDataCount(currentDataType);
+ }
+ }
+ } else if (dataToFetch.isEmpty()) {
+ dataSlots = 0;
+ if(type.equals(DataType.HEART_RATE)) {
+ currentDataType = DataType.SLEEP;
+ requestDataCount(currentDataType);
+ }
+ }
+ }
+
+
+ private void handleBpMeasureResult(byte[] value) {
+
+ if (value.length < 11) {
+ LOG.info(" BP Measure started. Waiting for result");
+ GB.toast("BP Measure started. Waiting for result...", Toast.LENGTH_LONG, GB.INFO);
+ } else {
+ LOG.info(" Received BP live data");
+ int high = Conversion.fromByteArr16(value[8], value[9]);
+ int low = Conversion.fromByteArr16(value[10], value[11]);
+ int timestamp = Conversion.fromByteArr16(value[12], value[13], value[14], value[15]);
+ GB.toast("Calculated BP data: low: " + low + ", high: " + high, Toast.LENGTH_LONG, GB.INFO);
+ LOG.info(" Calculated BP data: timestamp: " + timestamp + ", high: " + high + ", low: " + low);
+ }
+ }
+
+ private void handleAck() {
+ try {
+ TransactionBuilder builder = performInitialized("handleAck");
+
+ builder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE),
+ buildCommand());
+ builder.queue(getQueue());
+ } catch (IOException e) {
+ LOG.warn("Unable to response to ACK", e);
+ }
+ }
+
+ // This is only for ACK response
+ private byte[] buildCommand() {
+ byte[] result = new byte[7];
+ System.arraycopy(WatchXPlusConstants.CMD_HEADER, 0, result, 0, 5);
+
+ result[2] = (byte) (result.length - 6);
+ result[3] = WatchXPlusConstants.REQUEST;
+ result[4] = (byte) sequenceNumber++;
+ result[5] = (byte) 0;
+ result[result.length - 1] = calculateChecksum(result);
+
+ return result;
+ }
+
+ /** handle watch response for steps goal (show steps setting)
+ * @param value - watch reply
+ * for test purposes only
+ */
+ private void handleSportAimStatus(byte[] value) {
+ int stepsAim = Conversion.fromByteArr16(value[8], value[9]);
+ LOG.debug(" Received goal stepsAim: " + stepsAim);
+ }
+
+ private void handleStepsInfo(byte[] value) {
+ int steps = Conversion.fromByteArr16(value[8], value[9]);
+ LOG.debug(" Received steps count: " + steps);
+
+ // This code is from MakibesHR3DeviceSupport
+ Calendar date = GregorianCalendar.getInstance();
+ int timestamp = (int) (date.getTimeInMillis() / 1000);
+
+ // We need to subtract the day's total step count thus far.
+ int dayStepCount = this.getStepsOnDay(timestamp);
+
+ int newSteps = (steps - dayStepCount);
+
+ if (newSteps > 0) {
+ LOG.debug("adding " + newSteps + " steps");
+
+ try (DBHandler dbHandler = GBApplication.acquireDB()) {
+ WatchXPlusSampleProvider provider = new WatchXPlusSampleProvider(getDevice(), dbHandler.getDaoSession());
+
+ WatchXPlusActivitySample sample = createSample(dbHandler, timestamp);
+ sample.setTimestamp(timestamp);
+// sample.setRawKind(record.type);
+ sample.setRawKind(ActivityKind.TYPE_ACTIVITY);
+ sample.setSteps(newSteps);
+// sample.setDistance(record.distance);
+// sample.setCalories(record.calories);
+// sample.setDistance(record.distance);
+// sample.setHeartRate((record.maxHeartRate - record.minHeartRate) / 2); //TODO: Find an alternative approach for Day Summary Heart Rate
+// sample.setRawHPlusHealthData(record.getRawData());
+
+ sample.setProvider(provider);
+ provider.addGBActivitySample(sample);
+ } catch (Exception ex) {
+ LOG.info((ex.getMessage()));
+ }
+ }
+ }
+
+ /**
+ * @param timeStamp Time stamp at some point during the requested day.
+ */
+ private int getStepsOnDay(int timeStamp) {
+ try (DBHandler dbHandler = GBApplication.acquireDB()) {
+
+ Calendar dayStart = new GregorianCalendar();
+ Calendar dayEnd = new GregorianCalendar();
+
+ this.getDayStartEnd(timeStamp, dayStart, dayEnd);
+
+ WatchXPlusSampleProvider provider = new WatchXPlusSampleProvider(this.getDevice(), dbHandler.getDaoSession());
+
+ List samples = provider.getAllActivitySamples(
+ (int) (dayStart.getTimeInMillis() / 1000L),
+ (int) (dayEnd.getTimeInMillis() / 1000L));
+
+ int totalSteps = 0;
+
+ for (WatchXPlusActivitySample sample : samples) {
+ totalSteps += sample.getSteps();
+ }
+
+ return totalSteps;
+
+ } catch (Exception ex) {
+ LOG.error(ex.getMessage());
+
+ return 0;
+ }
+ }
+
+ /**
+ * @param timeStamp seconds
+ */
+ private void getDayStartEnd(int timeStamp, Calendar start, Calendar end) {
+ final int DAY = (24 * 60 * 60);
+
+ int timeStampStart = ((timeStamp / DAY) * DAY);
+ int timeStampEnd = (timeStampStart + DAY);
+
+ start.setTimeInMillis(timeStampStart * 1000L);
+ end.setTimeInMillis(timeStampEnd * 1000L);
+ }
+
+ private WatchXPlusActivitySample createSample(DBHandler dbHandler, int timestamp) {
+ Long userId = DBHelper.getUser(dbHandler.getDaoSession()).getId();
+ Long deviceId = DBHelper.getDevice(getDevice(), dbHandler.getDaoSession()).getId();
+
+ return new WatchXPlusActivitySample(
+ timestamp, // ts
+ deviceId, userId, // User id
+ null, // Raw Data
+ ActivityKind.TYPE_UNKNOWN, // rawKind
+ ActivitySample.NOT_MEASURED, // rawIntensity
+ ActivitySample.NOT_MEASURED, // Steps
+ ActivitySample.NOT_MEASURED, // HR
+ ActivitySample.NOT_MEASURED, // Distance
+ ActivitySample.NOT_MEASURED // Calories
+ );
+ }
+
+ 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);
+ }
+
+ /** handle watch response for firmware version
+ * @param value - watch response
+ */
+ private void handleFirmwareInfo(byte[] value) {
+ versionInfo.fwVersion = String.format(Locale.US, "%d.%d.%d", value[8], value[9], value[10]);
+ handleGBDeviceEvent(versionInfo);
+ }
+
+ /** handle watch response for battery level
+ * @param value - returned value
+ */
+ private void handleBatteryState(byte[] value) {
+ batteryInfo.state = value[8] == 1 ? BatteryState.BATTERY_NORMAL : BatteryState.BATTERY_LOW;
+ batteryInfo.level = value[9];
+ handleGBDeviceEvent(batteryInfo);
+ }
+
+ /** handle watch response for lift wrist, and shake to refuse/ignore call
+ * @param value - watch response
+ * for test purposes only
+ */
+ private void handleShakeState(byte[] value) {
+ boolean z = true;
+ String light = "lightScreen";
+ if ((value[11] & 1) == 1) {
+ light = light + " on";
+ } else {
+ light = light + " off";
+ }
+ String refuse = "refuseCall";
+ if ((((value[11] & 2) >> 1) & 1) != 1) {
+ //z = false;
+ refuse = refuse + " off";
+ } else {
+ refuse = refuse + " on";
+ }
+ LOG.info(" handleShakeState: " + light + " " + refuse);
+ }
+
+ /** handle disconnect reminder (lost device) status
+ * @param value - watch response
+ * for test purposes only
+ */
+ private void handleDisconnectReminderState(byte[] value) {
+ boolean z = true;
+ if (1 != value[8]) {
+ z = false;
+ }
+ LOG.info(" disconnectReminder: " + z + " val: " + value[8]);
+ }
+
+// read preferences
+ private void syncPreferences(TransactionBuilder transaction) {
+ SharedPreferences sharedPreferences = GBApplication.getDeviceSpecificSharedPrefs(this.getDevice().getAddress());
+ this.setHeadsUpScreen(transaction, sharedPreferences); // lift wirst to screen on
+ this.setQuiteHours(transaction, sharedPreferences); // DND
+ this.setDisconnectReminder(transaction, sharedPreferences); // disconnect reminder
+ this.setLanguageAndTimeFormat(transaction, sharedPreferences); // set time mode 12/24h
+ this.setAltitude(transaction); // set altitude calibration
+ this.setLongSitHours(transaction, sharedPreferences); // set Long sit reminder
+ ActivityUser activityUser = new ActivityUser();
+ this.setPersonalInformation(transaction, activityUser.getHeightCm(), activityUser.getWeightKg(),
+ activityUser.getAge(),activityUser.getGender());
+ }
+
+ private final Handler mFindPhoneHandler = new Handler();
+
+ private void onReverseFindDevice(boolean start) {
+ if (start) {
+ SharedPreferences sharedPreferences = GBApplication.getDeviceSpecificSharedPrefs(
+ this.getDevice().getAddress());
+
+ int findPhone = WatchXPlusDeviceCoordinator.getFindPhone(sharedPreferences);
+
+ if (findPhone != WatchXPlusDeviceCoordinator.FindPhone_OFF) {
+ GBDeviceEventFindPhone findPhoneEvent = new GBDeviceEventFindPhone();
+
+ findPhoneEvent.event = GBDeviceEventFindPhone.Event.START;
+
+ evaluateGBDeviceEvent(findPhoneEvent);
+
+ if (findPhone > 0) {
+ this.mFindPhoneHandler.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ onReverseFindDevice(false);
+ }
+ }, findPhone * 1000);
+ }
+ }
+ } else {
+ // Always send stop, ignore preferences.
+ GBDeviceEventFindPhone findPhoneEvent = new GBDeviceEventFindPhone();
+
+ findPhoneEvent.event = GBDeviceEventFindPhone.Event.STOP;
+
+ evaluateGBDeviceEvent(findPhoneEvent);
+ }
+ }
+ // Set Lift Wrist to Light Screen based on saved preferences
+ private void setHeadsUpScreen(TransactionBuilder transactionBuilder, SharedPreferences sharedPreferences) {
+ this.setHeadsUpScreen(transactionBuilder,
+ WatchXPlusDeviceCoordinator.shouldEnableHeadsUpScreen(sharedPreferences));
+ }
+
+ // Command to toggle Lift Wrist to Light Screen, and shake to ignore/reject call
+ private WatchXPlusDeviceSupport setHeadsUpScreen(TransactionBuilder transactionBuilder, boolean enable) {
+ boolean shakeReject = WatchXPlusDeviceCoordinator.getShakeReject(getDevice().getAddress());
+ byte refuseCall = 0x00; // force shake wrist to ignore/reject call to OFF
+ // returned characteristic is equal with button press while ringing
+ if (shakeReject) refuseCall = 0x01;
+ byte lightScreen = 0x00;
+ if (enable) {
+ lightScreen = 0x01;
+ }
+ byte b = (byte) (lightScreen + (refuseCall << 1));
+ byte[] liftScreen = new byte[4];
+ liftScreen[0] = 0x00;
+ liftScreen[1] = 0x00;
+ liftScreen[2] = 0x00;
+ liftScreen[3] = b; //byte[11]
+ transactionBuilder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE),
+ buildCommand(WatchXPlusConstants.CMD_SHAKE_SWITCH,
+ WatchXPlusConstants.WRITE_VALUE,
+ liftScreen));
+ return this;
+ }
+
+ private void setDisconnectReminder(TransactionBuilder transactionBuilder, SharedPreferences sharedPreferences) {
+ this.setDisconnectReminder(transactionBuilder,
+ WatchXPlusDeviceCoordinator.shouldEnableDisconnectReminder(sharedPreferences));
+ }
+
+ private WatchXPlusDeviceSupport setDisconnectReminder(TransactionBuilder transactionBuilder, boolean enable) {
+ transactionBuilder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE),
+ buildCommand(WatchXPlusConstants.CMD_DISCONNECT_REMIND,
+ WatchXPlusConstants.WRITE_VALUE,
+ new byte[]{(byte) (enable ? 0x01 : 0x00)}));
+ return this;
+ }
+
+// Request status of Disconnect reminder
+ private void getDisconnectReminderStatus(TransactionBuilder transactionBuilder) {
+ transactionBuilder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE),
+ buildCommand(WatchXPlusConstants.CMD_DISCONNECT_REMIND,
+ WatchXPlusConstants.READ_VALUE));
+ }
+// Request status of Lift Wrist to Light Screen, and Shake to Ignore/Reject Call
+ private void getShakeStatus(TransactionBuilder transactionBuilder) {
+ transactionBuilder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE),
+ buildCommand(WatchXPlusConstants.CMD_SHAKE_SWITCH,
+ WatchXPlusConstants.READ_VALUE));
+ }
+
+// calibrate altitude
+ private void setAltitude(TransactionBuilder transactionBuilder) {
+ int mAltitude = WatchXPlusDeviceCoordinator.getAltitude(getDevice().getAddress());
+ if (mAltitude < 0) {
+ mAltitude = (Math.abs(mAltitude) ^ 65535) + 1;
+ }
+ int mAirPressure = Math.abs(0); // air pressure 0 ???
+ byte[] bArr = new byte[4];
+ bArr[0] = (byte) (mAltitude >> 8); // bytr[8]
+ bArr[1] = (byte) mAltitude; // bytr[9]
+ bArr[2] = (byte) (mAirPressure >> 8); // bytr[10]
+ bArr[3] = (byte) mAirPressure; // bytr[11]
+ transactionBuilder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE),
+ buildCommand(WatchXPlusConstants.CMD_ALTITUDE,
+ WatchXPlusConstants.WRITE_VALUE,
+ bArr));
+ LOG.info(" setAltitude: " + mAltitude);
+ }
+
+ // set time format
+ private WatchXPlusDeviceSupport setLanguageAndTimeFormat(TransactionBuilder transactionBuilder, byte timeMode, byte language) {
+ byte[] bArr = new byte[2];
+ bArr[0] = language; //byte[08] language
+ bArr[1] = timeMode; //byte[09] time
+ transactionBuilder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE),
+ buildCommand(WatchXPlusConstants.CMD_TIME_LANGUAGE,
+ WatchXPlusConstants.WRITE_VALUE,
+ bArr));
+ return this;
+ }
+
+ private void setLanguageAndTimeFormat(TransactionBuilder transactionBuilder, SharedPreferences sharedPreferences) {
+ this.setLanguageAndTimeFormat(transactionBuilder,
+ WatchXPlusDeviceCoordinator.getTimeMode(sharedPreferences),
+ WatchXPlusDeviceCoordinator.getLanguage(sharedPreferences));
+ }
+
+ @Override
+ public void dispose() {
+ LocalBroadcastManager broadcastManager = LocalBroadcastManager.getInstance(getContext());
+ broadcastManager.unregisterReceiver(broadcastReceiver);
+ super.dispose();
+ }
+
+ private static double onSamplingInterval(int i, int i2) {
+ switch (i) {
+ case 1:
+ return 1.0d * Math.pow(10.0d, -6.0d) * ((double) i2);
+ case 2:
+ return 1.0d * Math.pow(10.0d, -3.0d) * ((double) i2);
+ case 3:
+ return (double) (i2);
+ case 4:
+ return 10.0d * Math.pow(10.0d, -6.0d) * ((double) i2);
+ case 5:
+ return 10.0d * Math.pow(10.0d, -3.0d) * ((double) i2);
+ case 6:
+ return (double) (10 * i2);
+ default:
+ return (double) (10 * i2);
+ }
+ }
+
+
+
+ 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 int fromByteArr16(byte... value) { // equals calculateHigh
+ int intValue = 0;
+ for (int i2 = 0; i2 < value.length; i2++) {
+ intValue += (value[i2] & 255) << (((value.length - 1) - i2) * 8);
+ }
+ return intValue;
+ }
+
+ static byte[] toByteArr32(int value) {
+ return new byte[]{(byte) (value >> 24),
+ (byte) (value >> 16),
+ (byte) (value >> 8),
+ (byte) value};
+ }
+
+ static int calculateLow(byte... bArr) {
+ int i = 0;
+ int i2 = 0;
+ while (i < bArr.length) {
+ i2 += (bArr[i] & 255) << (i * 8);
+ i++;
+ }
+ return i2;
+ }
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xwatch/XWatchSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xwatch/XWatchSupport.java
index c3bc3a04c..4800cd200 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xwatch/XWatchSupport.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xwatch/XWatchSupport.java
@@ -65,7 +65,6 @@ public class XWatchSupport extends AbstractBTLEDeviceSupport {
private static final Logger LOG = LoggerFactory.getLogger(XWatchSupport.class);
private final GBDeviceEventVersionInfo versionCmd = new GBDeviceEventVersionInfo();
TransactionBuilder builder = null;
- private DeviceInfo mDeviceInfo;
private byte dayToFetch; //0 = Today; 1 = Yesterday ...
private byte maxDayToFetch;
long lastButtonTimestamp;
@@ -359,7 +358,7 @@ public class XWatchSupport extends AbstractBTLEDeviceSupport {
private void handleDeviceInfo(byte[] value, int status) {
if (status == BluetoothGatt.GATT_SUCCESS) {
- mDeviceInfo = new DeviceInfo(value);
+ DeviceInfo mDeviceInfo = new DeviceInfo(value);
LOG.warn("Device info: " + mDeviceInfo);
versionCmd.hwVersion = "1.0";
versionCmd.fwVersion = "1.0";
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DeviceHelper.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DeviceHelper.java
index ff91bdf73..ef967a8b4 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DeviceHelper.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DeviceHelper.java
@@ -72,6 +72,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.QHybridCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.roidmi.Roidmi1Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.roidmi.Roidmi3Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.vibratissimo.VibratissimoCoordinator;
+import nodomain.freeyourgadget.gadgetbridge.devices.lenovo.watchxplus.WatchXPlusDeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.watch9.Watch9DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.xwatch.XWatchCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.zetime.ZeTimeCoordinator;
@@ -232,6 +233,7 @@ public class DeviceHelper {
result.add(new ZeTimeCoordinator());
result.add(new ID115Coordinator());
result.add(new Watch9DeviceCoordinator());
+ result.add(new WatchXPlusDeviceCoordinator());
result.add(new Roidmi1Coordinator());
result.add(new Roidmi3Coordinator());
result.add(new CasioGB6900DeviceCoordinator());
diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml
index 9f96e3bd8..5c3b5e8e4 100644
--- a/app/src/main/res/values-bg/strings.xml
+++ b/app/src/main/res/values-bg/strings.xml
@@ -1,5 +1,22 @@
+ Бутон за ново устройство
+ Винаги видим
+ Видим само ако няма свързано устройство
+ Език и регион
+ За Вас
+ Година на раждане
+ Пол
+ Височина в cm
+ Тегло в kg
+ Настройки на Графиката
+ Показвай средни стойности
+ Настройки на графика
+ Max сърдечен ритъм
+ Min сърдечен ритъм
+ Обхват на Графиката
+ Обхвата е Месец
+ Обхвата е Седмица
Gadgetbridge
Gadgetbridge
Настройки
@@ -30,6 +47,12 @@
Приложения в кеша
Инсталирани приложения
Инсталирани циферблати
+ Включи екрана при вдигане
+ Известие при прекъсване
+ Намери телефона
+ Включи намиране на телефона
+ Използвайте устройството, за да накарате телефона да звъни.
+ Звънене - секунди
Изтриване
Изтриване и премахване от кеша
Преинсталиране
@@ -253,6 +276,51 @@
Име/Псевдоним
Брой на вибрациите
Когато часовникът завибрира, разклатете устройството или натиснете бутона
+ Минути:
+ Часове:
+ Секунди:
+ Задайте времето, което показва устройството Ви.
+ Сверяване
+ Watch 9 свързване
+ Watch 9 сверяване
+
+ Watch X Plus сверяване
+ Единици
+ Формат на часа
+ Калибриране на височината
+ Повтаряй известие за позвъняване
+ Възможни стойности min=0, max=10
+ Известявай докато телефона звъни
+ Известие за пропуснато обаждане
+ Watch X Plus настройки
+ Известия при обаждане
+ Известия при пропуснато обаждане
+ Изкл. - заглуши, Вкл. - откажи
+ Бутона заглушава/отказва повикване
+ Повтаря действието на бутона
+ Разклати за заглушаване/отказване на повикването
+ Калибриране на кръвно налягане
+ Кръвно налягане DIASTOLIC (ниска)
+ Кръвно налягане SYSTOLIC (висока)
+ Калибриране
+ Натисни тук за калибриране
+ Калибриране на сензорите
+ Настройки на устройството
+ Режим на часовника
+ Нормален
+ Икономичен
+ Само часовник
+ Повтаряй известие за пропуснато повикване всяка минута за X пъти
+ Повтаряй известието за пропуснато повикване
+ Управление на обажданията
+ Напомняне за бездействие
+ Напомняй ако няма активност за повече от X минути
+ Времевия интервал е от настройката за DND
+ Включи напомняне за активност
+ Период на неактивност (минути)
+ Език
+ Известия и Обаждания
+
Наблюдение/анализ на съня
Съхраняване на log файлове
Инициализиране
@@ -420,4 +488,113 @@
Специфични настройки за устройството
Средно: %1$s
nodomain.freeyourgadget.gadgetbridge.ButtonPressed
+ Отказ
+ Изтрий
+ Дълбок сън
+ Лек сън
+ Не е носен
+ Активност
+ Отговори
+ Управление на базата
+ Изчисти БД
+ Импорт БД
+ Изтрий стара БД
+ Експорт на БД
+ Експортиране на БД...
+ Стартирай авто експортиране
+ Локацията за експорт на БД е:
+ АвтоЕкспорт
+ ИзпразниБД
+ Внимание! Ако натиснете този бутон БД ще се изтрие.
+ Експорт и Импорт
+ Време за сън в часове
+ Активности
+ Активност
+ Колоездене
+ Дълбок сън
+ Лек сън
+ Упражнение
+ Не е измерено
+ Не е носено
+ Бягане
+ Плуване
+ Неизвестна активност
+ Ходене
+ Не е свързан, алармата не е настроена.
+ Аларма за %1$02d:%2$02d
+ Автоматично
+ Удостоверяване
+ Изисква удостоверяване
+ Сърдечна честота
+ Избери локация за експорт
+ Час
+ Замени
+ Firmware версия: %1$s
+ Hardware версия: %1$s
+ Не е свързан.
+ Неизвестно устройство
+ Опит за свързване с %1$s
+ Не се свързвай
+ Включи bluetooth за намиране на устройства.
+ Свързване с %1$s?
+ Опит за свързване с: %1$s
+ Свържи
+ Не филтрирай
+ Намери ме!
+ Шрифт
+ Невалидни данни
+ Сърдечен ритъм
+ Продължителност
+ Аларма
+ Активност
+ Компас
+ Музика
+ Настройки
+ Известия
+ Прогноза за време
+ Таймер
+ Изкл.
+ Разписание
+ Край
+ Начало
+ Не безпокой
+ Вие спахте от %1$s до %2$s
+ Стъпки: %1$02d
+ Сън: %1$s
+ 5 минути
+ 20 минути
+ 1 час
+ 10 минути
+ Крачки за месец
+ Сън за месец
+ Внимание!
+ Изчаква свързване
+ Всички аларми са изключени
+ Метрична
+ Инчова
+ Въведете поне една дума
+ Филтъра за известия е запазен
+ Вибрация
+ Филтър за известия
+ Управление на БД
+ Избери всички
+ Сподели
+ Запази настройките
+ Горна граница
+ Долна граница
+ От ляво на дясно
+ Аларми
+ метрични
+ инчови
+ крачки
+ разписание
+ изкл.
+ вкл.
+ изкл.
+ прогноза
+ настройки
+ известия
+ Изкл.
+ Вкл.
+ Няма данни
\ No newline at end of file
diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml
index a779f45fa..1afe54b64 100644
--- a/app/src/main/res/values/arrays.xml
+++ b/app/src/main/res/values/arrays.xml
@@ -69,6 +69,33 @@
- never
+
+ - @string/simplified_chinese
+ - @string/english
+
+
+ - 0
+ - 1
+
+
+ - @string/prefs_wxp_button_bp_calibration
+ - @string/Cancel
+
+
+ - 0
+ - 1
+
+
+ - @string/wxp_mode_normal
+ - @string/wxp_mode_saving
+ - @string/wxp_mode_watch
+
+
+ - 0
+ - 1
+ - 2
+
+
- @string/male
- @string/female
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index a0598e541..964245529 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -189,6 +189,44 @@
Screen on duration
All day heart rate measurement
HPlus/Makibes settings
+
+ Watch X Plus calibration
+ Units
+ Time format
+ Altitude calibration
+ Repeat call notification
+ Possible values min=0, max=10
+ Vibration during phone ring
+ Vibration on missed call
+ Watch X Plus settings
+ Notifications and Calls
+ Call notifications
+ MissCall notifications
+ Off - ignore, On - reject
+ Button ignore/reject call
+ Duplicates watch button action
+ Shake wrist ignore/reject call
+ Blood Pressure calibration
+ Blood Pressure DIASTOLIC (low)
+ Blood Pressure SYSTOLIC (high)
+ Calibration
+ Press here to begin calibration
+ Sensors Calibration
+ Device settings
+ Watch mode
+ Normal
+ Power saving
+ Only watch
+ Repeat missed call notification every minute for X times
+ Repeat missed call notification
+ Call Handling
+ Inactivity reminder
+ Remind if there is no activity for more than X minutes
+ Inactivity time interval is from DND setting
+ Enable inactivity reminder
+ Inactivity period (minutes)
+ Language
+
Makibes HR3 settings
@@ -683,6 +721,8 @@
MyKronoz ZeTime
ID115
Watch 9
+ Watch X
+ Watch X Plus
Roidmi
Roidmi 3
Casio GB-6900
diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml
index 6813452de..53735e300 100644
--- a/app/src/main/res/xml/preferences.xml
+++ b/app/src/main/res/xml/preferences.xml
@@ -580,6 +580,12 @@
android:icon="@drawable/ic_device_zetime"
android:key="pref_key_zetime"
android:title="@string/zetime_title_settings"/>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file