Zepp OS: Music upload (unstable)

- Only works in some regions apparently (eg. uk)
- Very unstable for files larger than 2MB
This commit is contained in:
José Rebelo
2025-04-02 20:27:38 +01:00
parent 9744f6d22f
commit e8d4558b0a
7 changed files with 297 additions and 0 deletions
@@ -67,6 +67,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.service
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsPhoneService; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsPhoneService;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsRemindersService; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsRemindersService;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsShortcutCardsService; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsShortcutCardsService;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.filetransfer.ZeppOsFileTransferImpl;
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils; import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
public abstract class ZeppOsCoordinator extends HuamiCoordinator { public abstract class ZeppOsCoordinator extends HuamiCoordinator {
@@ -122,6 +123,11 @@ public abstract class ZeppOsCoordinator extends HuamiCoordinator {
return mapsInstallHandler; return mapsInstallHandler;
} }
final ZeppOsMusicInstallHandler musicInstallHandler = new ZeppOsMusicInstallHandler(uri, context);
if (musicInstallHandler.isValid()) {
return musicInstallHandler;
}
final ZeppOsFwInstallHandler fwInstallHandler = new ZeppOsFwInstallHandler( final ZeppOsFwInstallHandler fwInstallHandler = new ZeppOsFwInstallHandler(
uri, uri,
context, context,
@@ -600,6 +606,13 @@ public abstract class ZeppOsCoordinator extends HuamiCoordinator {
return supportsDisplayItem(device, "map"); return supportsDisplayItem(device, "map");
} }
public boolean supportsMusicUpload(final GBDevice device) {
return supportsDisplayItem(device, "music") &&
getPrefs(device)
.getStringSet(ZeppOsFileTransferImpl.PREF_SUPPORTED_SERVICES, Collections.emptySet())
.contains("music");
}
private boolean supportsConfig(final GBDevice device, final ZeppOsConfigService.ConfigArg config) { private boolean supportsConfig(final GBDevice device, final ZeppOsConfigService.ConfigArg config) {
return ZeppOsConfigService.deviceHasConfig(getPrefs(device), config); return ZeppOsConfigService.deviceHasConfig(getPrefs(device), config);
} }
@@ -0,0 +1,136 @@
/* 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 androidx.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
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.util.FileUtils;
import nodomain.freeyourgadget.gadgetbridge.util.UriHelper;
import nodomain.freeyourgadget.gadgetbridge.util.audio.AudioInfo;
import nodomain.freeyourgadget.gadgetbridge.util.audio.MusicUtils;
public class ZeppOsMusicInstallHandler implements InstallHandler {
private static final Logger LOG = LoggerFactory.getLogger(ZeppOsMusicInstallHandler.class);
protected final Context mContext;
private final Uri mUri;
private final AudioInfo mAudioInfo;
public ZeppOsMusicInstallHandler(final Uri uri, final Context context) {
this.mContext = context;
this.mUri = uri;
this.mAudioInfo = MusicUtils.audioInfoFromUri(context, uri);
}
@Override
public boolean isValid() {
return mAudioInfo != null &&
"audio/mpeg".equals(mAudioInfo.getMimeType()) &&
mAudioInfo.getExtension().equals("mp3");
}
@Override
public void validateInstallation(final InstallActivity installActivity, final GBDevice device) {
if (device.isBusy()) {
installActivity.setInfoText(device.getBusyTask());
installActivity.setInstallEnabled(false);
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);
return;
}
final ZeppOsCoordinator zeppOsCoordinator = (ZeppOsCoordinator) coordinator;
if (!zeppOsCoordinator.supportsMusicUpload(device)) {
installActivity.setInfoText(mContext.getString(R.string.fwapp_install_device_not_supported));
installActivity.setInstallEnabled(false);
return;
}
if (!device.isInitialized()) {
installActivity.setInfoText(mContext.getString(R.string.fwapp_install_device_not_ready));
installActivity.setInstallEnabled(false);
return;
}
final GenericItem fwItem = createInstallItem(device);
fwItem.setIcon(coordinator.getDefaultIconResource());
installActivity.setInfoText(mContext.getString(R.string.fw_upgrade_notice, mContext.getString(R.string.menuitem_music)));
installActivity.setInstallItem(fwItem);
installActivity.setInstallEnabled(true);
}
@Override
public void onStartInstall(final GBDevice device) {
}
public AudioInfo getAudioInfo() {
return mAudioInfo;
}
@Nullable
public byte[] readFileBytes() {
final UriHelper uriHelper;
try {
uriHelper = UriHelper.get(mUri, mContext);
} catch (final IOException e) {
LOG.error("Failed to get uri", e);
return null;
}
try (InputStream in = new BufferedInputStream(uriHelper.openInputStream())) {
// FIXME we should be able to stream this
return FileUtils.readAll(in, 50 * 1024 * 1024); // 50MB
} catch (final Exception e) {
LOG.error("Failed to read music file", e);
}
return null;
}
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_music),
mAudioInfo.getFileName()
);
return new GenericItem(firmwareName);
}
}
@@ -90,6 +90,7 @@ import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventScreenshot
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventSilentMode; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventSilentMode;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdatePreferences; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdatePreferences;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.zeppos.ZeppOsMapsInstallHandler; import nodomain.freeyourgadget.gadgetbridge.devices.huami.zeppos.ZeppOsMapsInstallHandler;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.zeppos.ZeppOsMusicInstallHandler;
import nodomain.freeyourgadget.gadgetbridge.service.SleepAsAndroidSender; import nodomain.freeyourgadget.gadgetbridge.service.SleepAsAndroidSender;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiFWHelper; import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiFWHelper;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.zeppos.ZeppOsCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.huami.zeppos.ZeppOsCoordinator;
@@ -129,6 +130,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.upd
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.operations.ZeppOsFirmwareUpdateOperation; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.operations.ZeppOsFirmwareUpdateOperation;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.operations.ZeppOsAgpsUpdateOperation; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.operations.ZeppOsAgpsUpdateOperation;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.operations.ZeppOsGpxRouteUploadOperation; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.operations.ZeppOsGpxRouteUploadOperation;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.operations.ZeppOsMusicUploadOperation;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsAgpsService; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsAgpsService;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsAlarmsService; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsAlarmsService;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsAssistantService; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsAssistantService;
@@ -739,6 +741,26 @@ public class ZeppOsSupport extends HuamiSupport implements ZeppOsFileTransferSer
return; return;
} }
final ZeppOsMusicInstallHandler musicHandler = new ZeppOsMusicInstallHandler(uri, getContext());
if (musicHandler.isValid()) {
try {
final byte[] musicBytes = musicHandler.readFileBytes();
if (musicBytes == null) {
return;
}
new ZeppOsMusicUploadOperation(
this,
musicHandler.getAudioInfo(),
musicBytes,
fileTransferService
).perform();
} catch (final Exception e) {
GB.toast(getContext(), "Music install error: " + e.getMessage(), Toast.LENGTH_LONG, GB.ERROR, e);
}
return;
}
final ZeppOsMapsInstallHandler mapsHandler = new ZeppOsMapsInstallHandler(uri, getContext()); final ZeppOsMapsInstallHandler mapsHandler = new ZeppOsMapsInstallHandler(uri, getContext());
if (mapsHandler.isValid()) { if (mapsHandler.isValid()) {
mapsService.upload(mapsHandler.getFile()); mapsService.upload(mapsHandler.getFile());
@@ -0,0 +1,114 @@
/* 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 <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.operations;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.Locale;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEOperation;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetProgressAction;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.ZeppOsSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsFileTransferService;
import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.operations.OperationStatus;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.util.audio.AudioInfo;
public class ZeppOsMusicUploadOperation extends AbstractBTLEOperation<ZeppOsSupport>
implements ZeppOsFileTransferService.Callback {
private static final Logger LOG = LoggerFactory.getLogger(ZeppOsMusicUploadOperation.class);
private final AudioInfo audioInfo;
private final byte[] fileBytes;
private final ZeppOsFileTransferService fileTransferService;
public ZeppOsMusicUploadOperation(final ZeppOsSupport support,
final AudioInfo audioInfo,
final byte[] fileBytes,
final ZeppOsFileTransferService fileTransferService) {
super(support);
this.audioInfo = audioInfo;
this.fileBytes = fileBytes;
this.fileTransferService = fileTransferService;
}
@Override
protected void doPerform() throws IOException {
fileTransferService.sendFile(
String.format(
Locale.ROOT,
"music://file?songName=%s&singer=%s&end=1",
audioInfo.getTitle(),
audioInfo.getArtist()
),
audioInfo.getFileName(),
fileBytes,
false,
this
);
}
@Override
protected void operationFinished() {
operationStatus = OperationStatus.FINISHED;
if (getDevice() != null && getDevice().isConnected()) {
unsetBusy();
getDevice().sendDeviceUpdateIntent(getContext());
}
}
@Override
public void onFileUploadFinish(final boolean success) {
LOG.info("Finished music upload operation, success={}", success);
final String notificationMessage = success ?
getContext().getString(R.string.music_upload_complete) :
getContext().getString(R.string.music_upload_failed);
GB.updateInstallNotification(notificationMessage, false, 100, getContext());
operationFinished();
}
@Override
public void onFileUploadProgress(final int progress) {
LOG.trace("Music upload operation progress: {}", progress);
final int progressPercent = (int) ((((float) (progress)) / fileBytes.length) * 100);
updateProgress(progressPercent);
}
@Override
public void onFileDownloadFinish(final String url, final String filename, final byte[] data) {
LOG.warn("Received unexpected file: url={} filename={} length={}", url, filename, data.length);
}
private void updateProgress(final int progressPercent) {
try {
final TransactionBuilder builder = performInitialized("send music upload progress");
builder.add(new SetProgressAction(getContext().getString(R.string.music_upload_in_progress), true, progressPercent, getContext()));
builder.queue(getQueue());
} catch (final Exception e) {
LOG.error("Failed to update progress notification", e);
}
}
}
@@ -11,11 +11,13 @@ import java.io.ByteArrayOutputStream;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.ByteOrder; import java.nio.ByteOrder;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.zip.DataFormatException; import java.util.zip.DataFormatException;
import java.util.zip.Deflater; import java.util.zip.Deflater;
import java.util.zip.Inflater; import java.util.zip.Inflater;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdatePreferences;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiService; import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiService;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.ZeppOsSupport; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.ZeppOsSupport;
@@ -39,6 +41,8 @@ public abstract class ZeppOsFileTransferImpl {
protected static final byte FLAG_LAST_CHUNK = 0x02; protected static final byte FLAG_LAST_CHUNK = 0x02;
protected static final byte FLAG_CRC = 0x04; protected static final byte FLAG_CRC = 0x04;
public static final String PREF_SUPPORTED_SERVICES = "zepp_os_file_transfer_supported_service";
protected final ZeppOsFileTransferService mFileTransferService; protected final ZeppOsFileTransferService mFileTransferService;
protected final ZeppOsSupport mSupport; protected final ZeppOsSupport mSupport;
@@ -108,6 +112,7 @@ public abstract class ZeppOsFileTransferImpl {
for (int i = 0; i < numServices; i++) { for (int i = 0; i < numServices; i++) {
supportedServices.add(StringUtils.untilNullTerminator(buf)); supportedServices.add(StringUtils.untilNullTerminator(buf));
} }
// TODO: 3 unknown bytes for v3 // TODO: 3 unknown bytes for v3
final TransactionBuilder builder = mSupport.createTransactionBuilder("enable file transfer v3 notifications"); final TransactionBuilder builder = mSupport.createTransactionBuilder("enable file transfer v3 notifications");
@@ -116,6 +121,8 @@ public abstract class ZeppOsFileTransferImpl {
builder.queue(mSupport.getQueue()); builder.queue(mSupport.getQueue());
} }
mSupport.evaluateGBDeviceEvent(new GBDeviceEventUpdatePreferences(PREF_SUPPORTED_SERVICES, new HashSet<>(supportedServices)));
LOG.info( LOG.info(
"Got file transfer service: version={}, chunkSize={}, compressedChunkSize={}, supportedServices=[{}]", "Got file transfer service: version={}, chunkSize={}, compressedChunkSize={}, supportedServices=[{}]",
mVersion, mVersion,
@@ -41,6 +41,8 @@ public class ZeppOsFileTransferV3 extends ZeppOsFileTransferImpl {
private static final byte CMD_DATA_V3_SEND = 0x12; private static final byte CMD_DATA_V3_SEND = 0x12;
private static final byte CMD_DATA_V3_ACK = 0x13; private static final byte CMD_DATA_V3_ACK = 0x13;
private static final byte CMD_CANCEL_TRANSFER_REQ = 0x20; // 20:01
private static final byte CMD_CANCEL_TRANSFER_ACK = 0x21; // 21:01:00
private static final long TRANSFER_TIMEOUT_THRESHOLD_MILLIS = 5_000L; private static final long TRANSFER_TIMEOUT_THRESHOLD_MILLIS = 5_000L;
+3
View File
@@ -952,6 +952,9 @@
<string name="map_upload_failed">Map upload failed</string> <string name="map_upload_failed">Map upload failed</string>
<string name="map_upload_complete">Map upload complete</string> <string name="map_upload_complete">Map upload complete</string>
<string name="map_upload_in_progress">Uploading map</string> <string name="map_upload_in_progress">Uploading map</string>
<string name="music_upload_failed">Music upload failed</string>
<string name="music_upload_complete">Music upload complete</string>
<string name="music_upload_in_progress">Uploading music</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="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="chart_steps">Steps</string>
<string name="calories">Calories</string> <string name="calories">Calories</string>