Amazfit GTR 4 / GTS 4: Perform and receive phone calls on watch

This commit is contained in:
José Rebelo 2023-04-02 18:50:58 +01:00
parent f1dd4019bf
commit 340db0ca15
18 changed files with 463 additions and 9 deletions

View File

@ -4,6 +4,7 @@
* Amazfit Bip U: Remove alarm snooze option
* Amazfit GTR 4 / GTS 4: Add watch Wi-Fi Hotspot and FTP Server
* Amazfit GTR 4 / GTS 4: Perform and receive phone calls on watch
* Amazfit GTR 4: Whitelist fw 3.18.1.1 diff from 3.17.0.2
* Amazfit GTS 2 Mini: Add missing alexa menu item
* Bangle.js: Fix updating timezone in settings.json if the timezone is zero

View File

@ -235,6 +235,11 @@ public class DeviceSettingsPreferenceConst {
public static final String PREF_BT_CONNECTED_ADVERTISEMENT = "bt_connected_advertisement";
public static final String PREF_TRANSLITERATION_LANGUAGES = "pref_transliteration_languages";
public static final String PREF_BLUETOOTH_CALLS_PAIR = "bluetooth_calls_pair";
public static final String PREF_BLUETOOTH_CALLS_ENABLED = "bluetooth_calls_enabled";
public static final String PREF_DISPLAY_CALLER = "display_caller";
public static final String PREF_NOTIFICATION_DELAY_CALLS = "notification_delay_calls";
public static final String WIFI_HOTSPOT_SSID = "wifi_hotspot_ssid";
public static final String WIFI_HOTSPOT_PASSWORD = "wifi_hotspot_password";
public static final String WIFI_HOTSPOT_START = "wifi_hotspot_start";

View File

@ -438,6 +438,10 @@ public class DeviceSpecificSettingsFragment extends PreferenceFragmentCompat imp
addPreferenceHandlerFor(PREF_SOUNDS);
addPreferenceHandlerFor(PREF_CAMERA_REMOTE);
addPreferenceHandlerFor(PREF_BLUETOOTH_CALLS_ENABLED);
addPreferenceHandlerFor(PREF_DISPLAY_CALLER);
addPreferenceHandlerFor(PREF_NOTIFICATION_DELAY_CALLS);
addPreferenceHandlerFor(PREF_SLEEP_MODE_SLEEP_SCREEN);
addPreferenceHandlerFor(PREF_SLEEP_MODE_SMART_ENABLE);

View File

@ -45,6 +45,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsConfigService;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiLanguageType;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiVibrationPatternNotificationType;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsPhoneService;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
public abstract class Huami2021Coordinator extends HuamiCoordinator {
@ -231,6 +232,9 @@ public abstract class Huami2021Coordinator extends HuamiCoordinator {
// Notifications
//
settings.add(R.xml.devicesettings_header_notifications);
if (supportsBluetoothPhoneCalls(device)) {
settings.add(R.xml.devicesettings_phone_calls_watch_pair);
}
settings.add(R.xml.devicesettings_sound_and_vibration);
settings.add(R.xml.devicesettings_vibrationpatterns);
settings.add(R.xml.devicesettings_donotdisturb_withauto_and_always);
@ -362,6 +366,10 @@ public abstract class Huami2021Coordinator extends HuamiCoordinator {
return supportsConfig(device, ZeppOsConfigService.ConfigArg.SCREEN_AUTO_BRIGHTNESS);
}
public boolean supportsBluetoothPhoneCalls(final GBDevice device) {
return ZeppOsPhoneService.isSupported(getPrefs(device));
}
private boolean supportsConfig(final GBDevice device, final ZeppOsConfigService.ConfigArg config) {
return ZeppOsConfigService.deviceHasConfig(getPrefs(device), config);
}

View File

@ -17,7 +17,11 @@
package nodomain.freeyourgadget.gadgetbridge.devices.huami;
import android.os.Parcel;
import android.text.InputFilter;
import android.text.InputType;
import android.text.Spanned;
import androidx.preference.EditTextPreference;
import androidx.preference.ListPreference;
import androidx.preference.MultiSelectListPreference;
import androidx.preference.Preference;
@ -65,16 +69,25 @@ public class Huami2021SettingsCustomizer extends HuamiSettingsCustomizer {
if (config.getPrefKey() == null) {
continue;
}
switch (config.getConfigType(null)) {
final ZeppOsConfigService.ConfigType configType = config.getConfigType(null);
if (configType == null) {
// Should never happen
LOG.error("configType is null - this should never happen");
return;
}
switch (configType) {
case BYTE:
case BYTE_LIST:
case STRING_LIST:
// For list preferences, remove the unsupported items
removeUnsupportedElementsFromListPreference(config.getPrefKey(), handler, prefs);
break;
case BOOL:
case SHORT:
case INT:
hidePrefIfNoConfigSupported(handler, prefs, config.getPrefKey(), config.name());
enforceMinMax(handler, prefs, config);
break;
case BOOL:
case DATETIME_HH_MM:
case TIMESTAMP_MILLIS:
default:
@ -221,7 +234,7 @@ public class Huami2021SettingsCustomizer extends HuamiSettingsCustomizer {
));
setupGpsPreference(handler, prefs);
setupWifiFtpPreferences(handler);
setupButtonClickPreferences(handler);
}
@Override
@ -231,6 +244,7 @@ public class Huami2021SettingsCustomizer extends HuamiSettingsCustomizer {
preferenceKeysWithSummary.add(DeviceSettingsPreferenceConst.WIFI_HOTSPOT_SSID);
preferenceKeysWithSummary.add(DeviceSettingsPreferenceConst.WIFI_HOTSPOT_PASSWORD);
preferenceKeysWithSummary.add(DeviceSettingsPreferenceConst.WIFI_HOTSPOT_STATUS);
preferenceKeysWithSummary.add(DeviceSettingsPreferenceConst.FTP_SERVER_ROOT_DIR);
preferenceKeysWithSummary.add(DeviceSettingsPreferenceConst.FTP_SERVER_ADDRESS);
preferenceKeysWithSummary.add(DeviceSettingsPreferenceConst.FTP_SERVER_USERNAME);
@ -341,9 +355,10 @@ public class Huami2021SettingsCustomizer extends HuamiSettingsCustomizer {
}
}
private void setupWifiFtpPreferences(final DeviceSpecificSettingsHandler handler) {
private void setupButtonClickPreferences(final DeviceSpecificSettingsHandler handler) {
// Notify preference changed on button click, so we can react to them
final List<Preference> wifiFtpButtons = Arrays.asList(
handler.findPreference(DeviceSettingsPreferenceConst.PREF_BLUETOOTH_CALLS_PAIR),
handler.findPreference(DeviceSettingsPreferenceConst.WIFI_HOTSPOT_START),
handler.findPreference(DeviceSettingsPreferenceConst.WIFI_HOTSPOT_STOP),
handler.findPreference(DeviceSettingsPreferenceConst.FTP_SERVER_START),
@ -475,6 +490,42 @@ public class Huami2021SettingsCustomizer extends HuamiSettingsCustomizer {
pref.setVisible(false);
}
private void enforceMinMax(final DeviceSpecificSettingsHandler handler, final Prefs prefs, final ZeppOsConfigService.ConfigArg config) {
final String prefKey = config.getPrefKey();
final Preference pref = handler.findPreference(prefKey);
if (pref == null) {
return;
}
if (!(pref instanceof EditTextPreference)) {
return;
}
final int minValue = prefs.getInt(ZeppOsConfigService.getPrefMinKey(prefKey), Integer.MAX_VALUE);
if (minValue == Integer.MAX_VALUE) {
LOG.warn("Missing min value for {}", prefKey);
return;
}
final int maxValue = prefs.getInt(ZeppOsConfigService.getPrefMaxKey(prefKey), Integer.MIN_VALUE);
if (maxValue == Integer.MAX_VALUE) {
LOG.warn("Missing max value for {}", prefKey);
return;
}
if (minValue >= maxValue) {
LOG.warn("Invalid min/max values: {}/{}", minValue, maxValue);
return;
}
final EditTextPreference textPref = (EditTextPreference) pref;
textPref.setOnBindEditTextListener(editText -> {
editText.setInputType(InputType.TYPE_CLASS_NUMBER);
editText.setFilters(new InputFilter[]{new MinMaxInputFilter(minValue, maxValue)});
editText.setSelection(editText.getText().length());
});
}
public static final Creator<Huami2021SettingsCustomizer> CREATOR = new Creator<Huami2021SettingsCustomizer>() {
@Override
public Huami2021SettingsCustomizer createFromParcel(final Parcel in) {
@ -489,4 +540,26 @@ public class Huami2021SettingsCustomizer extends HuamiSettingsCustomizer {
return new Huami2021SettingsCustomizer[size];
}
};
public static final class MinMaxInputFilter implements InputFilter {
private final int min;
private final int max;
public MinMaxInputFilter(final int min, final int max) {
this.min = min;
this.max = max;
}
@Override
public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart, int dend) {
try {
final int input = Integer.parseInt(dest.toString() + source.toString());
if (input >= min && input <= max) {
return null;
}
} catch (final NumberFormatException ignored) {
}
return "";
}
}
}

View File

@ -27,6 +27,7 @@ import org.slf4j.LoggerFactory;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.Huami2021Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.AbstractHuami2021FWInstallHandler;
@ -60,4 +61,9 @@ public class AmazfitBand7Coordinator extends Huami2021Coordinator {
final AmazfitBand7FWInstallHandler handler = new AmazfitBand7FWInstallHandler(uri, context);
return handler.isValid() ? handler : null;
}
@Override
public boolean supportsBluetoothPhoneCalls(final GBDevice device) {
return false;
}
}

View File

@ -27,6 +27,7 @@ import org.slf4j.LoggerFactory;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.Huami2021Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.AbstractHuami2021FWInstallHandler;
@ -60,4 +61,9 @@ public class AmazfitGTR3Coordinator extends Huami2021Coordinator {
final AmazfitGTR3FWInstallHandler handler = new AmazfitGTR3FWInstallHandler(uri, context);
return handler.isValid() ? handler : null;
}
@Override
public boolean supportsBluetoothPhoneCalls(final GBDevice device) {
return false;
}
}

View File

@ -91,4 +91,9 @@ public class AmazfitGTR4Coordinator extends Huami2021Coordinator {
public boolean supportsFtpServer(final GBDevice device) {
return true;
}
@Override
public boolean supportsBluetoothPhoneCalls(final GBDevice device) {
return true;
}
}

View File

@ -27,6 +27,7 @@ import org.slf4j.LoggerFactory;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.Huami2021Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.AbstractHuami2021FWInstallHandler;
@ -60,4 +61,9 @@ public class AmazfitGTS3Coordinator extends Huami2021Coordinator {
final AmazfitGTS3FWInstallHandler handler = new AmazfitGTS3FWInstallHandler(uri, context);
return handler.isValid() ? handler : null;
}
@Override
public boolean supportsBluetoothPhoneCalls(final GBDevice device) {
return false;
}
}

View File

@ -91,4 +91,9 @@ public class AmazfitGTS4Coordinator extends Huami2021Coordinator {
public boolean supportsFtpServer(final GBDevice device) {
return true;
}
@Override
public boolean supportsBluetoothPhoneCalls(final GBDevice device) {
return true;
}
}

View File

@ -27,6 +27,7 @@ import org.slf4j.LoggerFactory;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.Huami2021Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.AbstractHuami2021FWInstallHandler;
@ -60,4 +61,9 @@ public class AmazfitGTS4MiniCoordinator extends Huami2021Coordinator {
final AmazfitGTS4MiniFWInstallHandler handler = new AmazfitGTS4MiniFWInstallHandler(uri, context);
return handler.isValid() ? handler : null;
}
@Override
public boolean supportsBluetoothPhoneCalls(final GBDevice device) {
return false;
}
}

View File

@ -27,6 +27,7 @@ import org.slf4j.LoggerFactory;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.Huami2021Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.AbstractHuami2021FWInstallHandler;
@ -70,4 +71,9 @@ public class AmazfitTRex2Coordinator extends Huami2021Coordinator {
public boolean supportsToDoList() {
return true;
}
@Override
public boolean supportsBluetoothPhoneCalls(final GBDevice device) {
return false;
}
}

View File

@ -27,6 +27,7 @@ import org.slf4j.LoggerFactory;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.Huami2021Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.AbstractHuami2021FWInstallHandler;
@ -60,4 +61,9 @@ public class MiBand7Coordinator extends Huami2021Coordinator {
final MiBand7FWInstallHandler handler = new MiBand7FWInstallHandler(uri, context);
return handler.isValid() ? handler : null;
}
@Override
public boolean supportsBluetoothPhoneCalls(final GBDevice device) {
return false;
}
}

View File

@ -124,6 +124,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.service
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsFileUploadService;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsFtpServerService;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsMorningUpdatesService;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsPhoneService;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsWifiService;
import nodomain.freeyourgadget.gadgetbridge.util.AlarmUtils;
import nodomain.freeyourgadget.gadgetbridge.util.BitmapUtil;
@ -155,6 +156,7 @@ public abstract class Huami2021Support extends HuamiSupport {
private final ZeppOsFtpServerService ftpServerService = new ZeppOsFtpServerService(this);
private final ZeppOsContactsService contactsService = new ZeppOsContactsService(this);
private final ZeppOsMorningUpdatesService morningUpdatesService = new ZeppOsMorningUpdatesService(this);
private final ZeppOsPhoneService phoneService = new ZeppOsPhoneService(this);
private final Map<Short, AbstractZeppOsService> mServiceMap = new HashMap<Short, AbstractZeppOsService>() {{
put(fileUploadService.getEndpoint(), fileUploadService);
@ -164,6 +166,7 @@ public abstract class Huami2021Support extends HuamiSupport {
put(ftpServerService.getEndpoint(), ftpServerService);
put(contactsService.getEndpoint(), contactsService);
put(morningUpdatesService.getEndpoint(), morningUpdatesService);
put(phoneService.getEndpoint(), phoneService);
}};
public Huami2021Support() {
@ -201,6 +204,14 @@ public abstract class Huami2021Support extends HuamiSupport {
// Handle button presses - these are not preferences
// See Huami2021SettingsCustomizer
switch (config) {
case DeviceSettingsPreferenceConst.PREF_BLUETOOTH_CALLS_PAIR:
if (!phoneService.isSupported()) {
GB.toast(getContext(), "Phone service is not supported.", Toast.LENGTH_LONG, GB.ERROR);
return;
}
phoneService.startPairing();
return;
case DeviceSettingsPreferenceConst.WIFI_HOTSPOT_START:
final String ssid = getDevicePrefs().getString(DeviceSettingsPreferenceConst.WIFI_HOTSPOT_SSID, "");
if (StringUtils.isNullOrEmpty(ssid)) {
@ -250,6 +261,16 @@ public abstract class Huami2021Support extends HuamiSupport {
return;
}
// phoneService preferences, they do not use the configService
switch (config) {
case DeviceSettingsPreferenceConst.PREF_BLUETOOTH_CALLS_ENABLED:
final boolean bluetoothCallsEnabled = prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_BLUETOOTH_CALLS_ENABLED, false);
LOG.info("Setting bluetooth calls enabled = {}", bluetoothCallsEnabled);
phoneService.setEnabled(bluetoothCallsEnabled);
return;
}
// Defer everything else to the configService
try {
if (configService.setConfig(prefs, config, configSetter)) {
// If the ConfigSetter was able to set the config, just write it and return
@ -260,11 +281,11 @@ public abstract class Huami2021Support extends HuamiSupport {
return;
}
super.onSendConfiguration(config);
} catch (final Exception e) {
GB.toast("Error setting configuration", Toast.LENGTH_LONG, GB.ERROR, e);
}
super.onSendConfiguration(config);
}
@Override
@ -1460,6 +1481,11 @@ public abstract class Huami2021Support extends HuamiSupport {
@Override
public void phase3Initialize(final TransactionBuilder builder) {
// Make sure that performInitialized is not called accidentally in here
// (eg. by creating a new TransactionBuilder).
// In those cases, the device will be initialized twice, which will change the shared
// session key during these phase3 requests and decrypting messages will fail
final Huami2021Coordinator coordinator = getCoordinator();
LOG.info("2021 phase3Initialize...");
@ -1483,6 +1509,10 @@ public abstract class Huami2021Support extends HuamiSupport {
}
requestAlarms(builder);
//requestReminders(builder);
if (coordinator.supportsBluetoothPhoneCalls(gbDevice)) {
phoneService.requestCapabilities(builder);
phoneService.requestEnabled(builder);
}
//contactsService.requestCapabilities(builder);
morningUpdatesService.getEnabled(builder);
morningUpdatesService.getCategories(builder);

View File

@ -372,11 +372,12 @@ public class ZeppOsConfigService extends AbstractZeppOsService {
DND_MODE(ConfigGroup.SYSTEM, ConfigType.BYTE, 0x0a, PREF_DO_NOT_DISTURB),
DND_SCHEDULED_START(ConfigGroup.SYSTEM, ConfigType.DATETIME_HH_MM, 0x0b, PREF_DO_NOT_DISTURB_START),
DND_SCHEDULED_END(ConfigGroup.SYSTEM, ConfigType.DATETIME_HH_MM, 0x0c, PREF_DO_NOT_DISTURB_END),
CALL_DELAY(ConfigGroup.SYSTEM, ConfigType.SHORT, 0x11, PREF_NOTIFICATION_DELAY_CALLS),
TEMPERATURE_UNIT(ConfigGroup.SYSTEM, ConfigType.BYTE, 0x12, SettingsActivity.PREF_MEASUREMENT_SYSTEM),
TIME_FORMAT_FOLLOWS_PHONE(ConfigGroup.SYSTEM, ConfigType.BOOL, 0x13, null /* special case, handled below */),
UPPER_BUTTON_LONG_PRESS(ConfigGroup.SYSTEM, ConfigType.STRING_LIST, 0x15, PREF_UPPER_BUTTON_LONG_PRESS),
LOWER_BUTTON_PRESS(ConfigGroup.SYSTEM, ConfigType.STRING_LIST, 0x16, PREF_LOWER_BUTTON_SHORT_PRESS),
DISPLAY_CALLER(ConfigGroup.SYSTEM, ConfigType.BOOL, 0x18, null), // TODO Handle
DISPLAY_CALLER(ConfigGroup.SYSTEM, ConfigType.BOOL, 0x18, PREF_DISPLAY_CALLER),
NIGHT_MODE_MODE(ConfigGroup.SYSTEM, ConfigType.BYTE, 0x1b, PREF_NIGHT_MODE),
NIGHT_MODE_SCHEDULED_START(ConfigGroup.SYSTEM, ConfigType.DATETIME_HH_MM, 0x1c, PREF_NIGHT_MODE_START),
NIGHT_MODE_SCHEDULED_END(ConfigGroup.SYSTEM, ConfigType.DATETIME_HH_MM, 0x1d, PREF_NIGHT_MODE_END),
@ -1028,8 +1029,14 @@ public class ZeppOsConfigService extends AbstractZeppOsService {
private Map<String, Object> convertShortToPrefs(final ConfigArg configArg, final ConfigShort value) {
if (configArg.getPrefKey() != null) {
// The arg maps to a number pref directly
final Map<String, Object> prefs = singletonMap(configArg.getPrefKey(), value.getValue());
final Map<String, Object> prefs;
if (configArg == ConfigArg.CALL_DELAY) {
// Persist as string, otherwise the EditText crashes
prefs = singletonMap(configArg.getPrefKey(), String.valueOf(value.getValue()));
} else {
// The arg maps to a number pref directly
prefs = singletonMap(configArg.getPrefKey(), value.getValue());
}
if (value.isMinMaxKnown()) {
prefs.put(getPrefMinKey(configArg.getPrefKey()), value.getMin());

View File

@ -0,0 +1,191 @@
/* Copyright (C) 2023 José Rebelo
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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services;
import android.bluetooth.BluetoothAdapter;
import androidx.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdatePreferences;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.Huami2021Support;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.AbstractZeppOsService;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
public class ZeppOsPhoneService extends AbstractZeppOsService {
private static final Logger LOG = LoggerFactory.getLogger(ZeppOsPhoneService.class);
private static final short ENDPOINT = 0x000b;
public static final byte CMD_CAPABILITIES_REQUEST = 0x01;
public static final byte CMD_CAPABILITIES_RESPONSE = 0x02;
public static final byte CMD_PAIRED_GET = 0x03;
public static final byte CMD_PAIRED_RET = 0x04;
public static final byte CMD_START_PAIR = 0x05;
public static final byte CMD_ENABLED_REQUEST = 0x06;
public static final byte CMD_ENABLED_RESPONSE = 0x07;
public static final byte CMD_ENABLED_SET = 0x08;
public static final byte CMD_ENABLED_SET_ACK = 0x09;
public static final String PREF_VERSION = "zepp_os_phone_service_version";
private int version = 0;
public ZeppOsPhoneService(final Huami2021Support support) {
super(support);
}
@Override
public short getEndpoint() {
return ENDPOINT;
}
@Override
public boolean isEncrypted() {
return true;
}
@Override
public void handlePayload(final byte[] payload) {
switch (payload[0]) {
case CMD_CAPABILITIES_RESPONSE:
version = payload[1];
getSupport().evaluateGBDeviceEvent(new GBDeviceEventUpdatePreferences(PREF_VERSION, version));
if (version != 1) {
LOG.warn("Unsupported phone service version {}", version);
return;
}
LOG.info("Phone version={}", version);
break;
case CMD_PAIRED_RET:
final byte pairedStatus = payload[1];
// 0 = unpaired
// 1 = paired
// 4 = ?
LOG.info("Got phone pair status = {}", pairedStatus);
break;
case CMD_ENABLED_RESPONSE:
if (payload.length != 4) {
LOG.error("Unexpected phone enabled payload size {}", payload.length);
return;
}
if (payload[1] != 0x01 || payload[2] != 0x01) {
LOG.error("Unexpected phone enabled bytes");
return;
}
final Boolean phoneEnabled = booleanFromByte(payload[3]);
if (phoneEnabled == null) {
LOG.error("Unexpected phone enabled byte");
return;
}
LOG.info("Got phone enabled = {}", phoneEnabled);
getSupport().evaluateGBDeviceEvent(new GBDeviceEventUpdatePreferences(DeviceSettingsPreferenceConst.PREF_BLUETOOTH_CALLS_ENABLED, phoneEnabled));
break;
case CMD_ENABLED_SET_ACK:
LOG.info("Got phone enabled set ack, status = {}", payload[1]);
break;
default:
LOG.warn("Unexpected phone byte {}", String.format("0x%02x", payload[0]));
}
}
public boolean isSupported() {
return version == 1;
}
public void requestCapabilities(final TransactionBuilder builder) {
write(builder, CMD_CAPABILITIES_REQUEST);
}
public void getPairedStatus() {
final String bluetoothName = getBluetoothName();
if (bluetoothName == null) {
LOG.error("bluetoothName is null");
return;
}
final byte[] nameBytes = bluetoothName.getBytes(StandardCharsets.UTF_8);
final ByteBuffer buf = ByteBuffer.allocate(2 + nameBytes.length)
.order(ByteOrder.LITTLE_ENDIAN);
buf.put(CMD_PAIRED_GET);
buf.put(nameBytes);
buf.put((byte) 0);
write("get paired status", buf.array());
}
public void startPairing() {
final String bluetoothName = getBluetoothName();
if (bluetoothName == null) {
LOG.error("bluetoothName is null");
return;
}
final byte[] nameBytes = bluetoothName.getBytes(StandardCharsets.UTF_8);
final ByteBuffer buf = ByteBuffer.allocate(2 + nameBytes.length)
.order(ByteOrder.LITTLE_ENDIAN);
buf.put(CMD_START_PAIR);
buf.put(nameBytes);
buf.put((byte) 0);
write("start phone pairing", buf.array());
}
public void requestEnabled(final TransactionBuilder builder) {
write(builder, CMD_ENABLED_REQUEST);
}
public void setEnabled(final boolean enabled) {
final byte[] cmd = new byte[]{
CMD_ENABLED_SET,
0x01,
0x01,
(byte) (enabled ? 0x01 : 0x00)
};
write("set phone enabled", cmd);
}
@Nullable
public String getBluetoothName() {
final BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
if (bluetoothAdapter == null) {
LOG.error("bluetoothAdapter is null");
return null;
}
return bluetoothAdapter.getName();
}
public static boolean isSupported(final Prefs devicePrefs) {
return devicePrefs.getInt(PREF_VERSION, 0) == 1;
}
}

View File

@ -220,6 +220,20 @@
<string name="pref_title_notifications_call">Phone Calls</string>
<string name="pref_title_notification_delay_calls">Call notification delay</string>
<string name="pref_summary_notification_delay_calls">Delay before sending incoming call notifications to the device, in seconds.</string>
<string name="pref_summary_receive_calls_watch">Perform and receive calls directly on the watch</string>
<string name="bluetooth_calls">Bluetooth calls</string>
<string name="bluetooth_calls_pairing">Bluetooth calls pairing</string>
<string name="bluetooth_calls_settings">Bluetooth calls settings</string>
<string name="pref_display_caller_title">Show contact information</string>
<string name="pref_display_caller_summary">Display phone number or name for incoming calls</string>
<string name="pref_pair_bluetooth_calls_title">Pair for bluetooth calls</string>
<string name="pref_pair_bluetooth_calls_summary">Click here to start the pairing process</string>
<string name="pref_pair_bluetooth_calls_help_title">How to receive bluetooth calls</string>
<string name="pref_pair_bluetooth_calls_help_summary">In order to receive bluetooth calls, you need to pair your phone with a second instance of the watch.</string>
<string name="pref_pair_bluetooth_calls_help_1">1. Tap the button below to start the pairing process.</string>
<string name="pref_pair_bluetooth_calls_help_2">2. Go to your phone\'s bluetooth settings, and pair with the new device that will show up (similar name to your current watch, but with a suffix, eg. \"Amazfit GTR 4 - AFC8\".</string>
<string name="pref_pair_bluetooth_calls_help_3">3. Enable the "Bluetooth calls" setting below.</string>
<string name="pref_pair_bluetooth_calls_help_warning">WARNING: If you enable bluetooth calls without pairing with the second instance, call notifications might not work as expected.</string>
<string name="pref_title_support_voip_calls">Enable VoIP app calls</string>
<string name="pref_title_ping_tone">Ping tone</string>
<string name="pref_title_notifications_sms">SMS</string>

View File

@ -0,0 +1,75 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<PreferenceScreen
android:icon="@drawable/ic_phone"
android:key="pref_phone_calls_header"
android:persistent="false"
android:summary="@string/pref_summary_receive_calls_watch"
android:title="@string/pref_title_notifications_call">
<PreferenceCategory
android:key="pref_bluetooth_calls_pairing_header"
android:title="@string/bluetooth_calls_pairing">
<Preference
android:icon="@drawable/ic_info"
android:key="phone_calls_pair_info"
android:summary="@string/pref_pair_bluetooth_calls_help_summary" />
<Preference
android:key="phone_calls_pair_info_1"
android:summary="@string/pref_pair_bluetooth_calls_help_1" />
<Preference
android:key="phone_calls_pair_info_2"
android:summary="@string/pref_pair_bluetooth_calls_help_2" />
<Preference
android:key="phone_calls_pair_info_3"
android:summary="@string/pref_pair_bluetooth_calls_help_3" />
<Preference
android:icon="@drawable/ic_warning_gray"
android:key="phone_calls_pair_info_warning"
android:summary="@string/pref_pair_bluetooth_calls_help_warning" />
<Preference
android:icon="@drawable/ic_link"
android:key="bluetooth_calls_pair"
android:summary="@string/pref_pair_bluetooth_calls_summary"
android:title="@string/pref_pair_bluetooth_calls_title" />
</PreferenceCategory>
<PreferenceCategory
android:key="pref_bluetooth_calls_settings_header"
android:title="@string/bluetooth_calls_settings">
<SwitchPreference
android:defaultValue="false"
android:icon="@drawable/ic_voice"
android:key="bluetooth_calls_enabled"
android:summary="@string/pref_summary_receive_calls_watch"
android:title="@string/bluetooth_calls" />
<SwitchPreference
android:defaultValue="true"
android:icon="@drawable/ic_person"
android:key="display_caller"
android:summary="@string/pref_display_caller_summary"
android:title="@string/pref_display_caller_title" />
<EditTextPreference
android:defaultValue="0"
android:digits="0123456789"
android:icon="@drawable/ic_access_time"
android:inputType="number"
android:key="notification_delay_calls"
android:maxLength="2"
android:summary="@string/pref_summary_notification_delay_calls"
android:title="@string/pref_title_notification_delay_calls" />
</PreferenceCategory>
</PreferenceScreen>
</androidx.preference.PreferenceScreen>