/* Copyright (C) 2019-2024 Andreas Shimokawa, Arjan Schrijver, Carsten
Pfeiffer, Daniel Dakhno, Hasan Ammar, Petr Vaněk
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see . */
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.fossil;
import static nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.QHybridSupport.ITEM_STEP_GOAL;
import static nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.QHybridSupport.ITEM_TIMEZONE_OFFSET;
import static nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.QHybridSupport.ITEM_VIBRATION_STRENGTH;
import static nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.QHybridSupport.QHYBRID_EVENT_BUTTON_PRESS;
import static nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.QHybridSupport.QHYBRID_EVENT_MULTI_BUTTON_PRESS;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
import android.widget.Toast;
import org.json.JSONArray;
import org.json.JSONException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.TimeZone;
import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.BuildConfig;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.HybridHRActivitySampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.NotificationConfiguration;
import nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.PackageConfigHelper;
import nodomain.freeyourgadget.gadgetbridge.entities.HybridHRActivitySample;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.Alarm;
import nodomain.freeyourgadget.gadgetbridge.model.GenericItem;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.QHybridSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.WatchAdapter;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.buttonconfig.ConfigFileBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.buttonconfig.ConfigPayload;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.file.FileHandle;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.parser.ActivityEntry;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.parser.ActivityFileParser;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.Request;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.FossilRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.RequestMtuRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.SetDeviceStateRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.alarm.AlarmsSetRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.configuration.ConfigurationPutRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.device_info.DeviceInfo;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.device_info.DeviceSecurityVersionInfo;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.device_info.GetDeviceInfoRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.device_info.SupportedFileVersionsInfo;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.file.FileDeleteRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.file.FileLookupAndGetRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.file.FilePutRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.file.FirmwareFilePutRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.notification.NotificationFilterPutRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.notification.PlayTextNotificationRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.AnimationRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.MoveHandsRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.ReleaseHandsControlRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.RequestHandControlRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.SaveCalibrationRequest;
import nodomain.freeyourgadget.gadgetbridge.util.AlarmUtils;
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.util.UriHelper;
public class FossilWatchAdapter extends WatchAdapter {
private ArrayList requestQueue = new ArrayList<>();
protected FossilRequest fossilRequest;
private int MTU = 23;
private final String ITEM_MTU = "MTU";
static public final String ITEM_BUTTONS = "BUTTONS";
private final String CONFIG_ITEM_STEP_GOAL = "step_goal";
private final String CONFIG_ITEM_VIBRATION_STRENGTH = "vibration_strength";
private final String CONFIG_ITEM_TIMEZONE_OFFSET = "timezone_offset";
public final String CONFIG_ITEM_BUTTONS = "buttons";
private int lastButtonIndex = -1;
protected final Logger LOG = LoggerFactory.getLogger(getClass().getSimpleName());
SupportedFileVersionsInfo supportedFileVersions;
public FossilWatchAdapter(QHybridSupport deviceSupport) {
super(deviceSupport);
}
private final int REQUEST_TIMEOUT = 60 * 1000;
private Looper timeoutLooper = null;
private Handler timeoutHandler;
protected Thread timeoutThread = new Thread(new Runnable() {
@Override
public void run() {
Looper.prepare();
timeoutLooper = Looper.myLooper();
timeoutHandler = new Handler(timeoutLooper);
Looper.loop();
}
});
private Runnable requestTimeoutRunnable = new Runnable() {
@Override
public void run() {
String requestName = "unknown";
if(fossilRequest != null){
requestName = fossilRequest.getName();
}
log(String.format("Request %s timed out, queing next request", requestName));
fossilRequest = null;
queueNextRequest();
}
};
@Override
public void initialize() {
timeoutThread.start();
playPairingAnimation();
queueWrite(new RequestMtuRequest(512), false);
getDeviceInfos();
}
@Override
public void dispose() {
if (timeoutLooper != null) {
timeoutHandler.removeCallbacks(requestTimeoutRunnable);
timeoutLooper.quit();
}
super.dispose();
}
private void restartRequestTimeout(){
if(timeoutLooper == null){
return;
}
stopRequestTimeout();
log("restarting request timeout");
timeoutHandler.postDelayed(
requestTimeoutRunnable,
REQUEST_TIMEOUT
);
}
private void stopRequestTimeout(){
if(timeoutLooper == null){
return;
}
timeoutHandler.removeCallbacks(requestTimeoutRunnable);
log("stopped request timeout");
}
public short getSupportedFileVersion(FileHandle handle) {
return this.supportedFileVersions.getSupportedFileVersion(handle);
}
protected void initializeWithSupportedFileVersions() {
syncConfiguration();
syncNotificationSettings();
syncButtonSettings();
queueWrite(new SetDeviceStateRequest(GBDevice.State.INITIALIZED), false);
}
protected void getDeviceInfos() {
queueWrite(new GetDeviceInfoRequest(this) {
@Override
public void handleDeviceInfos(DeviceInfo[] deviceInfos) {
for (DeviceInfo info : deviceInfos) {
if (info instanceof SupportedFileVersionsInfo) {
FossilWatchAdapter.this.supportedFileVersions = (SupportedFileVersionsInfo) info;
} else if (info instanceof DeviceSecurityVersionInfo) {
getDeviceSupport().getDevice().addDeviceInfo(new GenericItem("DEVICE_SECURITY_VERSION", info.toString()));
}
}
initializeWithSupportedFileVersions();
}
});
}
private void syncButtonSettings() {
String buttonConfig = getDeviceSpecificPreferences().getString(CONFIG_ITEM_BUTTONS, null);
getDeviceSupport().getDevice().addDeviceInfo(new GenericItem(ITEM_BUTTONS, buttonConfig));
overwriteButtons(buttonConfig);
}
@Override
public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
if (status != BluetoothGatt.GATT_SUCCESS) {
if (characteristic.getUuid().toString().equals("3dda0005-957f-7d4a-34a6-74696673696d")) {
GB.log("authentication failed", GB.ERROR, null);
setDeviceState(GBDevice.State.AUTHENTICATION_REQUIRED);
requestQueue.clear();
}
log("characteristic write failed: " + status);
if (BuildConfig.DEBUG)
GB.toast(fossilRequest.getName() + " characteristic write failed: " + status, Toast.LENGTH_SHORT, GB.ERROR);
fossilRequest = null;
queueNextRequest();
}
}
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
log("status " + status + " newState: " + newState);
if (newState != BluetoothGatt.STATE_CONNECTED) {
log("status " + newState + " clearing queue...");
requestQueue.clear();
fossilRequest = null;
}
}
protected SharedPreferences getDeviceSpecificPreferences() {
return GBApplication.getDeviceSpecificSharedPrefs(
getDeviceSupport().getDevice().getAddress()
);
}
private void syncConfiguration() {
SharedPreferences preferences = getDeviceSpecificPreferences();
int stepGoal = preferences.getInt(CONFIG_ITEM_STEP_GOAL, 1000000);
byte vibrationStrength = (byte) preferences.getInt(CONFIG_ITEM_VIBRATION_STRENGTH, 100);
int timezoneOffset = preferences.getInt(CONFIG_ITEM_TIMEZONE_OFFSET, 0);
GBDevice device = getDeviceSupport().getDevice();
device.addDeviceInfo(new GenericItem(ITEM_STEP_GOAL, String.valueOf(stepGoal)));
device.addDeviceInfo(new GenericItem(ITEM_VIBRATION_STRENGTH, String.valueOf(vibrationStrength)));
device.addDeviceInfo(new GenericItem(ITEM_TIMEZONE_OFFSET, String.valueOf(timezoneOffset)));
queueWrite(new ConfigurationPutRequest(new ConfigurationPutRequest.ConfigItem[]{
new ConfigurationPutRequest.DailyStepGoalConfigItem(stepGoal),
new ConfigurationPutRequest.VibrationStrengthConfigItem(vibrationStrength),
new ConfigurationPutRequest.TimezoneOffsetConfigItem((short) timezoneOffset)
}, this));
setTime();
}
public int getMTU() {
if (this.MTU < 0) throw new RuntimeException("MTU not configured");
return this.MTU;
}
@Override
public void playPairingAnimation() {
queueWrite(new AnimationRequest(), false);
}
@Override
public void playNotification(NotificationConfiguration config) {
if (config.getPackageName() == null) {
log("package name in notification not set");
return;
}
queueWrite(new PlayTextNotificationRequest(config.getPackageName(), this), false);
}
@Override
public void setTime() {
queueWrite(
new ConfigurationPutRequest(generateTimeConfigItemNow(), this), false
);
}
protected ConfigurationPutRequest.TimeConfigItem generateTimeConfigItemNow() {
long millis = System.currentTimeMillis();
TimeZone zone = new GregorianCalendar().getTimeZone();
return new ConfigurationPutRequest.TimeConfigItem(
(int) (millis / 1000 + getDeviceSupport().getTimeOffset() * 60),
(short) (millis % 1000),
(short) (zone.getOffset(millis) / 60000)
);
}
@Override
public void overwriteButtons(String jsonConfigString) {
try {
if (jsonConfigString == null) return;
getDeviceSpecificPreferences()
.edit()
.putString(CONFIG_ITEM_BUTTONS, jsonConfigString)
.apply();
JSONArray buttonConfigJson = new JSONArray(jsonConfigString);
// JSONArray buttonConfigJson = new JSONArray(getDeviceSupport().getDevice().getDeviceInfo(ITEM_BUTTONS).getDetails());
ConfigPayload[] payloads = new ConfigPayload[buttonConfigJson.length()];
for (int i = 0; i < buttonConfigJson.length(); i++) {
try {
payloads[i] = ConfigPayload.valueOf(buttonConfigJson.getString(i));
} catch (IllegalArgumentException e) {
payloads[i] = ConfigPayload.FORWARD_TO_PHONE;
}
}
ConfigFileBuilder builder = new ConfigFileBuilder(payloads);
FilePutRequest fileUploadRequets = new FilePutRequest(FileHandle.SETTINGS_BUTTONS, builder.build(true), this) {
@Override
public void onFilePut(boolean success) {
if (success)
GB.toast("successfully overwritten button settings", Toast.LENGTH_SHORT, GB.INFO);
else GB.toast("error overwriting button settings", Toast.LENGTH_SHORT, GB.INFO);
}
};
queueWrite(fileUploadRequets);
} catch (JSONException e) {
GB.log("error", GB.ERROR, e);
}
}
@Override
public void setActivityHand(double progress) {
queueWrite(new ConfigurationPutRequest(
new ConfigurationPutRequest.CurrentStepCountConfigItem(Math.min(999999, (int) (1000000 * progress))),
this
), false);
}
@Override
public void setHands(MoveHandsRequest.MovementConfiguration movement) {
queueWrite(new MoveHandsRequest(movement), false);
}
@Override
public void saveCalibration() {
queueWrite(new SaveCalibrationRequest());
}
public void vibrate(nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.PlayNotificationRequest.VibrationType vibration) {
// queueWrite(new nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.PlayNotificationRequest(vibration, -1, -1));
}
@Override
public void vibrateFindMyDevicePattern() {
}
@Override
public void requestHandsControl() {
queueWrite(new RequestHandControlRequest(), false);
}
@Override
public void releaseHandsControl() {
queueWrite(new ReleaseHandsControlRequest(), false);
}
@Override
public void setStepGoal(int stepGoal) {
getDeviceSpecificPreferences()
.edit()
.putInt(CONFIG_ITEM_STEP_GOAL, stepGoal)
.apply();
queueWrite(new ConfigurationPutRequest(new ConfigurationPutRequest.DailyStepGoalConfigItem(stepGoal), this) {
@Override
public void onFilePut(boolean success) {
if (success)
GB.toast("successfully updated step goal", Toast.LENGTH_SHORT, GB.INFO);
else GB.toast("error updating step goal", Toast.LENGTH_SHORT, GB.INFO);
}
}, false);
}
@Override
public void setVibrationStrength(short strength) {
getDeviceSpecificPreferences()
.edit()
.putInt(CONFIG_ITEM_VIBRATION_STRENGTH, (byte) strength)
.apply();
ConfigurationPutRequest.ConfigItem vibrationItem = new ConfigurationPutRequest.VibrationStrengthConfigItem((byte) strength);
queueWrite(
new ConfigurationPutRequest(new ConfigurationPutRequest.ConfigItem[]{vibrationItem}, this) {
@Override
public void onFilePut(boolean success) {
if (success)
GB.toast("successfully updated vibration strength", Toast.LENGTH_SHORT, GB.INFO);
else
GB.toast("error updating vibration strength", Toast.LENGTH_SHORT, GB.INFO);
}
}, false
);
// queueWrite(new FileVerifyRequest((short) 0x0800));
}
@Override
public void syncNotificationSettings() {
log("syncing notification settings...");
try {
PackageConfigHelper helper = new PackageConfigHelper(getContext());
final ArrayList configurations = helper.getNotificationConfigurations();
if (configurations.size() == 1) configurations.add(configurations.get(0));
queueWrite(new NotificationFilterPutRequest(configurations, FossilWatchAdapter.this) {
@Override
public void onFilePut(boolean success) {
super.onFilePut(success);
if (!success) {
GB.toast("error writing notification settings", Toast.LENGTH_SHORT, GB.ERROR);
getDeviceSupport().getDevice().setState(GBDevice.State.NOT_CONNECTED);
getDeviceSupport().getDevice().sendDeviceUpdateIntent(getContext());
}
getDeviceSupport().getDevice().setState(GBDevice.State.INITIALIZED);
getDeviceSupport().getDevice().sendDeviceUpdateIntent(getContext());
}
}, false);
} catch (Exception e) {
GB.log("error", GB.ERROR, e);
}
}
@Override
public void onTestNewFunction() {
queueWrite(new FilePutRequest(
FileHandle.HAND_ACTIONS,
new byte[]{
(byte) 0x01, (byte) 0x00, (byte) 0x08, (byte) 0x01, (byte) 0x01, (byte) 0x24, (byte) 0x00, (byte) 0x85, (byte) 0x01, (byte) 0x30, (byte) 0x52, (byte) 0xFF, (byte) 0x26, (byte) 0x00, (byte) 0x03, (byte) 0x00, (byte) 0x09, (byte) 0x04, (byte) 0x01, (byte) 0x03, (byte) 0xA0, (byte) 0x00, (byte) 0x00, (byte) 0xA0, (byte) 0x00, (byte) 0x00, (byte) 0x08, (byte) 0x01, (byte) 0x05, (byte) 0x00, (byte) 0x93, (byte) 0x00, (byte) 0x02, (byte) 0x09, (byte) 0x04, (byte) 0x01, (byte) 0x03, (byte) 0x00, (byte) 0x24, (byte) 0x00, (byte) 0x00, (byte) 0x24, (byte) 0x00, (byte) 0x08, (byte) 0x01, (byte) 0x50, (byte) 0x00, (byte) 0x01, (byte) 0x00, (byte) 0x1F, (byte) 0xBE, (byte) 0xB4, (byte) 0x1B
},
this)
);
}
@Override
public void setTimezoneOffsetMinutes(short offset) {
getDeviceSpecificPreferences()
.edit()
.putInt(CONFIG_ITEM_TIMEZONE_OFFSET, offset)
.apply();
queueWrite(new ConfigurationPutRequest(new ConfigurationPutRequest.TimezoneOffsetConfigItem(offset), this) {
@Override
public void onFilePut(boolean success) {
super.onFilePut(success);
if (success) GB.toast("successfully updated timezone", Toast.LENGTH_SHORT, GB.INFO);
else GB.toast("error updating timezone", Toast.LENGTH_SHORT, GB.ERROR);
}
});
}
@Override
public void onInstallApp(Uri uri) {
UriHelper uriHelper = null;
try {
uriHelper = UriHelper.get(uri, getContext());
} catch (IOException e) {
GB.toast(getContext(), "Could not open firmare: " + e.getMessage(), Toast.LENGTH_LONG, GB.ERROR, e);
}
if (uriHelper != null) {
try (InputStream in = new BufferedInputStream(uriHelper.openInputStream())) {
byte[] firmwareBytes = FileUtils.readAll(in, 1024 * 2024); // 2MB
queueWrite(new FirmwareFilePutRequest(firmwareBytes, this));
} catch (Exception e) {
GB.toast(getContext(), "Firmware cannot be installed: " + e.getMessage(), Toast.LENGTH_LONG, GB.ERROR, e);
}
}
}
@Override
public boolean supportsFindDevice() {
return false;
}
@Override
public boolean supportsExtendedVibration() {
String modelNumber = getDeviceSupport().getDevice().getModel();
switch (modelNumber) {
case "HW.0.0":
case "IV.0.0":
case "DN.1.0":
case "WA.0.0":
case "VA.0.0":
return true;
case "HL.0.0":
return false;
}
throw new UnsupportedOperationException("model " + modelNumber + " not supported");
}
@Override
public boolean supportsActivityHand() {
String modelNumber = getDeviceSupport().getDevice().getModel();
switch (modelNumber) {
case "HW.0.0":
return true;
case "HL.0.0":
case "IV.0.0":
case "DN.1.0":
case "WA.0.0":
case "VA.0.0":
return false;
}
throw new UnsupportedOperationException("Model " + modelNumber + " not supported");
}
@Override
public void onFetchActivityData() {
queueWrite(new FileLookupAndGetRequest(FileHandle.ACTIVITY_FILE, this) {
@Override
public void handleFileData(byte[] fileData) {
try (DBHandler dbHandler = GBApplication.acquireDB()) {
ActivityFileParser parser = new ActivityFileParser();
ArrayList entries = parser.parseFile(fileData).getKey();
HybridHRActivitySampleProvider provider = new HybridHRActivitySampleProvider(getDeviceSupport().getDevice(), dbHandler.getDaoSession());
HybridHRActivitySample[] samples = new HybridHRActivitySample[entries.size()];
Long userId = DBHelper.getUser(dbHandler.getDaoSession()).getId();
Long deviceId = DBHelper.getDevice(getDeviceSupport().getDevice(), dbHandler.getDaoSession()).getId();
for (int i = 0; i < entries.size(); i++) {
samples[i] = entries.get(i).toDAOActivitySample(userId, deviceId);
}
provider.addGBActivitySamples(samples);
queueWrite(new FileDeleteRequest(getHandle()));
GB.updateTransferNotification(null, "", false, 100, getContext());
GB.signalActivityDataFinish(getDeviceSupport().getDevice());
LOG.debug("Synchronized activity data");
} catch (Exception ex) {
GB.toast(getContext(), "Error saving steps data: " + ex.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR);
LOG.error("Error saving steps data: ", ex);
GB.updateTransferNotification(null, "Data transfer failed", false, 0, getContext());
}
getDeviceSupport().getDevice().unsetBusyTask();
getDeviceSupport().getDevice().sendDeviceUpdateIntent(getContext());
}
@Override
public void handleFileLookupError(FILE_LOOKUP_ERROR error) {
if(error == FILE_LOOKUP_ERROR.FILE_EMPTY){
LOG.debug("No activity data to sync");
}
getDeviceSupport().getDevice().unsetBusyTask();
GB.updateTransferNotification(null, "", false, 100, getContext());
getDeviceSupport().getDevice().sendDeviceUpdateIntent(getContext());
}
});
}
@Override
public void onSetAlarms(ArrayList extends Alarm> alarms) {
// handle one-shot alarm from the widget:
// this device doesn't have concept of on-off alarm, so use the last slot for this and store
// this alarm in the database so the user knows what is going on and can disable it
if (alarms.toArray().length == 1 && alarms.get(0).getRepetition() == 0) { //single shot?
Alarm oneshot = alarms.get(0);
alarms = (ArrayList extends Alarm>) AlarmUtils.mergeOneshotToDeviceAlarms(getDeviceSupport().getDevice(), (nodomain.freeyourgadget.gadgetbridge.entities.Alarm) oneshot, 5);
}
ArrayList activeAlarms = new ArrayList<>();
for (Alarm alarm : alarms) {
if (!alarm.getEnabled() || alarm.getUnused()) continue;
if (alarm.getRepetition() == 0) {
activeAlarms.add(new nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.alarm.Alarm(
(byte) alarm.getMinute(),
(byte) alarm.getHour(),
alarm.getTitle(),
alarm.getDescription()
));
continue;
}
int repitition = alarm.getRepetition();
repitition = (repitition << 1) | ((repitition >> 6) & 1);
activeAlarms.add(new nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.alarm.Alarm(
(byte) alarm.getMinute(),
(byte) alarm.getHour(),
(byte) repitition,
alarm.getTitle(),
alarm.getDescription()
));
}
queueWrite(new AlarmsSetRequest(activeAlarms.toArray(new nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.alarm.Alarm[0]), this) {
@Override
public void onFilePut(boolean success) {
super.onFilePut(success);
if (success) GB.toast(getContext().getString(R.string.user_feedback_miband_set_alarms_ok), Toast.LENGTH_SHORT, GB.INFO);
else GB.toast(getContext().getString(R.string.user_feedback_miband_set_alarms_failed), Toast.LENGTH_SHORT, GB.INFO);
}
});
}
@Override
public void onSendConfiguration(String config) {
}
@Override
public boolean onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
switch (characteristic.getUuid().toString()) {
case "3dda0006-957f-7d4a-34a6-74696673696d": {
handleBackgroundCharacteristic(characteristic);
break;
}
case "00002a37-0000-1000-8000-00805f9b34fb": {
handleHeartRateCharacteristic(characteristic);
break;
}
case "3dda0002-957f-7d4a-34a6-74696673696d":
case "3dda0004-957f-7d4a-34a6-74696673696d":
case "3dda0005-957f-7d4a-34a6-74696673696d":
case "3dda0003-957f-7d4a-34a6-74696673696d": {
if (fossilRequest != null) {
boolean requestFinished;
try {
if (characteristic.getUuid().toString().equals("3dda0003-957f-7d4a-34a6-74696673696d")) {
byte requestType = (byte) (characteristic.getValue()[0] & 0x0F);
if (requestType != 0x0A && requestType != fossilRequest.getType()) {
// throw new RuntimeException("Answer type " + requestType + " does not match current request " + fossilRequest.getType());
}
}
fossilRequest.handleResponse(characteristic);
requestFinished = fossilRequest.isFinished();
} catch (RuntimeException e) {
if (characteristic.getUuid().toString().equals("3dda0005-957f-7d4a-34a6-74696673696d")) {
GB.log("authentication failed", GB.ERROR, e);
// setDeviceState(GBDevice.State.AUTHENTICATION_REQUIRED);
}else {
GB.log("error", GB.ERROR, e);
if (BuildConfig.DEBUG)
GB.toast(fossilRequest.getName() + " failed", Toast.LENGTH_SHORT, GB.ERROR);
}
requestFinished = true;
}
if (requestFinished) {
log(fossilRequest.getName() + " finished");
fossilRequest = null;
stopRequestTimeout();
} else {
return true;
}
}
queueNextRequest();
}
}
return true;
}
public void handleHeartRateCharacteristic(BluetoothGattCharacteristic characteristic) {
}
@Override
public void onFindDevice(boolean start) {
try {
if (!this.supportsExtendedVibration()) {
if (start) {
new TransactionBuilder("vibrate find")
.write(
getDeviceSupport().getCharacteristic(UUID.fromString("3dda0005-957f-7d4a-34a6-74696673696d")),
new byte[]{(byte) 0x01, (byte) 0x04, (byte) 0x30, (byte) 0x75, (byte) 0x00, (byte) 0x00}
)
.queue(getDeviceSupport().getQueue());
} else {
new TransactionBuilder("vibrate find")
.write(
getDeviceSupport().getCharacteristic(UUID.fromString("3dda0005-957f-7d4a-34a6-74696673696d")),
new byte[]{(byte) 0x02, (byte) 0x05, (byte) 0x04}
)
.queue(getDeviceSupport().getQueue());
}
}
} catch (UnsupportedOperationException e) {
GB.log("error", GB.ERROR, e);
}
if (start && getDeviceSupport().searchDevice) return;
getDeviceSupport().searchDevice = start;
if (start) {
new Thread(new Runnable() {
@Override
public void run() {
int i = 0;
while (getDeviceSupport().searchDevice) {
vibrateFindMyDevicePattern();
try {
Thread.sleep(2500);
} catch (InterruptedException e) {
GB.log("error", GB.ERROR, e);
}
}
}
}).start();
}
}
protected void handleBackgroundCharacteristic(BluetoothGattCharacteristic characteristic) {
byte[] value = characteristic.getValue();
switch (value[1]) {
case 2: {
byte syncId = value[2];
getDeviceSupport().getDevice().addDeviceInfo(new GenericItem(QHybridSupport.ITEM_LAST_HEARTBEAT, DateFormat.getTimeInstance().format(new Date())));
break;
}
case 8: {
if (value.length != 12) {
throw new RuntimeException("wrong button message");
}
int index = value[2] & 0xFF;
int button = value[9] >> 4 & 0xFF;
if (index != this.lastButtonIndex) {
lastButtonIndex = index;
log("Button press on button " + button);
Intent i = new Intent(QHYBRID_EVENT_BUTTON_PRESS);
i.setPackage(BuildConfig.APPLICATION_ID);
i.putExtra("BUTTON", button);
getContext().sendBroadcast(i);
}
break;
}
case 5: {
if (value.length != 4) {
throw new RuntimeException("wrong button message");
}
int action = value[3];
String actionString = "SINGLE";
if (action == 3) actionString = "DOUBLE";
else if (action == 4) actionString = "LONG";
// lastButtonIndex = index;
log(actionString + " button press");
Intent i = new Intent(QHYBRID_EVENT_MULTI_BUTTON_PRESS);
i.putExtra("ACTION", actionString);
getContext().sendBroadcast(i);
break;
}
}
}
@Override
public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) {
super.onMtuChanged(gatt, mtu, status);
if (this.MTU == mtu) {
log("MTU changed, same value tho");
return;
}
log("MTU changed: " + mtu);
this.MTU = mtu;
getDeviceSupport().getDevice().addDeviceInfo(new GenericItem(ITEM_MTU, String.valueOf(mtu)));
getDeviceSupport().getDevice().sendDeviceUpdateIntent(getContext());
((RequestMtuRequest) fossilRequest).setFinished(true);
queueNextRequest();
}
public void queueWrite(RequestMtuRequest request, boolean priorise) {
log("is connected: " + getDeviceSupport().isConnected());
if (!getDeviceSupport().isConnected()) {
log("dropping requetst " + request.getName());
return;
}
new TransactionBuilder("requestMtu")
.requestMtu(512)
.queue(getDeviceSupport().getQueue());
this.fossilRequest = request;
}
private void log(String message) {
LOG.debug(message);
}
public void queueWrite(SetDeviceStateRequest request, boolean priorise) {
if (fossilRequest != null && !fossilRequest.isFinished()) {
log("queing request: " + request.getName());
if (priorise) {
requestQueue.add(0, request);
} else {
requestQueue.add(request);
}
return;
}
log("setting device state: " + request.getDeviceState());
setDeviceState(request.getDeviceState());
queueNextRequest();
}
private void setDeviceState(GBDevice.State state) {
getDeviceSupport().getDevice().setState(state);
getDeviceSupport().getDevice().sendDeviceUpdateIntent(getContext(), GBDevice.DeviceUpdateSubject.DEVICE_STATE);
}
public void queueWrite(FossilRequest request, boolean priorise) {
log("is connected: " + getDeviceSupport().isConnected());
if (!getDeviceSupport().isConnected()) {
log("dropping requetst " + request.getName());
return;
}
if (fossilRequest != null && !fossilRequest.isFinished()) {
log("queing request: " + request.getName());
if (priorise) {
requestQueue.add(0, request);
} else {
requestQueue.add(request);
}
return;
}
log("executing request: " + request.getName());
restartRequestTimeout();
this.fossilRequest = request;
new TransactionBuilder(request.getClass().getSimpleName()).write(getDeviceSupport().getCharacteristic(request.getRequestUUID()), request.getRequestData()).queue(getDeviceSupport().getQueue());
if (request.isFinished()) {
this.fossilRequest = null;
stopRequestTimeout();
queueNextRequest();
}
}
public void queueWrite(Request request, boolean priorise) {
log("is connected: " + getDeviceSupport().isConnected());
if (!getDeviceSupport().isConnected()) {
log("dropping requetst " + request.getName());
return;
}
restartRequestTimeout();
new TransactionBuilder(request.getClass().getSimpleName()).write(getDeviceSupport().getCharacteristic(request.getRequestUUID()), request.getRequestData()).queue(getDeviceSupport().getQueue());
queueNextRequest();
}
protected void queueWrite(Request request) {
log("is connected: " + getDeviceSupport().isConnected());
if (!getDeviceSupport().isConnected()) {
log("dropping requetst " + request.getName());
return;
}
if (request instanceof SetDeviceStateRequest)
queueWrite((SetDeviceStateRequest) request, false);
else if (request instanceof RequestMtuRequest)
queueWrite((RequestMtuRequest) request, false);
else if (request instanceof FossilRequest) queueWrite((FossilRequest) request, false);
else queueWrite(request, false);
}
private void queueNextRequest() {
try {
Request request = requestQueue.remove(0);
queueWrite(request);
} catch (IndexOutOfBoundsException e) {
log("requestsQueue empty");
}
}
}