/* 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 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) 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"); } } }