From c5714746fb1c7c56b423e220a7ed33f241bad4c3 Mon Sep 17 00:00:00 2001 From: "Martin.JM" Date: Sat, 23 Nov 2024 23:47:19 +0100 Subject: [PATCH] Huawei: Initial Freebuds (5i) support --- .../DeviceSettingsPreferenceConst.java | 4 + .../DeviceSpecificSettingsFragment.java | 3 + .../huawei/HuaweiFreebudsCoordinator.java | 94 ++++++++++ .../devices/huawei/HuaweiPacket.java | 9 + .../devices/huawei/HuaweiTLV.java | 8 + .../HuaweiFreebuds5iCoordinator.java | 35 ++++ .../devices/huawei/packets/DeviceConfig.java | 9 + .../devices/huawei/packets/Earphones.java | 119 +++++++++++++ .../gadgetbridge/model/DeviceType.java | 2 + .../AbstractHeadphoneDeviceSupport.java | 122 ++----------- .../gadgetbridge/service/HeadphoneHelper.java | 161 ++++++++++++++++++ .../devices/huawei/AsynchronousResponse.java | 46 +++++ .../devices/huawei/HuaweiBRSupport.java | 3 + .../devices/huawei/HuaweiFreebudsSupport.java | 135 +++++++++++++++ .../devices/huawei/HuaweiSupportProvider.java | 15 +- .../requests/GetBatteryLevelRequest.java | 36 +++- .../huawei/requests/SetAudioModeRequest.java | 41 +++++ .../SetPauseWhenRemovedFromEarRequest.java | 31 ++++ app/src/main/res/values/arrays.xml | 12 ++ app/src/main/res/values/strings.xml | 1 + .../xml/devicesettings_huawei_freebuds.xml | 18 ++ 21 files changed, 784 insertions(+), 120 deletions(-) create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiFreebudsCoordinator.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/freebuds5i/HuaweiFreebuds5iCoordinator.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/Earphones.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/HeadphoneHelper.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiFreebudsSupport.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SetAudioModeRequest.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SetPauseWhenRemovedFromEarRequest.java create mode 100644 app/src/main/res/xml/devicesettings_huawei_freebuds.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 a07090485..9e6fb8056 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 @@ -334,6 +334,10 @@ public class DeviceSettingsPreferenceConst { public static final String PREF_NOTHING_EAR1_INEAR = "pref_nothing_inear_detection"; public static final String PREF_NOTHING_EAR1_AUDIOMODE = "pref_nothing_audiomode"; + + public static final String PREF_HUAWEI_FREEBUDS_INEAR = "pref_freebuds_inear_detection"; + public static final String PREF_HUAWEI_FREEBUDS_AUDIOMODE = "pref_freebuds_audiomode"; + 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"; public static final String PREF_GALAXY_BUDS_AMBIENT_VOLUME = "pref_galaxy_buds_ambient_volume"; 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 89fc0eefe..aea295647 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 @@ -640,6 +640,9 @@ public class DeviceSpecificSettingsFragment extends AbstractPreferenceFragment i addPreferenceHandlerFor(PREF_NOTHING_EAR1_INEAR); addPreferenceHandlerFor(PREF_NOTHING_EAR1_AUDIOMODE); + addPreferenceHandlerFor(PREF_HUAWEI_FREEBUDS_INEAR); + addPreferenceHandlerFor(PREF_HUAWEI_FREEBUDS_AUDIOMODE); + addPreferenceHandlerFor(PREF_GALAXY_BUDS_AMBIENT_VOICE_FOCUS); addPreferenceHandlerFor(PREF_GALAXY_BUDS_AMBIENT_VOLUME); addPreferenceHandlerFor(PREF_GALAXY_BUDS_LOCK_TOUCH); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiFreebudsCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiFreebudsCoordinator.java new file mode 100644 index 000000000..a5d964ece --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiFreebudsCoordinator.java @@ -0,0 +1,94 @@ +package nodomain.freeyourgadget.gadgetbridge.devices.huawei; + +import androidx.annotation.NonNull; + +import nodomain.freeyourgadget.gadgetbridge.R; +import nodomain.freeyourgadget.gadgetbridge.GBException; +import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettings; +import nodomain.freeyourgadget.gadgetbridge.devices.AbstractBLClassicDeviceCoordinator; +import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; +import nodomain.freeyourgadget.gadgetbridge.entities.Device; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.model.BatteryConfig; + +public abstract class HuaweiFreebudsCoordinator extends AbstractBLClassicDeviceCoordinator implements HuaweiCoordinatorSupplier { + + private final HuaweiCoordinator huaweiCoordinator = new HuaweiCoordinator(this); + private GBDevice gbDevice; + + public HuaweiFreebudsCoordinator() { + huaweiCoordinator.setTransactionCrypted(false); + } + + @Override + protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) throws GBException { + // TODO: implement + } + + @Override + public String getManufacturer() { + return "Huawei"; + } + + @Override + public int getDefaultIconResource() { + return R.drawable.ic_device_nothingear; + } + + @Override + public int getDisabledIconResource() { + return R.drawable.ic_device_nothingear_disabled; + } + + @Override + public HuaweiCoordinator getHuaweiCoordinator() { + return huaweiCoordinator; + } + + @Override + public HuaweiDeviceType getHuaweiType() { + return HuaweiDeviceType.BR; + } + + @Override + public void setDevice(GBDevice gbDevice) { + this.gbDevice = gbDevice; + } + + @Override + public GBDevice getDevice() { + return this.gbDevice; + } + + @Override + public int getBondingStyle() { + // TODO: Check if correct + return BONDING_STYLE_ASK; + } + + @Override + public int getBatteryCount() { + return 3; + } + + @Override + public BatteryConfig[] getBatteryConfig(GBDevice device) { + BatteryConfig battery1 = new BatteryConfig(2, R.drawable.ic_tws_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(GBDevice device) { + DeviceSpecificSettings deviceSpecificSettings = new DeviceSpecificSettings(); + deviceSpecificSettings.addRootScreen(R.xml.devicesettings_huawei_freebuds); + deviceSpecificSettings.addRootScreen(R.xml.devicesettings_headphones); + return deviceSpecificSettings; + } + + @Override + public boolean addBatteryPollingSettings() { + return true; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiPacket.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiPacket.java index 7ddb07abf..ac074953e 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiPacket.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiPacket.java @@ -35,6 +35,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.App; import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Calls; import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.CameraRemote; import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Contacts; +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Earphones; import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.EphemerisFileUpload; import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.FileDownloadService0A; import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.FileDownloadService2C; @@ -446,6 +447,7 @@ public class HuaweiPacket { case DeviceConfig.Auth.id: return new DeviceConfig.Auth.Response(paramsProvider).fromPacket(this); case DeviceConfig.BatteryLevel.id: + case DeviceConfig.BatteryLevel.id_change: return new DeviceConfig.BatteryLevel.Response(paramsProvider).fromPacket(this); case DeviceConfig.DeviceStatus.id: return new DeviceConfig.DeviceStatus.Response(paramsProvider).fromPacket(this); @@ -652,6 +654,13 @@ public class HuaweiPacket { this.isEncrypted = this.attemptDecrypt(); // Helps with debugging return this; } + case Earphones.id: + switch (this.commandId) { + case Earphones.InEarStateResponse.id: + return new Earphones.InEarStateResponse(paramsProvider).fromPacket(this); + case Earphones.GetAudioModeRequest.id: + return new Earphones.GetAudioModeRequest.Response(paramsProvider).fromPacket(this); + } case FileDownloadService2C.id: switch (this.commandId) { case FileDownloadService2C.FileDownloadInit.id: diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiTLV.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiTLV.java index 4cd44bb92..4f902f617 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiTLV.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiTLV.java @@ -219,6 +219,14 @@ public class HuaweiTLV { throw new HuaweiPacket.MissingTagException(tag); } + public byte[] getBytes(int tag, byte[] defaultValue) { + try { + return getBytes(tag); + } catch (HuaweiPacket.MissingTagException e) { + return defaultValue; + } + } + public Byte getByte(int tag) throws HuaweiPacket.MissingTagException { return getBytes(tag)[0]; } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/freebuds5i/HuaweiFreebuds5iCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/freebuds5i/HuaweiFreebuds5iCoordinator.java new file mode 100644 index 000000000..5dae2fac8 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/freebuds5i/HuaweiFreebuds5iCoordinator.java @@ -0,0 +1,35 @@ +package nodomain.freeyourgadget.gadgetbridge.devices.huawei.freebuds5i; + +import androidx.annotation.NonNull; + +import java.util.regex.Pattern; + +import nodomain.freeyourgadget.gadgetbridge.R; +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiFreebudsCoordinator; +import nodomain.freeyourgadget.gadgetbridge.model.DeviceType; +import nodomain.freeyourgadget.gadgetbridge.service.DeviceSupport; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiFreebudsSupport; + +public class HuaweiFreebuds5iCoordinator extends HuaweiFreebudsCoordinator { + + @Override + protected Pattern getSupportedDeviceName() { + return Pattern.compile("huawei freebuds 5i.*", Pattern.CASE_INSENSITIVE); + } + + @NonNull + @Override + public Class getDeviceSupportClass() { + return HuaweiFreebudsSupport.class; + } + + @Override + public DeviceType getDeviceType() { + return DeviceType.HUAWEI_FREEBUDS5I; + } + + @Override + public int getDeviceNameResource() { + return R.string.devicetype_huawei_freebuds_5i; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/DeviceConfig.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/DeviceConfig.java index 6d43a9918..5a79c62c5 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/DeviceConfig.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/DeviceConfig.java @@ -666,6 +666,7 @@ public class DeviceConfig { public static class BatteryLevel { public static final byte id = 0x08; + public static final byte id_change = 0x27; // Same format, async (receive) only public static class Request extends HuaweiPacket { public Request(ParamsProvider paramsProvider) { @@ -683,6 +684,9 @@ public class DeviceConfig { public static class Response extends HuaweiPacket { public byte level; + public byte[] multi_level; + public byte[] status; // TODO: enum + public Response(ParamsProvider paramsProvider) { super(paramsProvider); @@ -693,6 +697,8 @@ public class DeviceConfig { @Override public void parseTlv() throws ParseException { this.level = this.tlv.getByte(0x01); + this.multi_level = this.tlv.getBytes(0x02, null); + this.status = this.tlv.getBytes(0x03, null); } } // TODO: implement parsing this request for the log parser support @@ -958,6 +964,9 @@ public class DeviceConfig { // TODO: implement parsing this request for the log parser support } + // TODO: set (earphone) double tap action 0x1f + // TODO: get (earphone) double tap action 0x20 + public static class HiChain { public static final int id = 0x28; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/Earphones.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/Earphones.java new file mode 100644 index 000000000..188ba148a --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/Earphones.java @@ -0,0 +1,119 @@ +/* Copyright (C) 2024 Martin.JM + + 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.huawei.packets; + +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket; +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiTLV; + +// Information from: +// https://mmk.pw/en/posts/freebuds-4i-proto/ and +// https://github.com/TheLastGimbus/FreeBuddy/blob/master/notes/mbb-protocol-wiki.md and +// https://codeberg.org/Freeyourgadget/Gadgetbridge/pulls/4325/ + +public class Earphones { + public static final byte id = 0x2b; + + public static class InEarStateResponse extends HuaweiPacket { + public static final byte id = 0x03; + + public byte leftState; + public byte rightState; + + public InEarStateResponse(ParamsProvider paramsProvider) { + super(paramsProvider); + this.serviceId = Earphones.id; + this.commandId = id; + this.complete = true; + } + + @Override + public void parseTlv() throws ParseException { + this.leftState = this.tlv.getByte(0x08, (byte) -1); + this.rightState = this.tlv.getByte(0x09, (byte) -1); + } + } + + public static class SetAudioModeRequest extends HuaweiPacket { + public static final byte id = 0x04; + + // TODO: enum for new state + public SetAudioModeRequest(ParamsProvider paramsProvider, byte newState) { + super(paramsProvider); + this.serviceId = Earphones.id; + this.commandId = id; + + byte[] data = {newState, newState == 0 ? 0x00 : (byte) 0xff}; + + this.tlv = new HuaweiTLV().put(0x01, data); + + this.complete = true; + } + } + + public static class SetPauseWhenRemovedFromEar extends HuaweiPacket { + public static final byte id = 0x10; + + // TODO: enum for new state + public SetPauseWhenRemovedFromEar(ParamsProvider paramsProvider, boolean newState) { + super(paramsProvider); + this.serviceId = Earphones.id; + this.commandId = id; + + this.tlv = new HuaweiTLV().put(0x01, newState); + + this.complete = true; + } + } + + // TODO: get pause when removed from ear 0x11 + // TODO: set long tap action 0x16 + // TODO: get long tap action 0x17 + // TODO: Audio mode cycle 0x19 + + public static class GetAudioModeRequest { + public static final byte id = 0x2a; + + public static class Request extends HuaweiPacket { + public Request(ParamsProvider paramsProvider) { + super(paramsProvider); + this.serviceId = Earphones.id; + this.commandId = id; + this.complete = true; + } + } + + public static class Response extends HuaweiPacket { + public short fullState; + + public byte state; // TODO: enum + + public Response(ParamsProvider paramsProvider) { + super(paramsProvider); + this.serviceId = Earphones.id; + this.commandId = id; + this.complete = true; + } + + @Override + public void parseTlv() throws ParseException { + this.fullState = this.tlv.getShort(0x01); + this.state = (byte) this.fullState; + } + } + } +} 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 ac00c3685..02d1c3733 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java @@ -171,6 +171,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.huami.miband5.MiBand5Coordin import nodomain.freeyourgadget.gadgetbridge.devices.huami.miband6.MiBand6Coordinator; import nodomain.freeyourgadget.gadgetbridge.devices.huami.miband7.MiBand7Coordinator; import nodomain.freeyourgadget.gadgetbridge.devices.huami.zeppe.ZeppECoordinator; +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.freebuds5i.HuaweiFreebuds5iCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.huawei.honorband3.HonorBand3Coordinator; import nodomain.freeyourgadget.gadgetbridge.devices.huawei.honorband4.HonorBand4Coordinator; import nodomain.freeyourgadget.gadgetbridge.devices.huawei.honorband5.HonorBand5Coordinator; @@ -549,6 +550,7 @@ public enum DeviceType { HUAWEIWATCHULTIMATE(HuaweiWatchUltimateCoordinator.class), HUAWEIWATCH3(HuaweiWatch3Coordinator.class), HUAWEIWATCH4PRO(HuaweiWatch4ProCoordinator.class), + HUAWEI_FREEBUDS5I(HuaweiFreebuds5iCoordinator.class), VESC(VescCoordinator.class), BINARY_SENSOR(BinarySensorCoordinator.class), FLIPPER_ZERO(FlipperZeroCoordinator.class), diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/AbstractHeadphoneDeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/AbstractHeadphoneDeviceSupport.java index 40c94f871..97f15870b 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/AbstractHeadphoneDeviceSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/AbstractHeadphoneDeviceSupport.java @@ -16,142 +16,50 @@ along with this program. If not, see . */ package nodomain.freeyourgadget.gadgetbridge.service; -import android.content.SharedPreferences; -import android.media.AudioManager; -import android.os.Handler; -import android.os.Looper; +import android.bluetooth.BluetoothAdapter; +import android.content.Context; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import nodomain.freeyourgadget.gadgetbridge.GBApplication; -import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst; -import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventCallControl; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.model.CallSpec; import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec; import nodomain.freeyourgadget.gadgetbridge.service.serial.AbstractSerialDeviceSupport; -import nodomain.freeyourgadget.gadgetbridge.util.GBTextToSpeech; -import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_SPEAK_NOTIFICATIONS_FOCUS_EXCLUSIVE; +public abstract class AbstractHeadphoneDeviceSupport extends AbstractSerialDeviceSupport implements HeadphoneHelper.Callback { -public abstract class AbstractHeadphoneDeviceSupport extends AbstractSerialDeviceSupport { - private static final Logger LOG = LoggerFactory.getLogger(AbstractHeadphoneDeviceSupport.class); - private GBTextToSpeech gbTextToSpeech; + private HeadphoneHelper headphoneHelper; @Override public void onSetCallState(CallSpec callSpec) { - SharedPreferences prefs = GBApplication.getDeviceSpecificSharedPrefs(getDevice().getAddress()); - - if (!prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_AUTO_REPLY_INCOMING_CALL, false)) - return; - - final int delayMillis = Integer.parseInt(prefs.getString(DeviceSettingsPreferenceConst.PREF_AUTO_REPLY_INCOMING_CALL_DELAY, "15")) * 1000; - - if (CallSpec.CALL_INCOMING != callSpec.command) - return; - - if (!gbTextToSpeech.isConnected()) { // schedule the automatic reply here, if the speech to text is not connected. Else it's done by the callback, and the timeout starts after the name or number have been spoken - Looper mainLooper = Looper.getMainLooper(); - LOG.debug("Incoming call, scheduling auto answer in {} seconds.", delayMillis / 1000); - - new Handler(mainLooper).postDelayed(() -> { - GBDeviceEventCallControl callCmd = new GBDeviceEventCallControl(); - callCmd.event = GBDeviceEventCallControl.Event.ACCEPT; - evaluateGBDeviceEvent(callCmd); - }, delayMillis); //15s - - return; - } - String speechText = callSpec.name; - if (callSpec.name.equals(callSpec.number)) { - StringBuilder numberSpeller = new StringBuilder(); - for (char c : callSpec.number.toCharArray()) { - numberSpeller.append(c).append(" "); - } - speechText = numberSpeller.toString(); - } - gbTextToSpeech.speak(speechText); - + headphoneHelper.onSetCallState(callSpec); } @Override public void onNotification(NotificationSpec notificationSpec) { - SharedPreferences prefs = GBApplication.getDeviceSpecificSharedPrefs(getDevice().getAddress()); + headphoneHelper.onNotification(notificationSpec); + } - if (!prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_SPEAK_NOTIFICATIONS_ALOUD, false)) - return; - - if (gbTextToSpeech.isConnected()) { - - String notificationSpeller = new StringBuilder() - .append(notificationSpec.sourceName == null ? "" : notificationSpec.sourceName).append(". ") - .append(notificationSpec.title == null ? "" : notificationSpec.title).append(": ") - .append(notificationSpec.body == null ? "" : notificationSpec.body).toString(); - - - gbTextToSpeech.speakNotification(notificationSpeller); - - } + @Override + public void setContext(GBDevice gbDevice, BluetoothAdapter btAdapter, Context context) { + super.setContext(gbDevice, btAdapter, context); + headphoneHelper = new HeadphoneHelper(getContext(), getDevice(), this); } @Override public boolean connect() { getDeviceIOThread().start(); - final SharedPreferences prefs = GBApplication.getDeviceSpecificSharedPrefs(getDevice().getAddress()); - gbTextToSpeech = new GBTextToSpeech(getContext(), new UtteranceProgressListener(), - prefs.getBoolean(PREF_SPEAK_NOTIFICATIONS_FOCUS_EXCLUSIVE, false) ? - AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE : - AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK - ); return true; } @Override public void onSendConfiguration(String config) { - if (PREF_SPEAK_NOTIFICATIONS_FOCUS_EXCLUSIVE.equals(config)) { - final SharedPreferences prefs = GBApplication.getDeviceSpecificSharedPrefs(getDevice().getAddress()); - gbTextToSpeech.setAudioFocus(prefs.getBoolean(PREF_SPEAK_NOTIFICATIONS_FOCUS_EXCLUSIVE, false) ? - AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE : - AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK); - } else { + if (!headphoneHelper.onSendConfiguration(config)) super.onSendConfiguration(config); - } } @Override public void dispose() { - gbTextToSpeech.shutdown(); + if (headphoneHelper != null) + headphoneHelper.dispose(); super.dispose(); } - - private class UtteranceProgressListener extends android.speech.tts.UtteranceProgressListener { - @Override - public void onStart(String utteranceId) { -// LOG.debug("UtteranceProgressListener onStart."); - } - - @Override - public void onDone(String utteranceId) { -// LOG.debug("UtteranceProgressListener onDone."); - - gbTextToSpeech.abandonFocus(); - if (utteranceId.equals("call")) { - SharedPreferences prefs = GBApplication.getDeviceSpecificSharedPrefs(getDevice().getAddress()); - final int delayMillis = Integer.parseInt(prefs.getString(DeviceSettingsPreferenceConst.PREF_AUTO_REPLY_INCOMING_CALL_DELAY, "15")) * 1000; - - - Looper mainLooper = Looper.getMainLooper(); - new Handler(mainLooper).postDelayed(() -> { - GBDeviceEventCallControl callCmd = new GBDeviceEventCallControl(); - callCmd.event = GBDeviceEventCallControl.Event.ACCEPT; - evaluateGBDeviceEvent(callCmd); - }, delayMillis); //15s - } - } - - @Override - public void onError(String utteranceId) { - LOG.error("UtteranceProgressListener returned error."); - } - } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/HeadphoneHelper.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/HeadphoneHelper.java new file mode 100644 index 000000000..b06e413f6 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/HeadphoneHelper.java @@ -0,0 +1,161 @@ +/* Copyright (C) 2024 Arjan Schrijver, Daniele Gobbetti, Martin.JM + + 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; + +import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_SPEAK_NOTIFICATIONS_FOCUS_EXCLUSIVE; + +import android.content.Context; +import android.content.SharedPreferences; +import android.media.AudioManager; +import android.os.Handler; +import android.os.Looper; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import nodomain.freeyourgadget.gadgetbridge.GBApplication; +import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst; +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent; +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventCallControl; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.model.CallSpec; +import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec; +import nodomain.freeyourgadget.gadgetbridge.util.GBTextToSpeech; + +public class HeadphoneHelper { + private static final Logger LOG = LoggerFactory.getLogger(HeadphoneHelper.class); + + public interface Callback { + void evaluateGBDeviceEvent(GBDeviceEvent event); + } + + private final GBDevice device; + private final GBTextToSpeech gbTextToSpeech; + private final Callback callback; + + public HeadphoneHelper(Context context, GBDevice device, Callback callback) { + this.device = device; + this.callback = callback; + final SharedPreferences prefs = GBApplication.getDeviceSpecificSharedPrefs(this.device.getAddress()); + gbTextToSpeech = new GBTextToSpeech(context, new UtteranceProgressListener(), + prefs.getBoolean(PREF_SPEAK_NOTIFICATIONS_FOCUS_EXCLUSIVE, false) ? + AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE : + AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK + ); + } + + public void dispose() { + gbTextToSpeech.shutdown(); + } + + public void onSetCallState(CallSpec callSpec) { + SharedPreferences prefs = GBApplication.getDeviceSpecificSharedPrefs(device.getAddress()); + + if (!prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_AUTO_REPLY_INCOMING_CALL, false)) + return; + + final int delayMillis = Integer.parseInt(prefs.getString(DeviceSettingsPreferenceConst.PREF_AUTO_REPLY_INCOMING_CALL_DELAY, "15")) * 1000; + + if (CallSpec.CALL_INCOMING != callSpec.command) + return; + + if (!gbTextToSpeech.isConnected()) { // schedule the automatic reply here, if the speech to text is not connected. Else it's done by the callback, and the timeout starts after the name or number have been spoken + Looper mainLooper = Looper.getMainLooper(); + LOG.debug("Incoming call, scheduling auto answer in {} seconds.", delayMillis / 1000); + + new Handler(mainLooper).postDelayed(() -> { + GBDeviceEventCallControl callCmd = new GBDeviceEventCallControl(); + callCmd.event = GBDeviceEventCallControl.Event.ACCEPT; + callback.evaluateGBDeviceEvent(callCmd); + }, delayMillis); //15s + + return; + } + String speechText = callSpec.name; + if (callSpec.name.equals(callSpec.number)) { + StringBuilder numberSpeller = new StringBuilder(); + for (char c : callSpec.number.toCharArray()) { + numberSpeller.append(c).append(" "); + } + speechText = numberSpeller.toString(); + } + gbTextToSpeech.speak(speechText); + } + + public void onNotification(NotificationSpec notificationSpec) { + SharedPreferences prefs = GBApplication.getDeviceSpecificSharedPrefs(device.getAddress()); + + if (!prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_SPEAK_NOTIFICATIONS_ALOUD, false)) + return; + + if (gbTextToSpeech.isConnected()) { + String notificationSpeller = new StringBuilder() + .append(notificationSpec.sourceName == null ? "" : notificationSpec.sourceName).append(". ") + .append(notificationSpec.title == null ? "" : notificationSpec.title).append(": ") + .append(notificationSpec.body == null ? "" : notificationSpec.body).toString(); + + gbTextToSpeech.speakNotification(notificationSpeller); + } + } + + /** + * + * @param config + * @return True if handled, false otherwise + */ + public boolean onSendConfiguration(String config) { + if (PREF_SPEAK_NOTIFICATIONS_FOCUS_EXCLUSIVE.equals(config)) { + final SharedPreferences prefs = GBApplication.getDeviceSpecificSharedPrefs(device.getAddress()); + gbTextToSpeech.setAudioFocus(prefs.getBoolean(PREF_SPEAK_NOTIFICATIONS_FOCUS_EXCLUSIVE, false) ? + AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE : + AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK); + return true; + } + return false; + } + + private class UtteranceProgressListener extends android.speech.tts.UtteranceProgressListener { + @Override + public void onStart(String utteranceId) { +// LOG.debug("UtteranceProgressListener onStart."); + } + + @Override + public void onDone(String utteranceId) { +// LOG.debug("UtteranceProgressListener onDone."); + + gbTextToSpeech.abandonFocus(); + if (utteranceId.equals("call")) { + SharedPreferences prefs = GBApplication.getDeviceSpecificSharedPrefs(HeadphoneHelper.this.device.getAddress()); + final int delayMillis = Integer.parseInt(prefs.getString(DeviceSettingsPreferenceConst.PREF_AUTO_REPLY_INCOMING_CALL_DELAY, "15")) * 1000; + + + Looper mainLooper = Looper.getMainLooper(); + new Handler(mainLooper).postDelayed(() -> { + GBDeviceEventCallControl callCmd = new GBDeviceEventCallControl(); + callCmd.event = GBDeviceEventCallControl.Event.ACCEPT; + callback.evaluateGBDeviceEvent(callCmd); + }, delayMillis); //15s + } + } + + @Override + public void onError(String utteranceId) { + LOG.error("UtteranceProgressListener returned error."); + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/AsynchronousResponse.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/AsynchronousResponse.java index e0f86daa1..8847df036 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/AsynchronousResponse.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/AsynchronousResponse.java @@ -40,6 +40,7 @@ import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.activities.CameraActivity; import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst; +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventCallControl; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventCameraRemote; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventFindPhone; @@ -59,6 +60,8 @@ import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.FileUpload; import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.P2P; import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Watchface; import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Weather; +import nodomain.freeyourgadget.gadgetbridge.model.BatteryState; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetBatteryLevelRequest; import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.Request; import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetPhoneInfoRequest; import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendFileUploadComplete; @@ -123,6 +126,7 @@ public class AsynchronousResponse { handleP2p(response); handleEphemeris(response); handleEphemerisUploadService(response); + handleAsyncBattery(response); } catch (Request.ResponseParseException e) { LOG.error("Response parse exception", e); } @@ -679,4 +683,46 @@ public class AsynchronousResponse { } } } + + private void handleAsyncBattery(HuaweiPacket response) { + if (response.serviceId == DeviceConfig.id && response.commandId == DeviceConfig.BatteryLevel.id_change) { + if (!(response instanceof DeviceConfig.BatteryLevel.Response)) { + // TODO: exception? + return; + } + + DeviceConfig.BatteryLevel.Response resp = (DeviceConfig.BatteryLevel.Response) response; + + if (resp.multi_level == null) { + byte batteryLevel = resp.level; + this.support.getDevice().setBatteryLevel(batteryLevel); + + GBDeviceEventBatteryInfo batteryInfo = new GBDeviceEventBatteryInfo(); + batteryInfo.state = BatteryState.BATTERY_NORMAL; + batteryInfo.level = (int) batteryLevel & 0xff; + this.support.evaluateGBDeviceEvent(batteryInfo); + } else { + // Handle multiple batteries + for (int i = 0; i < resp.multi_level.length; i++) { + int level = (int) resp.multi_level[i] & 0xff; + this.support.getDevice().setBatteryLevel(level, i); + + GBDeviceEventBatteryInfo batteryInfo = new GBDeviceEventBatteryInfo(); + batteryInfo.batteryIndex = i; + batteryInfo.state = resp.status != null && resp.status.length > i ? + GetBatteryLevelRequest.byteToBatteryState(resp.status[i]) : + BatteryState.BATTERY_NORMAL; + batteryInfo.level = level; + this.support.evaluateGBDeviceEvent(batteryInfo); + } + } + + if (GBApplication.getDevicePrefs(this.support.getDevice()).getBatteryPollingEnabled()) { + if (!this.support.startBatteryRunnerDelayed()) { + GB.toast(this.support.getContext(), R.string.battery_polling_failed_start, Toast.LENGTH_SHORT, GB.ERROR); + LOG.error("Failed to start the battery polling"); + } + } + } + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiBRSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiBRSupport.java index 67b216d22..685eb6163 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiBRSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiBRSupport.java @@ -51,7 +51,10 @@ public class HuaweiBRSupport extends AbstractBTBRDeviceSupport { addSupportedService(HuaweiConstants.UUID_SERVICE_HUAWEI_SDP); setBufferSize(1032); supportProvider = new HuaweiSupportProvider(this); + } + protected HuaweiSupportProvider getSupportProvider() { + return supportProvider; } @Override diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiFreebudsSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiFreebudsSupport.java new file mode 100644 index 000000000..e23d00573 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiFreebudsSupport.java @@ -0,0 +1,135 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.huawei; + +import android.bluetooth.BluetoothAdapter; +import android.content.Context; +import android.widget.Toast; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.UUID; + +import nodomain.freeyourgadget.gadgetbridge.GBApplication; +import nodomain.freeyourgadget.gadgetbridge.R; +import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst; +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Earphones; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.model.CallSpec; +import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec; +import nodomain.freeyourgadget.gadgetbridge.service.AbstractHeadphoneDeviceSupport; +import nodomain.freeyourgadget.gadgetbridge.service.HeadphoneHelper; +import nodomain.freeyourgadget.gadgetbridge.service.btbr.AbstractBTBRDeviceSupport; +import nodomain.freeyourgadget.gadgetbridge.service.btbr.TransactionBuilder; +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.SetAudioModeRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SetPauseWhenRemovedFromEarRequest; +import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceIoThread; +import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol; +import nodomain.freeyourgadget.gadgetbridge.util.GB; + +// TODO: Move from HuaweiBRSupport to AbstractBTBRDeviceSupport +public class HuaweiFreebudsSupport extends HuaweiBRSupport implements HeadphoneHelper.Callback { + private static final Logger LOG = LoggerFactory.getLogger(HuaweiFreebudsSupport.class); + + private HeadphoneHelper headphoneHelper; + + public HuaweiFreebudsSupport() { + super(); + addSupportedService(UUID.fromString("00001101-0000-1000-8000-00805f9b34fb")); + setBufferSize(1032); + } + + @Override + public void setContext(GBDevice gbDevice, BluetoothAdapter btAdapter, Context context) { + super.setContext(gbDevice, btAdapter, context); + headphoneHelper = new HeadphoneHelper(getContext(), getDevice(), this); + } + + @Override + protected TransactionBuilder initializeDevice(TransactionBuilder builder) { + LOG.info("Huawei Freebuds init" ); + + super.getSupportProvider().setup(getDevice(), getContext()); + + builder.add(new nodomain.freeyourgadget.gadgetbridge.service.btbr.actions.SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZING, getContext())); + try { + builder.setCallback(this); + final GetProductInformationRequest deviceProductReq = new GetProductInformationRequest(super.getSupportProvider()); + deviceProductReq.setFinalizeReq(new Request.RequestCallback(getSupportProvider()) { + @Override + public void call() { + // This also (optionally) starts the battery polling + getSupportProvider().getBatteryLevel(); + } + }); + deviceProductReq.doPerform(); + } catch (IOException e) { + LOG.error("Connection failed", e); + GB.toast("Connection failed", Toast.LENGTH_SHORT, GB.ERROR, e); + } + builder.add(new nodomain.freeyourgadget.gadgetbridge.service.btbr.actions.SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZED, getContext())); + return builder; + } + + @Override + public void dispose() { + if (headphoneHelper != null) + headphoneHelper.dispose(); + super.dispose(); + } + + @Override + public void onSocketRead(byte[] data) { + super.getSupportProvider().onSocketRead(data); + } + + @Override + public void onSetCallState(CallSpec callSpec) { + headphoneHelper.onSetCallState(callSpec); + } + + @Override + public void onNotification(NotificationSpec notificationSpec) { + headphoneHelper.onNotification(notificationSpec); + } + + @Override + public void onFetchRecordedData(int dataTypes) { + // Do nothing. + } + + @Override + public void onSendConfiguration(String config) { + if (headphoneHelper.onSendConfiguration(config)) + return; + + try { + switch (config) { + case DeviceSettingsPreferenceConst.PREF_HUAWEI_FREEBUDS_INEAR: + new SetPauseWhenRemovedFromEarRequest(getSupportProvider()).doPerform(); + break; + case DeviceSettingsPreferenceConst.PREF_HUAWEI_FREEBUDS_AUDIOMODE: + new SetAudioModeRequest(getSupportProvider()).doPerform(); + break; + case DeviceSettingsPreferenceConst.PREF_BATTERY_POLLING_ENABLE: + if (!GBApplication.getDevicePrefs(gbDevice).getBatteryPollingEnabled()) { + getSupportProvider().stopBatteryRunnerDelayed(); + break; + } + // Fall through if enabled + case DeviceSettingsPreferenceConst.PREF_BATTERY_POLLING_INTERVAL: + if (!getSupportProvider().startBatteryRunnerDelayed()) { + GB.toast(getContext(), R.string.battery_polling_failed_start, Toast.LENGTH_SHORT, GB.ERROR); + LOG.error("Failed to start the battery polling"); + } + break; + + } + } catch (IOException e) { + GB.toast(getContext(), "Configuration of Huawei device failed", Toast.LENGTH_SHORT, GB.ERROR, e); + LOG.error("Configuration of Huawei device failed", e); + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiSupportProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiSupportProvider.java index e41cc59b6..015d9a837 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiSupportProvider.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiSupportProvider.java @@ -376,11 +376,15 @@ public class HuaweiSupportProvider { this.gpsParametersResponse = response; } - protected nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder initializeDevice(nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder builder) { - this.gbDevice = leSupport.getDevice(); - this.context = leSupport.getContext(); + public void setup(GBDevice device, Context context) { + this.gbDevice = device; + this.context = context; this.huaweiType = getCoordinator().getHuaweiType(); this.paramsProvider.setTransactionsCrypted(this.getHuaweiCoordinator().isTransactionCrypted()); + } + + protected nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder initializeDevice(nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder builder) { + setup(leSupport.getDevice(), leSupport.getContext()); builder.setCallback(leSupport); final BluetoothGattCharacteristic characteristicRead = leSupport.getCharacteristic(HuaweiConstants.UUID_CHARACTERISTIC_HUAWEI_READ); if (characteristicRead == null) { @@ -397,10 +401,7 @@ public class HuaweiSupportProvider { } protected nodomain.freeyourgadget.gadgetbridge.service.btbr.TransactionBuilder initializeDevice(nodomain.freeyourgadget.gadgetbridge.service.btbr.TransactionBuilder builder) { - this.gbDevice = brSupport.getDevice(); - this.context = brSupport.getContext(); - this.huaweiType = getCoordinator().getHuaweiType(); - this.paramsProvider.setTransactionsCrypted(this.getHuaweiCoordinator().isTransactionCrypted()); + setup(brSupport.getDevice(), brSupport.getContext()); builder.setCallback(brSupport); builder.add(new nodomain.freeyourgadget.gadgetbridge.service.btbr.actions.SetDeviceStateAction(getDevice(), GBDevice.State.AUTHENTICATING, getContext())); final GetLinkParamsRequest linkParamsReq = new GetLinkParamsRequest(this, builder); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetBatteryLevelRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetBatteryLevelRequest.java index 8136803e8..5b02312ab 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetBatteryLevelRequest.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetBatteryLevelRequest.java @@ -50,6 +50,12 @@ public class GetBatteryLevelRequest extends Request { } } + public static BatteryState byteToBatteryState(byte state) { + if (state == 1) + return BatteryState.BATTERY_CHARGING; + return BatteryState.BATTERY_NORMAL; + } + @Override protected void processResponse() throws ResponseParseException { LOG.debug("handle Battery Level"); @@ -57,13 +63,31 @@ public class GetBatteryLevelRequest extends Request { if (!(receivedPacket instanceof DeviceConfig.BatteryLevel.Response)) throw new ResponseTypeMismatchException(receivedPacket, DeviceConfig.BatteryLevel.Response.class); - byte batteryLevel = ((DeviceConfig.BatteryLevel.Response) receivedPacket).level; - getDevice().setBatteryLevel(batteryLevel); + DeviceConfig.BatteryLevel.Response response = (DeviceConfig.BatteryLevel.Response) receivedPacket; - GBDeviceEventBatteryInfo batteryInfo = new GBDeviceEventBatteryInfo(); - batteryInfo.state = BatteryState.BATTERY_NORMAL; - batteryInfo.level = (int)batteryLevel & 0xff; - this.supportProvider.evaluateGBDeviceEvent(batteryInfo); + if (response.multi_level == null) { + byte batteryLevel = response.level; + getDevice().setBatteryLevel(batteryLevel); + + GBDeviceEventBatteryInfo batteryInfo = new GBDeviceEventBatteryInfo(); + batteryInfo.state = BatteryState.BATTERY_NORMAL; + batteryInfo.level = (int) batteryLevel & 0xff; + this.supportProvider.evaluateGBDeviceEvent(batteryInfo); + } else { + // Handle multiple batteries + for (int i = 0; i < response.multi_level.length; i++) { + int level = (int) response.multi_level[i] & 0xff; + getDevice().setBatteryLevel(level, i); + + GBDeviceEventBatteryInfo batteryInfo = new GBDeviceEventBatteryInfo(); + batteryInfo.batteryIndex = i; + batteryInfo.state = response.status != null && response.status.length > i ? + byteToBatteryState(response.status[i]) : + BatteryState.BATTERY_NORMAL; + batteryInfo.level = level; + this.supportProvider.evaluateGBDeviceEvent(batteryInfo); + } + } if (GBApplication.getDevicePrefs(getDevice()).getBatteryPollingEnabled()) { if (!this.supportProvider.startBatteryRunnerDelayed()) { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SetAudioModeRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SetAudioModeRequest.java new file mode 100644 index 000000000..4f2102522 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SetAudioModeRequest.java @@ -0,0 +1,41 @@ +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 SetAudioModeRequest extends Request { + + public SetAudioModeRequest(HuaweiSupportProvider supportProvider) { + super(supportProvider); + this.serviceId = Earphones.id; + this.commandId = Earphones.SetAudioModeRequest.id; + this.addToResponse = false; // Response with different command ID + } + + @Override + protected List createRequest() throws RequestCreationException { + try { + String audioMode = GBApplication + .getDeviceSpecificSharedPrefs(this.getDevice().getAddress()) + .getString(DeviceSettingsPreferenceConst.PREF_HUAWEI_FREEBUDS_AUDIOMODE, "off"); + byte mode = 0; // Off by default + switch (audioMode) { + case "anc": + mode = 1; + break; + case "transparency": + mode = 2; + break; + default: + } + return new Earphones.SetAudioModeRequest(this.paramsProvider, mode).serialize(); + } catch (HuaweiPacket.CryptoException e) { + throw new RequestCreationException(e); + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SetPauseWhenRemovedFromEarRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SetPauseWhenRemovedFromEarRequest.java new file mode 100644 index 000000000..5b6b06576 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SetPauseWhenRemovedFromEarRequest.java @@ -0,0 +1,31 @@ +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 SetPauseWhenRemovedFromEarRequest extends Request { + + public SetPauseWhenRemovedFromEarRequest(HuaweiSupportProvider supportProvider) { + super(supportProvider); + this.serviceId = Earphones.id; + this.commandId = Earphones.SetAudioModeRequest.id; + this.addToResponse = false; // Response with different command ID + } + + @Override + protected List createRequest() throws RequestCreationException { + try { + boolean newState = GBApplication + .getDeviceSpecificSharedPrefs(this.getDevice().getAddress()) + .getBoolean(DeviceSettingsPreferenceConst.PREF_HUAWEI_FREEBUDS_INEAR, false); + return new Earphones.SetPauseWhenRemovedFromEar(this.paramsProvider, newState).serialize(); + } catch (HuaweiPacket.CryptoException e) { + throw new RequestCreationException(e); + } + } +} diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index ea41681c4..c1ec9ee03 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -4473,4 +4473,16 @@ calories_active calories_segmented + + + @string/prefs_active_noise_cancelling + @string/prefs_active_noise_cancelling_transparency + @string/off + + + + anc + transparency + off + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ebba46bfa..41549e197 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1881,6 +1881,7 @@ Huawei Watch Ultimate Huawei Watch 3 (Pro) Huawei Watch 4 (Pro) + Huawei FreeBuds 5i Femometer Vinca II Xiaomi Watch Lite Redmi Watch 3 Active diff --git a/app/src/main/res/xml/devicesettings_huawei_freebuds.xml b/app/src/main/res/xml/devicesettings_huawei_freebuds.xml new file mode 100644 index 000000000..67c9b32ab --- /dev/null +++ b/app/src/main/res/xml/devicesettings_huawei_freebuds.xml @@ -0,0 +1,18 @@ + + + + + \ No newline at end of file