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 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 getPairingActivity() { + return LenovoWatchPairingActivity.class; + } + + @Override + public boolean supportsActivityDataFetching() { + return true; + } + + @Override + public boolean supportsActivityTracking() { + return true; + } + + @Override + public SampleProvider 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 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 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