Merge pull request 'huawei: Add missing features in HUAWEI FreeClip 2' (#6451)

Reviewed-on: https://codeberg.org/Freeyourgadget/Gadgetbridge/pulls/6451
This commit is contained in:
José Rebelo
2026-07-27 21:47:06 +02:00
21 changed files with 793 additions and 3 deletions
@@ -390,6 +390,10 @@ public class DeviceSettingsPreferenceConst {
public static final String PREF_HUAWEI_FREEBUDS_ANC_MODE = "pref_freebuds_anc_mode";
public static final String PREF_HUAWEI_FREEBUDS_VOICE_BOOST = "pref_freebuds_voice_boost";
public static final String PREF_HUAWEI_FREEBUDS_BETTER_AUDIO_QUALITY = "pref_freebuds_better_audio_quality";
public static final String PREF_HUAWEI_FREEBUDS_ADAPTIVE_VOLUME = "pref_freebuds_adaptive_volume_mode";
public static final String PREF_HUAWEI_FREEBUDS_ADAPTIVE_VOLUME_APPLIED = "pref_freebuds_adaptive_volume_mode_applied";
public static final String PREF_HUAWEI_FREEBUDS_EXTRA_MEDIA_VOLUME = "pref_freebuds_extra_media_volume";
public static final String PREF_HUAWEI_FREEBUDS_FIND_HEADPHONES = "pref_freebuds_find_headphones";
public static final String PREF_GALAXY_BUDS_AMBIENT_MODE = "pref_galaxy_buds_ambient_mode";
public static final String PREF_GALAXY_BUDS_AMBIENT_VOICE_FOCUS = "pref_galaxy_buds_ambient_voice_focus";
@@ -869,6 +869,9 @@ public class DeviceSpecificSettingsFragment extends AbstractPreferenceFragment i
addPreferenceHandlerFor(PREF_HUAWEI_FREEBUDS_ANC_MODE);
addPreferenceHandlerFor(PREF_HUAWEI_FREEBUDS_VOICE_BOOST);
addPreferenceHandlerFor(PREF_HUAWEI_FREEBUDS_BETTER_AUDIO_QUALITY);
addPreferenceHandlerFor(PREF_HUAWEI_FREEBUDS_ADAPTIVE_VOLUME);
addPreferenceHandlerFor(PREF_HUAWEI_FREEBUDS_EXTRA_MEDIA_VOLUME);
addPreferenceHandlerFor(PREF_HUAWEI_FREEBUDS_FIND_HEADPHONES);
addPreferenceHandlerFor(PREF_GALAXY_BUDS_AMBIENT_VOICE_FOCUS);
@@ -122,6 +122,18 @@ public abstract class HuaweiFreebudsCoordinator extends HuaweiBRCoordinator {
if (supports(device, HuaweiHeadphonesCapabilities.BetterAudioQuality)) {
deviceSpecificSettings.addRootScreen(R.xml.devicesettings_huawei_headphones_better_audio_quality);
}
if (supports(device, HuaweiHeadphonesCapabilities.AdaptiveVolume)) {
deviceSpecificSettings.addRootScreen(R.xml.devicesettings_huawei_headphones_adaptive_volume);
}
if (supports(device, HuaweiHeadphonesCapabilities.ExtraMediaVolume)) {
deviceSpecificSettings.addRootScreen(R.xml.devicesettings_huawei_headphones_extra_media_volume);
}
if (supports(device, HuaweiHeadphonesCapabilities.FindHeadphones)) {
deviceSpecificSettings.addRootScreen(R.xml.devicesettings_huawei_headphones_find_headphones);
}
if (supports(device, HuaweiHeadphonesCapabilities.LowLatency)) {
deviceSpecificSettings.addRootScreen(R.xml.devicesettings_headphones_low_latency);
}
deviceSpecificSettings.addRootScreen(R.xml.devicesettings_headphones);
return deviceSpecificSettings;
}
@@ -22,5 +22,9 @@ public enum HuaweiHeadphonesCapabilities {
AudioModes,
NoiseCancellationModes,
VoiceBoost,
BetterAudioQuality
BetterAudioQuality,
AdaptiveVolume,
ExtraMediaVolume,
FindHeadphones,
LowLatency
}
@@ -706,6 +706,18 @@ public class HuaweiPacket {
return new Earphones.SetBetterAudioQuality.Response(paramsProvider).fromPacket(this);
case Earphones.GetBetterAudioQuality.id:
return new Earphones.GetBetterAudioQuality.Response(paramsProvider).fromPacket(this);
case Earphones.AdaptiveVolume.id:
return new Earphones.AdaptiveVolume.Response(paramsProvider).fromPacket(this);
case Earphones.FindHeadphones.id:
return new Earphones.FindHeadphones.Response(paramsProvider, Earphones.FindHeadphones.id).fromPacket(this);
case Earphones.FindHeadphones.stateId:
return new Earphones.FindHeadphones.Response(paramsProvider, Earphones.FindHeadphones.stateId).fromPacket(this);
case Earphones.SetLowLatency.id:
return new Earphones.SetLowLatency.Response(paramsProvider).fromPacket(this);
case Earphones.SetExtraMediaVolume.id:
return new Earphones.SetExtraMediaVolume.Response(paramsProvider).fromPacket(this);
case Earphones.GetExtraMediaVolume.id:
return new Earphones.GetExtraMediaVolume.Response(paramsProvider).fromPacket(this);
}
case FileDownloadService2C.id:
switch (this.commandId) {
@@ -19,7 +19,6 @@ package nodomain.freeyourgadget.gadgetbridge.devices.huawei.freeclip;
import androidx.annotation.NonNull;
import java.util.Collections;
import java.util.Set;
import java.util.regex.Pattern;
@@ -39,7 +38,13 @@ public class HuaweiFreeClip2Coordinator extends HuaweiFreebudsCoordinator {
@Override
public Set<HuaweiHeadphonesCapabilities> getCapabilities() {
return Collections.singleton(HuaweiHeadphonesCapabilities.InEarDetection);
return Set.of(
HuaweiHeadphonesCapabilities.InEarDetection,
HuaweiHeadphonesCapabilities.AdaptiveVolume,
HuaweiHeadphonesCapabilities.ExtraMediaVolume,
HuaweiHeadphonesCapabilities.FindHeadphones,
HuaweiHeadphonesCapabilities.LowLatency
);
}
@NonNull
@@ -121,6 +121,180 @@ public class Earphones {
// TODO: get long tap action 0x17
// TODO: Audio mode cycle 0x19
public static class AdaptiveVolume {
public static final byte id = (byte) 0xb4;
public static final byte FEATURE_ID = 0x02;
public static class SetRequest extends HuaweiPacket {
public SetRequest(ParamsProvider paramsProvider, boolean enabled) {
super(paramsProvider);
this.serviceId = Earphones.id;
this.commandId = id;
this.tlv = new HuaweiTLV()
.put(0x01, FEATURE_ID)
.put(0x02, enabled ? (byte) 0x01 : (byte) 0x00);
this.complete = true;
}
}
public static class SetSensitivityRequest extends HuaweiPacket {
public SetSensitivityRequest(ParamsProvider paramsProvider, byte sensitivity) {
super(paramsProvider);
this.serviceId = Earphones.id;
this.commandId = id;
this.tlv = new HuaweiTLV()
.put(0x01, FEATURE_ID)
.put(0x03, sensitivity);
this.complete = true;
}
}
public static class GetRequest extends HuaweiPacket {
public GetRequest(ParamsProvider paramsProvider) {
super(paramsProvider);
this.serviceId = Earphones.id;
this.commandId = id;
this.tlv = new HuaweiTLV()
.put(0x01, FEATURE_ID);
this.complete = true;
}
}
public static class Response extends HuaweiPacket {
public boolean enabled;
public byte sensitivity;
public Response(ParamsProvider paramsProvider) {
super(paramsProvider);
this.serviceId = Earphones.id;
this.commandId = id;
this.complete = true;
}
@Override
public void parseTlv() throws ParseException {
this.enabled = this.tlv.getByte(0x02, (byte) 0) == 0x01;
this.sensitivity = this.tlv.getByte(0x03, (byte) 0x01);
}
}
}
public static class FindHeadphones {
public static final byte id = 0x5d;
public static final byte stateId = 0x5e;
public static class Request extends HuaweiPacket {
public Request(ParamsProvider paramsProvider, byte side, boolean stop) {
super(paramsProvider);
this.serviceId = Earphones.id;
this.commandId = id;
this.tlv = new HuaweiTLV().put(0x01, new byte[]{side, stop ? (byte) 0x01 : (byte) 0x00});
this.complete = true;
}
}
public static class Response extends HuaweiPacket {
public byte side;
public boolean stopped;
public Response(ParamsProvider paramsProvider, byte commandId) {
super(paramsProvider);
this.serviceId = Earphones.id;
this.commandId = commandId;
this.complete = true;
}
@Override
public void parseTlv() throws ParseException {
byte[] state = this.tlv.getBytes(0x02);
this.side = state[0];
this.stopped = state[1] == 0x01;
}
}
}
public static class SetLowLatency {
public static final byte id = 0x6c;
public static class Request extends HuaweiPacket {
public Request(ParamsProvider paramsProvider, boolean enabled) {
super(paramsProvider);
this.serviceId = Earphones.id;
this.commandId = id;
this.tlv = new HuaweiTLV().put(0x01, enabled);
this.complete = true;
}
}
public static class Response extends HuaweiPacket {
public Response(ParamsProvider paramsProvider) {
super(paramsProvider);
this.serviceId = Earphones.id;
this.commandId = id;
this.complete = true;
}
}
}
public static class SetExtraMediaVolume {
public static final byte id = (byte) 0x87;
public static class Request extends HuaweiPacket {
public Request(ParamsProvider paramsProvider, boolean enabled) {
super(paramsProvider);
this.serviceId = Earphones.id;
this.commandId = id;
this.tlv = new HuaweiTLV().put(0x01, enabled);
this.complete = true;
}
}
public static class Response extends HuaweiPacket {
public Response(ParamsProvider paramsProvider) {
super(paramsProvider);
this.serviceId = Earphones.id;
this.commandId = id;
this.complete = true;
}
}
}
public static class GetExtraMediaVolume {
public static final byte id = (byte) 0x88;
public static class Request extends HuaweiPacket {
public Request(ParamsProvider paramsProvider) {
super(paramsProvider);
this.serviceId = Earphones.id;
this.commandId = id;
this.tlv = new HuaweiTLV().put(0x01);
this.complete = true;
}
}
public static class Response extends HuaweiPacket {
public boolean enabled;
public Response(ParamsProvider paramsProvider) {
super(paramsProvider);
this.serviceId = Earphones.id;
this.commandId = id;
this.complete = true;
}
@Override
public void parseTlv() throws ParseException {
this.enabled = this.tlv.getByte(0x01) == 0x01;
}
}
}
public static class GetAudioModeRequest {
public static final byte id = 0x2a;
@@ -53,6 +53,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.DataSync;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.DeviceConfig;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Ephemeris;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.EphemerisFileUpload;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Earphones;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.FileDownloadService2C;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.FindPhone;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.GpsAndTime;
@@ -137,12 +138,27 @@ public class AsynchronousResponse {
handleDataSyncCommands(response);
handleOTA(response);
handleFileDownload(response);
handleExtraMediaVolume(response);
} catch (Request.ResponseParseException e) {
LOG.error("Response parse exception", e);
}
}
private void handleExtraMediaVolume(HuaweiPacket response) throws Request.ResponseParseException {
if (response.serviceId != Earphones.id || response.commandId != Earphones.GetExtraMediaVolume.id) {
return;
}
if (!(response instanceof Earphones.GetExtraMediaVolume.Response)) {
throw new Request.ResponseTypeMismatchException(response, Earphones.GetExtraMediaVolume.Response.class);
}
boolean enabled = ((Earphones.GetExtraMediaVolume.Response) response).enabled;
GBApplication.getDeviceSpecificSharedPrefs(support.getDeviceMac()).edit()
.putBoolean(DeviceSettingsPreferenceConst.PREF_HUAWEI_FREEBUDS_EXTRA_MEDIA_VOLUME, enabled)
.apply();
}
private void handleFindPhone(HuaweiPacket response) throws Request.ResponseParseException {
if (response.serviceId == FindPhone.id && response.commandId == FindPhone.Response.id) {
if (!(response instanceof FindPhone.Response))
@@ -37,12 +37,18 @@ import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
import nodomain.freeyourgadget.gadgetbridge.service.HeadphoneHelper;
import nodomain.freeyourgadget.gadgetbridge.service.btbr.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetAdaptiveVolumeRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetBetterAudioQualityRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetExtraMediaVolumeRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetProductInformationRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.Request;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SetANCModeRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SetAudioModeRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SetBetterAudioQualityRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SetAdaptiveVolumeRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SetExtraMediaVolumeRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SetFindHeadphonesRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SetLowLatencyRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SetVoiceBoostRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SetPauseWhenRemovedFromEarRequest;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
@@ -71,6 +77,18 @@ public class HuaweiFreebudsSupport extends HuaweiBRSupport implements HeadphoneH
GetBetterAudioQualityRequest req = new GetBetterAudioQualityRequest(super.getSupportProvider());
req.doPerform();
}
if (coordinator.supports(this.gbDevice, HuaweiHeadphonesCapabilities.AdaptiveVolume)) {
GetAdaptiveVolumeRequest req = new GetAdaptiveVolumeRequest(super.getSupportProvider());
req.doPerform();
}
if (coordinator.supports(this.gbDevice, HuaweiHeadphonesCapabilities.ExtraMediaVolume)) {
new GetExtraMediaVolumeRequest(super.getSupportProvider()).doPerform();
}
if (coordinator.supports(this.gbDevice, HuaweiHeadphonesCapabilities.FindHeadphones)) {
GBApplication.getDeviceSpecificSharedPrefs(this.gbDevice.getAddress()).edit()
.putString(DeviceSettingsPreferenceConst.PREF_HUAWEI_FREEBUDS_FIND_HEADPHONES, "0")
.apply();
}
} catch (IOException e) {
GB.toast(this.getContext(), "Final initialization of Huawei device failed", Toast.LENGTH_SHORT, GB.ERROR, e);
@@ -154,6 +172,18 @@ public class HuaweiFreebudsSupport extends HuaweiBRSupport implements HeadphoneH
case DeviceSettingsPreferenceConst.PREF_HUAWEI_FREEBUDS_BETTER_AUDIO_QUALITY:
new SetBetterAudioQualityRequest(getSupportProvider()).doPerform();
break;
case DeviceSettingsPreferenceConst.PREF_HUAWEI_FREEBUDS_ADAPTIVE_VOLUME:
new SetAdaptiveVolumeRequest(getSupportProvider()).doPerform();
break;
case DeviceSettingsPreferenceConst.PREF_HUAWEI_FREEBUDS_EXTRA_MEDIA_VOLUME:
new SetExtraMediaVolumeRequest(getSupportProvider()).doPerform();
break;
case DeviceSettingsPreferenceConst.PREF_HUAWEI_FREEBUDS_FIND_HEADPHONES:
new SetFindHeadphonesRequest(getSupportProvider()).doPerform();
break;
case DeviceSettingsPreferenceConst.PREF_HEADPHONES_LOW_LATENCY:
new SetLowLatencyRequest(getSupportProvider()).doPerform();
break;
case DeviceSettingsPreferenceConst.PREF_BATTERY_POLLING_ENABLE:
if (!GBApplication.getDevicePrefs(gbDevice).getBatteryPollingEnabled()) {
getSupportProvider().stopBatteryRunnerDelayed();
@@ -0,0 +1,67 @@
/* Copyright (C) 2026
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 <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests;
import android.content.SharedPreferences;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Earphones;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
public class GetAdaptiveVolumeRequest extends Request {
private static final Logger LOG = LoggerFactory.getLogger(GetAdaptiveVolumeRequest.class);
public GetAdaptiveVolumeRequest(HuaweiSupportProvider support) {
super(support);
this.serviceId = Earphones.id;
this.commandId = Earphones.AdaptiveVolume.id;
}
@Override
protected List<byte[]> createRequest() throws Request.RequestCreationException {
try {
return new Earphones.AdaptiveVolume.GetRequest(paramsProvider).serialize();
} catch (HuaweiPacket.CryptoException e) {
throw new Request.RequestCreationException(e);
}
}
@Override
protected void processResponse() throws Request.ResponseParseException {
LOG.debug("handle GetAdaptiveVolume");
if (!(receivedPacket instanceof Earphones.AdaptiveVolume.Response))
throw new Request.ResponseTypeMismatchException(receivedPacket, Earphones.AdaptiveVolume.Response.class);
Earphones.AdaptiveVolume.Response packet = (Earphones.AdaptiveVolume.Response) receivedPacket;
SharedPreferences prefs = GBApplication.getDeviceSpecificSharedPrefs(this.getDevice().getAddress());
String mode = SetAdaptiveVolumeRequest.Mode.fromResponse(packet).toPreference();
SharedPreferences.Editor editor = prefs.edit();
editor.putString(DeviceSettingsPreferenceConst.PREF_HUAWEI_FREEBUDS_ADAPTIVE_VOLUME, mode);
editor.putString(DeviceSettingsPreferenceConst.PREF_HUAWEI_FREEBUDS_ADAPTIVE_VOLUME_APPLIED, mode);
editor.apply();
}
}
@@ -0,0 +1,55 @@
/* Copyright (C) 2026
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 <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Earphones;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
public class GetExtraMediaVolumeRequest extends Request {
public GetExtraMediaVolumeRequest(HuaweiSupportProvider supportProvider) {
super(supportProvider);
this.serviceId = Earphones.id;
this.commandId = Earphones.GetExtraMediaVolume.id;
}
@Override
protected List<byte[]> createRequest() throws RequestCreationException {
try {
return new Earphones.GetExtraMediaVolume.Request(paramsProvider).serialize();
} catch (HuaweiPacket.CryptoException e) {
throw new RequestCreationException(e);
}
}
@Override
protected void processResponse() throws ResponseParseException {
if (!(receivedPacket instanceof Earphones.GetExtraMediaVolume.Response)) {
throw new ResponseTypeMismatchException(receivedPacket, Earphones.GetExtraMediaVolume.Response.class);
}
boolean enabled = ((Earphones.GetExtraMediaVolume.Response) receivedPacket).enabled;
GBApplication.getDeviceSpecificSharedPrefs(getDevice().getAddress()).edit()
.putBoolean(DeviceSettingsPreferenceConst.PREF_HUAWEI_FREEBUDS_EXTRA_MEDIA_VOLUME, enabled)
.apply();
}
}
@@ -0,0 +1,100 @@
/* Copyright (C) 2026
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 <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests;
import android.content.SharedPreferences;
import java.util.ArrayList;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Earphones;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
public class SetAdaptiveVolumeRequest extends Request {
public enum Mode {
OFF((byte) -1),
LOW((byte) 0x00),
DEFAULT((byte) 0x01),
HIGH((byte) 0x02);
private final byte sensitivity;
Mode(byte sensitivity) {
this.sensitivity = sensitivity;
}
public static Mode fromPreference(String value) {
try {
return valueOf(value.toUpperCase());
} catch (IllegalArgumentException e) {
return OFF;
}
}
public static Mode fromResponse(Earphones.AdaptiveVolume.Response response) {
if (!response.enabled) {
return OFF;
}
switch (response.sensitivity) {
case 0x00:
return LOW;
case 0x02:
return HIGH;
default:
return DEFAULT;
}
}
public String toPreference() {
return name().toLowerCase();
}
}
public SetAdaptiveVolumeRequest(HuaweiSupportProvider supportProvider) {
super(supportProvider);
this.serviceId = Earphones.id;
this.commandId = Earphones.AdaptiveVolume.id;
}
@Override
protected List<byte[]> createRequest() throws RequestCreationException {
try {
SharedPreferences prefs = GBApplication.getDeviceSpecificSharedPrefs(this.getDevice().getAddress());
Mode mode = Mode.fromPreference(prefs.getString(DeviceSettingsPreferenceConst.PREF_HUAWEI_FREEBUDS_ADAPTIVE_VOLUME, "off"));
Mode appliedMode = Mode.fromPreference(prefs.getString(DeviceSettingsPreferenceConst.PREF_HUAWEI_FREEBUDS_ADAPTIVE_VOLUME_APPLIED, "off"));
List<byte[]> requests = new ArrayList<>();
if (mode == Mode.OFF) {
requests.addAll(new Earphones.AdaptiveVolume.SetRequest(this.paramsProvider, false).serialize());
} else {
if (appliedMode == Mode.OFF) {
requests.addAll(new Earphones.AdaptiveVolume.SetRequest(this.paramsProvider, true).serialize());
}
requests.addAll(new Earphones.AdaptiveVolume.SetSensitivityRequest(this.paramsProvider, mode.sensitivity).serialize());
}
prefs.edit().putString(DeviceSettingsPreferenceConst.PREF_HUAWEI_FREEBUDS_ADAPTIVE_VOLUME_APPLIED, mode.toPreference()).apply();
return requests;
} catch (HuaweiPacket.CryptoException e) {
throw new RequestCreationException(e);
}
}
}
@@ -0,0 +1,45 @@
/* Copyright (C) 2026
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 <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Earphones;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
public class SetExtraMediaVolumeRequest extends Request {
public SetExtraMediaVolumeRequest(HuaweiSupportProvider supportProvider) {
super(supportProvider);
this.serviceId = Earphones.id;
this.commandId = Earphones.SetExtraMediaVolume.id;
}
@Override
protected List<byte[]> createRequest() throws RequestCreationException {
try {
boolean enabled = GBApplication.getDeviceSpecificSharedPrefs(getDevice().getAddress())
.getBoolean(DeviceSettingsPreferenceConst.PREF_HUAWEI_FREEBUDS_EXTRA_MEDIA_VOLUME, false);
return new Earphones.SetExtraMediaVolume.Request(paramsProvider, enabled).serialize();
} catch (HuaweiPacket.CryptoException e) {
throw new RequestCreationException(e);
}
}
}
@@ -0,0 +1,49 @@
/* Copyright (C) 2026
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 <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests;
import java.util.ArrayList;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Earphones;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
public class SetFindHeadphonesRequest extends Request {
public SetFindHeadphonesRequest(HuaweiSupportProvider supportProvider) {
super(supportProvider);
this.serviceId = Earphones.id;
this.commandId = Earphones.FindHeadphones.id;
}
@Override
protected List<byte[]> createRequest() throws RequestCreationException {
try {
int mode = Integer.parseInt(GBApplication.getDeviceSpecificSharedPrefs(getDevice().getAddress())
.getString(DeviceSettingsPreferenceConst.PREF_HUAWEI_FREEBUDS_FIND_HEADPHONES, "0"));
List<byte[]> requests = new ArrayList<>();
requests.addAll(new Earphones.FindHeadphones.Request(paramsProvider, (byte) 0x00, (mode & 0x01) == 0).serialize());
requests.addAll(new Earphones.FindHeadphones.Request(paramsProvider, (byte) 0x01, (mode & 0x02) == 0).serialize());
return requests;
} catch (HuaweiPacket.CryptoException e) {
throw new RequestCreationException(e);
}
}
}
@@ -0,0 +1,45 @@
/* Copyright (C) 2026
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 <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Earphones;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
public class SetLowLatencyRequest extends Request {
public SetLowLatencyRequest(HuaweiSupportProvider supportProvider) {
super(supportProvider);
this.serviceId = Earphones.id;
this.commandId = Earphones.SetLowLatency.id;
}
@Override
protected List<byte[]> createRequest() throws RequestCreationException {
try {
boolean enabled = GBApplication.getDeviceSpecificSharedPrefs(getDevice().getAddress())
.getBoolean(DeviceSettingsPreferenceConst.PREF_HEADPHONES_LOW_LATENCY, false);
return new Earphones.SetLowLatency.Request(paramsProvider, enabled).serialize();
} catch (HuaweiPacket.CryptoException e) {
throw new RequestCreationException(e);
}
}
}
+26
View File
@@ -5403,6 +5403,32 @@
<item>calories_segmented</item>
</string-array>
<string-array name="huawei_find_headphones_names">
<item>@string/off</item>
<item>@string/left_earbud</item>
<item>@string/right_earbud</item>
</string-array>
<string-array name="huawei_find_headphones_values">
<item>0</item>
<item>1</item>
<item>2</item>
</string-array>
<string-array name="huawei_adaptive_volume_names">
<item>@string/off</item>
<item>@string/low</item>
<item>@string/pref_default</item>
<item>@string/high</item>
</string-array>
<string-array name="huawei_adaptive_volume_values">
<item>off</item>
<item>low</item>
<item>default</item>
<item>high</item>
</string-array>
<string-array name="huawei_freebuds_audio_mode_names">
<item>@string/prefs_active_noise_cancelling</item>
<item>@string/prefs_active_noise_cancelling_transparency</item>
+4
View File
@@ -4656,6 +4656,10 @@
<string name="huawei_voice_boost">Voice Boost</string>
<string name="huawei_better_audio_quality_title">Prioritise better audio quality.</string>
<string name="huawei_better_audio_quality_summary">Enable better audio codecs (LDAC). This should improve sound quality, but it increases power consumption and can cause delays.</string>
<string name="huawei_adaptive_volume_title">Adaptive Volume</string>
<string name="huawei_adaptive_volume_summary">Automatically adjust volume based on your environment. (Increases power usage)</string>
<string name="huawei_extra_media_volume_title">Extra media volume</string>
<string name="huawei_extra_media_volume_summary">Increase the maximum media volume. This may affect audio quality. May damage hearing.</string>
<string name="maps">Maps</string>
<string name="workout_charts">Workout Charts</string>
<string name="maps_settings">Maps settings</string>
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<ListPreference
android:defaultValue="off"
android:entries="@array/huawei_adaptive_volume_names"
android:entryValues="@array/huawei_adaptive_volume_values"
android:icon="@drawable/ic_noise_control_on"
android:key="pref_freebuds_adaptive_volume_mode"
android:title="@string/huawei_adaptive_volume_title"
app:useSimpleSummaryProvider="true" />
</androidx.preference.PreferenceScreen>
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<SwitchPreferenceCompat
android:defaultValue="false"
android:icon="@drawable/ic_volume_up"
android:key="pref_freebuds_extra_media_volume"
android:layout="@layout/preference_checkbox"
android:summary="@string/huawei_extra_media_volume_summary"
android:title="@string/huawei_extra_media_volume_title" />
</androidx.preference.PreferenceScreen>
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<ListPreference
android:defaultValue="0"
android:entries="@array/huawei_find_headphones_names"
android:entryValues="@array/huawei_find_headphones_values"
android:icon="@drawable/ic_action_find_lost_device"
android:key="pref_freebuds_find_headphones"
android:title="@string/earfun_find_headphones"
app:useSimpleSummaryProvider="true" />
<Preference
android:key="pref_freebuds_find_headphones_hint"
android:selectable="false"
android:summary="@string/earfun_find_headphones_hint" />
</androidx.preference.PreferenceScreen>
@@ -22,6 +22,7 @@ import org.junit.Test;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Earphones;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.FindPhone;
public class TestHuaweiPacket {
@@ -216,6 +217,106 @@ public class TestHuaweiPacket {
Assert.assertArrayEquals(expected, output.get(0));
}
@Test
public void testAdaptiveVolumeSerialize() throws HuaweiPacket.CryptoException {
byte[] expectedEnabled = {(byte) 0x5a, 0x00, 0x09, 0x00, 0x2b, (byte) 0xb4, 0x01, 0x01, 0x02, 0x02, 0x01, 0x01, 0x03, (byte) 0xc0};
byte[] expectedDisabled = {(byte) 0x5a, 0x00, 0x09, 0x00, 0x2b, (byte) 0xb4, 0x01, 0x01, 0x02, 0x02, 0x01, 0x00, 0x13, (byte) 0xe1};
byte[] expectedLow = {(byte) 0x5a, 0x00, 0x09, 0x00, 0x2b, (byte) 0xb4, 0x01, 0x01, 0x02, 0x03, 0x01, 0x00, 0x24, (byte) 0xd1};
byte[] expectedDefault = {(byte) 0x5a, 0x00, 0x09, 0x00, 0x2b, (byte) 0xb4, 0x01, 0x01, 0x02, 0x03, 0x01, 0x01, 0x34, (byte) 0xf0};
byte[] expectedHigh = {(byte) 0x5a, 0x00, 0x09, 0x00, 0x2b, (byte) 0xb4, 0x01, 0x01, 0x02, 0x03, 0x01, 0x02, 0x04, (byte) 0x93};
Assert.assertArrayEquals(expectedEnabled, new Earphones.AdaptiveVolume.SetRequest(paramsProvider, true).serialize().get(0));
Assert.assertArrayEquals(expectedDisabled, new Earphones.AdaptiveVolume.SetRequest(paramsProvider, false).serialize().get(0));
Assert.assertArrayEquals(expectedLow, new Earphones.AdaptiveVolume.SetSensitivityRequest(paramsProvider, (byte) 0x00).serialize().get(0));
Assert.assertArrayEquals(expectedDefault, new Earphones.AdaptiveVolume.SetSensitivityRequest(paramsProvider, (byte) 0x01).serialize().get(0));
Assert.assertArrayEquals(expectedHigh, new Earphones.AdaptiveVolume.SetSensitivityRequest(paramsProvider, (byte) 0x02).serialize().get(0));
}
@Test
public void testAdaptiveVolumeResponseParse() throws HuaweiPacket.ParseException {
byte[] enabled = {(byte) 0x5a, 0x00, 0x0c, 0x00, 0x2b, (byte) 0xb4, 0x01, 0x01, 0x02, 0x02, 0x01, 0x01, 0x03, 0x01, 0x01, 0x75, (byte) 0x8a};
byte[] disabled = {(byte) 0x5a, 0x00, 0x0c, 0x00, 0x2b, (byte) 0xb4, 0x01, 0x01, 0x02, 0x02, 0x01, 0x00, 0x03, 0x01, 0x01, 0x03, 0x3e};
Earphones.AdaptiveVolume.Response enabledResponse = (Earphones.AdaptiveVolume.Response) new HuaweiPacket(paramsProvider).parse(enabled);
enabledResponse.parseTlv();
Assert.assertTrue(enabledResponse.enabled);
Assert.assertEquals(0x01, enabledResponse.sensitivity);
Earphones.AdaptiveVolume.Response disabledResponse = (Earphones.AdaptiveVolume.Response) new HuaweiPacket(paramsProvider).parse(disabled);
disabledResponse.parseTlv();
Assert.assertFalse(disabledResponse.enabled);
}
@Test
public void testExtraMediaVolumeSerialize() throws HuaweiPacket.CryptoException {
byte[] expectedEnabled = {(byte) 0x5a, 0x00, 0x06, 0x00, 0x2b, (byte) 0x87, 0x01, 0x01, 0x01, 0x3e, (byte) 0xe4};
byte[] expectedDisabled = {(byte) 0x5a, 0x00, 0x06, 0x00, 0x2b, (byte) 0x87, 0x01, 0x01, 0x00, 0x2e, (byte) 0xc5};
byte[] expectedGet = {(byte) 0x5a, 0x00, 0x05, 0x00, 0x2b, (byte) 0x88, 0x01, 0x00, (byte) 0x91, (byte) 0x82};
Assert.assertArrayEquals(expectedEnabled, new Earphones.SetExtraMediaVolume.Request(paramsProvider, true).serialize().get(0));
Assert.assertArrayEquals(expectedDisabled, new Earphones.SetExtraMediaVolume.Request(paramsProvider, false).serialize().get(0));
Assert.assertArrayEquals(expectedGet, new Earphones.GetExtraMediaVolume.Request(paramsProvider).serialize().get(0));
}
@Test
public void testExtraMediaVolumeResponseParse() throws HuaweiPacket.ParseException {
byte[] enabled = {(byte) 0x5a, 0x00, 0x06, 0x00, 0x2b, (byte) 0x88, 0x01, 0x01, 0x01, (byte) 0xea, 0x0a};
byte[] disabled = {(byte) 0x5a, 0x00, 0x06, 0x00, 0x2b, (byte) 0x88, 0x01, 0x01, 0x00, (byte) 0xfa, 0x2b};
Earphones.GetExtraMediaVolume.Response enabledResponse = (Earphones.GetExtraMediaVolume.Response) new HuaweiPacket(paramsProvider).parse(enabled);
enabledResponse.parseTlv();
Assert.assertTrue(enabledResponse.enabled);
Earphones.GetExtraMediaVolume.Response disabledResponse = (Earphones.GetExtraMediaVolume.Response) new HuaweiPacket(paramsProvider).parse(disabled);
disabledResponse.parseTlv();
Assert.assertFalse(disabledResponse.enabled);
}
@Test
public void testFindHeadphonesSerialize() throws HuaweiPacket.CryptoException {
byte[] leftStart = {(byte) 0x5a, 0x00, 0x07, 0x00, 0x2b, 0x5d, 0x01, 0x02, 0x00, 0x00, 0x6f, 0x63};
byte[] leftStop = {(byte) 0x5a, 0x00, 0x07, 0x00, 0x2b, 0x5d, 0x01, 0x02, 0x00, 0x01, 0x7f, 0x42};
byte[] rightStart = {(byte) 0x5a, 0x00, 0x07, 0x00, 0x2b, 0x5d, 0x01, 0x02, 0x01, 0x00, 0x5c, 0x52};
byte[] rightStop = {(byte) 0x5a, 0x00, 0x07, 0x00, 0x2b, 0x5d, 0x01, 0x02, 0x01, 0x01, 0x4c, 0x73};
Assert.assertArrayEquals(leftStart, new Earphones.FindHeadphones.Request(paramsProvider, (byte) 0x00, false).serialize().get(0));
Assert.assertArrayEquals(leftStop, new Earphones.FindHeadphones.Request(paramsProvider, (byte) 0x00, true).serialize().get(0));
Assert.assertArrayEquals(rightStart, new Earphones.FindHeadphones.Request(paramsProvider, (byte) 0x01, false).serialize().get(0));
Assert.assertArrayEquals(rightStop, new Earphones.FindHeadphones.Request(paramsProvider, (byte) 0x01, true).serialize().get(0));
}
@Test
public void testFindHeadphonesResponseParse() throws HuaweiPacket.ParseException {
byte[] acknowledgement = {(byte) 0x5a, 0x00, 0x07, 0x00, 0x2b, 0x5d, 0x02, 0x02, 0x00, 0x00, (byte) 0xf4, (byte) 0xbf};
byte[] notification = {(byte) 0x5a, 0x00, 0x07, 0x00, 0x2b, 0x5e, 0x02, 0x02, 0x01, 0x01, 0x39, 0x7d};
Earphones.FindHeadphones.Response acknowledgementResponse = (Earphones.FindHeadphones.Response) new HuaweiPacket(paramsProvider).parse(acknowledgement);
acknowledgementResponse.parseTlv();
Assert.assertEquals(0x00, acknowledgementResponse.side);
Assert.assertFalse(acknowledgementResponse.stopped);
Earphones.FindHeadphones.Response notificationResponse = (Earphones.FindHeadphones.Response) new HuaweiPacket(paramsProvider).parse(notification);
notificationResponse.parseTlv();
Assert.assertEquals(0x01, notificationResponse.side);
Assert.assertTrue(notificationResponse.stopped);
}
@Test
public void testLowLatencySerialize() throws HuaweiPacket.CryptoException {
byte[] expectedEnabled = {(byte) 0x5a, 0x00, 0x06, 0x00, 0x2b, 0x6c, 0x01, 0x01, 0x01, (byte) 0xa4, 0x11};
byte[] expectedDisabled = {(byte) 0x5a, 0x00, 0x06, 0x00, 0x2b, 0x6c, 0x01, 0x01, 0x00, (byte) 0xb4, 0x30};
Assert.assertArrayEquals(expectedEnabled, new Earphones.SetLowLatency.Request(paramsProvider, true).serialize().get(0));
Assert.assertArrayEquals(expectedDisabled, new Earphones.SetLowLatency.Request(paramsProvider, false).serialize().get(0));
}
@Test
public void testLowLatencyResponseParse() throws HuaweiPacket.ParseException {
byte[] response = {(byte) 0x5a, 0x00, 0x09, 0x00, 0x2b, 0x6c, 0x7f, 0x04, 0x00, 0x01, (byte) 0x86, (byte) 0xa0, (byte) 0xa4, (byte) 0x97};
Assert.assertEquals(Earphones.SetLowLatency.Response.class, new HuaweiPacket(paramsProvider).parse(response).getClass());
}
@Test
public void testEncryptedUnslicedSerialize() throws HuaweiPacket.CryptoException {
byte serviceId = 0x01;