diff --git a/app/build.gradle b/app/build.gradle index cc97804f6..0392369ad 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -85,7 +85,7 @@ dependencies { implementation "org.greenrobot:greendao:2.2.1" implementation "org.apache.commons:commons-lang3:3.5" implementation "org.cyanogenmod:platform.sdk:6.0" - + implementation 'com.jaredrummler:colorpicker:1.0.2' // implementation project(":DaoCore") } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/GBDeviceAdapterv2.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/GBDeviceAdapterv2.java index ed9fdd9a6..e9b057f05 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/GBDeviceAdapterv2.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/GBDeviceAdapterv2.java @@ -22,16 +22,19 @@ import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; +import android.graphics.drawable.GradientDrawable; import android.support.annotation.NonNull; import android.support.design.widget.Snackbar; import android.support.v4.content.LocalBroadcastManager; import android.support.v7.widget.CardView; import android.support.v7.widget.RecyclerView; +import android.text.InputType; import android.transition.TransitionManager; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ArrayAdapter; +import android.widget.EditText; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.ListView; @@ -40,7 +43,11 @@ import android.widget.RelativeLayout; import android.widget.TextView; import android.widget.Toast; +import com.jaredrummler.android.colorpicker.ColorPickerDialog; +import com.jaredrummler.android.colorpicker.ColorPickerDialogListener; + import java.util.List; +import java.util.Locale; import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.R; @@ -125,16 +132,22 @@ public class GBDeviceAdapterv2 extends RecyclerView.Adapter 108.0) { + new AlertDialog.Builder(context) + .setTitle(R.string.pref_invalid_frequency_title) + .setMessage(R.string.pref_invalid_frequency_message) + .setNeutralButton(android.R.string.ok, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + } + }) + .show(); + } else { + device.setExtraInfo("fm_frequency", frequency); + fmFrequencyLabel.setText(String.format(Locale.getDefault(), "%.1f", (float) device.getExtraInfo("fm_frequency"))); + GBApplication.deviceService().onSetFmFrequency(frequency); + } + } + }); + builder.setNegativeButton(context.getResources().getString(R.string.Cancel), new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.cancel(); + } + }); + + builder.show(); + } + }); + + holder.ledColor.setVisibility(View.GONE); + if (device.isInitialized() && device.getExtraInfo("led_color") != null && coordinator.supportsLedColor()) { + holder.ledColor.setVisibility(View.VISIBLE); + final GradientDrawable ledColor = (GradientDrawable) holder.ledColor.getDrawable().mutate(); + ledColor.setColor((int) device.getExtraInfo("led_color")); + holder.ledColor.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + ColorPickerDialog.Builder builder = ColorPickerDialog.newBuilder(); + builder.setDialogTitle(R.string.preferences_led_color); + + builder.setColor((int) device.getExtraInfo("led_color")); + if (coordinator.supportsRgbLedColor()) { + builder.setAllowCustom(true); + builder.setShowAlphaSlider(false); + builder.setAllowPresets(true); + } else { + builder.setAllowCustom(false); + builder.setAllowPresets(true); + builder.setShowColorShades(false); + builder.setPresets(coordinator.getColorPresets()); + } + + ColorPickerDialog dialog = builder.create(); + dialog.setColorPickerDialogListener(new ColorPickerDialogListener() { + @Override + public void onColorSelected(int dialogId, int color) { + ledColor.setColor(color); + device.setExtraInfo("led_color", color); + GBApplication.deviceService().onSetLedColor(color); + } + + @Override + public void onDialogDismissed(int dialogId) { + // Nothing to do + } + }); + dialog.show(((Activity) context).getFragmentManager(), "color-picker-dialog"); + } + }); + } + //remove device, hidden under details holder.removeDevice.setOnClickListener(new View.OnClickListener() @@ -373,6 +481,9 @@ public class GBDeviceAdapterv2 extends RecyclerView.Adapter. */ +package nodomain.freeyourgadget.gadgetbridge.deviceevents; + +public class GBDeviceEventFmFrequency extends GBDeviceEvent { + public float frequency; +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/deviceevents/GBDeviceEventLEDColor.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/deviceevents/GBDeviceEventLEDColor.java new file mode 100644 index 000000000..a3c7ca807 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/deviceevents/GBDeviceEventLEDColor.java @@ -0,0 +1,21 @@ +/* Copyright (C) 2018 José Rebelo + + 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.deviceevents; + +public class GBDeviceEventLEDColor extends GBDeviceEvent { + public int color; +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractDeviceCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractDeviceCoordinator.java index de79565c7..d6b6795f7 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractDeviceCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractDeviceCoordinator.java @@ -135,4 +135,18 @@ public abstract class AbstractDeviceCoordinator implements DeviceCoordinator { public boolean supportsMusicInfo() { return false; } + + public boolean supportsLedColor() { + return false; + } + + @Override + public boolean supportsRgbLedColor() { + return false; + } + + @Override + public int[] getColorPresets() { + return new int[0]; + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceCoordinator.java index 859db7939..5364526eb 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceCoordinator.java @@ -254,4 +254,21 @@ public interface DeviceCoordinator { * like artist, title, album, play state etc. */ boolean supportsMusicInfo(); + + /** + * Indicates whether the device has an led which supports custom colors + */ + boolean supportsLedColor(); + + /** + * Indicates whether the device's led supports any RGB color, + * or only preset colors + */ + boolean supportsRgbLedColor(); + + /** + * Returns the preset colors supported by the device, if any, in ARGB, with alpha = 255 + */ + @NonNull + int[] getColorPresets(); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/EventHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/EventHandler.java index 4acf58aea..5e1fad92a 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/EventHandler.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/EventHandler.java @@ -99,4 +99,12 @@ public interface EventHandler { void onTestNewFunction(); void onSendWeather(WeatherSpec weatherSpec); + + void onSetFmFrequency(float frequency); + + /** + * Set the device's led color. + * @param color the new color, in ARGB, with alpha = 255 + */ + void onSetLedColor(int color); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/UnknownDeviceCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/UnknownDeviceCoordinator.java index c06455faa..6a030fa4a 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/UnknownDeviceCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/UnknownDeviceCoordinator.java @@ -186,4 +186,19 @@ public class UnknownDeviceCoordinator extends AbstractDeviceCoordinator { public boolean supportsFindDevice() { return false; } + + @Override + public boolean supportsLedColor() { + return false; + } + + @Override + public boolean supportsRgbLedColor() { + return false; + } + + @Override + public int[] getColorPresets() { + return new int[0]; + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/roidmi/Roidmi1Coordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/roidmi/Roidmi1Coordinator.java new file mode 100644 index 000000000..47351dd01 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/roidmi/Roidmi1Coordinator.java @@ -0,0 +1,57 @@ +/* Copyright (C) 2018 José Rebelo + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.devices.roidmi; + +import android.bluetooth.BluetoothDevice; +import android.support.annotation.NonNull; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate; +import nodomain.freeyourgadget.gadgetbridge.model.DeviceType; + +public class Roidmi1Coordinator extends RoidmiCoordinator { + private static final Logger LOG = LoggerFactory.getLogger(Roidmi1Coordinator.class); + + @NonNull + @Override + public DeviceType getSupportedType(GBDeviceCandidate candidate) { + try { + BluetoothDevice device = candidate.getDevice(); + String name = device.getName(); + + if (name != null && name.contains("睿米车载蓝牙播放器")) { + return DeviceType.ROIDMI; + } + } catch (Exception ex) { + LOG.error("unable to check device support", ex); + } + + return DeviceType.UNKNOWN; + } + + @Override + public DeviceType getDeviceType() { + return DeviceType.ROIDMI; + } + + @Override + public int[] getColorPresets() { + return RoidmiConst.COLOR_PRESETS; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/roidmi/Roidmi3Coordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/roidmi/Roidmi3Coordinator.java new file mode 100644 index 000000000..10c351844 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/roidmi/Roidmi3Coordinator.java @@ -0,0 +1,58 @@ +/* Copyright (C) 2018 José Rebelo + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.devices.roidmi; + +import android.bluetooth.BluetoothDevice; +import android.support.annotation.NonNull; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate; +import nodomain.freeyourgadget.gadgetbridge.model.DeviceType; + +public class Roidmi3Coordinator extends RoidmiCoordinator { + private static final Logger LOG = LoggerFactory.getLogger(Roidmi3Coordinator.class); + + @NonNull + @Override + public DeviceType getSupportedType(GBDeviceCandidate candidate) { + try { + BluetoothDevice device = candidate.getDevice(); + String name = device.getName(); + + if (name != null && name.contains("Roidmi Music Blue C")) { + LOG.warn("Found a Roidmi 3, but support is disabled."); + return DeviceType.UNKNOWN; // TODO Roidmi 3 is not working atm + } + } catch (Exception ex) { + LOG.error("unable to check device support", ex); + } + + return DeviceType.UNKNOWN; + } + + @Override + public DeviceType getDeviceType() { + return DeviceType.ROIDMI3; + } + + @Override + public boolean supportsRgbLedColor() { + return true; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/roidmi/RoidmiConst.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/roidmi/RoidmiConst.java new file mode 100644 index 000000000..f7992aa76 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/roidmi/RoidmiConst.java @@ -0,0 +1,36 @@ +/* Copyright (C) 2018 José Rebelo + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.devices.roidmi; + +import android.graphics.Color; + +public class RoidmiConst { + public static final String ACTION_GET_LED_COLOR = "roidmi_get_led_color"; + public static final String ACTION_GET_FM_FREQUENCY = "roidmi_get_frequency"; + public static final String ACTION_GET_VOLTAGE = "roidmi_get_voltage"; + + public static final int[] COLOR_PRESETS = new int[]{ + Color.rgb(0xFF, 0x00, 0x00), // red + Color.rgb(0x00, 0xFF, 0x00), // green + Color.rgb(0x00, 0x00, 0xFF), // blue + Color.rgb(0xFF, 0xFF, 0x01), // yellow + Color.rgb(0x00, 0xAA, 0xE5), // sky blue + Color.rgb(0xF0, 0x6E, 0xAA), // pink + Color.rgb(0xFF, 0xFF, 0xFF), // white + Color.rgb(0x00, 0x00, 0x00), // black + }; +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/roidmi/RoidmiCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/roidmi/RoidmiCoordinator.java new file mode 100644 index 000000000..39d7ce76e --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/roidmi/RoidmiCoordinator.java @@ -0,0 +1,135 @@ +/* Copyright (C) 2018 José Rebelo + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.devices.roidmi; + +import android.app.Activity; +import android.bluetooth.BluetoothDevice; +import android.content.Context; +import android.net.Uri; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import nodomain.freeyourgadget.gadgetbridge.GBException; +import nodomain.freeyourgadget.gadgetbridge.devices.AbstractDeviceCoordinator; +import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler; +import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider; +import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; +import nodomain.freeyourgadget.gadgetbridge.entities.Device; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample; + +public abstract class RoidmiCoordinator extends AbstractDeviceCoordinator { + private static final Logger LOG = LoggerFactory.getLogger(RoidmiCoordinator.class); + + @Override + public String getManufacturer() { + return "Roidmi"; + } + + @Override + public int getBondingStyle(GBDevice device) { + return BONDING_STYLE_BOND; + } + + @Override + protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) throws GBException { + } + + @Nullable + @Override + public Class getPairingActivity() { + return null; + } + + @Override + public boolean supportsActivityDataFetching() { + return false; + } + + @Override + public boolean supportsActivityTracking() { + return false; + } + + @Override + public SampleProvider getSampleProvider(GBDevice device, DaoSession session) { + return null; + } + + @Override + public InstallHandler findInstallHandler(Uri uri, Context context) { + return null; + } + + @Override + public boolean supportsScreenshots() { + return false; + } + + @Override + public boolean supportsAlarmConfiguration() { + return false; + } + + @Override + public boolean supportsSmartWakeup(GBDevice device) { + return false; + } + + @Override + public boolean supportsHeartRateMeasurement(GBDevice device) { + return false; + } + + @Override + public boolean supportsAppsManagement() { + return false; + } + + @Override + public Class getAppsManagementActivity() { + return null; + } + + @Override + public boolean supportsCalendarEvents() { + return false; + } + + @Override + public boolean supportsRealtimeData() { + return false; + } + + @Override + public boolean supportsWeather() { + return false; + } + + @Override + public boolean supportsFindDevice() { + return false; + } + + @Override + public boolean supportsLedColor() { + return true; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDevice.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDevice.java index 296ae7bb2..2adc2ae71 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDevice.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDevice.java @@ -30,6 +30,7 @@ import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.List; import nodomain.freeyourgadget.gadgetbridge.GBApplication; @@ -73,11 +74,13 @@ public class GBDevice implements Parcelable { private String mModel; private State mState = State.NOT_CONNECTED; private short mBatteryLevel = BATTERY_UNKNOWN; + private float mBatteryVoltage = BATTERY_UNKNOWN; private short mBatteryThresholdPercent = BATTERY_THRESHOLD_PERCENT; private BatteryState mBatteryState; private short mRssi = RSSI_UNKNOWN; private String mBusyTask; private List mDeviceInfos; + private HashMap mExtraInfos; public GBDevice(String address, String name, DeviceType deviceType) { this(address, null, name, deviceType); @@ -106,6 +109,7 @@ public class GBDevice implements Parcelable { mRssi = (short) in.readInt(); mBusyTask = in.readString(); mDeviceInfos = in.readArrayList(getClass().getClassLoader()); + mExtraInfos = (HashMap) in.readSerializable(); validate(); } @@ -126,6 +130,7 @@ public class GBDevice implements Parcelable { dest.writeInt(mRssi); dest.writeString(mBusyTask); dest.writeList(mDeviceInfos); + dest.writeSerializable(mExtraInfos); } private void validate() { @@ -371,6 +376,33 @@ public class GBDevice implements Parcelable { return mAddress.hashCode() ^ 37; } + + /** + * Returns the extra info value if it is set, null otherwise + * @param key the extra info key + * @return the extra info value if set, null otherwise + */ + public Object getExtraInfo(String key) { + if (mExtraInfos == null) { + return null; + } + + return mExtraInfos.get(key); + } + + /** + * Sets an extra info value, overwriting the current one, if any + * @param key the extra info key + * @param info the extra info value + */ + public void setExtraInfo(String key, Object info) { + if (mExtraInfos == null) { + mExtraInfos = new HashMap<>(); + } + + mExtraInfos.put(key, info); + } + /** * Ranges from 0-100 (percent), or -1 if unknown * @@ -388,6 +420,23 @@ public class GBDevice implements Parcelable { } } + public void setBatteryVoltage(float batteryVoltage) { + if (batteryVoltage >= 0 || batteryVoltage == BATTERY_UNKNOWN) { + mBatteryVoltage = batteryVoltage; + } else { + LOG.error("Battery voltage must be > 0: " + batteryVoltage); + } + } + + /** + * Voltage greater than zero (unit: Volt), or -1 if unknown + * + * @return the battery voltage, or -1 if unknown + */ + public float getBatteryVoltage() { + return mBatteryVoltage; + } + public BatteryState getBatteryState() { return mBatteryState; } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDeviceService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDeviceService.java index 478650f03..8a997a997 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDeviceService.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDeviceService.java @@ -405,4 +405,18 @@ public class GBDeviceService implements DeviceService { return name; } + + @Override + public void onSetFmFrequency(float frequency) { + Intent intent = createIntent().setAction(ACTION_SET_FM_FREQUENCY) + .putExtra(EXTRA_FM_FREQUENCY, frequency); + invokeService(intent); + } + + @Override + public void onSetLedColor(int color) { + Intent intent = createIntent().setAction(ACTION_SET_LED_COLOR) + .putExtra(EXTRA_LED_COLOR, color); + invokeService(intent); + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/BatteryState.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/BatteryState.java index 346612a43..25538d675 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/BatteryState.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/BatteryState.java @@ -22,5 +22,6 @@ public enum BatteryState { BATTERY_LOW, BATTERY_CHARGING, BATTERY_CHARGING_FULL, - BATTERY_NOT_CHARGING_FULL + BATTERY_NOT_CHARGING_FULL, + NO_BATTERY } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceService.java index 92b359701..d15e858db 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceService.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceService.java @@ -65,6 +65,8 @@ public interface DeviceService extends EventHandler { String ACTION_SEND_CONFIGURATION = PREFIX + ".action.send_configuration"; String ACTION_SEND_WEATHER = PREFIX + ".action.send_weather"; String ACTION_TEST_NEW_FUNCTION = PREFIX + ".action.test_new_function"; + String ACTION_SET_FM_FREQUENCY = PREFIX + ".action.set_fm_frequency"; + String ACTION_SET_LED_COLOR = PREFIX + ".action.set_led_color"; String EXTRA_NOTIFICATION_BODY = "notification_body"; String EXTRA_NOTIFICATION_FLAGS = "notification_flags"; String EXTRA_NOTIFICATION_ID = "notification_id"; @@ -106,6 +108,8 @@ public interface DeviceService extends EventHandler { String EXTRA_INTERVAL_SECONDS = "interval_seconds"; String EXTRA_WEATHER = "weather"; String EXTRA_RECORDED_DATA_TYPES = "data_types"; + String EXTRA_FM_FREQUENCY = "fm_frequency"; + String EXTRA_LED_COLOR = "led_color"; /** * Use EXTRA_REALTIME_SAMPLE instead diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java index 399d67f72..388a98aa2 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java @@ -49,6 +49,8 @@ public enum DeviceType { ZETIME(80, R.drawable.ic_device_default, R.drawable.ic_device_default_disabled, R.string.devicetype_mykronoz_zetime), ID115(90, R.drawable.ic_device_h30_h10, R.drawable.ic_device_h30_h10_disabled, R.string.devicetype_id115), WATCH9(100, R.drawable.ic_device_default, R.drawable.ic_device_default_disabled, R.string.devicetype_watch9), + ROIDMI(100, R.drawable.ic_device_roidmi, R.drawable.ic_device_roidmi_disabled, R.string.devicetype_roidmi), + ROIDMI3(102, R.drawable.ic_device_roidmi, R.drawable.ic_device_roidmi_disabled, R.string.devicetype_roidmi3), TEST(1000, R.drawable.ic_device_default, R.drawable.ic_device_default_disabled, R.string.devicetype_test); private final int key; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/AbstractDeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/AbstractDeviceSupport.java index 92d011471..a06bc27d4 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/AbstractDeviceSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/AbstractDeviceSupport.java @@ -52,7 +52,9 @@ import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventAppInfo; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventCallControl; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventDisplayMessage; +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventFmFrequency; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventFindPhone; +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventLEDColor; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventMusicControl; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventNotificationControl; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventScreenshot; @@ -159,6 +161,10 @@ public abstract class AbstractDeviceSupport implements DeviceSupport { handleGBDeviceEvent((GBDeviceEventBatteryInfo) deviceEvent); } else if (deviceEvent instanceof GBDeviceEventFindPhone) { handleGBDeviceEvent((GBDeviceEventFindPhone) deviceEvent); + } else if (deviceEvent instanceof GBDeviceEventLEDColor) { + handleGBDeviceEvent((GBDeviceEventLEDColor) deviceEvent); + } else if (deviceEvent instanceof GBDeviceEventFmFrequency) { + handleGBDeviceEvent((GBDeviceEventFmFrequency) deviceEvent); } } @@ -208,6 +214,26 @@ public abstract class AbstractDeviceSupport implements DeviceSupport { gbDevice.sendDeviceUpdateIntent(context); } + protected void handleGBDeviceEvent(GBDeviceEventLEDColor colorEvent) { + Context context = getContext(); + LOG.info("Got event for LED Color"); + if (gbDevice == null) { + return; + } + gbDevice.setExtraInfo("led_color", colorEvent.color); + gbDevice.sendDeviceUpdateIntent(context); + } + + protected void handleGBDeviceEvent(GBDeviceEventFmFrequency frequencyEvent) { + Context context = getContext(); + LOG.info("Got event for FM Frequency"); + if (gbDevice == null) { + return; + } + gbDevice.setExtraInfo("fm_frequency", frequencyEvent.frequency); + gbDevice.sendDeviceUpdateIntent(context); + } + private void handleGBDeviceEvent(GBDeviceEventAppInfo appInfoEvent) { Context context = getContext(); LOG.info("Got event for APP_INFO"); @@ -328,6 +354,7 @@ public abstract class AbstractDeviceSupport implements DeviceSupport { LOG.info("Got BATTERY_INFO device event"); gbDevice.setBatteryLevel(deviceEvent.level); gbDevice.setBatteryState(deviceEvent.state); + gbDevice.setBatteryVoltage(deviceEvent.voltage); //show the notification if the battery level is below threshold and only if not connected to charger if (deviceEvent.level <= gbDevice.getBatteryThresholdPercent() && diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceCommunicationService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceCommunicationService.java index b050177a6..2c183defb 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceCommunicationService.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceCommunicationService.java @@ -105,7 +105,9 @@ import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_SE import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_SETTIME; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_SET_ALARMS; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_SET_CONSTANT_VIBRATION; +import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_SET_FM_FREQUENCY; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_SET_HEARTRATE_MEASUREMENT_INTERVAL; +import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_SET_LED_COLOR; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_START; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_STARTAPP; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_TEST_NEW_FUNCTION; @@ -130,7 +132,9 @@ import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_CAN import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_CONFIG; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_CONNECT_FIRST_TIME; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_FIND_START; +import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_FM_FREQUENCY; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_INTERVAL_SECONDS; +import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_LED_COLOR; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_MUSIC_ALBUM; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_MUSIC_ARTIST; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_MUSIC_DURATION; @@ -549,6 +553,18 @@ public class DeviceCommunicationService extends Service implements SharedPrefere } break; } + case ACTION_SET_LED_COLOR: + int color = intent.getIntExtra(EXTRA_LED_COLOR, 0); + if (color != 0) { + mDeviceSupport.onSetLedColor(color); + } + break; + case ACTION_SET_FM_FREQUENCY: + float frequency = intent.getFloatExtra(EXTRA_FM_FREQUENCY, -1); + if (frequency != -1) { + mDeviceSupport.onSetFmFrequency(frequency); + } + break; } return START_STICKY; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceSupportFactory.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceSupportFactory.java index dd86a6c8d..10962047a 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceSupportFactory.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceSupportFactory.java @@ -38,6 +38,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.amazfitbip.Ama import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.MiBandSupport; import nodomain.freeyourgadget.gadgetbridge.service.devices.no1f1.No1F1Support; import nodomain.freeyourgadget.gadgetbridge.service.devices.pebble.PebbleSupport; +import nodomain.freeyourgadget.gadgetbridge.service.devices.roidmi.RoidmiSupport; import nodomain.freeyourgadget.gadgetbridge.service.devices.vibratissimo.VibratissimoSupport; import nodomain.freeyourgadget.gadgetbridge.service.devices.hplus.HPlusSupport; import nodomain.freeyourgadget.gadgetbridge.service.devices.jyou.TeclastH30Support; @@ -164,6 +165,12 @@ public class DeviceSupportFactory { case WATCH9: deviceSupport = new ServiceDeviceSupport(new Watch9DeviceSupport(), EnumSet.of(ServiceDeviceSupport.Flags.THROTTLING, ServiceDeviceSupport.Flags.BUSY_CHECKING)); break; + case ROIDMI: + deviceSupport = new ServiceDeviceSupport(new RoidmiSupport(), EnumSet.of(ServiceDeviceSupport.Flags.BUSY_CHECKING)); + break; + case ROIDMI3: + deviceSupport = new ServiceDeviceSupport(new RoidmiSupport(), EnumSet.of(ServiceDeviceSupport.Flags.BUSY_CHECKING)); + break; } if (deviceSupport != null) { deviceSupport.setContext(gbDevice, mBtAdapter, mContext); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/ServiceDeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/ServiceDeviceSupport.java index 4c77c5a01..33e49e48a 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/ServiceDeviceSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/ServiceDeviceSupport.java @@ -374,4 +374,20 @@ public class ServiceDeviceSupport implements DeviceSupport { } delegate.onSendWeather(weatherSpec); } + + @Override + public void onSetFmFrequency(float frequency) { + if (checkBusy("set frequency event")) { + return; + } + delegate.onSetFmFrequency(frequency); + } + + @Override + public void onSetLedColor(int color) { + if (checkBusy("set led color event")) { + return; + } + delegate.onSetLedColor(color); + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btclassic/BtClassicIoThread.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btclassic/BtClassicIoThread.java index 5724bdfba..efa41e746 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btclassic/BtClassicIoThread.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btclassic/BtClassicIoThread.java @@ -81,6 +81,10 @@ public abstract class BtClassicIoThread extends GBDeviceIoThread { public synchronized void write(byte[] bytes) { if (null == bytes) return; + if (mOutStream == null) { + LOG.error("mOutStream is null"); + return; + } LOG.debug("writing:" + GB.hexdump(bytes, 0, bytes.length)); try { mOutStream.write(bytes); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/AbstractBTLEDeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/AbstractBTLEDeviceSupport.java index e7f157dfe..c65153ba0 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/AbstractBTLEDeviceSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/AbstractBTLEDeviceSupport.java @@ -318,4 +318,14 @@ public abstract class AbstractBTLEDeviceSupport extends AbstractDeviceSupport im profile.onReadRemoteRssi(gatt, rssi, status); } } + + @Override + public void onSetFmFrequency(float frequency) { + + } + + @Override + public void onSetLedColor(int color) { + + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/roidmi/Roidmi1Protocol.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/roidmi/Roidmi1Protocol.java new file mode 100644 index 000000000..196559e35 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/roidmi/Roidmi1Protocol.java @@ -0,0 +1,150 @@ +/* Copyright (C) 2018 José Rebelo + + 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.roidmi; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent; +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventFmFrequency; +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventLEDColor; +import nodomain.freeyourgadget.gadgetbridge.devices.roidmi.RoidmiConst; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.util.GB; + +public class Roidmi1Protocol extends RoidmiProtocol { + private static final Logger LOG = LoggerFactory.getLogger(Roidmi1Protocol.class); + + public Roidmi1Protocol(GBDevice device) { + super(device); + } + + private static final byte[] PACKET_HEADER = new byte[]{(byte) 0xaa, 0x55}; + private static final byte[] PACKET_TRAILER = new byte[]{(byte) 0xc3, 0x3c}; + private static final byte COMMAND_SET_FREQUENCY = 0x10; + private static final byte COMMAND_GET_FREQUENCY = (byte) 0x80; + private static final byte COMMAND_SET_COLOR = 0x11; + private static final byte COMMAND_GET_COLOR = (byte) 0x81; + + private static final int PACKET_MIN_LENGTH = 6; + + private static final int LED_COLOR_RED = 1; + private static final int LED_COLOR_GREEN = 2; + private static final int LED_COLOR_BLUE = 3; + private static final int LED_COLOR_YELLOW = 4; // not official + private static final int LED_COLOR_SKY_BLUE = 5; + private static final int LED_COLOR_PINK = 6; // not official + private static final int LED_COLOR_WHITE = 7; // not official + private static final int LED_COLOR_OFF = 8; + + // Other commands: + // App periodically sends aa5502018588c33c and receives aa5506018515111804cec33c + private static final byte[] COMMAND_PERIODIC = new byte[]{(byte) 0xaa, 0x55, 0x02, 0x01, (byte) 0x85, (byte) 0x88, (byte) 0xc3, 0x3c}; + + @Override + public GBDeviceEvent[] decodeResponse(byte[] responseData) { + if (responseData.length <= PACKET_MIN_LENGTH) { + LOG.info("Response too small"); + return null; + } + + for (int i = 0; i < packetHeader().length; i++) { + if (responseData[i] != packetHeader()[i]) { + LOG.info("Invalid response header"); + return null; + } + } + + for (int i = 0; i < packetTrailer().length; i++) { + if (responseData[responseData.length - packetTrailer().length + i] != packetTrailer()[i]) { + LOG.info("Invalid response trailer"); + return null; + } + } + + if (calcChecksum(responseData) != responseData[responseData.length - packetTrailer().length - 1]) { + LOG.info("Invalid response checksum"); + return null; + } + + switch (responseData[3]) { + case COMMAND_GET_COLOR: + int color = responseData[5]; + LOG.debug("Got color: " + color); + GBDeviceEventLEDColor evColor = new GBDeviceEventLEDColor(); + evColor.color = RoidmiConst.COLOR_PRESETS[color - 1]; + return new GBDeviceEvent[]{evColor}; + case COMMAND_GET_FREQUENCY: + String frequencyHex = GB.hexdump(responseData, 4, 2); + float frequency = Float.valueOf(frequencyHex) / 10.0f; + LOG.debug("Got frequency: " + frequency); + GBDeviceEventFmFrequency evFrequency = new GBDeviceEventFmFrequency(); + evFrequency.frequency = frequency; + return new GBDeviceEvent[]{evFrequency}; + default: + LOG.error("Unrecognized response type 0x" + GB.hexdump(responseData, packetHeader().length, 1)); + return null; + } + } + + @Override + public byte[] encodeLedColor(int color) { + int[] presets = RoidmiConst.COLOR_PRESETS; + int color_id = -1; + for (int i = 0; i < presets.length; i++) { + if (presets[i] == color) { + color_id = (i + 1) & 255; + break; + } + } + + if (color_id < 0 || color_id > 8) + throw new IllegalArgumentException("color must belong to RoidmiConst.COLOR_PRESETS"); + + return encodeCommand(COMMAND_SET_COLOR, (byte) 0, (byte) color_id); + } + + @Override + public byte[] encodeFmFrequency(float frequency) { + if (frequency < 87.5 || frequency > 108.0) + throw new IllegalArgumentException("Frequency must be >= 87.5 and <= 180.0"); + + byte[] freq = frequencyToBytes(frequency); + + return encodeCommand(COMMAND_SET_FREQUENCY, freq[0], freq[1]); + } + + public byte[] encodeGetLedColor() { + return encodeCommand(COMMAND_GET_COLOR, (byte) 0, (byte) 0); + } + + public byte[] encodeGetFmFrequency() { + return encodeCommand(COMMAND_GET_FREQUENCY, (byte) 0, (byte) 0); + } + + public byte[] encodeGetVoltage() { + return null; + } + + public byte[] packetHeader() { + return PACKET_HEADER; + } + + public byte[] packetTrailer() { + return PACKET_TRAILER; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/roidmi/Roidmi3Protocol.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/roidmi/Roidmi3Protocol.java new file mode 100644 index 000000000..a4d6412e7 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/roidmi/Roidmi3Protocol.java @@ -0,0 +1,154 @@ +/* Copyright (C) 2018 José Rebelo + + 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.roidmi; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent; +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo; +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventFmFrequency; +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventLEDColor; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.util.GB; + +public class Roidmi3Protocol extends RoidmiProtocol { + private static final Logger LOG = LoggerFactory.getLogger(Roidmi3Protocol.class); + + public Roidmi3Protocol(GBDevice device) { + super(device); + } + + // Commands below need to be wrapped in a packet + + private static final byte[] COMMAND_GET_COLOR = new byte[]{0x02, (byte) 0x81}; + private static final byte[] COMMAND_GET_FREQUENCY = new byte[]{0x05, (byte) 0x81}; + private static final byte[] COMMAND_GET_VOLTAGE = new byte[]{0x06, (byte) 0x81}; + + private static final byte[] COMMAND_SET_COLOR = new byte[]{0x02, 0x01, 0x00, 0x00, 0x00}; + private static final byte[] COMMAND_SET_FREQUENCY = new byte[]{0x05, (byte) 0x81, 0x09, 0x64}; + private static final byte[] COMMAND_DENOISE_ON = new byte[]{0x05, 0x06, 0x12}; + private static final byte[] COMMAND_DENOISE_OFF = new byte[]{0x05, 0x06, 0x00}; + + private static final byte RESPONSE_COLOR = 0x02; + private static final byte RESPONSE_FREQUENCY = 0x05; + private static final byte RESPONSE_VOLTAGE = 0x06; + // Next response byte is always 0x81, followed by the value + + private static final int PACKET_MIN_LENGTH = 4; + + @Override + public GBDeviceEvent[] decodeResponse(byte[] res) { + if (res.length <= PACKET_MIN_LENGTH) { + LOG.info("Response too small"); + return null; + } + + if (calcChecksum(res) != res[res.length - 2]) { + LOG.info("Invalid response checksum"); + return null; + } + + if (res[0] + 2 != res.length) { + LOG.info("Packet length doesn't match"); + return null; + } + + if (res[1] != (byte) 0x81) { + LOG.error("Unrecognized response" + GB.hexdump(res, 0, res.length)); + return null; + } + + if (res[1] == RESPONSE_VOLTAGE) { + String voltageHex = GB.hexdump(res, 3, 2); + float voltage = Float.valueOf(voltageHex) / 10.0f; + LOG.debug("Got voltage: " + voltage); + GBDeviceEventBatteryInfo evBattery = new GBDeviceEventBatteryInfo(); + evBattery.voltage = voltage; + return new GBDeviceEvent[]{evBattery}; + } else if (res[1] == RESPONSE_COLOR) { + LOG.debug("Got color: " + GB.hexdump(res, 3, 3)); + int color = res[3] << 16 | res[4] << 8 | res[4]; + GBDeviceEventLEDColor evColor = new GBDeviceEventLEDColor(); + evColor.color = color; + return new GBDeviceEvent[]{evColor}; + } else if (res[1] == RESPONSE_FREQUENCY) { + String frequencyHex = GB.hexdump(res, 3, 2); + float frequency = Float.valueOf(frequencyHex) / 10.0f; + LOG.debug("Got frequency: " + frequency); + GBDeviceEventFmFrequency evFrequency = new GBDeviceEventFmFrequency(); + evFrequency.frequency = frequency; + return new GBDeviceEvent[]{evFrequency}; + } else { + LOG.error("Unrecognized response" + GB.hexdump(res, 0, res.length)); + return null; + } + } + + @Override + public byte[] encodeLedColor(int color) { + byte[] cmd = COMMAND_SET_COLOR.clone(); + + cmd[2] = (byte) color; + cmd[3] = (byte) (color >> 8); + cmd[4] = (byte) (color >> 16); + + return encodeCommand(cmd); + } + + @Override + public byte[] encodeFmFrequency(float frequency) { + if (frequency < 87.5 || frequency > 108.0) + throw new IllegalArgumentException("Frequency must be >= 87.5 and <= 180.0"); + + byte[] cmd = COMMAND_SET_FREQUENCY.clone(); + byte[] freq = frequencyToBytes(frequency); + cmd[2] = freq[0]; + cmd[3] = freq[1]; + + return encodeCommand(cmd); + } + + @Override + public byte[] encodeGetLedColor() { + return encodeCommand(COMMAND_GET_COLOR); + } + + @Override + public byte[] encodeGetFmFrequency() { + return encodeCommand(COMMAND_GET_FREQUENCY); + } + + @Override + public byte[] packetHeader() { + return new byte[0]; + } + + @Override + public byte[] packetTrailer() { + return new byte[0]; + } + + public byte[] encodeGetVoltage() { + return COMMAND_GET_VOLTAGE; + } + + public byte[] encodeDenoise(boolean enabled) { + byte[] cmd = enabled ? COMMAND_DENOISE_ON : COMMAND_DENOISE_OFF; + return encodeCommand(cmd); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/roidmi/RoidmiIoThread.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/roidmi/RoidmiIoThread.java new file mode 100644 index 000000000..012f0c28d --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/roidmi/RoidmiIoThread.java @@ -0,0 +1,70 @@ +/* Copyright (C) 2018 José Rebelo + + 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.roidmi; + +import android.bluetooth.BluetoothAdapter; +import android.content.Context; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; + +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.service.btclassic.BtClassicIoThread; +import nodomain.freeyourgadget.gadgetbridge.util.GB; + +public class RoidmiIoThread extends BtClassicIoThread { + private static final Logger LOG = LoggerFactory.getLogger(RoidmiIoThread.class); + + private final byte[] HEADER; + private final byte[] TRAILER; + + public RoidmiIoThread(GBDevice gbDevice, Context context, RoidmiProtocol roidmiProtocol, RoidmiSupport roidmiSupport, BluetoothAdapter roidmiBtAdapter) { + super(gbDevice, context, roidmiProtocol, roidmiSupport, roidmiBtAdapter); + + HEADER = roidmiProtocol.packetHeader(); + TRAILER = roidmiProtocol.packetTrailer(); + } + + @Override + protected byte[] parseIncoming(InputStream inputStream) throws IOException { + ByteArrayOutputStream msgStream = new ByteArrayOutputStream(); + + boolean finished = false; + byte[] incoming = new byte[1]; + + while (!finished) { + inputStream.read(incoming); + msgStream.write(incoming); + + byte[] arr = msgStream.toByteArray(); + if (arr.length > HEADER.length) { + int expectedLength = HEADER.length + TRAILER.length + arr[HEADER.length] + 2; + if (arr.length == expectedLength) { + finished = true; + } + } + } + + byte[] msgArray = msgStream.toByteArray(); + LOG.debug("Packet: " + GB.hexdump(msgArray, 0, msgArray.length)); + return msgArray; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/roidmi/RoidmiProtocol.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/roidmi/RoidmiProtocol.java new file mode 100644 index 000000000..07f809905 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/roidmi/RoidmiProtocol.java @@ -0,0 +1,93 @@ +/* Copyright (C) 2018 José Rebelo + + 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.roidmi; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Locale; + +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol; + +public abstract class RoidmiProtocol extends GBDeviceProtocol { + private static final Logger LOG = LoggerFactory.getLogger(RoidmiProtocol.class); + + // Packet structure: HEADER N_PARAMS PARAM_1 ... PARAM_N CHECKSUM TRAILER + + public RoidmiProtocol(GBDevice device) { + super(device); + } + + @Override + public abstract GBDeviceEvent[] decodeResponse(byte[] responseData); + + @Override + public abstract byte[] encodeLedColor(int color); + + @Override + public abstract byte[] encodeFmFrequency(float frequency); + + public abstract byte[] encodeGetLedColor(); + + public abstract byte[] encodeGetFmFrequency(); + + public abstract byte[] encodeGetVoltage(); + + public abstract byte[] packetHeader(); + + public abstract byte[] packetTrailer(); + + public byte[] encodeCommand(byte... params) { + byte[] cmd = new byte[packetHeader().length + packetTrailer().length + params.length + 2]; + + for (int i = 0; i < packetHeader().length; i++) + cmd[i] = packetHeader()[i]; + for (int i = 0; i < packetTrailer().length; i++) + cmd[cmd.length - packetTrailer().length + i] = packetTrailer()[i]; + + cmd[packetHeader().length] = (byte) params.length; + for (int i = 0; i < params.length; i++) { + cmd[packetHeader().length + 1 + i] = params[i]; + } + cmd[cmd.length - packetTrailer().length - 1] = calcChecksum(cmd); + + return cmd; + } + + public byte calcChecksum(byte[] packet) { + int chk = 0; + for (int i = packetHeader().length; i < packet.length - packetTrailer().length - 1; i++) { + chk += packet[i] & 255; + } + return (byte) chk; + } + + public byte[] frequencyToBytes(float frequency) { + byte[] res = new byte[2]; + String format = String.format(Locale.getDefault(), "%04d", (int) (10.0f * frequency)); + try { + res[0] = (byte) (Integer.parseInt(format.substring(0, 2), 16) & 255); + res[1] = (byte) (Integer.parseInt(format.substring(2), 16) & 255); + } catch (Exception e) { + LOG.error(e.getMessage()); + } + + return res; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/roidmi/RoidmiSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/roidmi/RoidmiSupport.java new file mode 100644 index 000000000..7736387a0 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/roidmi/RoidmiSupport.java @@ -0,0 +1,171 @@ +/* Copyright (C) 2018 José Rebelo + + 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.roidmi; + +import android.net.Uri; +import android.os.Handler; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.UUID; + +import nodomain.freeyourgadget.gadgetbridge.devices.roidmi.RoidmiConst; +import nodomain.freeyourgadget.gadgetbridge.model.Alarm; +import nodomain.freeyourgadget.gadgetbridge.model.DeviceType; +import nodomain.freeyourgadget.gadgetbridge.service.serial.AbstractSerialDeviceSupport; +import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceIoThread; +import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol; + +public class RoidmiSupport extends AbstractSerialDeviceSupport { + private static final Logger LOG = LoggerFactory.getLogger(RoidmiSupport.class); + + private final Handler handler = new Handler(); + private int infoRequestTries = 0; + private final Runnable infosRunnable = new Runnable() { + public void run() { + infoRequestTries += 1; + + try { + boolean infoMissing = false; + + if (getDevice().getExtraInfo("led_color") == null) { + infoMissing = true; + onSendConfiguration(RoidmiConst.ACTION_GET_LED_COLOR); + } + + if (getDevice().getExtraInfo("fm_frequency") == null) { + infoMissing = true; + + onSendConfiguration(RoidmiConst.ACTION_GET_FM_FREQUENCY); + } + + if (getDevice().getType() == DeviceType.ROIDMI3) { + if (getDevice().getBatteryVoltage() == -1) { + infoMissing = true; + + onSendConfiguration(RoidmiConst.ACTION_GET_VOLTAGE); + } + } + + if (infoMissing) { + if (infoRequestTries < 6) { + requestDeviceInfos(500 + infoRequestTries * 120); + } else { + LOG.error("Failed to get Roidmi infos after 6 tries"); + } + } + } catch (Exception e) { + LOG.error("Failed to get Roidmi infos", e); + } + } + }; + + private void requestDeviceInfos(int delayMillis) { + handler.postDelayed(infosRunnable, delayMillis); + } + + @Override + public boolean connect() { + getDeviceIOThread().start(); + + requestDeviceInfos(1500); + + return true; + } + + @Override + protected GBDeviceProtocol createDeviceProtocol() { + if (getDevice().getType() == DeviceType.ROIDMI) { + return new Roidmi1Protocol(getDevice()); + } else if (getDevice().getType() == DeviceType.ROIDMI3) { + return new Roidmi3Protocol(getDevice()); + } + + LOG.error("Unsupported device type with key = " + getDevice().getType().getKey()); + return null; + } + + @Override + public void onSendConfiguration(final String config) { + LOG.debug("onSendConfiguration " + config); + + RoidmiIoThread roidmiIoThread = getDeviceIOThread(); + RoidmiProtocol roidmiProtocol = (RoidmiProtocol) getDeviceProtocol(); + + switch (config) { + case RoidmiConst.ACTION_GET_LED_COLOR: + roidmiIoThread.write(roidmiProtocol.encodeGetLedColor()); + break; + case RoidmiConst.ACTION_GET_FM_FREQUENCY: + roidmiIoThread.write(roidmiProtocol.encodeGetFmFrequency()); + break; + case RoidmiConst.ACTION_GET_VOLTAGE: + roidmiIoThread.write(roidmiProtocol.encodeGetVoltage()); + break; + default: + LOG.error("Invalid Roidmi configuration " + config); + break; + } + } + + @Override + protected GBDeviceIoThread createDeviceIOThread() { + return new RoidmiIoThread(getDevice(), getContext(), (RoidmiProtocol) getDeviceProtocol(), RoidmiSupport.this, getBluetoothAdapter()); + } + + @Override + public synchronized RoidmiIoThread getDeviceIOThread() { + return (RoidmiIoThread) super.getDeviceIOThread(); + } + + @Override + public boolean useAutoConnect() { + return false; + } + + @Override + public void onInstallApp(Uri uri) { + // Nothing to do + } + + @Override + public void onAppConfiguration(UUID uuid, String config, Integer id) { + // Nothing to do + } + + @Override + public void onHeartRateTest() { + // Nothing to do + } + + @Override + public void onSetConstantVibration(int intensity) { + // Nothing to do + } + + @Override + public void onSetHeartRateMeasurementInterval(int seconds) { + // Nothing to do + } + + @Override + public void onSetAlarms(ArrayList alarms) { + // Nothing to do + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/serial/AbstractSerialDeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/serial/AbstractSerialDeviceSupport.java index e73527eab..c9050af91 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/serial/AbstractSerialDeviceSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/serial/AbstractSerialDeviceSupport.java @@ -250,4 +250,16 @@ public abstract class AbstractSerialDeviceSupport extends AbstractDeviceSupport byte[] bytes = gbDeviceProtocol.encodeSendWeather(weatherSpec); sendToDevice(bytes); } + + @Override + public void onSetFmFrequency(float frequency) { + byte[] bytes = gbDeviceProtocol.encodeFmFrequency(frequency); + sendToDevice(bytes); + } + + @Override + public void onSetLedColor(int color) { + byte[] bytes = gbDeviceProtocol.encodeLedColor(color); + sendToDevice(bytes); + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/serial/GBDeviceProtocol.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/serial/GBDeviceProtocol.java index 5070ce8e6..f29b1c23b 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/serial/GBDeviceProtocol.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/serial/GBDeviceProtocol.java @@ -133,4 +133,12 @@ public abstract class GBDeviceProtocol { public byte[] encodeSendWeather(WeatherSpec weatherSpec) { return null; } + + public byte[] encodeLedColor(int color) { + return null; + } + + public byte[] encodeFmFrequency(float frequency) { + return null; + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DeviceHelper.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DeviceHelper.java index 78255160e..cc1fc17cc 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DeviceHelper.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DeviceHelper.java @@ -52,6 +52,8 @@ import nodomain.freeyourgadget.gadgetbridge.devices.huami.miband2.MiBand2HRXCoor import nodomain.freeyourgadget.gadgetbridge.devices.huami.miband3.MiBand3Coordinator; import nodomain.freeyourgadget.gadgetbridge.devices.id115.ID115Coordinator; import nodomain.freeyourgadget.gadgetbridge.devices.jyou.TeclastH30Coordinator; +import nodomain.freeyourgadget.gadgetbridge.devices.roidmi.Roidmi1Coordinator; +import nodomain.freeyourgadget.gadgetbridge.devices.roidmi.Roidmi3Coordinator; import nodomain.freeyourgadget.gadgetbridge.devices.liveview.LiveviewCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst; import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandCoordinator; @@ -217,6 +219,8 @@ public class DeviceHelper { result.add(new ZeTimeCoordinator()); result.add(new ID115Coordinator()); result.add(new Watch9DeviceCoordinator()); + result.add(new Roidmi1Coordinator()); + result.add(new Roidmi3Coordinator()); return result; } diff --git a/app/src/main/res/drawable-hdpi/ic_device_roidmi.png b/app/src/main/res/drawable-hdpi/ic_device_roidmi.png new file mode 100644 index 000000000..84b60d631 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_device_roidmi.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_device_roidmi_disabled.png b/app/src/main/res/drawable-hdpi/ic_device_roidmi_disabled.png new file mode 100644 index 000000000..a7c66947b Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_device_roidmi_disabled.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_device_roidmi.png b/app/src/main/res/drawable-mdpi/ic_device_roidmi.png new file mode 100644 index 000000000..8e3af1efb Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_device_roidmi.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_device_roidmi_disabled.png b/app/src/main/res/drawable-mdpi/ic_device_roidmi_disabled.png new file mode 100644 index 000000000..f6e6526f5 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_device_roidmi_disabled.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_device_roidmi.png b/app/src/main/res/drawable-xhdpi/ic_device_roidmi.png new file mode 100644 index 000000000..fdd34e5bc Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_device_roidmi.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_device_roidmi_disabled.png b/app/src/main/res/drawable-xhdpi/ic_device_roidmi_disabled.png new file mode 100644 index 000000000..be31b8025 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_device_roidmi_disabled.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_device_roidmi.png b/app/src/main/res/drawable-xxhdpi/ic_device_roidmi.png new file mode 100644 index 000000000..34fec7f48 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_device_roidmi.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_device_roidmi_disabled.png b/app/src/main/res/drawable-xxhdpi/ic_device_roidmi_disabled.png new file mode 100644 index 000000000..1c67417d0 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_device_roidmi_disabled.png differ diff --git a/app/src/main/res/drawable/ic_led_color.xml b/app/src/main/res/drawable/ic_led_color.xml new file mode 100644 index 000000000..8c0a801e2 --- /dev/null +++ b/app/src/main/res/drawable/ic_led_color.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_radio.xml b/app/src/main/res/drawable/ic_radio.xml new file mode 100644 index 000000000..d14122596 --- /dev/null +++ b/app/src/main/res/drawable/ic_radio.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/device_itemv2.xml b/app/src/main/res/layout/device_itemv2.xml index f8ff97f1b..c780f23f5 100644 --- a/app/src/main/res/layout/device_itemv2.xml +++ b/app/src/main/res/layout/device_itemv2.xml @@ -139,13 +139,58 @@ + + + + + + + + + + diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index dbcbe3cf7..2896cce0c 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -9,6 +9,8 @@ Monitor de sono (ALPHA) Procurar dispositivo perdido Captura de ecrã + Mudar cor do LED + Mudar frequência FM Desligar Apagar dispositivo Apagar %1$s @@ -523,4 +525,14 @@ Calibrar Emparelhamento do Watch 9 Calibração do Watch 9 + + + + Cor do LED + + + Frequência FM + Frequência inválida + Por favor introduza uma frequência entre 87.5 e 108.0 + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5092d9a06..32df5a596 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -10,6 +10,8 @@ Synchronize Find lost device Take Screenshot + Change LED Color + Change FM Frequency Connect Disconnect Delete Device @@ -605,6 +607,8 @@ MyKronoz ZeTime ID115 Watch 9 + Roidmi + Roidmi 3 Choose export location Gadgetbridge notifications @@ -634,4 +638,12 @@ Share log Please keep in mind Gadgetbridge log files may contain lots of personal information, including but not limited to health data, unique identifiers (such as a device MAC address), music preferences, etc. Consider editing the file and removing this information before sending the file to a public issue report. Warning! + + + LED Color + + + FM Frequency + Invalid frequency + Please enter a frequency between 87.5 and 108.0 diff --git a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/TestDeviceSupport.java b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/TestDeviceSupport.java index e4ccc90ea..1c4bcb8da 100644 --- a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/TestDeviceSupport.java +++ b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/TestDeviceSupport.java @@ -188,4 +188,14 @@ class TestDeviceSupport extends AbstractDeviceSupport { public void onSendWeather(WeatherSpec weatherSpec) { } + + @Override + public void onSetFmFrequency(float frequency) { + + } + + @Override + public void onSetLedColor(int color) { + + } }