diff --git a/CHANGELOG.md b/CHANGELOG.md index 248fa36bc..262a01f70 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 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 11f546269..5e92f97d2 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 @@ -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"; 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 26758c370..4afac80bc 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 @@ -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); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/Huami2021Coordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/Huami2021Coordinator.java index 729ccd7bd..b1bdef2aa 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/Huami2021Coordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/Huami2021Coordinator.java @@ -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); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/Huami2021SettingsCustomizer.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/Huami2021SettingsCustomizer.java index b127d648d..051eb07d2 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/Huami2021SettingsCustomizer.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/Huami2021SettingsCustomizer.java @@ -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 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 CREATOR = new Creator() { @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 ""; + } + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitband7/AmazfitBand7Coordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitband7/AmazfitBand7Coordinator.java index be96aa481..a883072ca 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitband7/AmazfitBand7Coordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitband7/AmazfitBand7Coordinator.java @@ -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; + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitgtr3/AmazfitGTR3Coordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitgtr3/AmazfitGTR3Coordinator.java index df2b97f3d..becc670e6 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitgtr3/AmazfitGTR3Coordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitgtr3/AmazfitGTR3Coordinator.java @@ -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; + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitgtr4/AmazfitGTR4Coordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitgtr4/AmazfitGTR4Coordinator.java index 0b1f0c699..7f2bcbe11 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitgtr4/AmazfitGTR4Coordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitgtr4/AmazfitGTR4Coordinator.java @@ -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; + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitgts3/AmazfitGTS3Coordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitgts3/AmazfitGTS3Coordinator.java index d6a34951d..f3fadf3e1 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitgts3/AmazfitGTS3Coordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitgts3/AmazfitGTS3Coordinator.java @@ -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; + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitgts4/AmazfitGTS4Coordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitgts4/AmazfitGTS4Coordinator.java index 6e5ed0f02..333a508a9 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitgts4/AmazfitGTS4Coordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitgts4/AmazfitGTS4Coordinator.java @@ -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; + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitgts4mini/AmazfitGTS4MiniCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitgts4mini/AmazfitGTS4MiniCoordinator.java index 0f822c34d..bf11cd31e 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitgts4mini/AmazfitGTS4MiniCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitgts4mini/AmazfitGTS4MiniCoordinator.java @@ -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; + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfittrex2/AmazfitTRex2Coordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfittrex2/AmazfitTRex2Coordinator.java index 243131595..a9316117a 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfittrex2/AmazfitTRex2Coordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfittrex2/AmazfitTRex2Coordinator.java @@ -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; + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/miband7/MiBand7Coordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/miband7/MiBand7Coordinator.java index 5726c1d41..d7ecb8890 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/miband7/MiBand7Coordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/miband7/MiBand7Coordinator.java @@ -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; + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/Huami2021Support.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/Huami2021Support.java index c3a767972..95678872b 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/Huami2021Support.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/Huami2021Support.java @@ -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 mServiceMap = new HashMap() {{ 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); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/zeppos/services/ZeppOsConfigService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/zeppos/services/ZeppOsConfigService.java index a7f3e625e..32c4f47b2 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/zeppos/services/ZeppOsConfigService.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/zeppos/services/ZeppOsConfigService.java @@ -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 convertShortToPrefs(final ConfigArg configArg, final ConfigShort value) { if (configArg.getPrefKey() != null) { - // The arg maps to a number pref directly - final Map prefs = singletonMap(configArg.getPrefKey(), value.getValue()); + final Map 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()); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/zeppos/services/ZeppOsPhoneService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/zeppos/services/ZeppOsPhoneService.java new file mode 100644 index 000000000..e1632536a --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/zeppos/services/ZeppOsPhoneService.java @@ -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 . */ +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; + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2b978ebd0..e42147057 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -220,6 +220,20 @@ Phone Calls Call notification delay Delay before sending incoming call notifications to the device, in seconds. + Perform and receive calls directly on the watch + Bluetooth calls + Bluetooth calls pairing + Bluetooth calls settings + Show contact information + Display phone number or name for incoming calls + Pair for bluetooth calls + Click here to start the pairing process + How to receive bluetooth calls + In order to receive bluetooth calls, you need to pair your phone with a second instance of the watch. + 1. Tap the button below to start the pairing process. + 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\". + 3. Enable the "Bluetooth calls" setting below. + WARNING: If you enable bluetooth calls without pairing with the second instance, call notifications might not work as expected. Enable VoIP app calls Ping tone SMS diff --git a/app/src/main/res/xml/devicesettings_phone_calls_watch_pair.xml b/app/src/main/res/xml/devicesettings_phone_calls_watch_pair.xml new file mode 100644 index 000000000..1554d2e83 --- /dev/null +++ b/app/src/main/res/xml/devicesettings_phone_calls_watch_pair.xml @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +