1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-06-21 04:20:27 +02:00
Gadgetbridge/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/makibeshr3/MakibesHR3DeviceSupport.java
2023-06-13 12:06:13 +00:00

1066 lines
41 KiB
Java

/* Copyright (C) 2019-2021 Andreas Shimokawa, Cre3per, 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/>. */
// TODO: All the commands that aren't supported by GB should be added to device specific settings.
// TODO: It'd be cool if we could change the language. There's no official way to do so, but the
// TODO: watch is sold as chinese/english. Screen-on-time would be nice too.
// TODO: Firmware upgrades. WearFit tries to connect to Wake up Technology at
// TODO: http://47.112.119.52/app.php/Api/hardUpdate/type/55
// TODO: But that server resets the connection.
// TODO: The host is supposed to be www.iwhop.com, but that domain no longer exists.
// TODO: I think /app.php is missing a closing php tag.
package nodomain.freeyourgadget.gadgetbridge.service.devices.makibeshr3;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.CountDownTimer;
import android.os.Handler;
import android.widget.Toast;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.List;
import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
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.GBDeviceEventFindPhone;
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.makibeshr3.MakibesHR3Constants;
import nodomain.freeyourgadget.gadgetbridge.devices.makibeshr3.MakibesHR3Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.makibeshr3.MakibesHR3SampleProvider;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.entities.MakibesHR3ActivitySample;
import nodomain.freeyourgadget.gadgetbridge.entities.User;
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.DeviceService;
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.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class MakibesHR3DeviceSupport extends AbstractBTLEDeviceSupport implements SharedPreferences.OnSharedPreferenceChangeListener {
private static final Logger LOG = LoggerFactory.getLogger(MakibesHR3DeviceSupport.class);
// The delay must be at least as long as it takes the watch to respond.
// Reordering the requests could maybe reduce the delay, but this works fine too.
private final CountDownTimer mFetchCountDown = new CountDownTimer(2000, 2000) {
@Override
public void onTick(long millisUntilFinished) {
}
@Override
public void onFinish() {
LOG.debug("download finished");
GB.updateTransferNotification(null, "", false, 100, getContext());
}
};
private final Handler mFindPhoneHandler = new Handler();
private BluetoothGattCharacteristic mControlCharacteristic = null;
private BluetoothGattCharacteristic mReportCharacteristic = null;
public MakibesHR3DeviceSupport() {
super(LOG);
addSupportedService(MakibesHR3Constants.UUID_SERVICE);
}
/**
* Called whenever data is received to postpone the removing of the progress notification.
*
* @param start Start showing the notification
*/
private void fetch(boolean start) {
if (start) {
// We don't know how long the watch is going to take to reply. Keep progress at 0.
GB.updateTransferNotification(null, getContext().getString(R.string.busy_task_fetch_activity_data), true, 0, getContext());
}
this.mFetchCountDown.cancel();
this.mFetchCountDown.start();
}
@Override
public boolean useAutoConnect() {
return false;
}
/**
* @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);
}
/**
* @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);
MakibesHR3SampleProvider provider = new MakibesHR3SampleProvider(this.getDevice(), dbHandler.getDaoSession());
List<MakibesHR3ActivitySample> samples = provider.getAllActivitySamples(
(int) (dayStart.getTimeInMillis() / 1000L),
(int) (dayEnd.getTimeInMillis() / 1000L));
int totalSteps = 0;
for (MakibesHR3ActivitySample sample : samples) {
totalSteps += sample.getSteps();
}
return totalSteps;
} catch (Exception ex) {
LOG.error(ex.getMessage());
return 0;
}
}
public MakibesHR3ActivitySample createActivitySample(Device device, User user, int timestampInSeconds, SampleProvider provider) {
MakibesHR3ActivitySample sample = new MakibesHR3ActivitySample();
sample.setDevice(device);
sample.setUser(user);
sample.setTimestamp(timestampInSeconds);
sample.setProvider(provider);
return sample;
}
@Override
public void onNotification(NotificationSpec notificationSpec) {
TransactionBuilder transactionBuilder = this.createTransactionBuilder("onnotificaiton");
byte sender;
switch (notificationSpec.type) {
case FACEBOOK:
case FACEBOOK_MESSENGER:
sender = MakibesHR3Constants.ARG_SEND_NOTIFICATION_SOURCE_FACEBOOK;
break;
case LINE:
sender = MakibesHR3Constants.ARG_SEND_NOTIFICATION_SOURCE_LINE;
break;
case TELEGRAM:
sender = MakibesHR3Constants.ARG_SEND_NOTIFICATION_SOURCE_MESSAGE;
break;
case TWITTER:
sender = MakibesHR3Constants.ARG_SEND_NOTIFICATION_SOURCE_TWITTER;
break;
case WECHAT:
sender = MakibesHR3Constants.ARG_SEND_NOTIFICATION_SOURCE_WECHAT;
break;
case WHATSAPP:
sender = MakibesHR3Constants.ARG_SEND_NOTIFICATION_SOURCE_WHATSAPP;
break;
case KAKAO_TALK:
sender = MakibesHR3Constants.ARG_SEND_NOTIFICATION_SOURCE_KAKOTALK;
break;
default:
sender = MakibesHR3Constants.ARG_SEND_NOTIFICATION_SOURCE_MESSAGE;
break;
}
String message = "";
if (notificationSpec.title != null) {
message += (notificationSpec.title + ": ");
}
message += notificationSpec.body;
this.sendNotification(transactionBuilder,
sender, message);
try {
this.performConnected(transactionBuilder.getTransaction());
} catch (Exception ex) {
LoggerFactory.getLogger(this.getClass()).error("notification failed");
}
}
@Override
public void onSetTime() {
TransactionBuilder transactionBuilder = this.createTransactionBuilder("settime");
this.setDateTime(transactionBuilder);
try {
this.performConnected(transactionBuilder.getTransaction());
} catch (Exception ex) {
LoggerFactory.getLogger(this.getClass()).error("factory reset failed");
}
}
@Override
public void onSetAlarms(ArrayList<? extends Alarm> alarms) {
TransactionBuilder transactionBuilder = this.createTransactionBuilder("setalarms");
for (int i = 0; i < alarms.size(); ++i) {
Alarm alarm = alarms.get(i);
byte repetition = 0x00;
switch (alarm.getRepetition()) {
case Alarm.ALARM_ONCE:
repetition = MakibesHR3Constants.ARG_SET_ALARM_REMINDER_REPEAT_ONE_TIME;
break;
case Alarm.ALARM_MON:
repetition |= MakibesHR3Constants.ARG_SET_ALARM_REMINDER_REPEAT_MONDAY;
case Alarm.ALARM_TUE:
repetition |= MakibesHR3Constants.ARG_SET_ALARM_REMINDER_REPEAT_TUESDAY;
case Alarm.ALARM_WED:
repetition |= MakibesHR3Constants.ARG_SET_ALARM_REMINDER_REPEAT_WEDNESDAY;
case Alarm.ALARM_THU:
repetition |= MakibesHR3Constants.ARG_SET_ALARM_REMINDER_REPEAT_THURSDAY;
case Alarm.ALARM_FRI:
repetition |= MakibesHR3Constants.ARG_SET_ALARM_REMINDER_REPEAT_FRIDAY;
case Alarm.ALARM_SAT:
repetition |= MakibesHR3Constants.ARG_SET_ALARM_REMINDER_REPEAT_SATURDAY;
case Alarm.ALARM_SUN:
repetition |= MakibesHR3Constants.ARG_SET_ALARM_REMINDER_REPEAT_SUNDAY;
break;
default:
LOG.warn("invalid alarm repetition " + alarm.getRepetition());
break;
}
// Should we use @alarm.getPosition() rather than @i?
this.setAlarmReminder(
transactionBuilder,
i,
alarm.getEnabled(),
alarm.getHour(),
alarm.getMinute(),
repetition);
}
try {
this.performConnected(transactionBuilder.getTransaction());
} catch (Exception ex) {
LoggerFactory.getLogger(this.getClass()).error("setalarms failed");
}
}
@Override
public void onSetCallState(CallSpec callSpec) {
TransactionBuilder transactionBuilder = this.createTransactionBuilder("callstate");
LOG.debug("callSpec " + callSpec.command);
if (callSpec.command == CallSpec.CALL_INCOMING) {
this.sendNotification(transactionBuilder, MakibesHR3Constants.ARG_SEND_NOTIFICATION_SOURCE_CALL, callSpec.name);
} else {
this.sendNotification(transactionBuilder, MakibesHR3Constants.ARG_SEND_NOTIFICATION_SOURCE_STOP_CALL, "");
}
try {
this.performConnected(transactionBuilder.getTransaction());
} catch (Exception ex) {
LoggerFactory.getLogger(this.getClass()).error("call state failed");
}
}
@Override
public void onReset(int flags) {
if ((flags & GBDeviceProtocol.RESET_FLAGS_FACTORY_RESET) != 0) {
TransactionBuilder transactionBuilder = this.createTransactionBuilder("reset");
this.factoryReset(transactionBuilder);
try {
this.performConnected(transactionBuilder.getTransaction());
} catch (Exception ex) {
LoggerFactory.getLogger(this.getClass()).error("factory reset failed");
}
} else if ((flags & GBDeviceProtocol.RESET_FLAGS_REBOOT) != 0) {
TransactionBuilder transactionBuilder = this.createTransactionBuilder("reboot");
this.reboot(transactionBuilder);
try {
this.performConnected(transactionBuilder.getTransaction());
} catch (Exception ex) {
LoggerFactory.getLogger(this.getClass()).error("factory reset failed");
}
}
}
@Override
public void onEnableRealtimeHeartRateMeasurement(boolean enable) {
TransactionBuilder transactionBuilder = this.createTransactionBuilder("finddevice");
this.setEnableRealTimeHeartRate(transactionBuilder, enable);
try {
this.performConnected(transactionBuilder.getTransaction());
} catch (Exception e) {
LOG.debug("ERROR");
}
}
private void onReverseFindDevice(boolean start) {
if (start) {
SharedPreferences sharedPreferences = GBApplication.getDeviceSpecificSharedPrefs(
this.getDevice().getAddress());
int findPhone = MakibesHR3Coordinator.getFindPhone(sharedPreferences);
if (findPhone != MakibesHR3Coordinator.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);
}
}
@Override
public void onFindDevice(boolean start) {
if (!start) {
return;
}
TransactionBuilder transactionBuilder = this.createTransactionBuilder("finddevice");
this.findDevice(transactionBuilder);
try {
this.performConnected(transactionBuilder.getTransaction());
} catch (Exception e) {
LOG.debug("ERROR");
}
}
private void syncPreferences(TransactionBuilder transaction) {
SharedPreferences sharedPreferences = GBApplication.getDeviceSpecificSharedPrefs(this.getDevice().getAddress());
this.setTimeMode(transaction, sharedPreferences);
this.setDateTime(transaction);
this.setQuiteHours(transaction, sharedPreferences);
this.setHeadsUpScreen(transaction, sharedPreferences);
this.setLostReminder(transaction, sharedPreferences);
ActivityUser activityUser = new ActivityUser();
this.setPersonalInformation(transaction,
(byte) Math.round(activityUser.getHeightCm() * 0.43), // Thanks no1f1
activityUser.getAge(),
activityUser.getHeightCm(),
activityUser.getWeightKg(),
activityUser.getStepsGoal() / 1000);
this.fetch(true);
}
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
LOG.debug(key + " changed");
if (!this.isConnected()) {
LOG.debug("ignoring change, we're disconnected");
return;
}
TransactionBuilder transactionBuilder = this.createTransactionBuilder("onSharedPreferenceChanged");
if (key.equals(DeviceSettingsPreferenceConst.PREF_TIMEFORMAT)) {
this.setTimeMode(transactionBuilder, sharedPreferences);
} else if (key.equals(DeviceSettingsPreferenceConst.PREF_ACTIVATE_DISPLAY_ON_LIFT)) {
this.setHeadsUpScreen(transactionBuilder, sharedPreferences);
} else if (key.equals(DeviceSettingsPreferenceConst.PREF_DISCONNECT_NOTIFICATION)) {
this.setLostReminder(transactionBuilder, sharedPreferences);
} else if (key.equals(DeviceSettingsPreferenceConst.PREF_DO_NOT_DISTURB_NOAUTO) ||
key.equals(DeviceSettingsPreferenceConst.PREF_DO_NOT_DISTURB_NOAUTO_START) ||
key.equals(DeviceSettingsPreferenceConst.PREF_DO_NOT_DISTURB_NOAUTO_END)) {
this.setQuiteHours(transactionBuilder, sharedPreferences);
} else if (key.equals(DeviceSettingsPreferenceConst.PREF_FIND_PHONE) ||
key.equals(DeviceSettingsPreferenceConst.PREF_FIND_PHONE_DURATION)) {
// No action, we check the shared preferences when the device tries to ring the phone.
} else {
return;
}
try {
this.performConnected(transactionBuilder.getTransaction());
} catch (Exception ex) {
LOG.warn(ex.getMessage());
}
}
/**
* Use to show the battery icon in the device card.
* If the icon shows up later, the user might be trying to tap one thing but the battery icon
* will shift everything.
* This is hacky. There should be a "supportsBattery" function in the coordinator that displays
* the battery icon before the battery level is received.
*/
private void fakeBattery() {
GBDeviceEventBatteryInfo batteryInfo = new GBDeviceEventBatteryInfo();
batteryInfo.level = 100;
batteryInfo.state = BatteryState.UNKNOWN;
this.handleGBDeviceEvent(batteryInfo);
}
@Override
protected TransactionBuilder initializeDevice(TransactionBuilder builder) {
this.fakeBattery();
GB.updateTransferNotification(null, getContext().getString(R.string.busy_task_fetch_activity_data), true, 0, getContext());
gbDevice.setState(GBDevice.State.INITIALIZING);
gbDevice.sendDeviceUpdateIntent(getContext());
this.mControlCharacteristic = getCharacteristic(MakibesHR3Constants.UUID_CHARACTERISTIC_CONTROL);
this.mReportCharacteristic = getCharacteristic(MakibesHR3Constants.UUID_CHARACTERISTIC_REPORT);
builder.notify(this.mReportCharacteristic, true);
builder.setCallback(this);
// Allow modifications
builder.write(this.mControlCharacteristic, new byte[]{0x01, 0x00});
// Initialize device
this.syncPreferences(builder);
this.requestFitness(builder);
gbDevice.setState(GBDevice.State.INITIALIZED);
gbDevice.sendDeviceUpdateIntent(getContext());
getDevice().setFirmwareVersion("N/A");
getDevice().setFirmwareVersion2("N/A");
SharedPreferences preferences = GBApplication.getDeviceSpecificSharedPrefs(this.getDevice().getAddress());
preferences.registerOnSharedPreferenceChangeListener(this);
return builder;
}
private void addGBActivitySamples(MakibesHR3ActivitySample[] samples) {
try (DBHandler dbHandler = GBApplication.acquireDB()) {
User user = DBHelper.getUser(dbHandler.getDaoSession());
Device device = DBHelper.getDevice(this.getDevice(), dbHandler.getDaoSession());
MakibesHR3SampleProvider provider = new MakibesHR3SampleProvider(this.getDevice(), dbHandler.getDaoSession());
for (MakibesHR3ActivitySample sample : samples) {
sample.setDevice(device);
sample.setUser(user);
sample.setProvider(provider);
sample.setRawIntensity(ActivitySample.NOT_MEASURED);
provider.addGBActivitySample(sample);
}
} catch (Exception ex) {
// Why is this a toast? The user doesn't care about the error.
GB.toast(getContext(), "Error saving samples: " + ex.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR);
GB.updateTransferNotification(null, "Data transfer failed", false, 0, getContext());
LOG.error(ex.getMessage());
}
}
private void addGBActivitySample(MakibesHR3ActivitySample sample) {
this.addGBActivitySamples(new MakibesHR3ActivitySample[]{sample});
}
/**
* Should only be called after the sample has been populated by
* {@link MakibesHR3DeviceSupport#addGBActivitySample} or
* {@link MakibesHR3DeviceSupport#addGBActivitySamples}
*/
private void broadcastSample(MakibesHR3ActivitySample sample) {
Intent intent = new Intent(DeviceService.ACTION_REALTIME_SAMPLES)
.putExtra(DeviceService.EXTRA_REALTIME_SAMPLE, sample)
.putExtra(DeviceService.EXTRA_TIMESTAMP, sample.getTimestamp());
LocalBroadcastManager.getInstance(getContext()).sendBroadcast(intent);
}
private void onReceiveFitness(int steps) {
LOG.info("steps: " + steps);
this.onReceiveStepsSample(steps);
}
private void onReceiveHeartRate(int heartRate) {
LOG.info("heart rate: " + heartRate);
MakibesHR3ActivitySample sample = new MakibesHR3ActivitySample();
if (heartRate > 0) {
sample.setHeartRate(heartRate);
sample.setTimestamp((int) (System.currentTimeMillis() / 1000));
sample.setRawKind(ActivityKind.TYPE_ACTIVITY);
} else {
if (heartRate == MakibesHR3Constants.ARG_HEARTRATE_NO_TARGET) {
sample.setRawKind(ActivityKind.TYPE_NOT_WORN);
} else if (heartRate == MakibesHR3Constants.ARG_HEARTRATE_NO_READING) {
sample.setRawKind(ActivityKind.TYPE_NOT_MEASURED);
} else {
LOG.warn("invalid heart rate reading: " + heartRate);
return;
}
}
this.addGBActivitySample(sample);
this.broadcastSample(sample);
}
private void onReceiveHeartRateSample(int year, int month, int day, int hour, int minute, int heartRate) {
LOG.debug("received heart rate sample " + year + "-" + month + "-" + day + " " + hour + ":" + minute + " " + heartRate);
MakibesHR3ActivitySample sample = new MakibesHR3ActivitySample();
Calendar calendar = new GregorianCalendar(year, month - 1, day, hour, minute);
int timeStamp = (int) (calendar.getTimeInMillis() / 1000);
sample.setHeartRate(heartRate);
sample.setTimestamp(timeStamp);
sample.setRawKind(ActivityKind.TYPE_ACTIVITY);
this.addGBActivitySample(sample);
}
private void onReceiveStepsSample(int timeStamp, int steps) {
MakibesHR3ActivitySample sample = new MakibesHR3ActivitySample();
// 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");
sample.setSteps(steps - dayStepCount);
sample.setTimestamp(timeStamp);
sample.setRawKind(ActivityKind.TYPE_ACTIVITY);
this.addGBActivitySample(sample);
}
}
/**
* The time is the start of the measurement. Each measurement lasts 1h.
*/
private void onReceiveStepsSample(int year, int month, int day, int hour, int minute, int steps) {
LOG.debug("received steps sample " + year + "-" + month + "-" + day + " " + hour + ":" + minute + " " + steps);
Calendar calendar = new GregorianCalendar(year, month - 1, day, hour + 1, minute);
int timeStamp = (int) (calendar.getTimeInMillis() / 1000);
this.onReceiveStepsSample(timeStamp, steps);
}
private void onReceiveStepsSample(int steps) {
this.onReceiveStepsSample((int) (Calendar.getInstance().getTimeInMillis() / 1000l), steps);
}
@Override
public boolean onCharacteristicChanged(BluetoothGatt gatt,
BluetoothGattCharacteristic characteristic) {
if (super.onCharacteristicChanged(gatt, characteristic)) {
return true;
}
byte[] data = characteristic.getValue();
if (data.length < 6)
return true;
this.fetch(false);
UUID characteristicUuid = characteristic.getUuid();
if (characteristicUuid.equals(mReportCharacteristic.getUuid())) {
byte[] value = characteristic.getValue();
byte[] arguments = new byte[value.length - 6];
if (arguments.length >= 0) {
System.arraycopy(value, 6, arguments, 0, arguments.length);
}
byte[] report = new byte[]{value[4], value[5]};
switch (report[0]) {
case MakibesHR3Constants.RPRT_REVERSE_FIND_DEVICE:
this.onReverseFindDevice(arguments[0] == 0x01);
break;
case MakibesHR3Constants.RPRT_HEARTRATE:
if (value.length == 7) {
this.onReceiveHeartRate((int) arguments[0]);
}
break;
case MakibesHR3Constants.RPRT_BATTERY:
if (arguments.length == 2) {
GBDeviceEventBatteryInfo batteryInfo = new GBDeviceEventBatteryInfo();
batteryInfo.level = (short) (arguments[1] & 0xff);
batteryInfo.state = ((arguments[0] == 0x01) ? BatteryState.BATTERY_CHARGING : BatteryState.BATTERY_NORMAL);
this.handleGBDeviceEvent(batteryInfo);
}
break;
case MakibesHR3Constants.RPRT_SOFTWARE:
if (arguments.length == 11) {
this.getDevice().setFirmwareVersion(((int) (arguments[0] & 0xff)) + "." + (arguments[1] & 0xff));
}
break;
default: // Non-80 reports
if (Arrays.equals(report, MakibesHR3Constants.RPRT_FITNESS)) {
int steps = (arguments[1] & 0xff) * 0x100 + (arguments[2] & 0xff);
this.onReceiveFitness(
steps
);
} else if (Arrays.equals(report, MakibesHR3Constants.RPRT_HEART_RATE_SAMPLE)) {
this.onReceiveHeartRateSample(
(arguments[0] & 0xff) + 2000, (arguments[1] & 0xff), (arguments[2] & 0xff),
(arguments[3] & 0xff), (arguments[4] & 0xff),
(arguments[5] & 0xff));
} else if (Arrays.equals(report, MakibesHR3Constants.RPRT_STEPS_SAMPLE)) {
this.onReceiveStepsSample(
(arguments[0] & 0xff) + 2000, (arguments[1] & 0xff), (arguments[2] & 0xff),
(arguments[3] & 0xff), 0,
((arguments[5] & 0xff) * 0x100) + (arguments[6] & 0xff));
}
break;
}
}
return false;
}
private byte[] craftData(byte[] command, byte[] data) {
byte[] result = new byte[MakibesHR3Constants.DATA_TEMPLATE.length + data.length];
System.arraycopy(MakibesHR3Constants.DATA_TEMPLATE, 0, result, 0, MakibesHR3Constants.DATA_TEMPLATE.length);
result[MakibesHR3Constants.DATA_ARGUMENT_COUNT_INDEX] = (byte) (data.length + 3);
System.arraycopy(command, 0, result, 4, command.length);
System.arraycopy(data, 0, result, 6, data.length);
return result;
}
private byte[] craftData(byte command, byte[] data) {
return this.craftData(new byte[]{command}, data);
}
private byte[] craftData(byte command) {
return this.craftData(command, new byte[]{});
}
private void writeSafe(BluetoothGattCharacteristic characteristic, TransactionBuilder builder, byte[] data) {
final int maxMessageLength = 20;
// For every split, we need 1 byte extra.
int extraBytes = 0;
if (data.length > 20) {
extraBytes = (((data.length - maxMessageLength) / maxMessageLength) + 1);
}
int totalDataLength = (data.length + extraBytes);
int segmentCount = (((totalDataLength - 1) / maxMessageLength) + 1);
byte[] indexedData = new byte[totalDataLength];
int it = 0;
int segmentIndex = 0;
for (int i = 0; i < data.length; ++i) {
if ((i != 0) && ((it % maxMessageLength) == 0)) {
indexedData[it++] = (byte) segmentIndex++;
}
indexedData[it++] = data[i];
}
for (int i = 0; i < segmentCount; ++i) {
int segmentStart = (i * maxMessageLength);
int segmentLength;
if (i == (segmentCount - 1)) {
segmentLength = (indexedData.length - segmentStart);
} else {
segmentLength = maxMessageLength;
}
byte[] segment = new byte[segmentLength];
System.arraycopy(indexedData, segmentStart, segment, 0, segmentLength);
builder.write(characteristic, segment);
}
}
private MakibesHR3DeviceSupport factoryReset(TransactionBuilder transaction) {
transaction.write(this.mControlCharacteristic, this.craftData(MakibesHR3Constants.CMD_FACTORY_RESET));
return this.reboot(transaction);
}
/**
* Ugly because I don't like Date.
* All non-zero records after the given times will be returned via
* {@link MakibesHR3Constants#RPRT_HEART_RATE_SAMPLE},
* {@link MakibesHR3Constants#RPRT_STEPS_SAMPLE},
* {@link MakibesHR3Constants#RPRT_FITNESS}
*/
private MakibesHR3DeviceSupport requestFitness(TransactionBuilder transaction,
int yearStepsAfter, int monthStepsAfter, int dayStepsAfter,
int hourStepsAfter, int minuteStepsAfter,
int yearHeartRateAfter, int monthHeartRateAfter, int dayHeartRateAfter,
int hourHeartRateAfter, int minuteHeartRateAfter) {
byte[] data = this.craftData(MakibesHR3Constants.CMD_REQUEST_FITNESS,
new byte[]{
(byte) 0x00,
(byte) (yearStepsAfter - 2000),
(byte) monthStepsAfter,
(byte) dayStepsAfter,
(byte) hourStepsAfter,
(byte) minuteStepsAfter,
(byte) (yearHeartRateAfter - 2000),
(byte) monthHeartRateAfter,
(byte) dayHeartRateAfter,
(byte) hourHeartRateAfter,
(byte) minuteHeartRateAfter
});
transaction.write(this.mControlCharacteristic, data);
this.fetch(true);
return this;
}
/**
* Ugly because I don't like Date.
* All non-zero records after the given times will be returned via
* {@link MakibesHR3Constants#RPRT_HEART_RATE_SAMPLE},
* {@link MakibesHR3Constants#RPRT_STEPS_SAMPLE},
* {@link MakibesHR3Constants#RPRT_FITNESS}
*/
private MakibesHR3DeviceSupport requestFitness(TransactionBuilder transaction) {
try (DBHandler dbHandler = GBApplication.acquireDB()) {
MakibesHR3SampleProvider provider = new MakibesHR3SampleProvider(this.getDevice(), dbHandler.getDaoSession());
MakibesHR3ActivitySample latestSample = provider.getLatestActivitySample();
if (latestSample == null) {
this.requestFitness(transaction,
2000, 0, 0, 0, 0,
2000, 0, 0, 0, 0);
} else {
Calendar calendar = new GregorianCalendar();
calendar.setTime(new Date(latestSample.getTimestamp() * 1000l));
int year = calendar.get(Calendar.YEAR);
int month = (calendar.get(Calendar.MONTH) + 1);
int day = calendar.get(Calendar.DAY_OF_MONTH);
int hour = calendar.get(Calendar.HOUR_OF_DAY);
int minute = calendar.get(Calendar.MINUTE);
this.requestFitness(transaction,
year, month, day, hour, minute,
year, month, day, hour, minute);
}
} catch (Exception ex) {
LOG.error(ex.getMessage());
}
return this;
}
private MakibesHR3DeviceSupport findDevice(TransactionBuilder transaction) {
transaction.write(this.mControlCharacteristic, this.craftData(MakibesHR3Constants.CMD_FIND_DEVICE));
return this;
}
private MakibesHR3DeviceSupport sendNotification(TransactionBuilder transaction,
byte source, String message) {
byte[] data = new byte[message.length() + 2];
data[0] = source;
data[1] = (byte) 0x02;
for (int i = 0; i < message.length(); ++i) {
data[i + 2] = (byte) message.charAt(i);
}
this.writeSafe(
this.mControlCharacteristic,
transaction,
this.craftData(MakibesHR3Constants.CMD_SEND_NOTIFICATION, data));
return this;
}
private MakibesHR3DeviceSupport setAlarmReminder(TransactionBuilder transaction,
int id, boolean enable, int hour, int minute, byte repeat) {
transaction.write(this.mControlCharacteristic,
this.craftData(MakibesHR3Constants.CMD_SET_ALARM_REMINDER, new byte[]{
(byte) id,
(byte) (enable ? 0x01 : 0x00),
(byte) hour,
(byte) minute,
repeat
}));
return this;
}
/**
* @param transactionBuilder
* @param stepLength cm
* @param age years
* @param height cm
* @param weight kg
* @param stepGoal kilo
*/
private MakibesHR3DeviceSupport setPersonalInformation(TransactionBuilder transactionBuilder,
int stepLength, int age, int height, int weight, int stepGoal) {
byte[] data = this.craftData(MakibesHR3Constants.CMD_SET_PERSONAL_INFORMATION,
new byte[]{
(byte) stepLength,
(byte) age,
(byte) height,
(byte) weight,
MakibesHR3Constants.ARG_SET_PERSONAL_INFORMATION_UNIT_DISTANCE_KILOMETERS,
(byte) stepGoal,
(byte) 0x5a,
(byte) 0x82,
(byte) 0x3c,
(byte) 0x5a,
(byte) 0x28,
(byte) 0xb4,
(byte) 0x5d,
(byte) 0x64,
});
transactionBuilder.write(this.mControlCharacteristic, data);
return this;
}
private MakibesHR3DeviceSupport setHeadsUpScreen(TransactionBuilder transactionBuilder, boolean enable) {
byte[] data = this.craftData(MakibesHR3Constants.CMD_SET_HEADS_UP_SCREEN,
new byte[]{(byte) (enable ? 0x01 : 0x00)});
transactionBuilder.write(this.mControlCharacteristic, data);
return this;
}
private MakibesHR3DeviceSupport setQuiteHours(TransactionBuilder transactionBuilder,
boolean enable,
int hourStart, int minuteStart,
int hourEnd, int minuteEnd) {
byte[] data = this.craftData(MakibesHR3Constants.CMD_SET_QUITE_HOURS, new byte[]{
(byte) (enable ? 0x01 : 0x00),
(byte) hourStart, (byte) minuteStart,
(byte) hourEnd, (byte) minuteEnd
});
transactionBuilder.write(this.mControlCharacteristic, data);
return this;
}
private MakibesHR3DeviceSupport setQuiteHours(TransactionBuilder transactionBuilder,
SharedPreferences sharedPreferences) {
Calendar start = new GregorianCalendar();
Calendar end = new GregorianCalendar();
boolean enable = MakibesHR3Coordinator.getQuiteHours(sharedPreferences, start, end);
return this.setQuiteHours(transactionBuilder, enable,
start.get(Calendar.HOUR_OF_DAY), start.get(Calendar.MINUTE),
end.get(Calendar.HOUR_OF_DAY), end.get(Calendar.MINUTE));
}
private MakibesHR3DeviceSupport setHeadsUpScreen(TransactionBuilder transactionBuilder, SharedPreferences sharedPreferences) {
return this.setHeadsUpScreen(transactionBuilder,
MakibesHR3Coordinator.shouldEnableHeadsUpScreen(sharedPreferences));
}
private MakibesHR3DeviceSupport setLostReminder(TransactionBuilder transactionBuilder, boolean enable) {
byte[] data = this.craftData(MakibesHR3Constants.CMD_SET_LOST_REMINDER,
new byte[]{(byte) (enable ? 0x01 : 0x00)});
transactionBuilder.write(this.mControlCharacteristic, data);
return this;
}
private MakibesHR3DeviceSupport setLostReminder(TransactionBuilder transactionBuilder, SharedPreferences sharedPreferences) {
return this.setLostReminder(transactionBuilder,
MakibesHR3Coordinator.shouldEnableLostReminder(sharedPreferences));
}
private MakibesHR3DeviceSupport setTimeMode(TransactionBuilder transactionBuilder, byte timeMode) {
byte[] data = this.craftData(MakibesHR3Constants.CMD_SET_TIMEMODE, new byte[]{timeMode});
transactionBuilder.write(this.mControlCharacteristic, data);
return this;
}
private MakibesHR3DeviceSupport setTimeMode(TransactionBuilder transactionBuilder, SharedPreferences sharedPreferences) {
return this.setTimeMode(transactionBuilder,
MakibesHR3Coordinator.getTimeMode(sharedPreferences));
}
private MakibesHR3DeviceSupport setEnableRealTimeHeartRate(TransactionBuilder transaction, boolean enable) {
byte[] data = this.craftData(MakibesHR3Constants.CMD_SET_REAL_TIME_HEART_RATE, new byte[]{(byte) (enable ? 0x01 : 0x00)});
transaction.write(this.mControlCharacteristic, data);
return this;
}
private MakibesHR3DeviceSupport setDateTime(TransactionBuilder transaction,
int year,
int month,
int day,
int hour,
int minute,
int second) {
byte[] data = this.craftData(MakibesHR3Constants.CMD_SET_DATE_TIME,
new byte[]{
(byte) 0x00,
(byte) ((year & 0xff00) >> 8),
(byte) (year & 0x00ff),
(byte) month,
(byte) day,
(byte) hour,
(byte) minute,
(byte) second
});
transaction.write(this.mControlCharacteristic, data);
return this;
}
private MakibesHR3DeviceSupport setDateTime(TransactionBuilder transaction) {
Calendar calendar = Calendar.getInstance();
return this.setDateTime(transaction,
calendar.get(Calendar.YEAR),
calendar.get(Calendar.MONTH) + 1,
calendar.get(Calendar.DAY_OF_MONTH),
calendar.get(Calendar.HOUR_OF_DAY),
calendar.get(Calendar.MINUTE),
calendar.get(Calendar.SECOND)
);
}
private MakibesHR3DeviceSupport reboot(TransactionBuilder transaction) {
transaction.write(this.mControlCharacteristic, this.craftData(MakibesHR3Constants.CMD_REBOOT));
return this;
}
}