diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/earfun/EarFunFreePro3Coordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/earfun/EarFunFreePro3Coordinator.java new file mode 100644 index 0000000000..8a62d5de61 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/earfun/EarFunFreePro3Coordinator.java @@ -0,0 +1,120 @@ +/* Copyright (C) 2026 Obside + + 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.earfun; + +import androidx.annotation.NonNull; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import nodomain.freeyourgadget.gadgetbridge.R; +import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettings; +import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsCustomizer; +import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsScreen; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate; +import nodomain.freeyourgadget.gadgetbridge.model.BatteryConfig; +import nodomain.freeyourgadget.gadgetbridge.service.DeviceSupport; +import nodomain.freeyourgadget.gadgetbridge.service.devices.earfun.freepro3.EarFunFreePro3DeviceSupport; +import nodomain.freeyourgadget.gadgetbridge.service.devices.earfun.freepro3.EarFunFreePro3SettingsCustomizer; + +public class EarFunFreePro3Coordinator extends AbstractEarFunCoordinator { + @Override + public int getDeviceNameResource() { + return R.string.devicetype_earfun_free_pro_3; + } + + @Override + public boolean supports(GBDeviceCandidate candidate) { + if (candidate.getName().startsWith("EarFun Free Pro 3")) { + return true; + } + + // fallback: check UUIDs and MAC prefix to detect even if device name was changed + // Free Pro 3 has 9 UUIDs: 5 standard audio + 4 vendor (EB04-EB07) + // Same MAC prefix (70:5A:6F) as Air Pro 4, so we must distinguish by UUID set + String[] uuids = { + "00001101-0000-1000-8000-00805f9b34fb", + "0000111e-0000-1000-8000-00805f9b34fb", + "0000110b-0000-1000-8000-00805f9b34fb", + "0000110c-0000-1000-8000-00805f9b34fb", + "0000110e-0000-1000-8000-00805f9b34fb", + "0000eb04-d102-11e1-9b23-00025b00a5a5", + "0000eb06-d102-11e1-9b23-00025b00a5a5", + "0000eb07-d102-11e1-9b23-00025b00a5a5", + "0000eb05-d102-11e1-9b23-00025b00a5a5"}; + + boolean allServicesSupported = Arrays.stream(uuids) + .map(UUID::fromString) + .map(candidate::supportsService).allMatch(b -> b); + + // Must NOT have Air Pro 4 extras (df21fe2c, 180f battery, 180a device info) + boolean hasAirPro4Extras = candidate.supportsService( + UUID.fromString("df21fe2c-2515-4fdb-8886-f12c4d67927c")); + + boolean macAddressMatches = candidate.getMacAddress().toUpperCase().startsWith("70:5A:6F"); + + return allServicesSupported && !hasAirPro4Extras && macAddressMatches; + } + + @NonNull + @Override + public Class getDeviceSupportClass(final GBDevice device) { + return EarFunFreePro3DeviceSupport.class; + } + + @Override + public int getBatteryCount(final GBDevice device) { + return 3; + } + + @Override + public BatteryConfig[] getBatteryConfig(final GBDevice device) { + BatteryConfig battery1 = new BatteryConfig(2, R.drawable.ic_buds_pro_case, R.string.battery_case); + BatteryConfig battery2 = new BatteryConfig(0, R.drawable.ic_nothing_ear_l, R.string.left_earbud); + BatteryConfig battery3 = new BatteryConfig(1, 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(); + // Category Audio Experience + deviceSpecificSettings.addRootScreen(R.xml.devicesettings_earfun_header_audio_experience); + deviceSpecificSettings.addRootScreen(R.xml.devicesettings_earfun_10_band_equalizer); + deviceSpecificSettings.addRootScreen(R.xml.devicesettings_earfun_free_pro_3_sound_control); + + // Category Audio Quality & Connectivity + deviceSpecificSettings.addRootScreen(R.xml.devicesettings_earfun_header_connectivity); + deviceSpecificSettings.addRootScreen(R.xml.devicesettings_earfun_free_pro_3_audio_quality); + + // Category System Settings + deviceSpecificSettings.addRootScreen(R.xml.devicesettings_earfun_header_system_settings); + deviceSpecificSettings.addRootScreen(R.xml.devicesettings_earfun_free_pro_3_gestures); + deviceSpecificSettings.addRootScreen(R.xml.devicesettings_earfun_find_device); + deviceSpecificSettings.addRootScreen(R.xml.devicesettings_earfun_device_name); + final List callsAndNotif = deviceSpecificSettings.addRootScreen(DeviceSpecificSettingsScreen.CALLS_AND_NOTIFICATIONS); + callsAndNotif.add(R.xml.devicesettings_headphones); + return deviceSpecificSettings; + } + + @Override + public DeviceSpecificSettingsCustomizer getDeviceSpecificSettingsCustomizer(final GBDevice device) { + return new EarFunFreePro3SettingsCustomizer(device); + } +} 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 35b35e3b0c..78e6780de0 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java @@ -54,6 +54,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.cycling_sensor.coordinator.C import nodomain.freeyourgadget.gadgetbridge.devices.divoom.PixooCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.domyos.DomyosT540Coordinator; import nodomain.freeyourgadget.gadgetbridge.devices.earfun.EarFunAirPro4Coordinator; +import nodomain.freeyourgadget.gadgetbridge.devices.earfun.EarFunFreePro3Coordinator; import nodomain.freeyourgadget.gadgetbridge.devices.earfun.EarFunAirSCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.evenrealities.G1DeviceCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.femometer.FemometerVinca2DeviceCoordinator; @@ -969,6 +970,7 @@ public enum DeviceType { B_AND_W_P_SERIES(BandWPSeriesDeviceCoordinator.class), EARFUN_AIR_S(EarFunAirSCoordinator.class), EARFUN_AIR_PRO_4(EarFunAirPro4Coordinator.class), + EARFUN_FREE_PRO_3(EarFunFreePro3Coordinator.class), SCANNABLE(ScannableDeviceCoordinator.class), CYCLING_SENSOR(CyclingSensorCoordinator.class), BLE_GATT_CLIENT(BleGattClientCoordinator.class), diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/earfun/freepro3/EarFunFreePro3DeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/earfun/freepro3/EarFunFreePro3DeviceSupport.java new file mode 100644 index 0000000000..9fe8b01aee --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/earfun/freepro3/EarFunFreePro3DeviceSupport.java @@ -0,0 +1,27 @@ +/* Copyright (C) 2026 Obside + + 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.earfun.freepro3; + +import nodomain.freeyourgadget.gadgetbridge.service.devices.earfun.EarFunDeviceSupport; + +public class EarFunFreePro3DeviceSupport extends EarFunDeviceSupport { + + @Override + protected EarFunFreePro3Protocol createDeviceProtocol() { + return new EarFunFreePro3Protocol(getDevice()); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/earfun/freepro3/EarFunFreePro3Protocol.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/earfun/freepro3/EarFunFreePro3Protocol.java new file mode 100644 index 0000000000..fe3396bbc2 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/earfun/freepro3/EarFunFreePro3Protocol.java @@ -0,0 +1,64 @@ +/* Copyright (C) 2026 Obside + + 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.earfun.freepro3; + +import static nodomain.freeyourgadget.gadgetbridge.service.devices.earfun.EarFunPacketEncoder.joinPackets; + +import android.os.Bundle; + +import androidx.annotation.Nullable; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.model.DeviceType; +import nodomain.freeyourgadget.gadgetbridge.service.devices.earfun.EarFunPacketEncoder; +import nodomain.freeyourgadget.gadgetbridge.service.devices.earfun.EarFunProtocol; +import nodomain.freeyourgadget.gadgetbridge.service.devices.earfun.prefs.Equalizer; +import nodomain.freeyourgadget.gadgetbridge.util.Prefs; + +public class EarFunFreePro3Protocol extends EarFunProtocol { + + private static final Logger LOG = LoggerFactory.getLogger(EarFunFreePro3Protocol.class); + + @Override + public byte[] encodeTestNewFunction(@Nullable Bundle options) { + return joinPackets( + // new EarFunPacket(EarFunPacket.Command.COMMAND_REBOOT).encode() + ); + } + + @Override + public byte[] encodeSendConfiguration(String config) { + if (Equalizer.containsKey(Equalizer.TenBandEqualizer, config)) { + Prefs prefs = getDevicePrefs(); + return EarFunPacketEncoder.encodeSetEqualizerTenBands(prefs); + } + return super.encodeSendConfiguration(config); + } + + @Override + public byte[] encodeSettingsReq() { + return EarFunPacketEncoder.encodeAirPro4SettingsReq(); + } + + protected EarFunFreePro3Protocol(GBDevice device) { + super(device); + DeviceType type = device.getType(); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/earfun/freepro3/EarFunFreePro3SettingsCustomizer.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/earfun/freepro3/EarFunFreePro3SettingsCustomizer.java new file mode 100644 index 0000000000..f1fd52106c --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/earfun/freepro3/EarFunFreePro3SettingsCustomizer.java @@ -0,0 +1,95 @@ +/* Copyright (C) 2026 Obside + + 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.earfun.freepro3; + +import static nodomain.freeyourgadget.gadgetbridge.service.devices.earfun.prefs.EarFunSettingsPreferenceConst.PREF_EARFUN_AMBIENT_SOUND_CONTROL; +import static nodomain.freeyourgadget.gadgetbridge.service.devices.earfun.prefs.EarFunSettingsPreferenceConst.PREF_EARFUN_ANC_MODE; +import static nodomain.freeyourgadget.gadgetbridge.service.devices.earfun.prefs.EarFunSettingsPreferenceConst.PREF_EARFUN_EQUALIZER_PRESET; +import static nodomain.freeyourgadget.gadgetbridge.service.devices.earfun.prefs.EarFunSettingsPreferenceConst.PREF_EARFUN_TRANSPARENCY_MODE; +import static nodomain.freeyourgadget.gadgetbridge.service.devices.earfun.prefs.Equalizer.TenBandEqualizerPresets; + +import androidx.preference.ListPreference; +import androidx.preference.Preference; + +import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsHandler; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.service.devices.earfun.EarFunSettingsCustomizer; +import nodomain.freeyourgadget.gadgetbridge.service.devices.earfun.prefs.Equalizer; +import nodomain.freeyourgadget.gadgetbridge.util.Prefs; + +public class EarFunFreePro3SettingsCustomizer extends EarFunSettingsCustomizer { + + @Override + public void onPreferenceChange(Preference preference, DeviceSpecificSettingsHandler handler) { + super.onPreferenceChange(preference, handler); + String key = preference.getKey(); + if (key == null) { + return; + } + switch (key) { + case PREF_EARFUN_AMBIENT_SOUND_CONTROL: + onPreferenceChangeAmbientSoundControl(handler); + break; + case PREF_EARFUN_EQUALIZER_PRESET: + onPreferenceChangeEqualizerPreset(handler, Equalizer.TenBandEqualizer, TenBandEqualizerPresets); + break; + } + // if the band sliders match a preset, update the preset list + if (Equalizer.containsKey(Equalizer.TenBandEqualizer, key)) { + int equalizerPreset = getSelectedPresetFromEqualizerBands(handler, + Equalizer.TenBandEqualizer, TenBandEqualizerPresets); + ListPreference listPreferenceEqualizerPreset = handler.findPreference(PREF_EARFUN_EQUALIZER_PRESET); + if (listPreferenceEqualizerPreset != null) { + listPreferenceEqualizerPreset.setValue(Integer.toString(equalizerPreset)); + } + } + } + + @Override + public void customizeSettings(DeviceSpecificSettingsHandler handler, Prefs prefs, String rootKey) { + super.customizeSettings(handler, prefs, rootKey); + initializeEqualizerPresetListPreference(handler, TenBandEqualizerPresets); + } + + private void onPreferenceChangeAmbientSoundControl(DeviceSpecificSettingsHandler handler) { + ListPreference listPreferenceAmbientSound = handler.findPreference(PREF_EARFUN_AMBIENT_SOUND_CONTROL); + ListPreference listPreferenceTransparencyMode = handler.findPreference(PREF_EARFUN_TRANSPARENCY_MODE); + ListPreference listPreferenceAncMode = handler.findPreference(PREF_EARFUN_ANC_MODE); + + if (listPreferenceAmbientSound == null || listPreferenceTransparencyMode == null || listPreferenceAncMode == null) { + return; + } + + switch (listPreferenceAmbientSound.getValue()) { + case "1": // noise cancelling + listPreferenceTransparencyMode.setVisible(false); + listPreferenceAncMode.setVisible(true); + break; + case "2": // transparency + listPreferenceTransparencyMode.setVisible(true); + listPreferenceAncMode.setVisible(false); + break; + default: + listPreferenceTransparencyMode.setVisible(false); + listPreferenceAncMode.setVisible(false); + } + } + + public EarFunFreePro3SettingsCustomizer(final GBDevice device) { + super(device); + } +} diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index 071f45c6d5..34cc1d25af 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -4171,6 +4171,18 @@ 8 + + @string/pref_title_stable_connection + aptX + aptX Adaptive + + + + 0 + 1 + 3 + + Google Fast Pair LE Audio @@ -4251,6 +4263,38 @@ 13 + + @string/none + @string/pref_media_volumeup + @string/pref_media_volumedown + @string/pref_media_next + @string/pref_media_previous + @string/pref_media_playpause + @string/prefs_game_mode + @string/pref_title_touch_voice_assistant + @string/earfun_tap_action_noise_control_all_modes + @string/earfun_tap_action_noise_control_normal_wind + @string/earfun_tap_action_noise_control_normal_ambient + @string/earfun_tap_action_noise_control_wind_ambient + @string/earfun_tap_action_redial + + + + 16 + 2 + 3 + 5 + 4 + 1 + 7 + 6 + 8 + 9 + 10 + 11 + 13 + + @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 78c338ca10..8a7ab54857 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2281,6 +2281,7 @@ Bowers and Wilkins P series EarFun Air S EarFun Air Pro 4 + EarFun Free Pro 3 ATC_BLE_OEPL iGPSPORT BSC200 iGPSPORT BSC200S diff --git a/app/src/main/res/xml/devicesettings_earfun_free_pro_3_audio_quality.xml b/app/src/main/res/xml/devicesettings_earfun_free_pro_3_audio_quality.xml new file mode 100644 index 0000000000..93bd3a8c7c --- /dev/null +++ b/app/src/main/res/xml/devicesettings_earfun_free_pro_3_audio_quality.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + diff --git a/app/src/main/res/xml/devicesettings_earfun_free_pro_3_gestures.xml b/app/src/main/res/xml/devicesettings_earfun_free_pro_3_gestures.xml new file mode 100644 index 0000000000..ab5df603e5 --- /dev/null +++ b/app/src/main/res/xml/devicesettings_earfun_free_pro_3_gestures.xml @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/devicesettings_earfun_free_pro_3_sound_control.xml b/app/src/main/res/xml/devicesettings_earfun_free_pro_3_sound_control.xml new file mode 100644 index 0000000000..a77f578471 --- /dev/null +++ b/app/src/main/res/xml/devicesettings_earfun_free_pro_3_sound_control.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + +