From 4e680cfcce13ca99d53d7a694f8e18d3d3db68ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Rebelo?= Date: Sat, 21 Oct 2023 22:07:06 +0100 Subject: [PATCH] Mi Band 8: List watchface in app management --- .../devices/xiaomi/XiaomiCoordinator.java | 43 +++++- .../devices/xiaomi/XiaomiInstallHandler.java | 55 ++++++++ .../xiaomi/miband8/MiBand8Coordinator.java | 5 +- .../service/devices/xiaomi/XiaomiSupport.java | 41 ++---- .../services/XiaomiWatchfaceService.java | 123 ++++++++++++++++++ app/src/main/proto/xiaomi.proto | 46 ++++++- 6 files changed, 275 insertions(+), 38 deletions(-) create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/xiaomi/XiaomiInstallHandler.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/services/XiaomiWatchfaceService.java diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/xiaomi/XiaomiCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/xiaomi/XiaomiCoordinator.java index 0dc89863f..5fde67d31 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/xiaomi/XiaomiCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/xiaomi/XiaomiCoordinator.java @@ -16,6 +16,7 @@ along with this program. If not, see . */ package nodomain.freeyourgadget.gadgetbridge.devices.xiaomi; +import android.app.Activity; import android.bluetooth.le.ScanFilter; import androidx.annotation.NonNull; @@ -23,6 +24,8 @@ import androidx.annotation.Nullable; import org.apache.commons.lang3.ArrayUtils; +import java.io.File; +import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -30,6 +33,7 @@ import java.util.List; import nodomain.freeyourgadget.gadgetbridge.GBException; import nodomain.freeyourgadget.gadgetbridge.R; +import nodomain.freeyourgadget.gadgetbridge.activities.appmanager.AppManagerActivity; import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsCustomizer; import nodomain.freeyourgadget.gadgetbridge.capabilities.HeartRateCapability; import nodomain.freeyourgadget.gadgetbridge.capabilities.password.PasswordCapabilityImpl; @@ -48,11 +52,13 @@ import nodomain.freeyourgadget.gadgetbridge.model.SleepRespiratoryRateSample; import nodomain.freeyourgadget.gadgetbridge.model.Spo2Sample; import nodomain.freeyourgadget.gadgetbridge.model.StressSample; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiLanguageType; +import nodomain.freeyourgadget.gadgetbridge.util.FileUtils; public abstract class XiaomiCoordinator extends AbstractBLEDeviceCoordinator { @NonNull @Override public Collection createBLEScanFilters() { + // TODO return super.createBLEScanFilters(); } @@ -131,10 +137,41 @@ public abstract class XiaomiCoordinator extends AbstractBLEDeviceCoordinator { return true; } - @Override public boolean supportsAppsManagement(final GBDevice device) { - // TODO maybe for watchfaces or widgets? - return super.supportsAppsManagement(device); + return true; + } + + @Override + public Class getAppsManagementActivity() { + return AppManagerActivity.class; + } + + @Override + public File getAppCacheDir() throws IOException { + // TODO we don't need this + return new File(FileUtils.getExternalFilesDir(), "xiaomi-app-cache"); + } + + @Override + public String getAppCacheSortFilename() { + // TODO we don't need this + return "xiaomi-app-cache-order.txt"; + } + + @Override + public String getAppFileExtension() { + // TODO we don't need this + return ".bin"; + } + + @Override + public boolean supportsAppListFetching() { + return true; + } + + @Override + public boolean supportsAppReordering() { + return false; } @Override diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/xiaomi/XiaomiInstallHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/xiaomi/XiaomiInstallHandler.java new file mode 100644 index 000000000..79ca4cb44 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/xiaomi/XiaomiInstallHandler.java @@ -0,0 +1,55 @@ +/* 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.devices.xiaomi; + +import static nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiFirmwareType.AGPS_UIHH; + +import android.content.Context; +import android.net.Uri; + +import nodomain.freeyourgadget.gadgetbridge.R; +import nodomain.freeyourgadget.gadgetbridge.activities.InstallActivity; +import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator; +import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.model.GenericItem; + +public class XiaomiInstallHandler implements InstallHandler { + protected final Uri mUri; + protected final Context mContext; + + public XiaomiInstallHandler(final Uri uri, final Context context) { + this.mUri = uri; + this.mContext = context; + } + + @Override + public boolean isValid() { + // TODO + return false; + } + + @Override + public void validateInstallation(final InstallActivity installActivity, final GBDevice device) { + // TODO + } + + @Override + public void onStartInstall(final GBDevice device) { + // nothing to do + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/xiaomi/miband8/MiBand8Coordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/xiaomi/miband8/MiBand8Coordinator.java index 63afd1f52..7f0f0d9ce 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/xiaomi/miband8/MiBand8Coordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/xiaomi/miband8/MiBand8Coordinator.java @@ -27,6 +27,7 @@ import java.util.regex.Pattern; import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler; import nodomain.freeyourgadget.gadgetbridge.devices.xiaomi.XiaomiCoordinator; +import nodomain.freeyourgadget.gadgetbridge.devices.xiaomi.XiaomiInstallHandler; import nodomain.freeyourgadget.gadgetbridge.service.DeviceSupport; import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.XiaomiEncryptedSupport; @@ -39,8 +40,8 @@ public class MiBand8Coordinator extends XiaomiCoordinator { @Nullable @Override public InstallHandler findInstallHandler(final Uri uri, final Context context) { - // TODO implement this - return super.findInstallHandler(uri, context); + final XiaomiInstallHandler handler = new XiaomiInstallHandler(uri, context); + return handler.isValid() ? handler : null; } @Override diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiSupport.java index 8c5e45785..9de912287 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiSupport.java @@ -57,6 +57,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.services.Xiao import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.services.XiaomiNotificationService; import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.services.XiaomiScheduleService; import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.services.XiaomiSystemService; +import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.services.XiaomiWatchfaceService; import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.services.XiaomiWeatherService; import nodomain.freeyourgadget.gadgetbridge.util.GB; import nodomain.freeyourgadget.gadgetbridge.util.Prefs; @@ -77,6 +78,7 @@ public abstract class XiaomiSupport extends AbstractBTLEDeviceSupport { protected final XiaomiWeatherService weatherService = new XiaomiWeatherService(this); protected final XiaomiSystemService systemService = new XiaomiSystemService(this); protected final XiaomiCalendarService calendarService = new XiaomiCalendarService(this); + protected final XiaomiWatchfaceService watchfaceService = new XiaomiWatchfaceService(this); private String mFirmwareVersion = null; @@ -89,6 +91,7 @@ public abstract class XiaomiSupport extends AbstractBTLEDeviceSupport { put(XiaomiWeatherService.COMMAND_TYPE, weatherService); put(XiaomiSystemService.COMMAND_TYPE, systemService); put(XiaomiCalendarService.COMMAND_TYPE, calendarService); + put(XiaomiWatchfaceService.COMMAND_TYPE, watchfaceService); }}; public XiaomiSupport() { @@ -324,44 +327,24 @@ public abstract class XiaomiSupport extends AbstractBTLEDeviceSupport { @Override public void onInstallApp(final Uri uri) { - // TODO - super.onInstallApp(uri); + watchfaceService.installWatchface(uri); } @Override public void onAppInfoReq() { - // TODO - super.onAppInfoReq(); + watchfaceService.requestWatchfaceList(); } @Override public void onAppStart(final UUID uuid, boolean start) { - // TODO - super.onAppStart(uuid, start); - } - - @Override - public void onAppDownload(final UUID uuid) { - // TODO - super.onAppDownload(uuid); + if (start) { + watchfaceService.setWatchface(uuid); + } } @Override public void onAppDelete(final UUID uuid) { - // TODO - super.onAppDelete(uuid); - } - - @Override - public void onAppConfiguration(final UUID appUuid, String config, Integer id) { - // TODO - super.onAppConfiguration(appUuid, config, id); - } - - @Override - public void onAppReorder(final UUID[] uuids) { - // TODO - super.onAppReorder(uuids); + watchfaceService.deleteWatchface(uuid); } @Override @@ -369,12 +352,6 @@ public abstract class XiaomiSupport extends AbstractBTLEDeviceSupport { healthService.onFetchRecordedData(dataTypes); } - @Override - public void onReset(final int flags) { - // TODO - super.onReset(flags); - } - @Override public void onHeartRateTest() { healthService.onHeartRateTest(); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/services/XiaomiWatchfaceService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/services/XiaomiWatchfaceService.java new file mode 100644 index 000000000..f1898d658 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/services/XiaomiWatchfaceService.java @@ -0,0 +1,123 @@ +/* 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.xiaomi.services; + +import android.net.Uri; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventAppInfo; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceApp; +import nodomain.freeyourgadget.gadgetbridge.proto.xiaomi.XiaomiProto; +import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; +import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.XiaomiSupport; +import nodomain.freeyourgadget.gadgetbridge.util.Prefs; + +public class XiaomiWatchfaceService extends AbstractXiaomiService { + private static final Logger LOG = LoggerFactory.getLogger(XiaomiWatchfaceService.class); + + public static final int COMMAND_TYPE = 4; + + public static final int CMD_WATCHFACE_LIST = 0; + public static final int CMD_WATCHFACE_SET = 1; + public static final int CMD_WATCHFACE_INSTALL = 4; + + + public XiaomiWatchfaceService(final XiaomiSupport support) { + super(support); + } + + @Override + public void initialize(final TransactionBuilder builder) { + //getSupport().sendCommand(builder, COMMAND_TYPE, CMD_WATCHFACE_LIST); + } + + @Override + public void handleCommand(final XiaomiProto.Command cmd) { + // TODO + switch (cmd.getSubtype()) { + case CMD_WATCHFACE_LIST: + handleWatchfaceList(cmd.getWatchface().getWatchfaceList()); + // TODO handle + return; + case CMD_WATCHFACE_SET: + // watchface set ack + return; + case CMD_WATCHFACE_INSTALL: + return; + } + + LOG.warn("Unknown watchface command {}", cmd.getSubtype()); + } + + @Override + public boolean onSendConfiguration(final String config, final Prefs prefs) { + // TODO set watchface + return super.onSendConfiguration(config, prefs); + } + + public void requestWatchfaceList() { + getSupport().sendCommand("request watchface list", COMMAND_TYPE, CMD_WATCHFACE_LIST); + } + + private void handleWatchfaceList(final XiaomiProto.WatchfaceList watchfaceList) { + LOG.debug("Got {} watchfaces", watchfaceList.getWatchfaceCount()); + + final List gbDeviceApps = new ArrayList<>(); + + for (final XiaomiProto.WatchfaceInfo watchface : watchfaceList.getWatchfaceList()) { + GBDeviceApp gbDeviceApp = new GBDeviceApp( + UUID.randomUUID(), + watchface.getName(), + "", + "", + GBDeviceApp.Type.WATCHFACE + ); + gbDeviceApps.add(gbDeviceApp); + } + + final GBDeviceEventAppInfo appInfoCmd = new GBDeviceEventAppInfo(); + appInfoCmd.apps = gbDeviceApps.toArray(new GBDeviceApp[0]); + getSupport().evaluateGBDeviceEvent(appInfoCmd); + } + + public void setWatchface(final UUID uuid) { + // TODO + } + + public void setWatchface(final String watchfaceId) { + // TODO + } + + public void deleteWatchface(final UUID uuid) { + // TODO + } + + public void deleteWatchface(final String watchfaceId) { + // TODO + // TODO prevent uninstall of non-uninstallable watchface + } + + public void installWatchface(final Uri uri) { + // TODO + } +} diff --git a/app/src/main/proto/xiaomi.proto b/app/src/main/proto/xiaomi.proto index d8325375a..083384c6c 100644 --- a/app/src/main/proto/xiaomi.proto +++ b/app/src/main/proto/xiaomi.proto @@ -11,6 +11,7 @@ message Command { optional Auth auth = 3; optional System system = 4; + optional Watchface watchface = 6; optional Health health = 10; optional Calendar calendar = 14; optional Music music = 20; @@ -277,6 +278,49 @@ message Charger { optional uint32 state = 1; // 1 charging, 2 not charging } +// +// Watchface +// + +message Watchface { + optional WatchfaceList watchfaceList = 1; + + // 4, 2 delete | 4, 1 set + optional string watchfaceId = 2; + optional uint32 ack = 4; // 1 + + // 4, 4 + optional uint32 installStatus = 5; // 0 not installed, 2 already installed + optional WatchfaceInstallStart watchfaceInstallStart = 6; + optional WatchfaceInstallFinish watchfaceInstallFinish = 7; +} + +message WatchfaceList { + repeated WatchfaceInfo watchface = 1; +} + +message WatchfaceInfo { + optional string id = 1; + optional string name = 2; + optional bool active = 3; + optional bool canDelete = 4; + optional uint32 unknown5 = 5; // 0 + optional uint32 unknown6 = 6; // 0 + optional uint32 unknown11 = 11; // 0 +} + +message WatchfaceInstallStart { + optional string id = 1; + optional uint32 size = 2; +} + +message WatchfaceInstallFinish { + optional string id = 1; + optional uint32 unknown2 = 2; // 2 + optional uint32 unknown3 = 3; // 0 + optional uint32 unknown4 = 4; // 0 +} + // // Health // @@ -706,7 +750,7 @@ message DataUpload { } message DataUploadRequest { - optional uint32 unknown1 = 1; // 16 for watchface, 50 for notification icons, 32 for firmware? + optional uint32 type = 1; // 16 for watchface, 50 for notification icons, 32 for firmware? optional bytes md5sum = 2; optional uint32 size = 3; }