1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-07-24 15:43:46 +02:00
Gadgetbridge/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lenovo/watchxplus/WatchXPlusDeviceSupport.java
2023-06-13 12:06:13 +00:00

2132 lines
94 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* Copyright (C) 2018-2021 Andreas Böhler, Andreas Shimokawa, Carsten
Pfeiffer, Daniele Gobbetti, mamucho, maxirnilian, mkusnierz, Sebastian Kranz,
Taavi Eomäe
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.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.GBDeviceEventFindPhone;
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.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.BcdUtil;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
public class WatchXPlusDeviceSupport extends AbstractBTLEDeviceSupport {
private boolean needsAuth;
private int sequenceNumber = 0;
private boolean isCalibrationActive = false;
private final Map<Integer, Integer> 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 {
if (getQueue() == null) {
LOG.warn("Unable to cancel notification, queue is null");
return;
}
getQueue().clear();
TransactionBuilder builder = performInitialized("cancelNotification");
int mPosition = 1024; // all positions
int mMessageId = 0xFF; // all messages
byte[] 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);
}
}
private String transliterate(String inputText) {
String outText = "";
String returnText = "";
for (int messageIndex = 0; messageIndex < inputText.length(); messageIndex++) {
String checkLetter = inputText.substring(messageIndex, messageIndex + 1);
returnText = checkLetter;
switch (checkLetter) {
case "а":
returnText = "А";
break;
case "б":
returnText = "Б";
break;
case "в":
returnText = "В";
break;
case "г":
returnText = "Г";
break;
case "д":
returnText = "Д";
break;
case "е":
returnText = "Е";
break;
case "ж":
returnText = "Ж";
break;
case "з":
returnText = "З";
break;
case "и":
returnText = "И";
break;
case "й":
returnText = "Й";
break;
case "к":
returnText = "К";
break;
case "л":
returnText = "Л";
break;
case "м":
returnText = "М";
break;
case "н":
returnText = "Н";
break;
case "о":
returnText = "О";
break;
case "п":
returnText = "П";
break;
case "Р":
returnText = "р";
break;
case "С":
returnText = "с";
break;
case "Т":
returnText = "т";
break;
case "У":
returnText = "у";
break;
case "Ф":
returnText = "ф";
break;
case "Х":
returnText = "х";
break;
case "Ц":
returnText = "ц";
break;
case "Ч":
returnText = "ч";
break;
case "Ш":
returnText = "ш";
break;
case "Щ":
returnText = "щ";
break;
case "Ъ":
returnText = "ъ";
break;
case "Ь":
returnText = "ь";
break;
case "Ю":
returnText = "ю";
break;
case "Я":
returnText = "я";
break;
case "Ы":
returnText = "ы";
break;
case "Э":
returnText = "э";
break;
}
outText = outText + returnText;
}
return outText;
}
/** 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[] text = transliterate(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 WatchXPlusDeviceSupport checkInitTime(TransactionBuilder builder) {
LOG.info(" Check init time ");
builder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE),
buildCommand(WatchXPlusConstants.CMD_TIME_SETTINGS,
WatchXPlusConstants.READ_VALUE));
return this;
}
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 + BcdUtil.fromBcd8(time[8]);
nowDevice.set(year,
BcdUtil.fromBcd8(time[9]) - 1,
BcdUtil.fromBcd8(time[10]),
BcdUtil.fromBcd8(time[11]),
BcdUtil.fromBcd8(time[12]),
BcdUtil.fromBcd8(time[13]));
nowDevice.set(Calendar.DAY_OF_WEEK, BcdUtil.fromBcd8(time[16]) + 1);
long timeDiff = (Math.abs(now.getTimeInMillis() - nowDevice.getTimeInMillis())) / 1000;
LOG.info(" Time diff: " + timeDiff);
if (10 < timeDiff && timeDiff < 120) {
LOG.info(" Auto set time ");
enableCalibration(true);
setTime(BLETypeConversions.createCalendar());
enableCalibration(false);
} else if (timeDiff > 120) {
LOG.info(" Time diff is too big ");
GB.toast("Manual time calibration needed!", Toast.LENGTH_LONG, GB.WARN);
sendNotification(WatchXPlusConstants.NOTIFICATION_CHANNEL_DEFAULT, "Calibrate time");
boolean forceTime = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getBoolean(WatchXPlusConstants.PREF_FORCE_TIME, false);
if (forceTime) {
LOG.info(" Force set time ");
enableCalibration(true);
setTime(BLETypeConversions.createCalendar());
enableCalibration(false);
GB.toast("Check analog time!", Toast.LENGTH_LONG, GB.WARN);
sendNotification(WatchXPlusConstants.NOTIFICATION_CHANNEL_DEFAULT, "Check analog time");
}
}
}
// set only digital time
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[]{BcdUtil.toBcd8(calendar.get(Calendar.YEAR) % 100),
BcdUtil.toBcd8(calendar.get(Calendar.MONTH) + 1),
BcdUtil.toBcd8(calendar.get(Calendar.DAY_OF_MONTH)),
BcdUtil.toBcd8(calendar.get(Calendar.HOUR_OF_DAY)),
BcdUtil.toBcd8(calendar.get(Calendar.MINUTE)),
BcdUtil.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
.checkInitTime(builder)
.syncPreferences(builder); // read preferences from app and set them to watch
builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZED, getContext()));
builder.setCallback(this);
return this;
}
@Override
public void onDeleteNotification(int id) {
isMissedCall = false;
cancelNotification();
}
@Override
public void onSetTime() {
LOG.info(" Get time ");
getTime();
}
@Override
public void onSetAlarms(ArrayList<? extends Alarm> alarms) {
try {
TransactionBuilder builder = performInitialized("setAlarms");
for (Alarm alarm : alarms) {
setAlarm(alarm, alarm.getPosition() + 1, builder);
}
builder.queue(getQueue());
} catch (IOException e) {
LOG.warn(" Unable to set alarms ", e);
}
}
// No useful use case at the moment, used to clear alarm slots for testing.
private void deleteAlarm(TransactionBuilder builder, int index) {
if (0 < index && index < 4) {
byte[] alarmValue = new byte[]{(byte) index, 0x00, 0x00, 0x00, 0x00, 0x00};
builder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE),
buildCommand(WatchXPlusConstants.CMD_ALARM_SETTINGS,
WatchXPlusConstants.WRITE_VALUE,
alarmValue));
}
}
private void setAlarm(Alarm alarm, int index, TransactionBuilder builder) {
// Shift the GB internal repetition mask to match the device specific one.
byte repetitionMask = (byte) ((alarm.getRepetition() << 1) | (alarm.isRepetitive() ? 0x80 : 0x00));
repetitionMask |= (alarm.getRepetition(Alarm.ALARM_SUN) ? 0x01 : 0x00);
if (0 < index && index < 4) {
byte[] alarmValue = new byte[]{(byte) index,
BcdUtil.toBcd8(AlarmUtils.toCalendar(alarm).get(Calendar.HOUR_OF_DAY)),
BcdUtil.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 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
// set settings for missed call
//int repeatCount = WatchXPlusDeviceCoordinator.getRepeatOnCall();
final boolean continuousRing = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getBoolean(WatchXPlusConstants.PREF_CONTINIOUS_RING, false);
int repeatCount = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getInt(WatchXPlusConstants.PREF_REPEAT_RING, 0);
final boolean enableMissedCall = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getBoolean(WatchXPlusConstants.PREF_MISSED_CALL_ENABLE, false);
int repeatCountMissed = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getInt(WatchXPlusConstants.PREF_MISSED_CALL_REPEAT, 0);
switch (callSpec.command) {
case CallSpec.CALL_INCOMING:
isRinging = true;
remainingRepeats = repeatCount;
LOG.info(" Incoming 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 {
// possible missed call
isMissedCall = true;
// 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;
isMissedCall = false;
cancelNotification();
LOG.info(" Call start ");
break;
case CallSpec.CALL_REJECT:
isRinging = false;
isMissedCall = false;
cancelNotification();
LOG.info(" Call reject ");
break;
case CallSpec.CALL_ACCEPT:
isRinging = false;
isMissedCall = false;
cancelNotification();
LOG.info(" Call accept ");
break;
case CallSpec.CALL_OUTGOING:
isRinging = false;
isMissedCall = false;
cancelNotification();
LOG.info(" Outgoing call ");
break;
case CallSpec.CALL_END:
LOG.info(" End call ");
isRinging = false;
// it's a missed call, don't clear notification to preserve small icon near bluetooth
if (isMissedCall) {
remainingMissedRepeats = repeatCountMissed;
// send missed call notification if enabled in settings
if (enableMissedCall) {
LOG.info(" Missed call reminder ");
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");
LOG.info(" Missed call reminder repeats to go: " + remainingMissedRepeats);
// re-run handler
handler.postDelayed(this, repeatMissedDelay);
} else {
remainingMissedRepeats = 0;
LOG.info(" Missed call reminder repeats to go: " + remainingMissedRepeats);
isMissedCall = false;
// stop handler
handler.removeCallbacks(this);
cancelNotification();
}
}
}, repeatMissedDelay);
} else {
remainingMissedRepeats = 0;
isMissedCall = false;
cancelNotification();
}
} else {
isRinging = 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
*/
private void handleButtonWhenRing() {
GBDeviceEventCallControl callCmd = new GBDeviceEventCallControl();
// get saved settings if true - reject call, otherwise ignore call
boolean buttonReject = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getBoolean(WatchXPlusConstants.PREF_BUTTON_REJECT, false);
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.info(" 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 onFetchRecordedData(int dataTypes) {
TransactionBuilder builder;
// get battery state
try {
builder = performInitialized("getBatteryInfo");
builder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE),
buildCommand(WatchXPlusConstants.CMD_BATTERY_INFO,
WatchXPlusConstants.READ_VALUE));
builder.queue(getQueue());
} catch (IOException e) {
LOG.warn(" Unable to retrieve battery data ", e);
}
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 onScreenshotReq() {
sendBloodPressureCalibration();
}
@Override
public void onSendConfiguration(String config) {
TransactionBuilder builder;
SharedPreferences sharedPreferences = GBApplication.getDeviceSpecificSharedPrefs(this.getDevice().getAddress());
LOG.info(" onSendConfiguration: " + config);
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 DeviceSettingsPreferenceConst.PREF_POWER_MODE:
setPowerMode();
break;
case DeviceSettingsPreferenceConst.PREF_LANGUAGE:
setLanguageAndTimeFormat(builder);
break;
case DeviceSettingsPreferenceConst.PREF_INACTIVITY_THRESHOLD:
case DeviceSettingsPreferenceConst.PREF_INACTIVITY_ENABLE:
setLongSitHours(builder);
break;
// calibrations
case DeviceSettingsPreferenceConst.PREF_ALTITUDE_CALIBRATE:
setAltitude(builder);
break;
case DeviceSettingsPreferenceConst.PREF_BUTTON_BP_CALIBRATE:
sendBloodPressureCalibration();
break;
// settings from device card
case DeviceSettingsPreferenceConst.PREF_LIFTWRIST_NOSHED:
setHeadsUpScreen(builder);
getShakeStatus(builder);
break;
case DeviceSettingsPreferenceConst.PREF_DISCONNECTNOTIF_NOSHED:
setDisconnectReminder(builder);
break;
case DeviceSettingsPreferenceConst.PREF_TIMEFORMAT:
setLanguageAndTimeFormat(builder);
break;
case DeviceSettingsPreferenceConst.PREF_DO_NOT_DISTURB_NOAUTO:
setDNDHours(builder);
break;
}
builder.queue(getQueue());
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void onReadConfiguration(String config) {
LOG.info(" onReadConfiguration : " + 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
*/
private void setLongSitHours(TransactionBuilder builder, boolean enable, int hourStart, int minuteStart, int hourEnd, int minuteEnd, int period) {
LOG.info(" Setting Long sit reminder... Enabled:"+enable+" Period:"+period);
LOG.info(" Setting Long sit 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];
// do not remind
bArr[0] = (byte) hourEnd; // byte[08]
bArr[1] = (byte) minuteEnd; // byte[09]
bArr[2] = (byte) hourStart; // byte[10]
bArr[3] = (byte) minuteStart; // byte[11]
// remind
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);
}
/** get Long sit settings from app, and send it to watch
* @param builder - transaction builder
*/
private void setLongSitHours(TransactionBuilder builder) {
Calendar start = new GregorianCalendar();
Calendar end = new GregorianCalendar();
boolean enable = WatchXPlusDeviceCoordinator.getLongSitHours(gbDevice.getAddress(), start, end);
if (enable) {
String periodString = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getString(DeviceSettingsPreferenceConst.PREF_INACTIVITY_THRESHOLD, "60");
int period = Integer.parseInt(periodString);
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
*/
private void setLongSitSwitch(TransactionBuilder tbuilder, boolean enable) {
LOG.info(" 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)}));
}
/** 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
*/
private void setDNDHours(TransactionBuilder builder, boolean enable, int hourStart, int minuteStart, int hourEnd, int minuteEnd) {
LOG.info(" Setting DND time... Hs:"+hourStart+" Ms:"+minuteStart+" He:"+hourEnd+" Me:"+minuteEnd);
// set DND time
byte[] command = WatchXPlusConstants.CMD_SET_DND_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)
setDNDHoursSwitch(builder, enable);
}
/** set do not disturb switch
* @param tbuilder - transaction builder
* @param enable - true or false
*/
private void setDNDHoursSwitch(TransactionBuilder tbuilder, boolean enable) {
LOG.info(" Setting DND switch to: " + enable);
tbuilder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE),
buildCommand(WatchXPlusConstants.CMD_SET_DND_HOURS_SWITCH,
WatchXPlusConstants.WRITE_VALUE,
new byte[]{(byte) (enable ? 0x01 : 0x00)}));
}
/** get DND settings from app, and send it to watch
* @param builder - transaction builder
*/
private void setDNDHours(TransactionBuilder builder) {
Calendar start = new GregorianCalendar();
Calendar end = new GregorianCalendar();
boolean enable = WatchXPlusDeviceCoordinator.getDNDHours(gbDevice.getAddress(), start, end);
if (enable) {
this.setDNDHours(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.setDNDHoursSwitch(builder, enable);
}
}
/** set watch power
* switch watch power mode
* modes (0- normal, 1- energysaving, 2- only watch)
*/
private void setPowerMode() {
byte setWatchPowerMode = 0x00;
String powermodeStr = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getString(DeviceSettingsPreferenceConst.PREF_POWER_MODE, "0");
if (powermodeStr.equals("0")) {
setWatchPowerMode = 0x00;
} else if (powermodeStr.equals("1")) {
setWatchPowerMode = 0x01;
} else if (powermodeStr.equals("2")) {
setWatchPowerMode = 0x02;
}
byte[] bArr = new byte[1];
bArr[0] = (byte) setWatchPowerMode;
LOG.info(" setting power mode to: " + setWatchPowerMode);
try {
TransactionBuilder builder = performInitialized("setWatchPowerMode");
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);
}
//prefs.getPreferences().edit().putInt("PREF_POWER_MODE", 0).apply();
}
/** 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;
String unitsPref = GBApplication.getPrefs().getString(SettingsActivity.PREF_MEASUREMENT_SYSTEM, GBApplication.getContext().getString(R.string.p_unit_metric));
if (unitsPref.equals(GBApplication.getContext().getString(R.string.p_unit_imperial))) {
units = 1;
LOG.info(" Changed units: imperial ");
} else {
LOG.info(" Changed units: metric ");
}
byte[] bArr = new byte[3];
bArr[0] = (byte) units; // metric - 0/imperial - 1
bArr[1] = (byte) 0x00; // time unit 12/24h (there is a 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 {
String beginCalibration = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getString(DeviceSettingsPreferenceConst.PREF_BUTTON_BP_CALIBRATE, "0");
if (beginCalibration.equals("1")) {
LOG.info(" Calibrating BP - cancel " + beginCalibration);
return;
}
String mLowPString = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getString(WatchXPlusConstants.PREF_BP_CAL_LOW, "80");
int mLowP = Integer.parseInt(mLowPString);
String mHighPString = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getString(WatchXPlusConstants.PREF_BP_CAL_HIGH, "130");
int mHighP = Integer.parseInt(mHighPString);
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.info(" 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);
}
}
@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.info(" 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();
} 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: " + value);
handleDataCount(value);
} else if (ArrayUtils.equals(value, WatchXPlusConstants.RESP_DATA_DETAILS, 5)) {
LOG.info(" Received data details: " + value);
handleDataDetails(value);
} else if (ArrayUtils.equals(value, WatchXPlusConstants.RESP_DATA_CONTENT, 5)) {
LOG.info(" Received data content: " + value);
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<Integer, Integer> 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<WatchXPlusActivitySample> samples = new ArrayList<>();
if (DataType.SLEEP.equals(type)) {
WatchXPlusHealthActivityOverlayDao overlayDao = dbHandler.getDaoSession().getWatchXPlusHealthActivityOverlayDao();
List<WatchXPlusHealthActivityOverlay> 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(" SLEEP requested timestamp " + requestedDataTimestamp + " chunkNo " + chunkNo + " Got data: " + new Date((long) tsWithOffset * 1000) + ", rawIntensity: " + 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(" HEART RATE 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);
for (int i = 4; i < value.length; i++) {
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(" UNSUPPORTED requested timestamp for type: " + type + " " + requestedDataTimestamp + " chunkNo " + chunkNo + " Got data: " + new Date((long) tsWithOffset * 1000) + ", rawIntensity: " + val);
}
}
} catch (Exception ex) {
LOG.warn((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<Integer, Integer> 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.info(" Received goal stepsAim: " + stepsAim);
}
private void handleStepsInfo(byte[] value) {
int steps = Conversion.fromByteArr16(value[8], value[9]);
LOG.info(" 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.info("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.warn(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<WatchXPlusActivitySample> 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.warn(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) {
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) {
this.setHeadsUpScreen(transaction); // lift wirst to screen on
this.setDNDHours(transaction); // DND
this.setDisconnectReminder(transaction); // disconnect reminder
this.setLanguageAndTimeFormat(transaction); // set time mode 12/24h
this.setAltitude(transaction); // set altitude calibration
this.setLongSitHours(transaction); // 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);
}
}
// Command to toggle Lift Wrist to Light Screen, and shake to ignore/reject call
private void setHeadsUpScreen(TransactionBuilder transactionBuilder) {
boolean enable = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getBoolean(DeviceSettingsPreferenceConst.PREF_LIFTWRIST_NOSHED, false);
boolean shakeReject = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getBoolean(WatchXPlusConstants.PREF_SHAKE_REJECT, false);
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));
}
// command to set disconnect reminder
private void setDisconnectReminder(TransactionBuilder transactionBuilder) {
boolean enable = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getBoolean(DeviceSettingsPreferenceConst.PREF_DISCONNECTNOTIF_NOSHED, false);
transactionBuilder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE),
buildCommand(WatchXPlusConstants.CMD_DISCONNECT_REMIND,
WatchXPlusConstants.WRITE_VALUE,
new byte[]{(byte) (enable ? 0x01 : 0x00)}));
}
// 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) {
String altitudeString = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getString(DeviceSettingsPreferenceConst.PREF_ALTITUDE_CALIBRATE, "200");
int mAltitude = Integer.parseInt(altitudeString);
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 void setLanguageAndTimeFormat(TransactionBuilder transactionBuilder) {
byte setLanguage, setTimeMode;
String languageString = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getString(DeviceSettingsPreferenceConst.PREF_LANGUAGE, "1");
if (languageString == null || languageString.equals("1")) {
setLanguage = 0x01;
} else {
setLanguage = 0x00;
}
String timeformatString = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getString(DeviceSettingsPreferenceConst.PREF_TIMEFORMAT, "1");
assert timeformatString != null;
if (timeformatString.equals(getContext().getString(R.string.p_timeformat_24h))) {
setTimeMode = WatchXPlusConstants.ARG_SET_TIMEMODE_24H;
} else {
setTimeMode = WatchXPlusConstants.ARG_SET_TIMEMODE_12H;
}
byte[] bArr = new byte[2];
bArr[0] = setLanguage; //byte[08] language
bArr[1] = setTimeMode; //byte[09] time
transactionBuilder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE),
buildCommand(WatchXPlusConstants.CMD_TIME_LANGUAGE,
WatchXPlusConstants.WRITE_VALUE,
bArr));
}
@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[] 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};
}
// --Commented out by Inspection START (13.12.2019 г. 23:38):
// static int calculateLow(byte... bArr) {
// int i = 0;
// int i2 = 0;
// while (i < bArr.length) {
// i2 += (bArr[i] & 255) << (i * 8);
// i++;
// }
// return i2;
// }
// --Commented out by Inspection STOP (13.12.2019 г. 23:38)
}
}