/* Copyright (C) 2023-2024 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 org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventAppInfo;
import nodomain.freeyourgadget.gadgetbridge.devices.xiaomi.XiaomiFWHelper;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
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.btle.actions.SetProgressAction;
import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.XiaomiSupport;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class XiaomiWatchfaceService extends AbstractXiaomiService implements XiaomiDataUploadService.Callback {
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_DELETE = 2;
public static final int CMD_WATCHFACE_INSTALL = 4;
private final Set allWatchfaces = new HashSet<>();
private final Set userWatchfaces = new HashSet<>();
private UUID activeWatchface = null;
// Not null if we're installing a firmware
private XiaomiFWHelper fwHelper = null;
public XiaomiWatchfaceService(final XiaomiSupport support) {
super(support);
}
@Override
public void handleCommand(final XiaomiProto.Command cmd) {
switch (cmd.getSubtype()) {
case CMD_WATCHFACE_LIST:
handleWatchfaceList(cmd.getWatchface().getWatchfaceList());
return;
case CMD_WATCHFACE_SET:
LOG.debug("Got watchface set response, ack={}", cmd.getWatchface().getAck());
//requestWatchfaceList();
return;
case CMD_WATCHFACE_DELETE:
LOG.debug("Got watchface delete response, ack={}", cmd.getWatchface().getAck());
requestWatchfaceList();
return;
case CMD_WATCHFACE_INSTALL:
final int installStatus = cmd.getWatchface().getInstallStatus();
if (installStatus != 0) {
LOG.warn("Invalid watchface install status {} for {}", installStatus, fwHelper.getId());
return;
}
LOG.debug("Watchface install status 0, uploading");
setDeviceBusy();
getSupport().getDataUploadService().setCallback(this);
getSupport().getDataUploadService().requestUpload(XiaomiDataUploadService.TYPE_WATCHFACE, fwHelper.getBytes());
return;
}
LOG.warn("Unknown watchface command {}", cmd.getSubtype());
}
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());
allWatchfaces.clear();
userWatchfaces.clear();
activeWatchface = null;
final List gbDeviceApps = new ArrayList<>();
for (final XiaomiProto.WatchfaceInfo watchface : watchfaceList.getWatchfaceList()) {
final UUID uuid = toWatchfaceUUID(watchface.getId());
allWatchfaces.add(uuid);
if (watchface.getCanDelete()) {
userWatchfaces.add(uuid);
}
if (watchface.getActive()) {
activeWatchface = uuid;
}
GBDeviceApp gbDeviceApp = new GBDeviceApp(
uuid,
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) {
final String id = toWatchfaceId(uuid);
// TODO for now we need to allow when installing a watchface
//if (!allWatchfaces.contains(uuid)) {
// LOG.warn("Unknown watchface {}", uuid);
// return;
//}
activeWatchface = uuid;
LOG.debug("Set watchface to {}", id);
getSupport().sendCommand(
"set watchface to " + uuid,
XiaomiProto.Command.newBuilder()
.setType(COMMAND_TYPE)
.setSubtype(CMD_WATCHFACE_SET)
.setWatchface(XiaomiProto.Watchface.newBuilder().setWatchfaceId(id))
.build()
);
}
public void setWatchface(final String watchfaceId) {
setWatchface(toWatchfaceUUID(watchfaceId));
}
public void deleteWatchface(final UUID uuid) {
final String id = toWatchfaceId(uuid);
if (!userWatchfaces.contains(uuid)) {
LOG.warn("Refusing to delete non-user watchface {}", id);
return;
}
if (!allWatchfaces.contains(uuid)) {
LOG.warn("Refusing to delete unknown watchface {}", id);
return;
}
if (uuid.equals(activeWatchface)) {
LOG.warn("Refusing to delete active watchface {}", id);
return;
}
LOG.debug("Delete watchface {}", id);
allWatchfaces.remove(uuid);
userWatchfaces.remove(uuid);
getSupport().sendCommand(
"delete watchface " + id,
XiaomiProto.Command.newBuilder()
.setType(COMMAND_TYPE)
.setSubtype(CMD_WATCHFACE_DELETE)
.setWatchface(XiaomiProto.Watchface.newBuilder().setWatchfaceId(id))
.build()
);
}
public void installWatchface(final XiaomiFWHelper fwHelper) {
assert fwHelper.isValid();
assert fwHelper.isWatchface();
this.fwHelper = fwHelper;
getSupport().sendCommand(
"install watchface " + fwHelper.getId(),
XiaomiProto.Command.newBuilder()
.setType(COMMAND_TYPE)
.setSubtype(CMD_WATCHFACE_INSTALL)
.setWatchface(XiaomiProto.Watchface.newBuilder().setWatchfaceInstallStart(
XiaomiProto.WatchfaceInstallStart.newBuilder()
.setId(fwHelper.getId())
.setSize(fwHelper.getBytes().length)
))
.build()
);
}
public static UUID toWatchfaceUUID(final String id) {
// Watchface IDs are numbers as strings - pad them to the right with F
// and encode as UUID
final String padded = String.format("%-32s", id).replace(' ', 'F');
return UUID.fromString(
padded.substring(0, 8) + "-" +
padded.substring(8, 12) + "-" +
padded.substring(12, 16) + "-" +
padded.substring(16, 20) + "-" +
padded.substring(20, 32)
);
}
public static String toWatchfaceId(final UUID uuid) {
return uuid.toString()
.replaceAll("-", "")
.replaceAll("f", "")
.replaceAll("F", "");
}
@Override
public void onUploadFinish(final boolean success) {
final int notificationMessage = success ?
R.string.uploadwatchfaceoperation_complete :
R.string.uploadwatchfaceoperation_failed;
onUploadProgress(notificationMessage, 100, false);
if (getSupport().getConnectionSpecificSupport() != null) {
getSupport().getConnectionSpecificSupport().runOnQueue("watchface upload finish", () -> {
LOG.debug("Watchface upload finished: {}", success);
getSupport().getDataUploadService().setCallback(null);
unsetDeviceBusy();
if (success) {
setWatchface(fwHelper.getId());
requestWatchfaceList();
}
fwHelper = null;
});
}
}
@Override
public void onUploadProgress(final int progressPercent) {
onUploadProgress(R.string.uploadwatchfaceoperation_in_progress, progressPercent, true);
}
private void onUploadProgress(final int stringResource, final int progressPercent, final boolean ongoing) {
if (getSupport().getConnectionSpecificSupport() != null)
getSupport().getConnectionSpecificSupport().onUploadProgress(stringResource, progressPercent, ongoing);
}
private void setDeviceBusy() {
final GBDevice device = getSupport().getDevice();
device.setBusyTask(getSupport().getContext().getString(R.string.uploading_watchface));
device.sendDeviceUpdateIntent(getSupport().getContext());
}
private void unsetDeviceBusy() {
final GBDevice device = getSupport().getDevice();
if (device != null && device.isConnected()) {
if (device.isBusy()) {
device.unsetBusyTask();
device.sendDeviceUpdateIntent(getSupport().getContext());
}
device.sendDeviceUpdateIntent(getSupport().getContext());
}
}
}