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 5e92f97d2..bf96f607f 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 @@ -249,6 +249,8 @@ public class DeviceSettingsPreferenceConst { public static final String MORNING_UPDATES_ENABLED = "morning_updates_enabled"; public static final String MORNING_UPDATES_CATEGORIES_SORTABLE = "morning_updates_categories"; + public static final String SHORTCUT_CARDS_SORTABLE = "shortcut_cards_sortable"; + public static final String FTP_SERVER_ROOT_DIR = "ftp_server_root_dir"; public static final String FTP_SERVER_ADDRESS = "ftp_server_address"; public static final String FTP_SERVER_USERNAME = "ftp_server_username"; 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 4afac80bc..41ebc7ec9 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 @@ -501,6 +501,8 @@ public class DeviceSpecificSettingsFragment extends PreferenceFragmentCompat imp addPreferenceHandlerFor(MORNING_UPDATES_ENABLED); addPreferenceHandlerFor(MORNING_UPDATES_CATEGORIES_SORTABLE); + addPreferenceHandlerFor(SHORTCUT_CARDS_SORTABLE); + addPreferenceHandlerFor(PREF_SONY_AMBIENT_SOUND_CONTROL); addPreferenceHandlerFor(PREF_SONY_AMBIENT_SOUND_CONTROL_BUTTON_MODE); addPreferenceHandlerFor(PREF_SONY_FOCUS_VOICE); 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 b1bdef2aa..4673fc7ae 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 @@ -42,6 +42,7 @@ import nodomain.freeyourgadget.gadgetbridge.entities.Device; import nodomain.freeyourgadget.gadgetbridge.entities.HuamiExtendedActivitySampleDao; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsShortcutCardsService; 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; @@ -194,6 +195,9 @@ public abstract class Huami2021Coordinator extends HuamiCoordinator { if (supportsControlCenter()) { settings.add(R.xml.devicesettings_huami2021_control_center); } + if (supportsShortcutCards(device)) { + settings.add(R.xml.devicesettings_huami2021_shortcut_cards); + } settings.add(R.xml.devicesettings_nightmode); settings.add(R.xml.devicesettings_sleep_mode); settings.add(R.xml.devicesettings_liftwrist_display_sensitivity_with_smart); @@ -370,6 +374,10 @@ public abstract class Huami2021Coordinator extends HuamiCoordinator { return ZeppOsPhoneService.isSupported(getPrefs(device)); } + public boolean supportsShortcutCards(final GBDevice device) { + return ZeppOsShortcutCardsService.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 051eb07d2..a6463d82b 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 @@ -63,6 +63,7 @@ public class Huami2021SettingsCustomizer extends HuamiSettingsCustomizer { removeUnsupportedElementsFromListPreference(HuamiConst.PREF_DISPLAY_ITEMS_SORTABLE, handler, prefs); removeUnsupportedElementsFromListPreference(HuamiConst.PREF_SHORTCUTS_SORTABLE, handler, prefs); removeUnsupportedElementsFromListPreference(HuamiConst.PREF_CONTROL_CENTER_SORTABLE, handler, prefs); + removeUnsupportedElementsFromListPreference(DeviceSettingsPreferenceConst.SHORTCUT_CARDS_SORTABLE, handler, prefs); removeUnsupportedElementsFromListPreference(DeviceSettingsPreferenceConst.MORNING_UPDATES_CATEGORIES_SORTABLE, handler, prefs); for (final ZeppOsConfigService.ConfigArg config : ZeppOsConfigService.ConfigArg.values()) { 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 a6d5c18d6..28f0be98e 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 @@ -17,6 +17,7 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.huami; import static org.apache.commons.lang3.ArrayUtils.subarray; +import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.SHORTCUT_CARDS_SORTABLE; import static nodomain.freeyourgadget.gadgetbridge.devices.huami.Huami2021Service.*; import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiService.SUCCESS; import static nodomain.freeyourgadget.gadgetbridge.model.ActivityUser.PREF_USER_NAME; @@ -119,6 +120,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.Upd import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.AbstractZeppOsService; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.operations.ZeppOsAgpsUpdateOperation; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsAgpsService; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsShortcutCardsService; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsConfigService; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsContactsService; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsFileUploadService; @@ -157,6 +159,7 @@ public abstract class Huami2021Support extends HuamiSupport { private final ZeppOsContactsService contactsService = new ZeppOsContactsService(this); private final ZeppOsMorningUpdatesService morningUpdatesService = new ZeppOsMorningUpdatesService(this); private final ZeppOsPhoneService phoneService = new ZeppOsPhoneService(this); + private final ZeppOsShortcutCardsService shortcutCardsService = new ZeppOsShortcutCardsService(this); private final Map mServiceMap = new HashMap() {{ put(fileUploadService.getEndpoint(), fileUploadService); @@ -167,6 +170,7 @@ public abstract class Huami2021Support extends HuamiSupport { put(contactsService.getEndpoint(), contactsService); put(morningUpdatesService.getEndpoint(), morningUpdatesService); put(phoneService.getEndpoint(), phoneService); + put(shortcutCardsService.getEndpoint(), shortcutCardsService); }}; public Huami2021Support() { @@ -270,6 +274,15 @@ public abstract class Huami2021Support extends HuamiSupport { return; } + // shortcutCardsService preferences, they do not use the configService + switch (config) { + case DeviceSettingsPreferenceConst.SHORTCUT_CARDS_SORTABLE: + final List shortcutCards = prefs.getList(SHORTCUT_CARDS_SORTABLE, Collections.emptyList()); + LOG.info("Setting shortcut cards to {}", shortcutCards); + shortcutCardsService.setShortcutCards(shortcutCards); + return; + } + // Other preferences switch (config) { case HuamiConst.PREF_CONTROL_CENTER_SORTABLE: @@ -1530,6 +1543,8 @@ public abstract class Huami2021Support extends HuamiSupport { //contactsService.requestCapabilities(builder); morningUpdatesService.getEnabled(builder); morningUpdatesService.getCategories(builder); + shortcutCardsService.requestCapabilities(builder); + shortcutCardsService.requestShortcutCards(builder); } @Override diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/zeppos/services/ZeppOsShortcutCardsService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/zeppos/services/ZeppOsShortcutCardsService.java new file mode 100644 index 000000000..70e24190b --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/zeppos/services/ZeppOsShortcutCardsService.java @@ -0,0 +1,251 @@ +/* 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 static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.SHORTCUT_CARDS_SORTABLE; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdatePreferences; +import nodomain.freeyourgadget.gadgetbridge.devices.huami.Huami2021Coordinator; +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; +import nodomain.freeyourgadget.gadgetbridge.util.StringUtils; + +public class ZeppOsShortcutCardsService extends AbstractZeppOsService { + private static final Logger LOG = LoggerFactory.getLogger(ZeppOsShortcutCardsService.class); + + private static final short ENDPOINT = 0x0009; + + public static final byte CMD_CAPABILITIES_REQUEST = 0x01; + public static final byte CMD_CAPABILITIES_RESPONSE = 0x02; + public static final byte CMD_LIST_GET = 0x07; + public static final byte CMD_LIST_RET = 0x08; + public static final byte CMD_ENABLED_SET = 0x09; + public static final byte CMD_ENABLED_SET_ACK = 0x0a; + + public static final String PREF_VERSION = "zepp_os_shortcut_cards_version"; + + public enum ShortcutCard { + WEATHER("2", "1"), + FORECAST("2", "3"), + PAI("3", "1"), + ALARM("4", "1"), + SLEEP("5", "1"), + HEARTRATE("6", "1"), + ACTIVITY("7", "1"), + SPO2("8", "1"), + PHONE("9", "1"), + EVENTS("10", "1"), + STRESS("11", "1"), + WORLDCLOCK("13", "1"), + TODO("17", "1"), + COUNTDOWN("18", "1"), + LAST_WORKOUT("19", "1"), + TOTAL_WORKOUT("19", "2"), + WORKOUT_STATUS("19", "3"), + VO2_MAX("19", "4"), + MUSIC("20", "1"), + CYCLE_TRACKING("21", "1"), + ONE_TAP_MEASURING("22", "1"), + BREATHING("24", "1"), + STOPWATCH("25", "1"), + RECOMMENDATION("28", "1"), + ; + + private final String appNum; + private final String cardNum; + + ShortcutCard(final String appNum, final String cardNum) { + this.appNum = appNum; + this.cardNum = cardNum; + } + + public String getAppNum() { + return appNum; + } + + public String getCardNum() { + return cardNum; + } + + public static ShortcutCard fromCodes(final String appNum, final String cardNum) { + for (ShortcutCard value : ShortcutCard.values()) { + if (value.getAppNum().equals(appNum) && value.getCardNum().equals(cardNum)) { + return value; + } + } + + return null; + } + } + + private int version = 0; + private int maxCards = 0; + + public ZeppOsShortcutCardsService(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 != 3) { + LOG.warn("Unsupported shortcut cards service version {}", version); + return; + } + maxCards = payload[2] & 0xFF; + LOG.info("Contacts version={}, maxCards={}", version, maxCards); + break; + case CMD_LIST_RET: + LOG.info("Got shortcut cards list"); + parseShortcutCards(payload); + break; + case CMD_ENABLED_SET_ACK: + LOG.info("Got enabled shortcut cards ack, status = {}", payload[1]); + break; + default: + LOG.warn("Unexpected contacts byte {}", String.format("0x%02x", payload[0])); + } + } + + public void requestCapabilities(final TransactionBuilder builder) { + write(builder, CMD_CAPABILITIES_REQUEST); + } + + public void requestShortcutCards(final TransactionBuilder builder) { + write(builder, CMD_LIST_GET); + } + + public void parseShortcutCards(final byte[] payload) { + final ByteBuffer buf = ByteBuffer.wrap(payload).order(ByteOrder.LITTLE_ENDIAN); + buf.get(); // discard the command byte + final int numCards = buf.get() & 0xFF; + final List allCards = new ArrayList<>(); + final List enabledCards = new ArrayList<>(); + + for (int i = 0; i < numCards; i++) { + final String appNum = StringUtils.untilNullTerminator(buf); + final String cardNum = StringUtils.untilNullTerminator(buf); + final boolean enabled = buf.get() == 0x01; + final byte b = buf.get(); + if (b != 0) { + LOG.warn("Unexpected byte {} at pos {}", b, buf.position() - 1); + return; + } + final ShortcutCard card = ShortcutCard.fromCodes(appNum, cardNum); + final String cardPrefValue; + if (card != null) { + cardPrefValue = card.name().toLowerCase(Locale.ROOT); + } else { + LOG.warn("Unknown shortcut card [{}, {}]", appNum, cardNum); + cardPrefValue = appNum + "/" + cardNum; + } + allCards.add(cardPrefValue); + if (enabled) { + enabledCards.add(cardPrefValue); + } + } + + final GBDeviceEventUpdatePreferences evt = new GBDeviceEventUpdatePreferences() + .withPreference(SHORTCUT_CARDS_SORTABLE, String.join(",", enabledCards)) + .withPreference(Huami2021Coordinator.getPrefPossibleValuesKey(SHORTCUT_CARDS_SORTABLE), String.join(",", allCards)); + getSupport().evaluateGBDeviceEvent(evt); + } + + public void setShortcutCards(final List cardsPrefValue) { + if (maxCards == 0) { + LOG.warn("maxCards == 0, refusing"); + return; + } + + final List cards = new ArrayList<>(cardsPrefValue); + if (cards.size() > maxCards) { + LOG.warn("Number of cards {} > maxCards {}, truncating", cards.size(), maxCards); + cards.subList(maxCards, cards.size()).clear(); + } + + final ByteArrayOutputStream baos = new ByteArrayOutputStream(); + + baos.write(CMD_ENABLED_SET); + baos.write(cards.size()); + + for (final String cardPrefValue : cards) { + String appNum; + String cardNum; + try { + final ShortcutCard card = ShortcutCard.valueOf(cardPrefValue.toUpperCase(Locale.ROOT)); + appNum = card.getAppNum(); + cardNum = card.getCardNum(); + } catch (final IllegalArgumentException e) { + // attempt to parse as appNum/cardNum + final Matcher matcher = Pattern.compile("^([0-9a-fA-F]+)/([0-9a-fA-F]+)$").matcher(cardPrefValue); + if (matcher.find()) { + appNum = matcher.group(1); + cardNum = matcher.group(2); + } else { + LOG.warn("Unexpected format for shortcut cards pref value {}", cardPrefValue); + return; + } + } + + try { + baos.write(appNum.getBytes(StandardCharsets.UTF_8)); + baos.write(0); + baos.write(cardNum.getBytes(StandardCharsets.UTF_8)); + baos.write(0); + baos.write(1); // enabled + baos.write(0); // ? + } catch (final IOException e) { + throw new RuntimeException(e); + } + } + + write("set enabled shortcut cards", baos.toByteArray()); + } + + public static boolean isSupported(final Prefs devicePrefs) { + return devicePrefs.getInt(PREF_VERSION, 0) == 3; + } +} diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index 91f9303c2..25fe101f2 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -846,6 +846,60 @@ more + + @string/menuitem_weather + @string/menuitem_forecast + @string/menuitem_pai + @string/menuitem_alarm + @string/menuitem_sleep + @string/menuitem_hr + @string/menuitem_activity + @string/menuitem_spo2 + @string/menuitem_phone + @string/menuitem_events + @string/menuitem_stress + @string/menuitem_worldclock + @string/menuitem_todo + @string/menuitem_countdown + @string/menuitem_last_workout + @string/menuitem_total_workout + @string/menuitem_workout_status + @string/menuitem_vo2_max + @string/menuitem_music + @string/menuitem_cycles + @string/menuitem_one_tap_measuring + @string/menuitem_breathing + @string/menuitem_stopwatch + @string/menuitem_recommendation + + + + weather + forecast + pai + alarm + sleep + heartrate + activity + spo2 + phone + events + stress + worldclock + todo + countdown + last_workout + total_workout + workout_status + vo2_max + music + cycle_tracking + one_tap_measuring + breathing + stopwatch + recommendation + + @string/battery @string/menuitem_dnd diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 044760fb1..ef82fa9ec 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -914,6 +914,8 @@ Lower band screen brightness automatically at night Shortcuts Choose the shortcuts on the band screen + Shortcut Cards + Shortcut cards seen when swiping right on the watchface. When an app is running, auto-generated cards are not affected by this setting. Control Center Choose the items on the control center dropdown Sensitivity @@ -1295,6 +1297,11 @@ Notifications Workout History Weather + Forecast + Last workout + Total workout + VO₂ Max + Recommendation Breathing Cycle Tracking Alarm diff --git a/app/src/main/res/xml/devicesettings_huami2021_shortcut_cards.xml b/app/src/main/res/xml/devicesettings_huami2021_shortcut_cards.xml new file mode 100644 index 000000000..36e3df9fe --- /dev/null +++ b/app/src/main/res/xml/devicesettings_huami2021_shortcut_cards.xml @@ -0,0 +1,13 @@ + + + +