1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-12-24 17:45:50 +01:00

Huawei: Initial Freebuds (5i) support

This commit is contained in:
Martin.JM 2024-11-23 23:47:19 +01:00 committed by José Rebelo
parent 55238f4ee9
commit c5714746fb
21 changed files with 784 additions and 120 deletions

View File

@ -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";

View File

@ -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);

View File

@ -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;
}
}

View File

@ -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:

View File

@ -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];
}

View File

@ -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<? extends DeviceSupport> getDeviceSupportClass() {
return HuaweiFreebudsSupport.class;
}
@Override
public DeviceType getDeviceType() {
return DeviceType.HUAWEI_FREEBUDS5I;
}
@Override
public int getDeviceNameResource() {
return R.string.devicetype_huawei_freebuds_5i;
}
}

View File

@ -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;

View File

@ -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 <https://www.gnu.org/licenses/>. */
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;
}
}
}
}

View File

@ -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),

View File

@ -16,142 +16,50 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. */
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());
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);
headphoneHelper.onNotification(notificationSpec);
}
@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.");
}
}
}

View File

@ -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 <https://www.gnu.org/licenses/>. */
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.");
}
}
}

View File

@ -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");
}
}
}
}
}

View File

@ -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

View File

@ -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);
}
}
}

View File

@ -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);

View File

@ -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;
DeviceConfig.BatteryLevel.Response response = (DeviceConfig.BatteryLevel.Response) receivedPacket;
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()) {

View File

@ -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<byte[]> 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);
}
}
}

View File

@ -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<byte[]> 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);
}
}
}

View File

@ -4473,4 +4473,16 @@
<item>calories_active</item>
<item>calories_segmented</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>
<item>@string/off</item>
</string-array>
<string-array name="huawei_freebuds_audio_mode_values">
<item>anc</item>
<item>transparency</item>
<item>off</item>
</string-array>
</resources>

View File

@ -1881,6 +1881,7 @@
<string name="devicetype_huawei_watchultimate">Huawei Watch Ultimate</string>
<string name="devicetype_huawei_watch3">Huawei Watch 3 (Pro)</string>
<string name="devicetype_huawei_watch4pro">Huawei Watch 4 (Pro)</string>
<string name="devicetype_huawei_freebuds_5i">Huawei FreeBuds 5i</string>
<string name="devicetype_femometer_vinca2">Femometer Vinca II</string>
<string name="devicetype_xiaomi_watch_lite">Xiaomi Watch Lite</string>
<string name="devicetype_redmiwatch3active">Redmi Watch 3 Active</string>

View File

@ -0,0 +1,18 @@
<?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">
<SwitchPreferenceCompat
android:defaultValue="true"
android:icon="@drawable/ic_extension"
android:key="pref_freebuds_inear_detection"
android:layout="@layout/preference_checkbox"
android:summary="@string/nothing_prefs_inear_summary"
android:title="@string/nothing_prefs_inear_title" />
<ListPreference
android:entries="@array/huawei_freebuds_audio_mode_names"
android:entryValues="@array/huawei_freebuds_audio_mode_values"
android:icon="@drawable/ic_extension"
android:key="pref_freebuds_audiomode"
android:summary="%s"
android:title="@string/nothing_prefs_audiomode_title" />
</androidx.preference.PreferenceScreen>