From 287a759e2892a74c815a64caba880a3c2fa195ab Mon Sep 17 00:00:00 2001 From: Jonathan Gobbo Date: Thu, 28 Nov 2024 18:16:17 +0000 Subject: [PATCH] Add support for Redmi Buds 5 Pro (#4343) This PR introduces support for the Redmi Buds 5 Pro earbuds. Working: - Connection and authentication; - Firmware version; - Battery percentage; - Switching between ANC/Transparency/OFF, their relative strength settings and Adaptive ANC toggle; - Configuring all touch options; - Ear detection play/pause, Auto call answer, Double connection, Adaptive sound settings toggles; - Equalizer presets and custom curve; Not working: - Personalized ANC (code for toggle is present but commented-out, as the logic for ear calibration is missing); - Spatial Audio - Find my earbuds - Firmware Update Closes #3566 Reviewed-on: https://codeberg.org/Freeyourgadget/Gadgetbridge/pulls/4343 Co-authored-by: Jonathan Gobbo Co-committed-by: Jonathan Gobbo --- .../DeviceSettingsPreferenceConst.java | 33 ++ .../DeviceSpecificSettingsFragment.java | 33 ++ .../RedmiBuds5ProCoordinator.java | 103 ++++ .../RedmiBuds5ProSettingsCustomizer.java | 161 +++++ .../redmibuds5pro/prefs/Configuration.java | 47 ++ .../xiaomi/redmibuds5pro/prefs/Gestures.java | 22 + .../gadgetbridge/model/DeviceType.java | 2 + .../RedmiBuds5ProDeviceSupport.java | 46 ++ .../redmibuds5pro/RedmiBuds5ProIOThread.java | 66 +++ .../redmibuds5pro/RedmiBuds5ProProtocol.java | 548 ++++++++++++++++++ .../redmibuds5pro/protocol/AuthData.java | 25 + .../protocol/Authentication.java | 181 ++++++ .../redmibuds5pro/protocol/Message.java | 124 ++++ .../redmibuds5pro/protocol/MessageType.java | 35 ++ .../redmibuds5pro/protocol/Opcode.java | 33 ++ app/src/main/res/values/arrays.xml | 154 +++++ app/src/main/res/values/strings.xml | 48 ++ .../devicesettings_redmibuds5pro_gestures.xml | 123 ++++ ...evicesettings_redmibuds5pro_headphones.xml | 50 ++ .../devicesettings_redmibuds5pro_sound.xml | 144 +++++ 20 files changed, 1978 insertions(+) create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/xiaomi/redmibuds5pro/RedmiBuds5ProCoordinator.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/xiaomi/redmibuds5pro/RedmiBuds5ProSettingsCustomizer.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/xiaomi/redmibuds5pro/prefs/Configuration.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/xiaomi/redmibuds5pro/prefs/Gestures.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/redmibuds5pro/RedmiBuds5ProDeviceSupport.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/redmibuds5pro/RedmiBuds5ProIOThread.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/redmibuds5pro/RedmiBuds5ProProtocol.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/redmibuds5pro/protocol/AuthData.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/redmibuds5pro/protocol/Authentication.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/redmibuds5pro/protocol/Message.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/redmibuds5pro/protocol/MessageType.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/redmibuds5pro/protocol/Opcode.java create mode 100644 app/src/main/res/xml/devicesettings_redmibuds5pro_gestures.xml create mode 100644 app/src/main/res/xml/devicesettings_redmibuds5pro_headphones.xml create mode 100644 app/src/main/res/xml/devicesettings_redmibuds5pro_sound.xml diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSettingsPreferenceConst.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSettingsPreferenceConst.java index 9455310d9..a07090485 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSettingsPreferenceConst.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSettingsPreferenceConst.java @@ -365,6 +365,39 @@ public class DeviceSettingsPreferenceConst { public static final String PREF_GALAXY_BUDS_PRO_ANC_LEVEL="pref_galaxy_buds_pro_anc_level"; public static final String PREFS_GALAXY_BUDS_SEAMLESS_CONNECTION="prefs_galaxy_buds_seamless_connection"; + public static final String PREF_REDMI_BUDS_5_PRO_AMBIENT_SOUND_CONTROL="pref_redmi_buds_5_pro_ambient_sound_control"; + public static final String PREF_REDMI_BUDS_5_PRO_NOISE_CANCELLING_STRENGTH="pref_redmi_buds_5_pro_noise_cancelling_strength"; + public static final String PREF_REDMI_BUDS_5_PRO_TRANSPARENCY_STRENGTH="pref_redmi_buds_5_pro_transparency_strength"; + public static final String PREF_REDMI_BUDS_5_PRO_ADAPTIVE_NOISE_CANCELLING="pref_redmi_buds_5_pro_adaptive_noise_cancelling"; +// public static final String PREF_REDMI_BUDS_5_PRO_PERSONALIZED_NOISE_CANCELLING="pref_redmi_buds_5_pro_personalized_noise_cancelling"; + public static final String PREF_REDMI_BUDS_5_PRO_CONTROL_SINGLE_TAP_LEFT="pref_redmi_buds_5_pro_control_single_tap_left"; + public static final String PREF_REDMI_BUDS_5_PRO_CONTROL_SINGLE_TAP_RIGHT="pref_redmi_buds_5_pro_control_single_tap_right"; + public static final String PREF_REDMI_BUDS_5_PRO_CONTROL_DOUBLE_TAP_LEFT="pref_redmi_buds_5_pro_control_double_tap_left"; + public static final String PREF_REDMI_BUDS_5_PRO_CONTROL_DOUBLE_TAP_RIGHT="pref_redmi_buds_5_pro_control_double_tap_right"; + public static final String PREF_REDMI_BUDS_5_PRO_CONTROL_TRIPLE_TAP_LEFT="pref_redmi_buds_5_pro_control_triple_tap_left"; + public static final String PREF_REDMI_BUDS_5_PRO_CONTROL_TRIPLE_TAP_RIGHT="pref_redmi_buds_5_pro_control_triple_tap_right"; + public static final String PREF_REDMI_BUDS_5_PRO_CONTROL_LONG_TAP_MODE_LEFT ="pref_redmi_buds_5_pro_control_long_tap_mode_left"; + public static final String PREF_REDMI_BUDS_5_PRO_CONTROL_LONG_TAP_MODE_RIGHT ="pref_redmi_buds_5_pro_control_long_tap_mode_right"; + public static final String PREF_REDMI_BUDS_5_PRO_CONTROL_LONG_TAP_SETTINGS_LEFT ="pref_redmi_buds_5_pro_control_long_tap_settings_left"; + public static final String PREF_REDMI_BUDS_5_PRO_CONTROL_LONG_TAP_SETTINGS_RIGHT ="pref_redmi_buds_5_pro_control_long_tap_settings_right"; + public static final String PREF_REDMI_BUDS_5_PRO_WEARING_DETECTION="pref_redmi_buds_5_pro_wearing_detection"; + public static final String PREF_REDMI_BUDS_5_PRO_AUTO_REPLY_PHONECALL="pref_redmi_buds_5_pro_auto_reply_phonecall"; + public static final String PREF_REDMI_BUDS_5_PRO_DOUBLE_CONNECTION="pref_redmi_buds_5_pro_double_connection"; +// public static final String PREF_REDMI_BUDS_5_PRO_SURROUND_SOUND="pref_redmi_buds_5_pro_surround_sound"; +// public static final String PREF_REDMI_BUDS_5_PRO_SURROUND_SOUND_MODE="pref_redmi_buds_5_pro_surround_sound_mode"; + public static final String PREF_REDMI_BUDS_5_PRO_EQUALIZER_PRESET="pref_redmi_buds_5_pro_equalizer_preset"; + public static final String PREF_REDMI_BUDS_5_PRO_EQUALIZER_BAND_62="pref_redmi_buds_5_pro_equalizer_band_62"; + public static final String PREF_REDMI_BUDS_5_PRO_EQUALIZER_BAND_125="pref_redmi_buds_5_pro_equalizer_band_125"; + public static final String PREF_REDMI_BUDS_5_PRO_EQUALIZER_BAND_250="pref_redmi_buds_5_pro_equalizer_band_250"; + public static final String PREF_REDMI_BUDS_5_PRO_EQUALIZER_BAND_500="pref_redmi_buds_5_pro_equalizer_band_500"; + public static final String PREF_REDMI_BUDS_5_PRO_EQUALIZER_BAND_1k="pref_redmi_buds_5_pro_equalizer_band_1k"; + public static final String PREF_REDMI_BUDS_5_PRO_EQUALIZER_BAND_2k="pref_redmi_buds_5_pro_equalizer_band_2k"; + public static final String PREF_REDMI_BUDS_5_PRO_EQUALIZER_BAND_4k="pref_redmi_buds_5_pro_equalizer_band_4k"; + public static final String PREF_REDMI_BUDS_5_PRO_EQUALIZER_BAND_8k="pref_redmi_buds_5_pro_equalizer_band_8k"; + public static final String PREF_REDMI_BUDS_5_PRO_EQUALIZER_BAND_12k="pref_redmi_buds_5_pro_equalizer_band_12k"; + public static final String PREF_REDMI_BUDS_5_PRO_EQUALIZER_BAND_16k="pref_redmi_buds_5_pro_equalizer_band_16k"; + public static final String PREF_REDMI_BUDS_5_PRO_ADAPTIVE_SOUND="pref_redmi_buds_5_pro_adaptive_sound"; + public static final String PREF_SONY_AUDIO_CODEC = "pref_sony_audio_codec"; public static final String PREF_SONY_PROTOCOL_VERSION = "pref_protocol_version"; public static final String PREF_SONY_ACTUAL_PROTOCOL_VERSION = "pref_actual_protocol_version"; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSpecificSettingsFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSpecificSettingsFragment.java index 112ff24e1..89fc0eefe 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSpecificSettingsFragment.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSpecificSettingsFragment.java @@ -676,6 +676,39 @@ public class DeviceSpecificSettingsFragment extends AbstractPreferenceFragment i addPreferenceHandlerFor(PREF_WATCHFACE); + addPreferenceHandlerFor(PREF_REDMI_BUDS_5_PRO_AMBIENT_SOUND_CONTROL); + addPreferenceHandlerFor(PREF_REDMI_BUDS_5_PRO_NOISE_CANCELLING_STRENGTH); + addPreferenceHandlerFor(PREF_REDMI_BUDS_5_PRO_TRANSPARENCY_STRENGTH); + addPreferenceHandlerFor(PREF_REDMI_BUDS_5_PRO_ADAPTIVE_NOISE_CANCELLING); +// addPreferenceHandlerFor(PREF_REDMI_BUDS_5_PRO_PERSONALIZED_NOISE_CANCELLING); + addPreferenceHandlerFor(PREF_REDMI_BUDS_5_PRO_CONTROL_SINGLE_TAP_LEFT); + addPreferenceHandlerFor(PREF_REDMI_BUDS_5_PRO_CONTROL_SINGLE_TAP_RIGHT); + addPreferenceHandlerFor(PREF_REDMI_BUDS_5_PRO_CONTROL_DOUBLE_TAP_LEFT); + addPreferenceHandlerFor(PREF_REDMI_BUDS_5_PRO_CONTROL_DOUBLE_TAP_RIGHT); + addPreferenceHandlerFor(PREF_REDMI_BUDS_5_PRO_CONTROL_TRIPLE_TAP_LEFT); + addPreferenceHandlerFor(PREF_REDMI_BUDS_5_PRO_CONTROL_TRIPLE_TAP_RIGHT); + addPreferenceHandlerFor(PREF_REDMI_BUDS_5_PRO_CONTROL_LONG_TAP_MODE_LEFT); + addPreferenceHandlerFor(PREF_REDMI_BUDS_5_PRO_CONTROL_LONG_TAP_MODE_RIGHT); + addPreferenceHandlerFor(PREF_REDMI_BUDS_5_PRO_CONTROL_LONG_TAP_SETTINGS_LEFT); + addPreferenceHandlerFor(PREF_REDMI_BUDS_5_PRO_CONTROL_LONG_TAP_SETTINGS_RIGHT); + addPreferenceHandlerFor(PREF_REDMI_BUDS_5_PRO_WEARING_DETECTION); + addPreferenceHandlerFor(PREF_REDMI_BUDS_5_PRO_AUTO_REPLY_PHONECALL); + addPreferenceHandlerFor(PREF_REDMI_BUDS_5_PRO_DOUBLE_CONNECTION); +// addPreferenceHandlerFor(PREF_REDMI_BUDS_5_PRO_SURROUND_SOUND); + addPreferenceHandlerFor(PREF_REDMI_BUDS_5_PRO_ADAPTIVE_SOUND); +// addPreferenceHandlerFor(PREF_REDMI_BUDS_5_PRO_SURROUND_SOUND_MODE); + addPreferenceHandlerFor(PREF_REDMI_BUDS_5_PRO_EQUALIZER_PRESET); + addPreferenceHandlerFor(PREF_REDMI_BUDS_5_PRO_EQUALIZER_BAND_62); + addPreferenceHandlerFor(PREF_REDMI_BUDS_5_PRO_EQUALIZER_BAND_125); + addPreferenceHandlerFor(PREF_REDMI_BUDS_5_PRO_EQUALIZER_BAND_250); + addPreferenceHandlerFor(PREF_REDMI_BUDS_5_PRO_EQUALIZER_BAND_500); + addPreferenceHandlerFor(PREF_REDMI_BUDS_5_PRO_EQUALIZER_BAND_1k); + addPreferenceHandlerFor(PREF_REDMI_BUDS_5_PRO_EQUALIZER_BAND_2k); + addPreferenceHandlerFor(PREF_REDMI_BUDS_5_PRO_EQUALIZER_BAND_4k); + addPreferenceHandlerFor(PREF_REDMI_BUDS_5_PRO_EQUALIZER_BAND_8k); + addPreferenceHandlerFor(PREF_REDMI_BUDS_5_PRO_EQUALIZER_BAND_12k); + addPreferenceHandlerFor(PREF_REDMI_BUDS_5_PRO_EQUALIZER_BAND_16k); + addPreferenceHandlerFor(PREF_SONY_AMBIENT_SOUND_CONTROL); addPreferenceHandlerFor(PREF_SONY_AMBIENT_SOUND_CONTROL_BUTTON_MODE); addPreferenceHandlerFor(PREF_SONY_FOCUS_VOICE); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/xiaomi/redmibuds5pro/RedmiBuds5ProCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/xiaomi/redmibuds5pro/RedmiBuds5ProCoordinator.java new file mode 100644 index 000000000..e9b5fdf43 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/xiaomi/redmibuds5pro/RedmiBuds5ProCoordinator.java @@ -0,0 +1,103 @@ +/* Copyright (C) 2024 Jonathan Gobbo + + 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.xiaomi.redmibuds5pro; + +import androidx.annotation.NonNull; + +import java.util.regex.Pattern; + +import nodomain.freeyourgadget.gadgetbridge.GBException; +import nodomain.freeyourgadget.gadgetbridge.R; +import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettings; +import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsCustomizer; +import nodomain.freeyourgadget.gadgetbridge.devices.AbstractDeviceCoordinator; +import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; +import nodomain.freeyourgadget.gadgetbridge.entities.Device; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.model.BatteryConfig; +import nodomain.freeyourgadget.gadgetbridge.service.DeviceSupport; +import nodomain.freeyourgadget.gadgetbridge.service.devices.redmibuds5pro.RedmiBuds5ProDeviceSupport; + +public class RedmiBuds5ProCoordinator extends AbstractDeviceCoordinator { + @Override + protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) throws GBException { + + } + + @Override + public String getManufacturer() { + return "Xiaomi"; + } + + @NonNull + @Override + public Class getDeviceSupportClass() { + return RedmiBuds5ProDeviceSupport.class; + } + + @Override + public int getDeviceNameResource() { + return R.string.devicetype_redmi_buds_5_pro; + } + + @Override + protected Pattern getSupportedDeviceName() { + return Pattern.compile("Redmi Buds 5 Pro"); + } + + @Override + public int getBondingStyle() { + return BONDING_STYLE_NONE; + } + + @Override + public int getBatteryCount() { + return 3; + } + + @Override + public BatteryConfig[] getBatteryConfig(GBDevice device) { + BatteryConfig battery1 = new BatteryConfig(0, R.drawable.ic_tws_case, R.string.battery_case); + BatteryConfig battery2 = new BatteryConfig(1, R.drawable.ic_nothing_ear_l, R.string.left_earbud); + BatteryConfig battery3 = new BatteryConfig(2, R.drawable.ic_nothing_ear_r, R.string.right_earbud); + return new BatteryConfig[]{battery1, battery2, battery3}; + } + + @Override + public DeviceSpecificSettings getDeviceSpecificSettings(final GBDevice device) { + final DeviceSpecificSettings deviceSpecificSettings = new DeviceSpecificSettings(); + deviceSpecificSettings.addRootScreen(R.xml.devicesettings_redmibuds5pro_headphones); + deviceSpecificSettings.addRootScreen(R.xml.devicesettings_redmibuds5pro_gestures); + deviceSpecificSettings.addRootScreen(R.xml.devicesettings_redmibuds5pro_sound); + return deviceSpecificSettings; + } + + @Override + public DeviceSpecificSettingsCustomizer getDeviceSpecificSettingsCustomizer(final GBDevice device) { + return new RedmiBuds5ProSettingsCustomizer(device); + } + + @Override + public int getDefaultIconResource() { + return R.drawable.ic_device_nothingear; + } + + @Override + public int getDisabledIconResource() { + return R.drawable.ic_device_nothingear_disabled; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/xiaomi/redmibuds5pro/RedmiBuds5ProSettingsCustomizer.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/xiaomi/redmibuds5pro/RedmiBuds5ProSettingsCustomizer.java new file mode 100644 index 000000000..9d0e5c972 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/xiaomi/redmibuds5pro/RedmiBuds5ProSettingsCustomizer.java @@ -0,0 +1,161 @@ +/* Copyright (C) 2024 Jonathan Gobbo + + 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.xiaomi.redmibuds5pro; + +import android.os.Parcel; + +import androidx.annotation.NonNull; +import androidx.preference.ListPreference; +import androidx.preference.Preference; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Set; + +import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst; +import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsCustomizer; +import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsHandler; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.util.Prefs; + +public class RedmiBuds5ProSettingsCustomizer implements DeviceSpecificSettingsCustomizer { + + final GBDevice device; + + public RedmiBuds5ProSettingsCustomizer(final GBDevice device) { + this.device = device; + } + + + @Override + public void onPreferenceChange(Preference preference, DeviceSpecificSettingsHandler handler) { + + // Hide or Show ANC/Transparency settings according to the current ambient sound control mode + if (preference.getKey().equals(DeviceSettingsPreferenceConst.PREF_REDMI_BUDS_5_PRO_AMBIENT_SOUND_CONTROL)) { + String mode = ((ListPreference) preference).getValue(); + final Preference ancLevel = handler.findPreference(DeviceSettingsPreferenceConst.PREF_REDMI_BUDS_5_PRO_NOISE_CANCELLING_STRENGTH); + final Preference transparencyLevel = handler.findPreference(DeviceSettingsPreferenceConst.PREF_REDMI_BUDS_5_PRO_TRANSPARENCY_STRENGTH); + final Preference adaptiveAnc = handler.findPreference(DeviceSettingsPreferenceConst.PREF_REDMI_BUDS_5_PRO_ADAPTIVE_NOISE_CANCELLING); +// final Preference customizedAnc = handler.findPreference(DeviceSettingsPreferenceConst.PREF_REDMI_BUDS_5_PRO_PERSONALIZED_NOISE_CANCELLING); + if (ancLevel != null) { + ancLevel.setVisible(mode.equals("1")); + } + if (transparencyLevel != null) { + transparencyLevel.setVisible(mode.equals("2")); + } + if (adaptiveAnc != null) { + adaptiveAnc.setVisible(mode.equals("1")); + } +// if (customizedAnc != null) { +// customizedAnc.setVisible(mode.equals("1")); +// } + } + } + + @Override + public void customizeSettings(DeviceSpecificSettingsHandler handler, Prefs prefs, String rootKey) { + + final ListPreference longPressLeft = handler.findPreference(DeviceSettingsPreferenceConst.PREF_REDMI_BUDS_5_PRO_CONTROL_LONG_TAP_MODE_LEFT); + final ListPreference longPressRight = handler.findPreference(DeviceSettingsPreferenceConst.PREF_REDMI_BUDS_5_PRO_CONTROL_LONG_TAP_MODE_RIGHT); + + final Preference longPressLeftSettings = handler.findPreference(DeviceSettingsPreferenceConst.PREF_REDMI_BUDS_5_PRO_CONTROL_LONG_TAP_SETTINGS_LEFT); + final Preference longPressRightSettings = handler.findPreference(DeviceSettingsPreferenceConst.PREF_REDMI_BUDS_5_PRO_CONTROL_LONG_TAP_SETTINGS_RIGHT); + + final ListPreference equalizerPreset = handler.findPreference(DeviceSettingsPreferenceConst.PREF_REDMI_BUDS_5_PRO_EQUALIZER_PRESET); + + if (longPressLeft != null) { + final Preference.OnPreferenceChangeListener longPressLeftButtonListener = (preference, newVal) -> { + String mode = newVal.toString(); + if (longPressLeftSettings != null) { + longPressLeftSettings.setVisible(mode.equals("6")); + } + return true; + }; + longPressLeftButtonListener.onPreferenceChange(longPressLeft, prefs.getString(DeviceSettingsPreferenceConst.PREF_REDMI_BUDS_5_PRO_CONTROL_LONG_TAP_MODE_LEFT, "6")); + handler.addPreferenceHandlerFor(DeviceSettingsPreferenceConst.PREF_REDMI_BUDS_5_PRO_CONTROL_LONG_TAP_MODE_LEFT, longPressLeftButtonListener); + } + if (longPressRight != null) { + final Preference.OnPreferenceChangeListener longPressRightButtonListener = (preference, newVal) -> { + String mode = newVal.toString(); + if (longPressRightSettings != null) { + longPressRightSettings.setVisible(mode.equals("6")); + } + return true; + }; + longPressRightButtonListener.onPreferenceChange(longPressRight, prefs.getString(DeviceSettingsPreferenceConst.PREF_REDMI_BUDS_5_PRO_CONTROL_LONG_TAP_MODE_RIGHT, "6")); + handler.addPreferenceHandlerFor(DeviceSettingsPreferenceConst.PREF_REDMI_BUDS_5_PRO_CONTROL_LONG_TAP_MODE_RIGHT, longPressRightButtonListener); + } + + if (equalizerPreset != null) { + + final Preference.OnPreferenceChangeListener equalizerPresetListener = (preference, newVal) -> { + + final List prefsToDisable = Arrays.asList( + handler.findPreference(DeviceSettingsPreferenceConst.PREF_REDMI_BUDS_5_PRO_EQUALIZER_BAND_62), + handler.findPreference(DeviceSettingsPreferenceConst.PREF_REDMI_BUDS_5_PRO_EQUALIZER_BAND_125), + handler.findPreference(DeviceSettingsPreferenceConst.PREF_REDMI_BUDS_5_PRO_EQUALIZER_BAND_250), + handler.findPreference(DeviceSettingsPreferenceConst.PREF_REDMI_BUDS_5_PRO_EQUALIZER_BAND_500), + handler.findPreference(DeviceSettingsPreferenceConst.PREF_REDMI_BUDS_5_PRO_EQUALIZER_BAND_1k), + handler.findPreference(DeviceSettingsPreferenceConst.PREF_REDMI_BUDS_5_PRO_EQUALIZER_BAND_2k), + handler.findPreference(DeviceSettingsPreferenceConst.PREF_REDMI_BUDS_5_PRO_EQUALIZER_BAND_4k), + handler.findPreference(DeviceSettingsPreferenceConst.PREF_REDMI_BUDS_5_PRO_EQUALIZER_BAND_8k), + handler.findPreference(DeviceSettingsPreferenceConst.PREF_REDMI_BUDS_5_PRO_EQUALIZER_BAND_12k), + handler.findPreference(DeviceSettingsPreferenceConst.PREF_REDMI_BUDS_5_PRO_EQUALIZER_BAND_16k) + ); + + String mode = newVal.toString(); + for (Preference pref : prefsToDisable) { + if (pref != null) { + pref.setEnabled(mode.equals("10")); + } + } + return true; + }; + equalizerPresetListener.onPreferenceChange(equalizerPreset, prefs.getString(DeviceSettingsPreferenceConst.PREF_REDMI_BUDS_5_PRO_EQUALIZER_PRESET, "0")); + handler.addPreferenceHandlerFor(DeviceSettingsPreferenceConst.PREF_REDMI_BUDS_5_PRO_EQUALIZER_PRESET, equalizerPresetListener); + } + } + + @Override + public Set getPreferenceKeysWithSummary() { + return Collections.emptySet(); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel parcel, int i) { + parcel.writeParcelable(device, 0); + } + + public static final Creator CREATOR = new Creator() { + @Override + public RedmiBuds5ProSettingsCustomizer createFromParcel(final Parcel in) { + final GBDevice device = in.readParcelable(RedmiBuds5ProSettingsCustomizer.class.getClassLoader()); + return new RedmiBuds5ProSettingsCustomizer(device); + } + + @Override + public RedmiBuds5ProSettingsCustomizer[] newArray(final int size) { + return new RedmiBuds5ProSettingsCustomizer[size]; + } + }; +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/xiaomi/redmibuds5pro/prefs/Configuration.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/xiaomi/redmibuds5pro/prefs/Configuration.java new file mode 100644 index 000000000..e42ada1ad --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/xiaomi/redmibuds5pro/prefs/Configuration.java @@ -0,0 +1,47 @@ +package nodomain.freeyourgadget.gadgetbridge.devices.xiaomi.redmibuds5pro.prefs; + +public class Configuration { + + public enum Config { + GESTURES((byte) 0x02), + AUTO_ANSWER((byte) 0x03), + DOUBLE_CONNECTION((byte) 0x04), + EAR_DETECTION((byte) 0x06), + EQ_PRESET((byte) 0x07), + LONG_GESTURES((byte) 0x0a), + EFFECT_STRENGTH((byte) 0x0b), + ADAPTIVE_ANC((byte) 0x25), + ADAPTIVE_SOUND((byte) 0x29), + EQ_CURVE((byte) 0x37), + CUSTOMIZED_ANC((byte) 0x3b), + UNKNOWN((byte) 0xff); + + public final byte value; + + Config(byte value) { + this.value = value; + } + + public static Config fromCode(final byte code) { + for (final Config config : values()) { + if (config.value == code) { + return config; + } + } + return Config.UNKNOWN; + } + + } + + public enum StrengthTarget { + ANC((byte) 0x01), + TRANSPARENCY((byte) 0x02); + + public final byte value; + + StrengthTarget(byte value) { + this.value = value; + } + } + +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/xiaomi/redmibuds5pro/prefs/Gestures.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/xiaomi/redmibuds5pro/prefs/Gestures.java new file mode 100644 index 000000000..e7b89711f --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/xiaomi/redmibuds5pro/prefs/Gestures.java @@ -0,0 +1,22 @@ +package nodomain.freeyourgadget.gadgetbridge.devices.xiaomi.redmibuds5pro.prefs; + +public class Gestures { + + public enum InteractionType { + SINGLE((byte) 0x04), + DOUBLE((byte) 0x01), + TRIPLE((byte) 0x02), + LONG((byte) 0x03); + + public final byte value; + + InteractionType(byte value) { + this.value = value; + } + } + + public enum Position { + LEFT, + RIGHT + } +} 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 70e32d1c6..11a206e1f 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java @@ -274,6 +274,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.xiaomi.miband8pro.MiBand8Pro import nodomain.freeyourgadget.gadgetbridge.devices.xiaomi.miband9.MiBand9Coordinator; import nodomain.freeyourgadget.gadgetbridge.devices.xiaomi.miwatch.MiWatchLiteCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.xiaomi.miwatchcolorsport.MiWatchColorSportCoordinator; +import nodomain.freeyourgadget.gadgetbridge.devices.xiaomi.redmibuds5pro.RedmiBuds5ProCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.xiaomi.redmismartband2.RedmiSmartBand2Coordinator; import nodomain.freeyourgadget.gadgetbridge.devices.xiaomi.redmismartbandpro.RedmiSmartBandProCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.xiaomi.redmiwatch2.RedmiWatch2Coordinator; @@ -346,6 +347,7 @@ public enum DeviceType { MIBAND9(MiBand9Coordinator.class), MIWATCHLITE(MiWatchLiteCoordinator.class), MIWATCHCOLORSPORT(MiWatchColorSportCoordinator.class), + REDMIBUDS5PRO(RedmiBuds5ProCoordinator.class), REDMIWATCH3ACTIVE(RedmiWatch3ActiveCoordinator.class), REDMIWATCH3(RedmiWatch3Coordinator.class), REDMISMARTBAND2(RedmiSmartBand2Coordinator.class), diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/redmibuds5pro/RedmiBuds5ProDeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/redmibuds5pro/RedmiBuds5ProDeviceSupport.java new file mode 100644 index 000000000..af69f495e --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/redmibuds5pro/RedmiBuds5ProDeviceSupport.java @@ -0,0 +1,46 @@ +/* Copyright (C) 2024 Jonathan Gobbo + + 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.redmibuds5pro; + +import nodomain.freeyourgadget.gadgetbridge.service.serial.AbstractSerialDeviceSupport; +import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceIoThread; +import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol; + +public class RedmiBuds5ProDeviceSupport extends AbstractSerialDeviceSupport { + @Override + protected GBDeviceProtocol createDeviceProtocol() { + return new RedmiBuds5ProProtocol(getDevice()); + } + + @Override + protected GBDeviceIoThread createDeviceIOThread() { + return new RedmiBuds5ProIOThread(getDevice(), getContext(), + (RedmiBuds5ProProtocol) getDeviceProtocol(), + RedmiBuds5ProDeviceSupport.this, getBluetoothAdapter()); + } + + @Override + public boolean connect() { + getDeviceIOThread().start(); + return true; + } + + @Override + public boolean useAutoConnect() { + return false; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/redmibuds5pro/RedmiBuds5ProIOThread.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/redmibuds5pro/RedmiBuds5ProIOThread.java new file mode 100644 index 000000000..e42bcc832 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/redmibuds5pro/RedmiBuds5ProIOThread.java @@ -0,0 +1,66 @@ +/* Copyright (C) 2024 Jonathan Gobbo + + 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.redmibuds5pro; + +import static nodomain.freeyourgadget.gadgetbridge.util.GB.hexdump; + +import android.bluetooth.BluetoothAdapter; +import android.content.Context; +import android.os.ParcelUuid; + +import androidx.annotation.NonNull; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; +import java.util.UUID; + +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.service.btclassic.BtClassicIoThread; + +public class RedmiBuds5ProIOThread extends BtClassicIoThread { + private static final Logger LOG = LoggerFactory.getLogger(RedmiBuds5ProIOThread.class); + private final RedmiBuds5ProProtocol redmiProtocol; + + public RedmiBuds5ProIOThread(GBDevice gbDevice, Context context, RedmiBuds5ProProtocol redmiProtocol, RedmiBuds5ProDeviceSupport deviceSupport, BluetoothAdapter btAdapter) { + super(gbDevice, context, redmiProtocol, deviceSupport, btAdapter); + this.redmiProtocol = redmiProtocol; + } + + @Override + protected byte[] parseIncoming(InputStream stream) throws IOException { + byte[] buffer = new byte[1048576]; + int bytes = stream.read(buffer); + LOG.debug("read {} bytes. {}", bytes, hexdump(buffer, 0, bytes)); + return Arrays.copyOf(buffer, bytes); + } + + @NonNull + @Override + protected UUID getUuidToConnect(@NonNull ParcelUuid[] uuids) { + return this.redmiProtocol.UUID_DEVICE_CTRL; + } + + @Override + protected void initialize() { + write(redmiProtocol.encodeStartAuthentication()); + setUpdateState(GBDevice.State.INITIALIZING); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/redmibuds5pro/RedmiBuds5ProProtocol.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/redmibuds5pro/RedmiBuds5ProProtocol.java new file mode 100644 index 000000000..a1d4076f6 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/redmibuds5pro/RedmiBuds5ProProtocol.java @@ -0,0 +1,548 @@ +/* Copyright (C) 2024 Jonathan Gobbo + + 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.redmibuds5pro; + +import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.*; +import static nodomain.freeyourgadget.gadgetbridge.util.GB.hexdump; + +import android.content.SharedPreferences; +import android.content.SharedPreferences.Editor; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent; +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo; +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventSendBytes; +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdateDeviceState; +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventVersionInfo; +import nodomain.freeyourgadget.gadgetbridge.devices.xiaomi.redmibuds5pro.prefs.Configuration.Config; +import nodomain.freeyourgadget.gadgetbridge.devices.xiaomi.redmibuds5pro.prefs.Configuration.StrengthTarget; +import nodomain.freeyourgadget.gadgetbridge.devices.xiaomi.redmibuds5pro.prefs.Gestures.InteractionType; +import nodomain.freeyourgadget.gadgetbridge.devices.xiaomi.redmibuds5pro.prefs.Gestures.Position; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice.State; +import nodomain.freeyourgadget.gadgetbridge.model.BatteryState; +import nodomain.freeyourgadget.gadgetbridge.service.devices.redmibuds5pro.protocol.Authentication; +import nodomain.freeyourgadget.gadgetbridge.service.devices.redmibuds5pro.protocol.Message; +import nodomain.freeyourgadget.gadgetbridge.service.devices.redmibuds5pro.protocol.MessageType; +import nodomain.freeyourgadget.gadgetbridge.service.devices.redmibuds5pro.protocol.Opcode; +import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol; +import nodomain.freeyourgadget.gadgetbridge.util.Prefs; + +public class RedmiBuds5ProProtocol extends GBDeviceProtocol { + + private static final Logger LOG = LoggerFactory.getLogger(RedmiBuds5ProProtocol.class); + final UUID UUID_DEVICE_CTRL = UUID.fromString("0000fd2d-0000-1000-8000-00805f9b34fb"); + + private byte sequenceNumber = 0; + + protected RedmiBuds5ProProtocol(GBDevice device) { + super(device); + } + + public byte[] encodeStartAuthentication() { + byte[] authRnd = Authentication.getRandomChallenge(); + LOG.debug("[AUTH] Sending challenge: {}", hexdump(authRnd)); + + byte[] payload = new byte[17]; + payload[0] = 0x01; + System.arraycopy(authRnd, 0, payload, 1, 16); + return new Message(MessageType.PHONE_REQUEST, Opcode.AUTH_CHALLENGE, sequenceNumber++, payload).encode(); + } + + @Override + public byte[] encodeSendConfiguration(String config) { + switch (config) { + case PREF_REDMI_BUDS_5_PRO_AMBIENT_SOUND_CONTROL: + return encodeSetAmbientSoundControl(); + case PREF_REDMI_BUDS_5_PRO_NOISE_CANCELLING_STRENGTH: + return encodeSetEffectStrength(config, StrengthTarget.ANC); + case PREF_REDMI_BUDS_5_PRO_TRANSPARENCY_STRENGTH: + return encodeSetEffectStrength(config, StrengthTarget.TRANSPARENCY); + case PREF_REDMI_BUDS_5_PRO_ADAPTIVE_NOISE_CANCELLING: + return encodeSetBooleanConfig(config, Config.ADAPTIVE_ANC); +// case PREF_REDMI_BUDS_5_PRO_PERSONALIZED_NOISE_CANCELLING: +// return encodeSetBooleanConfig(config, Config.CUSTOMIZED_ANC); + + case PREF_REDMI_BUDS_5_PRO_CONTROL_SINGLE_TAP_LEFT: + return encodeSetGesture(config, InteractionType.SINGLE, Position.LEFT); + case PREF_REDMI_BUDS_5_PRO_CONTROL_SINGLE_TAP_RIGHT: + return encodeSetGesture(config, InteractionType.SINGLE, Position.RIGHT); + case PREF_REDMI_BUDS_5_PRO_CONTROL_DOUBLE_TAP_LEFT: + return encodeSetGesture(config, InteractionType.DOUBLE, Position.LEFT); + case PREF_REDMI_BUDS_5_PRO_CONTROL_DOUBLE_TAP_RIGHT: + return encodeSetGesture(config, InteractionType.DOUBLE, Position.RIGHT); + case PREF_REDMI_BUDS_5_PRO_CONTROL_TRIPLE_TAP_LEFT: + return encodeSetGesture(config, InteractionType.TRIPLE, Position.LEFT); + case PREF_REDMI_BUDS_5_PRO_CONTROL_TRIPLE_TAP_RIGHT: + return encodeSetGesture(config, InteractionType.TRIPLE, Position.RIGHT); + case PREF_REDMI_BUDS_5_PRO_CONTROL_LONG_TAP_MODE_LEFT: + return encodeSetGesture(config, InteractionType.LONG, Position.LEFT); + case PREF_REDMI_BUDS_5_PRO_CONTROL_LONG_TAP_MODE_RIGHT: + return encodeSetGesture(config, InteractionType.LONG, Position.RIGHT); + case PREF_REDMI_BUDS_5_PRO_CONTROL_LONG_TAP_SETTINGS_LEFT: + return encodeSetLongGestureMode(config, Position.LEFT); + case PREF_REDMI_BUDS_5_PRO_CONTROL_LONG_TAP_SETTINGS_RIGHT: + return encodeSetLongGestureMode(config, Position.RIGHT); + + case PREF_REDMI_BUDS_5_PRO_WEARING_DETECTION: + return encodeSetEarDetection(); + case PREF_REDMI_BUDS_5_PRO_AUTO_REPLY_PHONECALL: + return encodeSetBooleanConfig(config, Config.AUTO_ANSWER); + case PREF_REDMI_BUDS_5_PRO_DOUBLE_CONNECTION: + return encodeSetBooleanConfig(config, Config.DOUBLE_CONNECTION); + case PREF_REDMI_BUDS_5_PRO_ADAPTIVE_SOUND: + return encodeSetBooleanConfig(config, Config.ADAPTIVE_SOUND); + + case PREF_REDMI_BUDS_5_PRO_EQUALIZER_PRESET: + return encodeSetIntegerConfig(config, Config.EQ_PRESET); + case PREF_REDMI_BUDS_5_PRO_EQUALIZER_BAND_62: + case PREF_REDMI_BUDS_5_PRO_EQUALIZER_BAND_125: + case PREF_REDMI_BUDS_5_PRO_EQUALIZER_BAND_250: + case PREF_REDMI_BUDS_5_PRO_EQUALIZER_BAND_500: + case PREF_REDMI_BUDS_5_PRO_EQUALIZER_BAND_1k: + case PREF_REDMI_BUDS_5_PRO_EQUALIZER_BAND_2k: + case PREF_REDMI_BUDS_5_PRO_EQUALIZER_BAND_4k: + case PREF_REDMI_BUDS_5_PRO_EQUALIZER_BAND_8k: + case PREF_REDMI_BUDS_5_PRO_EQUALIZER_BAND_12k: + case PREF_REDMI_BUDS_5_PRO_EQUALIZER_BAND_16k: + return encodeSetCustomEqualizer(); + + default: + LOG.debug("Unsupported config: {}", config); + } + + return super.encodeSendConfiguration(config); + } + + public byte[] encodeSetCustomEqualizer() { + Prefs prefs = getDevicePrefs(); + + List bands = List.of(PREF_REDMI_BUDS_5_PRO_EQUALIZER_BAND_62, PREF_REDMI_BUDS_5_PRO_EQUALIZER_BAND_125, + PREF_REDMI_BUDS_5_PRO_EQUALIZER_BAND_250, PREF_REDMI_BUDS_5_PRO_EQUALIZER_BAND_500, PREF_REDMI_BUDS_5_PRO_EQUALIZER_BAND_1k, + PREF_REDMI_BUDS_5_PRO_EQUALIZER_BAND_2k, PREF_REDMI_BUDS_5_PRO_EQUALIZER_BAND_4k, PREF_REDMI_BUDS_5_PRO_EQUALIZER_BAND_8k, + PREF_REDMI_BUDS_5_PRO_EQUALIZER_BAND_12k, PREF_REDMI_BUDS_5_PRO_EQUALIZER_BAND_16k); + + byte[] eqCurve = new byte[10]; + for (int i = 0; i < 10; i++) { + eqCurve[i] = (byte) Integer.parseInt(prefs.getString(bands.get(i), "0")); + } + return new Message(MessageType.PHONE_REQUEST, Opcode.SET_CONFIG, sequenceNumber++, new byte[]{ + 0x24, 0x00, 0x37, 0x05, 0x01, 0x01, 0x0A, + 0x00, 0x3E, eqCurve[0], 0x00, 0x7D, eqCurve[1], + 0x00, (byte) 0xFA, eqCurve[2], 0x01, (byte) 0xF4, eqCurve[3], + 0x03, (byte) 0xE8, eqCurve[4], 0x07, (byte) 0xE0, eqCurve[5], + 0x0F, (byte) 0xA0, eqCurve[6], 0x1F, 0x40, eqCurve[7], + 0x2E, (byte) 0xE0, eqCurve[8], 0x3E, (byte) 0x80, eqCurve[9] + }).encode(); + } + + public byte[] encodeSetEarDetection() { + Prefs prefs = getDevicePrefs(); + byte value = (byte) (prefs.getBoolean(PREF_REDMI_BUDS_5_PRO_WEARING_DETECTION, false) ? 0x00 : 0x01); + return new Message(MessageType.PHONE_REQUEST, Opcode.ANC, sequenceNumber++, new byte[]{0x02, 0x06, value}).encode(); + } + + public byte[] encodeSetLongGestureMode(String config, Position position) { + Prefs prefs = getDevicePrefs(); + byte value = (byte) Integer.parseInt(prefs.getString(config, "7")); + byte[] payload = new byte[]{0x04, 0x00, 0x0a, (byte) 0xFF, (byte) 0xFF}; + if (position == Position.LEFT) { + payload[3] = value; + } else { + payload[4] = value; + } + return new Message(MessageType.PHONE_REQUEST, Opcode.SET_CONFIG, sequenceNumber++, payload).encode(); + } + + public byte[] encodeSetGesture(String config, InteractionType interactionType, Position position) { + Prefs prefs = getDevicePrefs(); + byte value = (byte) Integer.parseInt(prefs.getString(config, "1")); + byte[] payload = new byte[]{0x05, 0x00, 0x02, interactionType.value, (byte) 0xFF, (byte) 0xFF}; + if (position == Position.LEFT) { + payload[4] = value; + } else { + payload[5] = value; + } + return new Message(MessageType.PHONE_REQUEST, Opcode.SET_CONFIG, sequenceNumber++, payload).encode(); + } + + public byte[] encodeSetEffectStrength(String pref, StrengthTarget effect) { + Prefs prefs = getDevicePrefs(); + byte mode = (byte) Integer.parseInt(prefs.getString(pref, "0")); + return new Message(MessageType.PHONE_REQUEST, Opcode.SET_CONFIG, sequenceNumber++, new byte[]{0x04, 0x00, 0x0b, effect.value, mode}).encode(); + } + + public byte[] encodeSetIntegerConfig(String pref, Config config) { + Prefs prefs = getDevicePrefs(); + byte value = (byte) Integer.parseInt(prefs.getString(pref, "0")); + return new Message(MessageType.PHONE_REQUEST, Opcode.SET_CONFIG, sequenceNumber++, new byte[]{0x03, 0x00, config.value, value}).encode(); + } + + public byte[] encodeSetBooleanConfig(String pref, Config config) { + Prefs prefs = getDevicePrefs(); + byte value = (byte) (prefs.getBoolean(pref, false) ? 0x01 : 0x00); + return new Message(MessageType.PHONE_REQUEST, Opcode.SET_CONFIG, sequenceNumber++, new byte[]{0x03, 0x00, config.value, value}).encode(); + } + + public byte[] encodeGetConfig() { + List configs = List.of(Config.EFFECT_STRENGTH, Config.ADAPTIVE_ANC, // Config.CUSTOMIZED_ANC, + Config.GESTURES, Config.LONG_GESTURES, Config.EAR_DETECTION, Config.DOUBLE_CONNECTION, + Config.AUTO_ANSWER, Config.ADAPTIVE_SOUND, Config.EQ_PRESET, Config.EQ_CURVE); + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + try { + for (Config config : configs) { + Message message = new Message(MessageType.PHONE_REQUEST, Opcode.GET_CONFIG, sequenceNumber++, new byte[]{0x00, config.value}); + outputStream.write(message.encode()); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + return outputStream.toByteArray(); + } + + public byte[] encodeSetAmbientSoundControl() { + Prefs prefs = getDevicePrefs(); + byte mode = (byte) Integer.parseInt(prefs.getString(PREF_REDMI_BUDS_5_PRO_AMBIENT_SOUND_CONTROL, "0")); + return new Message(MessageType.PHONE_REQUEST, Opcode.ANC, sequenceNumber++, new byte[]{0x02, 0x04, mode}).encode(); + } + + public void decodeGetConfig(byte[] configPayload) { + + SharedPreferences preferences = getDevicePrefs().getPreferences(); + Editor editor = preferences.edit(); + Config config = Config.fromCode(configPayload[2]); + switch (config) { + case GESTURES: + editor.putString(PREF_REDMI_BUDS_5_PRO_CONTROL_SINGLE_TAP_LEFT, Integer.toString(configPayload[4])); + editor.putString(PREF_REDMI_BUDS_5_PRO_CONTROL_SINGLE_TAP_RIGHT, Integer.toString(configPayload[5])); + + editor.putString(PREF_REDMI_BUDS_5_PRO_CONTROL_DOUBLE_TAP_LEFT, Integer.toString(configPayload[7])); + editor.putString(PREF_REDMI_BUDS_5_PRO_CONTROL_DOUBLE_TAP_RIGHT, Integer.toString(configPayload[8])); + + editor.putString(PREF_REDMI_BUDS_5_PRO_CONTROL_TRIPLE_TAP_LEFT, Integer.toString(configPayload[10])); + editor.putString(PREF_REDMI_BUDS_5_PRO_CONTROL_TRIPLE_TAP_RIGHT, Integer.toString(configPayload[11])); + + editor.putString(PREF_REDMI_BUDS_5_PRO_CONTROL_LONG_TAP_MODE_LEFT, Integer.toString(configPayload[13])); + editor.putString(PREF_REDMI_BUDS_5_PRO_CONTROL_LONG_TAP_MODE_RIGHT, Integer.toString(configPayload[14])); + break; + case AUTO_ANSWER: + editor.putBoolean(PREF_REDMI_BUDS_5_PRO_AUTO_REPLY_PHONECALL, configPayload[3] == 0x01); + break; + case DOUBLE_CONNECTION: + editor.putBoolean(PREF_REDMI_BUDS_5_PRO_DOUBLE_CONNECTION, configPayload[3] == 0x01); + break; + case EQ_PRESET: + editor.putString(PREF_REDMI_BUDS_5_PRO_EQUALIZER_PRESET, Integer.toString(configPayload[3])); + break; + case LONG_GESTURES: + editor.putString(PREF_REDMI_BUDS_5_PRO_CONTROL_LONG_TAP_SETTINGS_LEFT, Integer.toString(configPayload[3])); + editor.putString(PREF_REDMI_BUDS_5_PRO_CONTROL_LONG_TAP_SETTINGS_RIGHT, Integer.toString(configPayload[4])); + break; + case EFFECT_STRENGTH: + byte mode = configPayload[4]; + if (configPayload[3] == StrengthTarget.ANC.value) { + editor.putString(PREF_REDMI_BUDS_5_PRO_NOISE_CANCELLING_STRENGTH, Integer.toString(mode)); + } else if (configPayload[3] == StrengthTarget.TRANSPARENCY.value) { + editor.putString(PREF_REDMI_BUDS_5_PRO_TRANSPARENCY_STRENGTH, Integer.toString(mode)); + } + break; + case ADAPTIVE_ANC: + editor.putBoolean(PREF_REDMI_BUDS_5_PRO_ADAPTIVE_NOISE_CANCELLING, configPayload[3] == 0x01); + break; + case ADAPTIVE_SOUND: + editor.putBoolean(PREF_REDMI_BUDS_5_PRO_ADAPTIVE_SOUND, configPayload[3] == 0x01); + break; + case EQ_CURVE: + editor.putString(PREF_REDMI_BUDS_5_PRO_EQUALIZER_BAND_62, Integer.toString(configPayload[12] & 0xFF)); + editor.putString(PREF_REDMI_BUDS_5_PRO_EQUALIZER_BAND_125, Integer.toString(configPayload[15] & 0xFF)); + editor.putString(PREF_REDMI_BUDS_5_PRO_EQUALIZER_BAND_250, Integer.toString(configPayload[18] & 0xFF)); + editor.putString(PREF_REDMI_BUDS_5_PRO_EQUALIZER_BAND_500, Integer.toString(configPayload[21] & 0xFF)); + editor.putString(PREF_REDMI_BUDS_5_PRO_EQUALIZER_BAND_1k, Integer.toString(configPayload[24] & 0xFF)); + editor.putString(PREF_REDMI_BUDS_5_PRO_EQUALIZER_BAND_2k, Integer.toString(configPayload[27] & 0xFF)); + editor.putString(PREF_REDMI_BUDS_5_PRO_EQUALIZER_BAND_4k, Integer.toString(configPayload[30] & 0xFF)); + editor.putString(PREF_REDMI_BUDS_5_PRO_EQUALIZER_BAND_8k, Integer.toString(configPayload[33] & 0xFF)); + editor.putString(PREF_REDMI_BUDS_5_PRO_EQUALIZER_BAND_12k, Integer.toString(configPayload[36] & 0xFF)); + editor.putString(PREF_REDMI_BUDS_5_PRO_EQUALIZER_BAND_16k, Integer.toString(configPayload[39] & 0xFF)); + break; +// case CUSTOMIZED_ANC: +// editor.putBoolean(PREF_REDMI_BUDS_5_PRO_PERSONALIZED_NOISE_CANCELLING, configPayload[3] == 0x01); +// break; + default: + LOG.debug("Unhandled device update: {}", hexdump(configPayload)); + } + editor.apply(); + } + + private GBDeviceEventBatteryInfo parseBatteryInfo(byte batteryInfo, int index) { + + if (batteryInfo == (byte) 0xff) { + return null; + } + GBDeviceEventBatteryInfo batteryEvent = new GBDeviceEventBatteryInfo(); + batteryEvent.state = (batteryInfo & 128) != 0 ? BatteryState.BATTERY_CHARGING : BatteryState.BATTERY_NORMAL; + batteryEvent.batteryIndex = index; + batteryEvent.level = (batteryInfo & 127); + LOG.debug("Battery {}: {}", index, batteryEvent.level); + return batteryEvent; + } + + private GBDeviceEvent[] decodeDeviceInfo(byte[] deviceInfoPayload) { + + List events = new ArrayList<>(); + + GBDeviceEventVersionInfo info = new GBDeviceEventVersionInfo(); + byte[] fw = new byte[4]; + byte[] vidPid = new byte[4]; + byte[] batteryData = new byte[3]; + int i = 0; + while (i < deviceInfoPayload.length) { + byte len = deviceInfoPayload[i]; + byte index = deviceInfoPayload[i + 1]; + switch (index) { + case 0x01: + System.arraycopy(deviceInfoPayload, i + 2, fw, 0, 4); + break; + case 0x03: + System.arraycopy(deviceInfoPayload, i + 2, vidPid, 0, 4); + break; + case 0x07: + System.arraycopy(deviceInfoPayload, i + 2, batteryData, 0, 3); + break; + } + i += len + 1; + } + + String fwVersion1 = ((fw[0] >> 4) & 0xF) + "." + (fw[0] & 0xF) + "." + ((fw[1] >> 4) & 0xF) + "." + (fw[1] & 0xF); + String fwVersion2 = ((fw[2] >> 4) & 0xF) + "." + (fw[2] & 0xF) + "." + ((fw[3] >> 4) & 0xF) + "." + (fw[3] & 0xF); + String hwVersion = String.format("VID: 0x%02X%02X, PID: 0x%02X%02X", vidPid[0], vidPid[1], vidPid[2], vidPid[3]); + + info.fwVersion = fwVersion1; + info.fwVersion2 = fwVersion2; + info.hwVersion = hwVersion; + + events.add(parseBatteryInfo(batteryData[0], 1)); + events.add(parseBatteryInfo(batteryData[1], 2)); + events.add(parseBatteryInfo(batteryData[2], 0)); + events.add(info); + + return events.toArray(new GBDeviceEvent[0]); + } + + private void decodeDeviceRunInfo(byte[] deviceRunInfoPayload) { + int i = 0; + while (i < deviceRunInfoPayload.length) { + byte len = deviceRunInfoPayload[i]; + byte index = deviceRunInfoPayload[i + 1]; + SharedPreferences preferences = getDevicePrefs().getPreferences(); + Editor editor = preferences.edit(); + switch (index) { + case 0x09: + byte mode = deviceRunInfoPayload[i + 2]; + editor.putString(PREF_REDMI_BUDS_5_PRO_AMBIENT_SOUND_CONTROL, Integer.toString(mode)); + break; + case 0x0A: + editor.putBoolean(PREF_REDMI_BUDS_5_PRO_WEARING_DETECTION, deviceRunInfoPayload[i + 2] == 0x00); + } + editor.apply(); + i += len + 1; + } + } + + private GBDeviceEvent[] decodeDeviceUpdate(Message updateMessage) { + byte[] updatePayload = updateMessage.getPayload(); + List events = new ArrayList<>(); + + int i = 0; + while (i < updatePayload.length) { + byte len = updatePayload[i]; + byte index = updatePayload[i + 1]; + switch (index) { + case 0x00: + events.add(parseBatteryInfo(updatePayload[i + 2], 1)); + events.add(parseBatteryInfo(updatePayload[i + 3], 2)); + events.add(parseBatteryInfo(updatePayload[i + 4], 0)); + break; + case 0x04: + SharedPreferences preferences = getDevicePrefs().getPreferences(); + Editor editor = preferences.edit(); + + byte mode = updatePayload[i + 2]; + editor.putString(PREF_REDMI_BUDS_5_PRO_AMBIENT_SOUND_CONTROL, Integer.toString(mode)); + editor.apply(); + break; + default: + LOG.debug("Unimplemented device update: {}", hexdump(updatePayload)); + } + i += len + 1; + } + events.add(new GBDeviceEventSendBytes(new Message(MessageType.RESPONSE, Opcode.REPORT_STATUS, updateMessage.getSequenceNumber(), new byte[]{}).encode())); + return events.toArray(new GBDeviceEvent[0]); + } + + private GBDeviceEvent[] decodeNotifyConfig(Message notifyMessage) { + + byte[] notifyPayload = notifyMessage.getPayload(); + List events = new ArrayList<>(); + + int i = 0; + while (i < notifyPayload.length) { + byte len = notifyPayload[i]; + byte index = notifyPayload[i + 2]; + switch (index) { + case 0x0C: + LOG.debug("Received earbuds position info"); + /* + e.g. 0C 03 + 0011 + wearing left, wearing right, left in case, right in case + */ + break; + case 0x0B: + SharedPreferences preferences = getDevicePrefs().getPreferences(); + Editor editor = preferences.edit(); + + byte soundCtrlMode = notifyPayload[i + 3]; + editor.putString(PREF_REDMI_BUDS_5_PRO_AMBIENT_SOUND_CONTROL, Integer.toString(soundCtrlMode)); + + byte mode = notifyPayload[i + 4]; + if (notifyPayload[i + 3] == 0x01) { + editor.putString(PREF_REDMI_BUDS_5_PRO_NOISE_CANCELLING_STRENGTH, Integer.toString(mode)); + } else { + editor.putString(PREF_REDMI_BUDS_5_PRO_TRANSPARENCY_STRENGTH, Integer.toString(mode)); + } + + editor.apply(); + break; + } + + i += len + 1; + } + events.add(new GBDeviceEventSendBytes(new Message(MessageType.RESPONSE, Opcode.NOTIFY_CONFIG, notifyMessage.getSequenceNumber(), new byte[]{}).encode())); + return events.toArray(new GBDeviceEvent[0]); + } + + private GBDeviceEvent[] handleAuthentication(Message authMessage) { + List events = new ArrayList<>(); + switch (authMessage.getOpcode()) { + case AUTH_CHALLENGE: + if (authMessage.getType() == MessageType.RESPONSE) { + LOG.debug("[AUTH] Received Challenge Response"); + /* + Should check if equal, but does not really matter + */ + LOG.debug("[AUTH] Sending authentication confirmation"); + events.add(new GBDeviceEventSendBytes(new Message(MessageType.PHONE_REQUEST, Opcode.AUTH_CONFIRM, sequenceNumber++, new byte[]{0x01, 0x00}).encode())); + } else { + byte[] responsePayload = authMessage.getPayload(); + byte[] challenge = new byte[16]; + System.arraycopy(responsePayload, 1, challenge, 0, 16); + + LOG.info("[AUTH] Received Challenge: {}", hexdump(challenge)); + Authentication auth = new Authentication(); + byte[] challengeResponse = auth.computeChallengeResponse(challenge); + LOG.info("[AUTH] Sending Challenge Response: {}", hexdump(challengeResponse)); + + byte[] payload = new byte[17]; + payload[0] = 0x01; + System.arraycopy(challengeResponse, 0, payload, 1, 16); + Message res = new Message(MessageType.RESPONSE, Opcode.AUTH_CHALLENGE, authMessage.getSequenceNumber(), payload); + events.add(new GBDeviceEventSendBytes(res.encode())); + } + break; + case AUTH_CONFIRM: + if (authMessage.getType() == MessageType.RESPONSE) { + LOG.debug("[AUTH] Confirmed first authentication step"); + } else { + LOG.debug("[AUTH] Received authentication confirmation"); + Message res = new Message(MessageType.RESPONSE, Opcode.AUTH_CONFIRM, authMessage.getSequenceNumber(), new byte[]{0x01}); + LOG.debug("[AUTH] Sending final authentication confirmation"); + events.add(new GBDeviceEventSendBytes(res.encode())); + + LOG.debug("[INIT] Sending device info request"); + Message info = new Message(MessageType.PHONE_REQUEST, Opcode.GET_DEVICE_INFO, sequenceNumber++, new byte[]{(byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff}); + events.add(new GBDeviceEventSendBytes(info.encode())); + + LOG.debug("[INIT] Sending device run info request"); + Message runInfo = new Message(MessageType.PHONE_REQUEST, Opcode.GET_DEVICE_RUN_INFO, sequenceNumber++, new byte[]{(byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff}); + events.add(new GBDeviceEventSendBytes(runInfo.encode())); + + LOG.debug("[INIT] Sending configuration request"); + events.add(new GBDeviceEventSendBytes(encodeGetConfig())); + } + break; + } + return events.toArray(new GBDeviceEvent[0]); + } + + @Override + public GBDeviceEvent[] decodeResponse(byte[] responseData) { + + LOG.debug("Incoming message: {}", hexdump(responseData)); + + List events = new ArrayList<>(); + + List incomingMessages = Message.splitPiggybackedMessages(responseData); + + for (Message message : incomingMessages) { + + LOG.debug("Parsed message: {}", message); + + switch (message.getOpcode()) { + case AUTH_CHALLENGE: + case AUTH_CONFIRM: + events.addAll(Arrays.asList(handleAuthentication(message))); + break; + case GET_DEVICE_INFO: + LOG.debug("[INIT] Received device info"); + if (getDevice().getState() != State.INITIALIZED) { + events.addAll(Arrays.asList(decodeDeviceInfo(message.getPayload()))); + LOG.debug("[INIT] Device Initialized"); + events.add(new GBDeviceEventUpdateDeviceState(State.INITIALIZED)); + } + break; + case GET_DEVICE_RUN_INFO: + LOG.debug("[INIT] Received device run info"); + decodeDeviceRunInfo(message.getPayload()); + break; + case REPORT_STATUS: + events.addAll(Arrays.asList(decodeDeviceUpdate(message))); + break; + case GET_CONFIG: + decodeGetConfig(message.getPayload()); + break; + case NOTIFY_CONFIG: + events.addAll(Arrays.asList(decodeNotifyConfig(message))); + break; + default: + LOG.debug("Unhandled message: {}", message); + break; + } + } + return events.toArray(new GBDeviceEvent[0]); + } + +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/redmibuds5pro/protocol/AuthData.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/redmibuds5pro/protocol/AuthData.java new file mode 100644 index 000000000..7925698d5 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/redmibuds5pro/protocol/AuthData.java @@ -0,0 +1,25 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.redmibuds5pro.protocol; + +class AuthData { + + final static byte[] SEQ = { 0x11, 0x22, 0x33, 0x33, 0x22, 0x11, 0x11, 0x22, 0x33, 0x33, 0x22, 0x11, 0x11, 0x22, 0x33, 0x33 }; + + final static int[][] COEFFICIENTS = { + {2,1,1,1,4,2,1,1,2,2,4,2,4,4,16,8}, + {2,1,1,1,4,2,1,1,1,1,2,1,2,2,8,4}, + {1,1,4,2,2,2,4,2,16,8,4,4,2,1,1,1}, + {1,1,4,2,1,1,2,1,8,4,2,2,2,1,1,1}, + {16,8,2,2,4,2,4,4,1,1,4,2,1,1,2,1}, + {8,4,1,1,2,1,2,2,1,1,4,2,1,1,2,1}, + {2,2,4,2,4,4,16,8,2,1,1,1,4,2,1,1}, + {1,1,2,1,2,2,8,4,2,1,1,1,4,2,1,1}, + {4,2,4,4,16,8,2,2,1,1,2,1,1,1,4,2}, + {2,1,2,2,8,4,1,1,1,1,2,1,1,1,4,2}, + {4,4,16,8,1,1,2,1,4,2,1,1,4,2,2,2}, + {2,2,8,4,1,1,2,1,4,2,1,1,2,1,1,1}, + {1,1,2,1,1,1,4,2,4,4,16,8,2,2,4,2}, + {1,1,2,1,1,1,4,2,2,2,8,4,1,1,2,1}, + {4,2,1,1,2,1,1,1,4,2,2,2,16,8,4,4}, + {4,2,1,1,2,1,1,1,2,1,1,1,8,4,2,2} + }; +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/redmibuds5pro/protocol/Authentication.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/redmibuds5pro/protocol/Authentication.java new file mode 100644 index 000000000..867d897e4 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/redmibuds5pro/protocol/Authentication.java @@ -0,0 +1,181 @@ +/* Copyright (C) 2024 Jonathan Gobbo + + 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.redmibuds5pro.protocol; +import java.math.BigInteger; +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.List; + +/* + Authentication based on the custom Bluetooth version of the SAFER+ encryption algorithm with: + - 128 bit key size + - 8 rounds + */ +public class Authentication { + + private final static int PATTERN = 0x9999; + private final static int BLOCK_SIZE = 16; + + byte[][] biasMatrix; + byte[] expTab; + byte[] logTab; + + public Authentication() { + generateBiasMatrix(); + generateExpTab(); + generateLogTab(); + } + + private void generateBiasMatrix() { + biasMatrix = new byte[16][]; + + for (int i = 0; i < 16; i++) { + byte[] biasVec = new byte[16]; + for (int j = 0; j < 16; j++) { + int exponent = (17 * (i + 2) + (j + 1)); + BigInteger base = BigInteger.valueOf(45); + BigInteger modExp = base.pow(base.pow(exponent).mod(BigInteger.valueOf(257)).intValue()) + .mod(BigInteger.valueOf(257)); + byte val = (byte) (modExp.intValue() == 256 ? 0 : modExp.intValue()); + biasVec[j] = val; + } + biasMatrix[i] = biasVec; + } + } + + private void generateExpTab() { + expTab = new byte[256]; + + for (int i = 0; i < 256; i++) { + BigInteger base = BigInteger.valueOf(45); + BigInteger exp = base.pow(i).mod(BigInteger.valueOf(257)); + expTab[i] = (byte) (i == 128 ? 0 : exp.intValue()); + } + } + + private void generateLogTab() { + logTab = new byte[256]; + + for (int i = 0; i < 256; i++) { + if (i == 0) { + logTab[i] = (byte) 128; + } else { + BigInteger base = BigInteger.valueOf(45); + BigInteger modExp = base.pow(i).mod(BigInteger.valueOf(257)); + if (modExp.intValue() != 256) { + logTab[modExp.intValue()] = (byte) i; + } + } + } + } + + private byte[][] keySchedule(byte[] keyInit) { + + keyInit[15] ^= 6; + + byte[][] keys = new byte[17][]; + keys[0] = keyInit; + + List register = new ArrayList<>(); + for (byte b : keyInit) { + register.add(b); + } + byte xor = register.stream().reduce((byte) 0x0, (cSum, e) -> (byte) (cSum ^ e)); + register.add(xor); + + for (int keyIdx = 1; keyIdx < keys.length; keyIdx++) { + byte[] keyI = new byte[16]; + for (int i = 0; i < 17; i++) { + byte rot = (byte) (((register.get(i) & 0xff) >>> 5) | ((register.get(i) & 0xff) << (8 - 5))); + register.set(i, rot); + } + for (int i = 0; i < 16; i++) { + keyI[i] = (byte) (register.get((keyIdx + i) % 17) + biasMatrix[keyIdx - 1][i]); + } + keys[keyIdx] = keyI; + } + + return keys; + } + + private byte[] encrypt(byte[] plaintext, byte[][] keys) { + + byte[] ciphertext = plaintext.clone(); + for (int round = 0; round < 8; round++) { + if (round == 2) { + for (int i = 0; i < BLOCK_SIZE; i++) { + if ((1 << i & PATTERN) != 0) { + ciphertext[i] ^= plaintext[i]; + } else { + ciphertext[i] += plaintext[i]; + } + } + } + for (int i = 0; i < BLOCK_SIZE; i++) { + if ((1 << i & PATTERN) != 0) { + ciphertext[i] ^= keys[round * 2][i]; + } else { + ciphertext[i] += keys[round * 2][i]; + } + } + for (int i = 0; i < BLOCK_SIZE; i++) { + if ((1 << i & PATTERN) != 0) { + ciphertext[i] = expTab[ciphertext[i] & 0xff]; + } else { + ciphertext[i] = logTab[ciphertext[i] & 0xff]; + } + } + for (int i = 0; i < BLOCK_SIZE; i++) { + if ((1 << i & PATTERN) != 0) { + ciphertext[i] = (byte) (keys[round * 2 + 1][i] + ciphertext[i]); + } else { + ciphertext[i] = (byte) (keys[round * 2 + 1][i] ^ ciphertext[i]); + } + } + byte[] ciphertextCopy = ciphertext.clone(); + for (int i = 0; i < BLOCK_SIZE; i++) { + byte cSum = 0; + for (int j = 0; j < BLOCK_SIZE; j++) { + cSum += (byte) (AuthData.COEFFICIENTS[i][j] * ciphertextCopy[j]); + } + ciphertext[i] = cSum; + } + } + + for (int i = 0; i < BLOCK_SIZE; i++) { + if ((1 << i & PATTERN) != 0) { + ciphertext[i] = (byte) (keys[16][i] ^ ciphertext[i]); + } else { + ciphertext[i] = (byte) (keys[16][i] + ciphertext[i]); + } + } + return ciphertext; + } + + public static byte[] getRandomChallenge() { + byte[] res = new byte[BLOCK_SIZE]; + SecureRandom rnd = new SecureRandom(); + rnd.nextBytes(res); + return res; + } + + public byte[] computeChallengeResponse(byte[] challenge) { + byte[][] keys = keySchedule(challenge); + return encrypt(AuthData.SEQ, keys); + } + +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/redmibuds5pro/protocol/Message.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/redmibuds5pro/protocol/Message.java new file mode 100644 index 000000000..be88ea90a --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/redmibuds5pro/protocol/Message.java @@ -0,0 +1,124 @@ +/* Copyright (C) 2024 Jonathan Gobbo + + 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.redmibuds5pro.protocol; + +import static nodomain.freeyourgadget.gadgetbridge.util.GB.hexdump; + +import androidx.annotation.NonNull; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class Message { + + private static final byte[] MESSAGE_HEADER = {(byte) 0xfe, (byte) 0xdc, (byte) 0xba}; + private static final byte MESSAGE_TRAILER = (byte) 0xef; + + private static final int MESSAGE_OFFSET = MESSAGE_HEADER.length; + + private final MessageType type; + private final Opcode opcode; + private final byte sequenceNumber; + private final byte[] payload; + + public Message(final MessageType type, final Opcode opcode, final byte sequenceNumber, final byte[] payload) { + this.type = type; + this.opcode = opcode; + this.sequenceNumber = sequenceNumber; + this.payload = payload; + } + + public static Message fromBytes(byte[] message) { + MessageType type = MessageType.fromCode(message[MESSAGE_OFFSET]); + Opcode opcode = Opcode.fromCode(message[MESSAGE_OFFSET + 1]); + + int payloadOffset = MESSAGE_OFFSET + ((!type.isRequest()) ? 6 : 5); + byte sequenceNumber = message[payloadOffset - 1]; + + int actualPayloadLength = message.length - payloadOffset - 1; + byte[] payload = new byte[actualPayloadLength]; + System.arraycopy(message, payloadOffset, payload, 0, actualPayloadLength); + return new Message(type, opcode, sequenceNumber, payload); + } + + public static List splitPiggybackedMessages(byte[] input) { + List messages = new ArrayList<>(); + + List startHeader = new ArrayList<>(); + for (int i = 0; i < input.length - 3; i++) { + if (input[i] == MESSAGE_HEADER[0] && input[i + 1] == MESSAGE_HEADER[1] && input[i + 2] == MESSAGE_HEADER[2]) { + startHeader.add(i); + } + } + + for (int i = 0; i < startHeader.size(); i++) { + if (i == startHeader.size() - 1) { + messages.add(fromBytes(Arrays.copyOfRange(input, startHeader.get(i), input.length))); + } else { + messages.add(fromBytes(Arrays.copyOfRange(input, startHeader.get(i), startHeader.get(i + 1)))); + } + } + return messages; + } + + public byte[] encode() { + + int size = (!type.isRequest()) ? 2 : 1; + final ByteBuffer buf = ByteBuffer.allocate(payload.length + 8 + size); + int payloadLength = payload.length + size; + + buf.order(ByteOrder.BIG_ENDIAN); + + buf.put(MESSAGE_HEADER); + buf.put(type.getCode()); + buf.put(opcode.getOpcode()); + buf.putShort((short) payloadLength); + if (!type.isRequest()) { + buf.put((byte) 0x00); + } + buf.put(sequenceNumber); + buf.put(payload); + buf.put(MESSAGE_TRAILER); + + return buf.array(); + } + + @NonNull + @Override + public String toString() { + return "Message{" + "type=" + type + ", opcode=" + opcode + ", sequenceNumber=" + sequenceNumber + ", payload=" + hexdump(payload) + '}'; + } + + public MessageType getType() { + return type; + } + + public Opcode getOpcode() { + return opcode; + } + + public byte getSequenceNumber() { + return sequenceNumber; + } + + public byte[] getPayload() { + return payload; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/redmibuds5pro/protocol/MessageType.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/redmibuds5pro/protocol/MessageType.java new file mode 100644 index 000000000..112eb9885 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/redmibuds5pro/protocol/MessageType.java @@ -0,0 +1,35 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.redmibuds5pro.protocol; + +public enum MessageType { + PHONE_REQUEST(0xC4), + RESPONSE(0x04), + EARBUDS_REQUEST(0xC0), + EARBUDS_NOTIFY(0xC7), + UNKNOWN(0xFF); + + private final byte code; + private final boolean isRequest; + + MessageType(final int code) { + this.code = (byte) code; + this.isRequest = (this.code & 0x40) != 0; + } + + public byte getCode() { + return this.code; + } + + public boolean isRequest() { + return isRequest; + } + + public static MessageType fromCode(final byte code) { + for (final MessageType messageType : values()) { + if (messageType.code == code) { + return messageType; + } + } + + return MessageType.UNKNOWN; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/redmibuds5pro/protocol/Opcode.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/redmibuds5pro/protocol/Opcode.java new file mode 100644 index 000000000..f6b3ed157 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/redmibuds5pro/protocol/Opcode.java @@ -0,0 +1,33 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.redmibuds5pro.protocol; + +public enum Opcode { + GET_DEVICE_INFO(0x02), + ANC(0x08), + GET_DEVICE_RUN_INFO(0x09), + REPORT_STATUS(0x0E), + AUTH_CHALLENGE(0x50), + AUTH_CONFIRM(0x51), + SET_CONFIG(0xF2), + GET_CONFIG(0xF3), + NOTIFY_CONFIG(0xF4), + UNKNOWN(0xFF); + + private final byte opcode; + + Opcode(final int opcode) { + this.opcode = (byte) opcode; + } + + public byte getOpcode() { + return opcode; + } + + public static Opcode fromCode(final byte code) { + for (final Opcode opcode : values()) { + if (opcode.opcode == code) { + return opcode; + } + } + return Opcode.UNKNOWN; + } +} diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index fb8c5b5aa..96fca17fd 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -3377,6 +3377,160 @@ 3 + + @string/sony_ambient_sound_off + @string/sony_ambient_sound_noise_cancelling + @string/prefs_active_noise_cancelling_transparency + + + + 0 + 1 + 2 + + + + @string/redmi_buds_5_pro_anc_balanced + @string/redmi_buds_5_pro_anc_light + @string/redmi_buds_5_pro_anc_deep + + + + 0 + 1 + 2 + + + + @string/redmi_buds_5_pro_transparency_regular + @string/redmi_buds_5_pro_transparency_voice + @string/redmi_buds_5_pro_transparency_ambient + + + + 0 + 1 + 2 + + + + @string/none + @string/pref_media_playpause + @string/pref_media_previous + @string/pref_media_next + @string/pref_media_volumeup + @string/pref_media_volumedown + + + + 8 + 1 + 2 + 3 + 4 + 5 + + + + @string/pref_media_playpause + @string/pref_media_previous + @string/pref_media_next + @string/pref_media_volumeup + @string/pref_media_volumedown + + + + 1 + 2 + 3 + 4 + 5 + + + + @string/pref_title_touch_voice_assistant + @string/pref_header_sony_ambient_sound_control + + + + 0 + 6 + + + + @string/redmi_buds_5_pro_combo_anc_off + @string/redmi_buds_5_pro_combo_transparency_off + @string/redmi_buds_5_pro_combo_anc_transparency + @string/redmi_buds_5_pro_combo_all + + + + 3 + 5 + 6 + 7 + + + + + + + + + + + + + + + + @string/redmi_buds_5_pro_equalizer_preset_standard + @string/redmi_buds_5_pro_equalizer_preset_treble + @string/redmi_buds_5_pro_equalizer_preset_bass + @string/redmi_buds_5_pro_equalizer_preset_voice + @string/redmi_buds_5_pro_equalizer_preset_custom + + + + 0 + 6 + 5 + 1 + 10 + + + + @string/redmi_buds_5_pro_equalizer_neg6 + @string/redmi_buds_5_pro_equalizer_neg5 + @string/redmi_buds_5_pro_equalizer_neg4 + @string/redmi_buds_5_pro_equalizer_neg3 + @string/redmi_buds_5_pro_equalizer_neg2 + @string/redmi_buds_5_pro_equalizer_neg1 + @string/redmi_buds_5_pro_equalizer_zero + @string/redmi_buds_5_pro_equalizer_1 + @string/redmi_buds_5_pro_equalizer_2 + @string/redmi_buds_5_pro_equalizer_3 + @string/redmi_buds_5_pro_equalizer_4 + @string/redmi_buds_5_pro_equalizer_5 + @string/redmi_buds_5_pro_equalizer_6 + + + + 134 + 133 + 132 + 131 + 130 + 129 + 0 + 1 + 2 + 3 + 4 + 5 + 6 + + @string/sony_ambient_sound_off @string/sony_ambient_sound_noise_cancelling diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5b8763cb7..1a50c0019 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1877,6 +1877,7 @@ Xiaomi Watch Lite Redmi Watch 3 Active Redmi Watch 3 + Redmi Buds 5 Pro Redmi Smart Band 2 Redmi Watch 2 Redmi Watch 2 Lite @@ -2611,6 +2612,51 @@ %1$.2f kg %1$.2f lbs Target + Balanced + Light + Deep + Transparency Strength + Regular + Enhance Voices + Enhance Ambient Sounds + ANC / Off + Transparency / Off + ANC / Transparency + ANC / Transparency / Off + Double Connection + Allow the earbuds to connect to two devices at the same time + + + Adaptive Sound + Adjusts the sound according to the ear shape and the environment + Standard + Enhance Treble + Enhance Bass + Enhance Voice + Custom + 62 Hz + 125 Hz + 250 Hz + 500 Hz + 1 kHz + 2 kHz + 4 kHz + 8 kHz + 12 kHz + 16 kHz + -6 dB + -5 dB + -4 dB + -3 dB + -2 dB + -1 dB + 0 dB + 1 dB + 2 dB + 3 dB + 4 dB + 5 dB + 6 dB Mode Off Noise Cancelling @@ -2674,6 +2720,8 @@ Adaptive volume control Adaptive ANC Set the strength of the ANC automatically depending on the ambient sound level + + Speak-to-chat Turn off noise cancelling automatically when you start talking. Voice Detection Sensitivity diff --git a/app/src/main/res/xml/devicesettings_redmibuds5pro_gestures.xml b/app/src/main/res/xml/devicesettings_redmibuds5pro_gestures.xml new file mode 100644 index 000000000..fe413b159 --- /dev/null +++ b/app/src/main/res/xml/devicesettings_redmibuds5pro_gestures.xml @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/devicesettings_redmibuds5pro_headphones.xml b/app/src/main/res/xml/devicesettings_redmibuds5pro_headphones.xml new file mode 100644 index 000000000..c621abcce --- /dev/null +++ b/app/src/main/res/xml/devicesettings_redmibuds5pro_headphones.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/devicesettings_redmibuds5pro_sound.xml b/app/src/main/res/xml/devicesettings_redmibuds5pro_sound.xml new file mode 100644 index 000000000..163026f85 --- /dev/null +++ b/app/src/main/res/xml/devicesettings_redmibuds5pro_sound.xml @@ -0,0 +1,144 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +