From c3c4c2ce74b1c64b3d524057ca1c701ebf58a18e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Rebelo?= Date: Sun, 11 Jun 2023 18:23:54 +0100 Subject: [PATCH] Zepp OS: Add watch app logs developer option --- .../DeviceSettingsPreferenceConst.java | 3 + .../devices/huami/Huami2021Coordinator.java | 4 + .../huami/Huami2021SettingsCustomizer.java | 2 + .../devices/huami/Huami2021Support.java | 3 + .../zeppos/services/ZeppOsLogsService.java | 229 ++++++++++++++++++ app/src/main/res/values/strings.xml | 4 + .../devicesettings_app_logs_start_stop.xml | 26 ++ 7 files changed, 271 insertions(+) create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/zeppos/services/ZeppOsLogsService.java create mode 100644 app/src/main/res/xml/devicesettings_app_logs_start_stop.xml diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSettingsPreferenceConst.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSettingsPreferenceConst.java index 4fb5db292..ec4921872 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 @@ -248,6 +248,9 @@ public class DeviceSettingsPreferenceConst { public static final String WIFI_HOTSPOT_STOP = "wifi_hotspot_stop"; public static final String WIFI_HOTSPOT_STATUS = "wifi_hotspot_status"; + public static final String PREF_APP_LOGS_START = "pref_app_logs_start"; + public static final String PREF_APP_LOGS_STOP = "pref_app_logs_stop"; + public static final String MORNING_UPDATES_ENABLED = "morning_updates_enabled"; public static final String MORNING_UPDATES_CATEGORIES_SORTABLE = "morning_updates_categories"; 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 8365f03a5..45ab910be 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 @@ -51,6 +51,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.AbstractHuami2021FWInstallHandler; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsAlexaService; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsContactsService; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsLogsService; 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; @@ -377,6 +378,9 @@ public abstract class Huami2021Coordinator extends HuamiCoordinator { // Developer // settings.add(R.xml.devicesettings_header_developer); + if (ZeppOsLogsService.isSupported(getPrefs(device))) { + settings.add(R.xml.devicesettings_app_logs_start_stop); + } if (supportsAlexa(device)) { settings.add(R.xml.devicesettings_huami2021_alexa); } 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 b26c9de9c..315936d75 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 @@ -362,6 +362,8 @@ public class Huami2021SettingsCustomizer extends HuamiSettingsCustomizer { // 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.PREF_APP_LOGS_START), + handler.findPreference(DeviceSettingsPreferenceConst.PREF_APP_LOGS_STOP), handler.findPreference(DeviceSettingsPreferenceConst.WIFI_HOTSPOT_START), handler.findPreference(DeviceSettingsPreferenceConst.WIFI_HOTSPOT_STOP), handler.findPreference(DeviceSettingsPreferenceConst.FTP_SERVER_START), 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 17efc874f..6bd889345 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 @@ -112,6 +112,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.service import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsAppsService; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsCalendarService; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsCannedMessagesService; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsLogsService; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsNotificationService; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsShortcutCardsService; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsConfigService; @@ -153,6 +154,7 @@ public abstract class Huami2021Support extends HuamiSupport implements ZeppOsFil private final ZeppOsNotificationService notificationService = new ZeppOsNotificationService(this, fileTransferService); private final ZeppOsAlexaService alexaService = new ZeppOsAlexaService(this); private final ZeppOsAppsService appsService = new ZeppOsAppsService(this); + private final ZeppOsLogsService logsService = new ZeppOsLogsService(this); private final Map mServiceMap = new LinkedHashMap() {{ put(fileTransferService.getEndpoint(), fileTransferService); @@ -171,6 +173,7 @@ public abstract class Huami2021Support extends HuamiSupport implements ZeppOsFil put(notificationService.getEndpoint(), notificationService); put(alexaService.getEndpoint(), alexaService); put(appsService.getEndpoint(), appsService); + put(logsService.getEndpoint(), logsService); }}; public Huami2021Support() { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/zeppos/services/ZeppOsLogsService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/zeppos/services/ZeppOsLogsService.java new file mode 100644 index 000000000..1c36d916d --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/zeppos/services/ZeppOsLogsService.java @@ -0,0 +1,229 @@ +/* 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.annotation.SuppressLint; +import android.widget.Toast; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.charset.StandardCharsets; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.HashSet; +import java.util.Set; + +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.GB; +import nodomain.freeyourgadget.gadgetbridge.util.Prefs; +import nodomain.freeyourgadget.gadgetbridge.util.StringUtils; + +public class ZeppOsLogsService extends AbstractZeppOsService { + private static final Logger LOG = LoggerFactory.getLogger(ZeppOsLogsService.class); + + private static final short ENDPOINT = 0x003a; + + public static final byte CMD_CAPABILITIES_REQUEST = 0x01; + public static final byte CMD_CAPABILITIES_RESPONSE = 0x02; + public static final byte CMD_LOGS_START = 0x03; + public static final byte CMD_LOGS_START_ACK = 0x04; + public static final byte CMD_LOGS_STOP = 0x05; + public static final byte CMD_LOGS_STOP_ACK = 0x06; + public static final byte CMD_LOGS_DATA = 0x07; + public static final byte CMD_UNKNOWN_8 = 0x08; + public static final byte CMD_UNKNOWN_9 = 0x09; + + public static final String PREF_VERSION = "zepp_os_logs_version"; + + private String logsType; + private final Set sessions = new HashSet<>(); + + @SuppressLint("SimpleDateFormat") + private final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); + + public ZeppOsLogsService(final Huami2021Support support) { + super(support); + } + + @Override + public short getEndpoint() { + return ENDPOINT; + } + + @Override + public boolean isEncrypted() { + return false; + } + + @Override + public void handlePayload(final byte[] payload) { + switch (payload[0]) { + case CMD_CAPABILITIES_RESPONSE: + handleCapabilitiesResponse(payload); + break; + case CMD_LOGS_START_ACK: + if (payload[1] != 1) { + LOG.warn("Failed to start logs, status = {}", payload[1]); + GB.toast(getContext(), "Failed to start logs", Toast.LENGTH_SHORT, GB.WARN); + return; + } + final byte sessionId = payload[2]; + LOG.info("Got logs start ack, sessionId = {}", sessionId); + GB.toast(getContext(), "App logs started", Toast.LENGTH_SHORT, GB.INFO); + sessions.add(sessionId); + break; + case CMD_LOGS_STOP_ACK: + LOG.info("Got logs stop ack, status = {}", payload[1]); + GB.toast(getContext(), "App logs stopped", Toast.LENGTH_SHORT, GB.INFO); + break; + case CMD_LOGS_DATA: + handleLogsData(payload); + break; + case CMD_UNKNOWN_8: + LOG.info("Got unknown 8, replying with unknown 9"); + write("reply logs unknown 9", CMD_UNKNOWN_9); + break; + default: + LOG.warn("Unexpected logs payload byte {}", String.format("0x%02x", payload[0])); + } + } + + @Override + public boolean onSendConfiguration(final String config, final Prefs prefs) { + switch (config) { + case DeviceSettingsPreferenceConst.PREF_APP_LOGS_START: + start(); + return true; + case DeviceSettingsPreferenceConst.PREF_APP_LOGS_STOP: + stop(); + return true; + } + + return false; + } + + @Override + public void initialize(final TransactionBuilder builder) { + requestCapabilities(builder); + } + + public void requestCapabilities(final TransactionBuilder builder) { + write(builder, CMD_CAPABILITIES_REQUEST); + } + + public void start() { + if (logsType == null) { + LOG.error("logsType is null"); + return; + } + + LOG.info("Starting logs"); + + final byte[] logsTypeBytes = logsType.getBytes(StandardCharsets.UTF_8); + final ByteBuffer buf = ByteBuffer.allocate(1 + logsTypeBytes.length + 1) + .order(ByteOrder.LITTLE_ENDIAN); + + buf.put(CMD_LOGS_START); + buf.put(logsTypeBytes); + buf.put((byte) 0); + + write("start logs", buf.array()); + } + + public void stop() { + LOG.info("Stopping {} log sessions", sessions.size()); + + for (final Byte session : sessions) { + stop(session); + } + } + + private void stop(final byte sessionId) { + final ByteBuffer buf = ByteBuffer.allocate(3) + .order(ByteOrder.LITTLE_ENDIAN); + + buf.put(CMD_LOGS_STOP); + buf.put((byte) 0); + buf.put(sessionId); + + write("stop logs session " + sessionId, buf.array()); + } + + private void handleCapabilitiesResponse(final byte[] payload) { + final int version = payload[1] & 0xFF; + if (version != 1) { + LOG.warn("Unsupported logs service version {}", version); + return; + } + final byte var1 = payload[2]; + if (var1 != 1) { + LOG.warn("Unexpected value for var1 '{}'", var1); + } + final byte var2 = payload[3]; + if (var2 != 0) { + LOG.warn("Unexpected value for var2 '{}'", var2); + } + + logsType = StringUtils.untilNullTerminator(payload, 4); + + LOG.info("Logs version={}, var1={}, var2={}, logsType={}", version, var1, var2, logsType); + + getSupport().evaluateGBDeviceEvent(new GBDeviceEventUpdatePreferences(PREF_VERSION, version)); + } + + private void handleLogsData(final byte[] payload) { + final ByteBuffer buf = ByteBuffer.wrap(payload).order(ByteOrder.LITTLE_ENDIAN); + buf.get(); // discard first byte + final byte index = buf.get(); + final byte sessionId = buf.get(); + if (!sessions.contains(sessionId)) { + LOG.warn("Got log data for unknown session {}", sessionId); + } + final String appIdDecimal = StringUtils.untilNullTerminator(buf); + final byte unknown1 = buf.get(); + if (unknown1 != 0) { + LOG.warn("Unexpected value for unknown1 = {}", unknown1); + } + final long timestampMillis = buf.getLong(); + final byte unknown2 = buf.get(); + if (unknown2 != 2) { + LOG.warn("Unexpected value for unknown2 = {}", unknown2); + } + final String message = StringUtils.untilNullTerminator(buf); + if (buf.position() < buf.limit()) { + LOG.warn("There are {} log data bytes still in the buffer", (buf.limit() - buf.position())); + } + + LOG.info( + "Log entry - {} [{}] - {}", + sdf.format(new Date(timestampMillis)), + appIdDecimal, + message + ); + } + + 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 1236becaf..bc789338c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2121,4 +2121,8 @@ Voice service class Full service path handling voice commands Voice Service + App logs + Enable logs from watch apps + Start logging from watch apps + Stop logging from watch apps diff --git a/app/src/main/res/xml/devicesettings_app_logs_start_stop.xml b/app/src/main/res/xml/devicesettings_app_logs_start_stop.xml new file mode 100644 index 000000000..f6a20963d --- /dev/null +++ b/app/src/main/res/xml/devicesettings_app_logs_start_stop.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + +