Zepp OS: Maps upload

This commit is contained in:
José Rebelo
2025-03-24 19:36:46 +00:00
parent f42eb01811
commit efc06fe708
7 changed files with 562 additions and 26 deletions
@@ -117,13 +117,18 @@ public abstract class ZeppOsCoordinator extends HuamiCoordinator {
}
}
final ZeppOsFwInstallHandler handler = new ZeppOsFwInstallHandler(
final ZeppOsMapsInstallHandler mapsInstallHandler = new ZeppOsMapsInstallHandler(uri, context);
if (mapsInstallHandler.isValid()) {
return mapsInstallHandler;
}
final ZeppOsFwInstallHandler fwInstallHandler = new ZeppOsFwInstallHandler(
uri,
context,
getDeviceBluetoothName(),
getDeviceSources()
);
return handler.isValid() ? handler : null;
return fwInstallHandler.isValid() ? fwInstallHandler : null;
}
@Override
@@ -591,6 +596,10 @@ public abstract class ZeppOsCoordinator extends HuamiCoordinator {
return experimentalFeatures(device) && ZeppOsAssistantService.isSupported(getPrefs(device));
}
public boolean supportsMaps(final GBDevice device) {
return supportsDisplayItem(device, "map");
}
private boolean supportsConfig(final GBDevice device, final ZeppOsConfigService.ConfigArg config) {
return ZeppOsConfigService.deviceHasConfig(getPrefs(device), config);
}
@@ -0,0 +1,112 @@
/* Copyright (C) 2025 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 <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.huami.zeppos;
import android.content.Context;
import android.net.Uri;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.operations.ZeppOsMapsFile;
public class ZeppOsMapsInstallHandler implements InstallHandler {
private static final Logger LOG = LoggerFactory.getLogger(ZeppOsMapsInstallHandler.class);
protected final Context mContext;
private final ZeppOsMapsFile file;
public ZeppOsMapsInstallHandler(final Uri uri, final Context context) {
this.mContext = context;
file = new ZeppOsMapsFile(uri, context);
}
@Override
public boolean isValid() {
return file.isValid();
}
@Override
public void validateInstallation(final InstallActivity installActivity, final GBDevice device) {
if (device.isBusy()) {
installActivity.setInfoText(device.getBusyTask());
installActivity.setInstallEnabled(false);
installActivity.setCloseEnabled(true);
return;
}
final DeviceCoordinator coordinator = device.getDeviceCoordinator();
if (!(coordinator instanceof ZeppOsCoordinator)) {
LOG.warn("Coordinator is not a ZeppOsCoordinator: {}", coordinator.getClass());
installActivity.setInfoText(mContext.getString(R.string.fwapp_install_device_not_supported));
installActivity.setInstallEnabled(false);
installActivity.setCloseEnabled(true);
return;
}
final ZeppOsCoordinator zeppOsCoordinator = (ZeppOsCoordinator) coordinator;
if (!zeppOsCoordinator.supportsMaps(device) || zeppOsCoordinator.supportsWifiHotspot(device)) {
installActivity.setInfoText(mContext.getString(R.string.fwapp_install_device_not_supported));
installActivity.setInstallEnabled(false);
installActivity.setCloseEnabled(true);
return;
}
if (!device.isInitialized()) {
installActivity.setInfoText(mContext.getString(R.string.fwapp_install_device_not_ready));
installActivity.setInstallEnabled(false);
installActivity.setCloseEnabled(true);
return;
}
final GenericItem fwItem = createInstallItem(device);
fwItem.setIcon(coordinator.getDefaultIconResource());
final StringBuilder builder = new StringBuilder();
final String map = mContext.getString(R.string.menuitem_map);
builder.append(mContext.getString(R.string.fw_upgrade_notice, map));
builder.append("\n\n").append(mContext.getString(R.string.zepp_os_install_map_instructions));
installActivity.setInfoText(builder.toString());
installActivity.setInstallItem(fwItem);
installActivity.setInstallEnabled(true);
}
@Override
public void onStartInstall(final GBDevice device) {
}
public ZeppOsMapsFile getFile() {
return file;
}
private GenericItem createInstallItem(final GBDevice device) {
DeviceCoordinator coordinator = device.getDeviceCoordinator();
final String firmwareName = mContext.getString(
R.string.installhandler_firmware_name,
mContext.getString(coordinator.getDeviceNameResource()),
mContext.getString(R.string.menuitem_map),
""
);
return new GenericItem(firmwareName);
}
}
@@ -89,6 +89,7 @@ import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventFindPhone;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventScreenshot;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventSilentMode;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdatePreferences;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.zeppos.ZeppOsMapsInstallHandler;
import nodomain.freeyourgadget.gadgetbridge.service.SleepAsAndroidSender;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiFWHelper;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.zeppos.ZeppOsCoordinator;
@@ -138,6 +139,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.service
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsHttpService;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsLogsService;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsLoyaltyCardService;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsMapsService;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsMusicService;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsNotificationService;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsRemindersService;
@@ -190,10 +192,11 @@ public class ZeppOsSupport extends HuamiSupport implements ZeppOsFileTransferSer
private final ZeppOsAppsService appsService = new ZeppOsAppsService(this);
private final ZeppOsLogsService logsService = new ZeppOsLogsService(this);
private final ZeppOsDisplayItemsService displayItemsService = new ZeppOsDisplayItemsService(this);
private final ZeppOsHttpService httpService = new ZeppOsHttpService(this);
private final ZeppOsHttpService httpService = new ZeppOsHttpService(this, fileTransferService);
private final ZeppOsRemindersService remindersService = new ZeppOsRemindersService(this);
private final ZeppOsLoyaltyCardService loyaltyCardService = new ZeppOsLoyaltyCardService(this);
private final ZeppOsVoiceMemosService voiceMemosService = new ZeppOsVoiceMemosService(this);
private final ZeppOsMapsService mapsService = new ZeppOsMapsService(this, httpService);
private final ZeppOsMusicService musicService = new ZeppOsMusicService(this);
private final Set<Short> mSupportedServices = new HashSet<>();
@@ -225,6 +228,7 @@ public class ZeppOsSupport extends HuamiSupport implements ZeppOsFileTransferSer
put(loyaltyCardService.getEndpoint(), loyaltyCardService);
put(musicService.getEndpoint(), musicService);
put(voiceMemosService.getEndpoint(), voiceMemosService);
put(mapsService.getEndpoint(), mapsService);
}};
public ZeppOsSupport() {
@@ -735,6 +739,12 @@ public class ZeppOsSupport extends HuamiSupport implements ZeppOsFileTransferSer
return;
}
final ZeppOsMapsInstallHandler mapsHandler = new ZeppOsMapsInstallHandler(uri, getContext());
if (mapsHandler.isValid()) {
mapsService.upload(mapsHandler.getFile());
return;
}
try {
new ZeppOsFirmwareUpdateOperation(uri, this).perform();
} catch (final IOException ex) {
@@ -0,0 +1,112 @@
/* Copyright (C) 2025 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 <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.operations;
import android.content.Context;
import android.net.Uri;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.InputStream;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import nodomain.freeyourgadget.gadgetbridge.util.ArrayUtils;
import nodomain.freeyourgadget.gadgetbridge.util.UriHelper;
public class ZeppOsMapsFile {
private static final Logger LOG = LoggerFactory.getLogger(ZeppOsMapsFile.class);
private final Pattern imgPathPattern = Pattern.compile("^11/([0-9]+)/([0-9]+).img$");
private static final byte[] SIGNATURE = {'D', 'S', 'K', 'I', 'M', 'G', '\0'};
private final Uri uri;
private final Context context;
private long fileSize = 0;
private long uncompressedSize = 0;
public ZeppOsMapsFile(final Uri uri, final Context context) {
this.uri = uri;
this.context = context;
}
public boolean isValid() {
final UriHelper uriHelper;
try {
uriHelper = UriHelper.get(uri, context);
} catch (final IOException e) {
LOG.error("Failed to create urihelper", e);
return false;
}
fileSize = uriHelper.getFileSize();
try (InputStream is = context.getContentResolver().openInputStream(uri); ZipInputStream zis = new ZipInputStream(is)) {
ZipEntry zipEntry;
while ((zipEntry = zis.getNextEntry()) != null) {
if (zipEntry.isDirectory()) {
continue;
}
final Matcher matcher = imgPathPattern.matcher(zipEntry.getName());
if (!matcher.find()) {
LOG.error("Unknown file {}", zipEntry.getName());
}
final int num1 = Integer.parseInt(Objects.requireNonNull(matcher.group(1)), 10);
final int num2 = Integer.parseInt(Objects.requireNonNull(matcher.group(2)), 10);
if (num1 > 2048 || num2 > 2048) {
LOG.error("Invalid path {}", zipEntry.getName());
return false;
}
final byte[] header = new byte[512];
final int read = zis.read(header, 0, 512);
if (read < 512 || !ArrayUtils.equals(header, SIGNATURE, 0x10)) {
LOG.error("DSKIMG signature not found in {}", zipEntry.getName());
return false;
}
uncompressedSize += zipEntry.getSize();
}
} catch (final IOException e) {
LOG.error("Failed to read {}", uri, e);
return false;
}
return true;
}
public Uri getUri() {
return uri;
}
public long getFileSize() {
return fileSize;
}
public long getUncompressedSize() {
return uncompressedSize;
}
}
@@ -1,4 +1,4 @@
/* Copyright (C) 2023-2024 José Rebelo
/* Copyright (C) 2023-2025 José Rebelo
This file is part of Gadgetbridge.
@@ -16,21 +16,28 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services;
import android.net.Uri;
import androidx.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLDecoder;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.ZeppOsSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.ZeppOsWeather;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.AbstractZeppOsService;
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
import nodomain.freeyourgadget.gadgetbridge.util.HttpUtils;
import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
@@ -39,14 +46,26 @@ public class ZeppOsHttpService extends AbstractZeppOsService {
private static final short ENDPOINT = 0x0001;
public static final byte CMD_REQUEST = 0x01;
public static final byte CMD_RESPONSE = 0x02;
public static final byte CMD_SIMPLE_REQUEST = 0x01;
public static final byte CMD_SIMPLE_RESPONSE = 0x02;
public static final byte CMD_RAW_DOWNLOAD_REQUEST = 0x03;
public static final byte CMD_RAW_DOWNLOAD_START = 0x04;
public static final byte CMD_RAW_DOWNLOAD_FINISH = 0x05;
public static final byte RESPONSE_SUCCESS = 0x01;
public static final byte RESPONSE_NO_INTERNET = 0x02;
public ZeppOsHttpService(final ZeppOsSupport support) {
/**
* A map from url to local file URI that will be served to the watch.
*/
private final Map<String, Uri> urlToLocalFile = new HashMap<>();
private final Map<String, Callback> urlDownloadCallbacks = new HashMap<>();
private final ZeppOsFileTransferService fileTransferService;
public ZeppOsHttpService(final ZeppOsSupport support, final ZeppOsFileTransferService fileTransferService) {
super(support, true);
this.fileTransferService = fileTransferService;
}
@Override
@@ -56,33 +75,57 @@ public class ZeppOsHttpService extends AbstractZeppOsService {
@Override
public void handlePayload(final byte[] payload) {
final ByteBuffer buf = ByteBuffer.wrap(payload).order(ByteOrder.LITTLE_ENDIAN);
buf.get(); // discard command byte
switch (payload[0]) {
case CMD_REQUEST:
int pos = 1;
final byte requestId = payload[pos++];
final String method = StringUtils.untilNullTerminator(payload, pos);
case CMD_SIMPLE_REQUEST: {
final int requestId = buf.get() & 0xff;
final String method = StringUtils.untilNullTerminator(buf);
if (method == null) {
LOG.error("Failed to decode method from payload");
replyHttpNoInternet(requestId);
return;
}
pos += method.length() + 1;
final String url = StringUtils.untilNullTerminator(payload, pos);
final String url = StringUtils.untilNullTerminator(buf);
if (url == null) {
LOG.error("Failed to decode method from payload");
LOG.error("Failed to decode url from payload");
return;
}
// headers after pos += url.length() + 1;
// headers after, but we ignore them
LOG.info("Got HTTP {} request: {}", method, url);
LOG.info("Got simple HTTP {} request: {}", method, url);
handleUrlRequest(requestId, method, url);
return;
}
case CMD_RAW_DOWNLOAD_REQUEST: {
final int requestId = buf.get() & 0xff;
final String url = StringUtils.untilNullTerminator(buf);
if (url == null) {
LOG.error("Failed to decode raw download url from payload");
return;
}
// headers after, but we ignore them
LOG.info("Got raw download HTTP request: {}", url);
handleRawDownloadRequest(requestId, url);
return;
}
default:
LOG.warn("Unexpected HTTP payload byte {}", String.format("0x%02x", payload[0]));
}
}
private void handleUrlRequest(final byte requestId, final String method, final String urlString) {
public void registerForDownload(final String url, final Uri uri, @Nullable final Callback callback) {
this.urlToLocalFile.put(url, uri);
this.urlDownloadCallbacks.put(url, callback);
}
private void handleUrlRequest(final int requestId, final String method, final String urlString) {
if (!"GET".equals(method)) {
LOG.error("Unable to handle HTTP method {}", method);
// TODO: There's probably a "BAD REQUEST" response or similar
@@ -94,7 +137,7 @@ public class ZeppOsHttpService extends AbstractZeppOsService {
try {
url = new URL(urlString);
} catch (final MalformedURLException e) {
LOG.error("Failed to parse url", e);
LOG.error("Failed to parse simple request url", e);
replyHttpNoInternet(requestId);
return;
}
@@ -104,31 +147,119 @@ public class ZeppOsHttpService extends AbstractZeppOsService {
if (path.startsWith("/weather/")) {
final ZeppOsWeather.Response response = ZeppOsWeather.handleHttpRequest(path, query);
replyHttpSuccess(requestId, response.getHttpStatusCode(), response.toJson());
replySimpleHttpSuccess(requestId, response.getHttpStatusCode(), response.toJson());
return;
}
LOG.error("Unhandled URL {}", url);
LOG.error("Unhandled simple request URL {}", url);
replyHttpNoInternet(requestId);
}
private void replyHttpNoInternet(final byte requestId) {
private void handleRawDownloadRequest(final int requestId, final String urlString) {
final Uri uri = urlToLocalFile.remove(urlString);
if (uri == null) {
LOG.error("Unhandled raw download URL {}", urlString);
replyHttpNoInternet(requestId);
return;
}
final Callback downloadCallback = urlDownloadCallbacks.remove(urlString);
final URL url;
try {
url = new URL(urlString);
} catch (final MalformedURLException e) {
LOG.error("Failed to parse raw download url", e);
replyHttpNoInternet(requestId);
return;
}
final String filename = url.getPath().substring(url.getPath().lastIndexOf('/') + 1);
final byte[] rawBytes;
try (InputStream is = getContext().getContentResolver().openInputStream(uri)) {
// FIXME we probably should stream this
// with the current implementation, anything over ~2MB will overflow the chunk index
rawBytes = FileUtils.readAll(Objects.requireNonNull(is), 100 * 1024 * 1024); // 10MB
} catch (final IOException e) {
LOG.error("Failed to read local file for {}", urlString, e);
replyHttpNoInternet(requestId);
return;
}
LOG.debug("Starting raw download request {} with {} bytes", requestId, rawBytes.length);
fileTransferService.sendFile(
"httpproxy://download?sessionid=" + requestId,
filename,
rawBytes,
false,
new ZeppOsFileTransferService.Callback() {
@Override
public void onFileUploadFinish(final boolean success) {
LOG.info("Finished sending '{}' to http request id '{}', success={}", filename, requestId, success);
onRawDownloadFinish(requestId, success);
if (downloadCallback != null) {
downloadCallback.onFileDownloadFinish(success);
}
}
@Override
public void onFileUploadProgress(final int progress) {
LOG.trace("HTTP send progress: {}", progress);
if (downloadCallback != null) {
downloadCallback.onFileDownloadProgress(progress);
}
}
@Override
public void onFileDownloadFinish(final String url, final String filename, final byte[] data) {
LOG.warn("Receiver unexpected file: url={} filename={} length={}", url, filename, data.length);
}
}
);
final ByteBuffer buf = ByteBuffer.allocate(10);
buf.order(ByteOrder.LITTLE_ENDIAN);
buf.put(CMD_RAW_DOWNLOAD_START);
buf.put((byte) requestId);
buf.putInt(rawBytes.length);
buf.putInt(0); // ?
write("http raw download start", buf.array());
}
private void onRawDownloadFinish(final int requestId, final boolean success) {
LOG.debug("Download {} finished, success = {}", requestId, success);
final ByteBuffer buf = ByteBuffer.allocate(5);
buf.order(ByteOrder.LITTLE_ENDIAN);
buf.put(CMD_RAW_DOWNLOAD_FINISH);
buf.put((byte) requestId);
buf.put((byte) 1); // success?
buf.putShort((short) 200);
write("http raw download finish", buf.array());
}
private void replyHttpNoInternet(final int requestId) {
LOG.info("Replying with no internet to http request {}", requestId);
final byte[] cmd = new byte[]{CMD_RESPONSE, requestId, RESPONSE_NO_INTERNET, 0x00, 0x00, 0x00, 0x00};
final byte[] cmd = new byte[]{CMD_SIMPLE_RESPONSE, (byte) requestId, RESPONSE_NO_INTERNET, 0x00, 0x00, 0x00, 0x00};
write("http reply no internet", cmd);
}
private void replyHttpSuccess(final byte requestId, final int status, final String content) {
private void replySimpleHttpSuccess(final int requestId, final int status, final String content) {
LOG.debug("Replying with http {} request {} with {}", status, requestId, content);
final byte[] contentBytes = content.getBytes(StandardCharsets.UTF_8);
final ByteBuffer buf = ByteBuffer.allocate(8 + contentBytes.length);
buf.order(ByteOrder.LITTLE_ENDIAN);
buf.put(CMD_RESPONSE);
buf.put(requestId);
buf.put(CMD_SIMPLE_RESPONSE);
buf.put((byte) requestId);
buf.put(RESPONSE_SUCCESS);
buf.put((byte) status);
buf.putInt(contentBytes.length);
@@ -136,4 +267,10 @@ public class ZeppOsHttpService extends AbstractZeppOsService {
write("http reply success", buf.array());
}
public interface Callback {
void onFileDownloadFinish(boolean success);
void onFileDownloadProgress(int progress);
}
}
@@ -0,0 +1,152 @@
/* Copyright (C) 2025 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 <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services;
import android.content.Intent;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
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 nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.AbstractZeppOsService;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.ZeppOsSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.operations.ZeppOsMapsFile;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class ZeppOsMapsService extends AbstractZeppOsService {
private static final Logger LOG = LoggerFactory.getLogger(ZeppOsMapsService.class);
private static final short ENDPOINT = 0x0046;
private static final byte CMD_CAPABILITIES_REQUEST = 0x01;
private static final byte CMD_CAPABILITIES_RESPONSE = 0x02;
private static final byte CMD_DOWNLOAD_START_REQUEST = 0x05;
private static final byte CMD_DOWNLOAD_START_RESPONSE = 0x06;
private final ZeppOsHttpService httpService;
public ZeppOsMapsService(final ZeppOsSupport support, final ZeppOsHttpService httpService) {
super(support, true);
this.httpService = httpService;
}
@Override
public short getEndpoint() {
return ENDPOINT;
}
@Override
public void initialize(final TransactionBuilder builder) {
write(builder, CMD_CAPABILITIES_REQUEST);
}
@Override
public void handlePayload(final byte[] payload) {
final ByteBuffer buf = ByteBuffer.wrap(payload).order(ByteOrder.LITTLE_ENDIAN);
switch (buf.get()) {
case CMD_CAPABILITIES_RESPONSE:
final int version = payload[1] & 0xff; // 1 supported
final int unk1 = payload[2] & 0xff; // 1?
LOG.info("Maps version={}, unk1={}", version, unk1);
return;
case CMD_DOWNLOAD_START_RESPONSE:
LOG.info("Download start response, status = {}", payload[1]); // 1 = success
return;
}
LOG.warn("Unexpected maps byte {}", String.format("0x%02x", payload[0]));
}
public void upload(final ZeppOsMapsFile mapsFile) {
final List<String> urls = new ArrayList<>(1);
final int i = 0;
final long uncompressedSize = mapsFile.getUncompressedSize();
final String fakeUrl = String.format(
Locale.ROOT,
"https://gadgetbridge.freeyourgadget.nodomain/map/%d.zip?type=1&zipsize=%d",
i,
mapsFile.getFileSize()
);
urls.add(fakeUrl);
httpService.registerForDownload(fakeUrl, mapsFile.getUri(), new ZeppOsHttpService.Callback() {
@Override
public void onFileDownloadFinish(final boolean success) {
final String notificationMessage = success ?
getContext().getString(R.string.map_upload_complete) :
getContext().getString(R.string.map_upload_failed);
GB.updateInstallNotification(notificationMessage, false, 100, getContext());
final LocalBroadcastManager broadcastManager = LocalBroadcastManager.getInstance(getContext());
broadcastManager.sendBroadcast(new Intent(GB.ACTION_SET_INFO_TEXT).putExtra(GB.DISPLAY_MESSAGE_MESSAGE, ""));
broadcastManager.sendBroadcast(new Intent(GB.ACTION_SET_PROGRESS_TEXT).putExtra(GB.DISPLAY_MESSAGE_MESSAGE, notificationMessage));
broadcastManager.sendBroadcast(new Intent(GB.ACTION_SET_FINISHED));
}
@Override
public void onFileDownloadProgress(final int progress) {
final int progressPercent = (int) ((((float) (progress)) / mapsFile.getFileSize()) * 100);
updateProgress(progressPercent);
}
});
LOG.debug("Url for map file {}: {}", mapsFile.getUri(), fakeUrl);
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
try {
baos.write(CMD_DOWNLOAD_START_REQUEST);
baos.write(BLETypeConversions.fromUint32((int) uncompressedSize));
baos.write(BLETypeConversions.fromUint16(urls.size()));
for (final String url : urls) {
baos.write(url.getBytes(StandardCharsets.UTF_8));
baos.write(0);
}
baos.write(1); // ?
} catch (final IOException e) {
LOG.error("Failed to build command", e);
return;
}
write("upload maps", baos.toByteArray());
}
private void updateProgress(final int progressPercent) {
GB.updateInstallNotification(
getContext().getString(R.string.map_upload_in_progress),
true,
progressPercent,
getContext()
);
final LocalBroadcastManager broadcastManager = LocalBroadcastManager.getInstance(getContext());
broadcastManager.sendBroadcast(new Intent(GB.ACTION_SET_PROGRESS_BAR).putExtra(GB.PROGRESS_BAR_PROGRESS, progressPercent));
}
}
+4
View File
@@ -195,6 +195,7 @@
<string name="open_fw_installer_connect_minimum_one_device">Please connect AT LEAST ONE device you want to send the file to.</string>
<string name="open_fw_installer_connect_maximum_one_device">Please connect ONLY ONE device you want to send the file to.</string>
<string name="open_fw_installer_ensure_device_connected">Make sure that the device %s is connected</string>
<string name="zepp_os_install_map_instructions">After the installation starts on the phone, you need to accept on the watch as well.</string>
<!-- Strings related to MusicManager -->
<string name="title_activity_musicmanager">Music Manager</string>
<!-- Strings related to Settings -->
@@ -944,6 +945,9 @@
<string name="gpx_route_upload_failed">Gpx route upload failed</string>
<string name="gpx_route_upload_complete">Gpx route upload complete</string>
<string name="gpx_route_upload_in_progress">Uploading gpx route</string>
<string name="map_upload_failed">Map upload failed</string>
<string name="map_upload_complete">Map upload complete</string>
<string name="map_upload_in_progress">Uploading map</string>
<string name="updatefirmwareoperation_failed_low_mtu">Current MTU of %1$d is too low, please enable high MTU in the device settings and disconnect/re-connect the device.</string>
<string name="chart_steps">Steps</string>
<string name="calories">Calories</string>