1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-07-02 01:36:23 +02:00

Zepp OS: Add incoming file support to file transfer service

This commit is contained in:
José Rebelo 2023-06-11 15:21:39 +01:00
parent c77a5467e7
commit d38afe60c2
5 changed files with 189 additions and 55 deletions

View File

@ -96,15 +96,11 @@ import nodomain.freeyourgadget.gadgetbridge.model.Contact;
import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec;
import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec;
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
import nodomain.freeyourgadget.gadgetbridge.model.RecordedDataTypes;
import nodomain.freeyourgadget.gadgetbridge.model.Reminder;
import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
import nodomain.freeyourgadget.gadgetbridge.service.btle.GattCharacteristic;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.FetchActivityOperation;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.FetchSportsSummaryOperation;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.HuamiFetchDebugLogsOperation;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.UpdateFirmwareOperation;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.UpdateFirmwareOperation2021;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.AbstractZeppOsService;
@ -120,7 +116,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.service
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.zeppos.services.ZeppOsContactsService;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsFileUploadService;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsFileTransferService;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsFtpServerService;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsMorningUpdatesService;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsPhoneService;
@ -133,7 +129,7 @@ import nodomain.freeyourgadget.gadgetbridge.util.MapUtils;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
public abstract class Huami2021Support extends HuamiSupport {
public abstract class Huami2021Support extends HuamiSupport implements ZeppOsFileTransferService.Callback {
private static final Logger LOG = LoggerFactory.getLogger(Huami2021Support.class);
// Tracks whether realtime HR monitoring is already started, so we can just
@ -141,7 +137,7 @@ public abstract class Huami2021Support extends HuamiSupport {
private boolean heartRateRealtimeStarted;
// Services
private final ZeppOsFileUploadService fileUploadService = new ZeppOsFileUploadService(this);
private final ZeppOsFileTransferService fileTransferService = new ZeppOsFileTransferService(this);
private final ZeppOsConfigService configService = new ZeppOsConfigService(this);
private final ZeppOsAgpsService agpsService = new ZeppOsAgpsService(this);
private final ZeppOsWifiService wifiService = new ZeppOsWifiService(this);
@ -154,12 +150,12 @@ public abstract class Huami2021Support extends HuamiSupport {
private final ZeppOsAlarmsService alarmsService = new ZeppOsAlarmsService(this);
private final ZeppOsCalendarService calendarService = new ZeppOsCalendarService(this);
private final ZeppOsCannedMessagesService cannedMessagesService = new ZeppOsCannedMessagesService(this);
private final ZeppOsNotificationService notificationService = new ZeppOsNotificationService(this, fileUploadService);
private final ZeppOsNotificationService notificationService = new ZeppOsNotificationService(this, fileTransferService);
private final ZeppOsAlexaService alexaService = new ZeppOsAlexaService(this);
private final ZeppOsAppsService appsService = new ZeppOsAppsService(this);
private final Map<Short, AbstractZeppOsService> mServiceMap = new LinkedHashMap<Short, AbstractZeppOsService>() {{
put(fileUploadService.getEndpoint(), fileUploadService);
put(fileTransferService.getEndpoint(), fileTransferService);
put(configService.getEndpoint(), configService);
put(agpsService.getEndpoint(), agpsService);
put(wifiService.getEndpoint(), wifiService);
@ -645,7 +641,7 @@ public abstract class Huami2021Support extends HuamiSupport {
this,
agpsHandler.getFile(),
agpsService,
fileUploadService,
fileTransferService,
configService
).perform();
} catch (final Exception e) {
@ -661,7 +657,7 @@ public abstract class Huami2021Support extends HuamiSupport {
new ZeppOsGpxRouteUploadOperation(
this,
gpxRouteHandler.getFile(),
fileUploadService
fileTransferService
).perform();
} catch (final Exception e) {
GB.toast(getContext(), "Gpx route file cannot be installed: " + e.getMessage(), Toast.LENGTH_LONG, GB.ERROR, e);
@ -1868,6 +1864,21 @@ public abstract class Huami2021Support extends HuamiSupport {
}
}
@Override
public void onFileUploadFinish(final boolean success) {
LOG.warn("Unexpected file upload finish: {}", success);
}
@Override
public void onFileUploadProgress(final int progress) {
LOG.warn("Unexpected file upload progress: {}", progress);
}
@Override
public void onFileDownloadFinish(final String url, final String filename, final byte[] data) {
LOG.info("File received: url={} filename={} length={}", url, filename, data.length);
}
private byte bool(final boolean b) {
return (byte) (b ? 1 : 0);
}

View File

@ -28,20 +28,20 @@ import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetProgressActi
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.Huami2021Support;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsAgpsService;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsConfigService;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsFileUploadService;
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;
/**
* Updates the AGPS EPO on a Zepp OS device. Update goes as follows:
* 1. Request an upload start from {@link ZeppOsAgpsService}
* 2. After successful ack from 1, upload the file to agps://upgrade using {@link ZeppOsFileUploadService}
* 2. After successful ack from 1, upload the file to agps://upgrade using {@link ZeppOsFileTransferService}
* 3. After successful ack from 2, trigger the actual update with {@link ZeppOsAgpsService}
* 4. After successful ack from 3, update is finished. Trigger an AGPS config request from {@link ZeppOsConfigService}
* to reload the AGPS update and expiration timestamps.
*/
public class ZeppOsAgpsUpdateOperation extends AbstractBTLEOperation<Huami2021Support>
implements ZeppOsFileUploadService.Callback, ZeppOsAgpsService.Callback {
implements ZeppOsFileTransferService.Callback, ZeppOsAgpsService.Callback {
private static final Logger LOG = LoggerFactory.getLogger(ZeppOsAgpsUpdateOperation.class);
private static final String AGPS_UPDATE_URL = "agps://upgrade";
@ -51,19 +51,19 @@ public class ZeppOsAgpsUpdateOperation extends AbstractBTLEOperation<Huami2021Su
private final byte[] fileBytes;
private final ZeppOsAgpsService agpsService;
private final ZeppOsFileUploadService fileUploadService;
private final ZeppOsFileTransferService fileTransferService;
private final ZeppOsConfigService configService;
public ZeppOsAgpsUpdateOperation(final Huami2021Support support,
final ZeppOsAgpsFile file,
final ZeppOsAgpsService agpsService,
final ZeppOsFileUploadService fileUploadService,
final ZeppOsFileTransferService fileTransferService,
final ZeppOsConfigService configService) {
super(support);
this.file = file;
this.fileBytes = file.getUihhBytes();
this.agpsService = agpsService;
this.fileUploadService = fileUploadService;
this.fileTransferService = fileTransferService;
this.configService = configService;
}
@ -99,6 +99,11 @@ public class ZeppOsAgpsUpdateOperation extends AbstractBTLEOperation<Huami2021Su
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);
}
@Override
public void onAgpsUploadStartResponse(final boolean success) {
if (!success) {
@ -106,7 +111,7 @@ public class ZeppOsAgpsUpdateOperation extends AbstractBTLEOperation<Huami2021Su
return;
}
fileUploadService.sendFile(AGPS_UPDATE_URL, AGPS_UPDATE_FILE, fileBytes, this);
fileTransferService.sendFile(AGPS_UPDATE_URL, AGPS_UPDATE_FILE, fileBytes, this);
}
@Override

View File

@ -26,31 +26,31 @@ 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.Huami2021Support;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsFileUploadService;
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;
public class ZeppOsGpxRouteUploadOperation extends AbstractBTLEOperation<Huami2021Support>
implements ZeppOsFileUploadService.Callback {
implements ZeppOsFileTransferService.Callback {
private static final Logger LOG = LoggerFactory.getLogger(ZeppOsGpxRouteUploadOperation.class);
private final ZeppOsGpxRouteFile file;
private final byte[] fileBytes;
private final ZeppOsFileUploadService fileUploadService;
private final ZeppOsFileTransferService fileTransferService;
public ZeppOsGpxRouteUploadOperation(final Huami2021Support support,
final ZeppOsGpxRouteFile file,
final ZeppOsFileUploadService fileUploadService) {
final ZeppOsFileTransferService fileTransferService) {
super(support);
this.file = file;
this.fileBytes = file.getEncodedBytes();
this.fileUploadService = fileUploadService;
this.fileTransferService = fileTransferService;
}
@Override
protected void doPerform() throws IOException {
fileUploadService.sendFile(
fileTransferService.sendFile(
"sport://file_transfer?appId=7073283073&params={}",
"track_" + file.getTimestamp() + ".dat",
fileBytes,
@ -88,6 +88,11 @@ public class ZeppOsGpxRouteUploadOperation extends AbstractBTLEOperation<Huami20
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 gpx route upload progress");

View File

@ -31,28 +31,28 @@ 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.CheckSums;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
public class ZeppOsFileUploadService extends AbstractZeppOsService {
private static final Logger LOG = LoggerFactory.getLogger(ZeppOsFileUploadService.class);
public class ZeppOsFileTransferService extends AbstractZeppOsService {
private static final Logger LOG = LoggerFactory.getLogger(ZeppOsFileTransferService.class);
private static final short ENDPOINT = 0x000d;
private static final byte CMD_CAPABILITIES_REQUEST = 0x01;
private static final byte CMD_CAPABILITIES_RESPONSE = 0x02;
private static final byte CMD_UPLOAD_REQUEST = 0x03;
private static final byte CMD_UPLOAD_RESPONSE = 0x04;
private static final byte CMD_TRANSFER_REQUEST = 0x03;
private static final byte CMD_TRANSFER_RESPONSE = 0x04;
private static final byte CMD_DATA_SEND = 0x10;
private static final byte CMD_DATA_ACK = 0x11;
private static final byte FLAG_FIRST_CHUNK = 0x01;
private static final byte FLAG_LAST_CHUNK = 0x02;
private final Map<Byte, FileSendRequest> mSessionRequests = new HashMap<>();
private final Map<Byte, FileTransferRequest> mSessionRequests = new HashMap<>();
private int mChunkSize = -1;
public ZeppOsFileUploadService(final Huami2021Support support) {
public ZeppOsFileTransferService(final Huami2021Support support) {
super(support);
}
@ -75,25 +75,28 @@ public class ZeppOsFileUploadService extends AbstractZeppOsService {
case CMD_CAPABILITIES_RESPONSE:
final int version = payload[1] & 0xff;
if (version != 1 && version != 2) {
LOG.error("Unsupported file upload service version: {}", version);
LOG.error("Unsupported file transfer service version: {}", version);
return;
}
mChunkSize = BLETypeConversions.toUint16(payload, 2);
LOG.info("Got file upload service: version={}, chunkSize={}", version, mChunkSize);
LOG.info("Got file transfer service: version={}, chunkSize={}", version, mChunkSize);
return;
case CMD_UPLOAD_RESPONSE:
case CMD_TRANSFER_REQUEST:
handleFileTransferRequest(payload);
return;
case CMD_TRANSFER_RESPONSE:
session = payload[1];
status = payload[2];
final int existingProgress = BLETypeConversions.toUint32(payload, 3);
LOG.info("Band acknowledged file upload request: session={}, status={}, existingProgress={}", session, status, existingProgress);
LOG.info("Band acknowledged file transfer request: session={}, status={}, existingProgress={}", session, status, existingProgress);
if (status != 0) {
LOG.error("Unexpected status from band for session {}, aborting", session);
onFinish(session, false);
onUploadFinish(session, false);
return;
}
if (existingProgress != 0) {
LOG.info("Updating existing progress for session {} to {}", session, existingProgress);
final FileSendRequest request = mSessionRequests.get(session);
final FileTransferRequest request = mSessionRequests.get(session);
if (request == null) {
LOG.error("No request found for session {}", session);
return;
@ -102,19 +105,22 @@ public class ZeppOsFileUploadService extends AbstractZeppOsService {
}
sendNextQueuedData(session);
return;
case CMD_DATA_SEND:
handleFileTransferData(payload);
return;
case CMD_DATA_ACK:
session = payload[1];
status = payload[2];
LOG.info("Band acknowledged file upload data: session={}, status={}", session, status);
LOG.info("Band acknowledged file transfer data: session={}, status={}", session, status);
if (status != 0) {
LOG.error("Unexpected status from band, aborting session {}", session);
onFinish(session, false);
onUploadFinish(session, false);
return;
}
sendNextQueuedData(session);
return;
default:
LOG.warn("Unexpected file upload byte {}", String.format("0x%02x", payload[0]));
LOG.warn("Unexpected file transfer byte {}", String.format("0x%02x", payload[0]));
}
}
@ -127,6 +133,93 @@ public class ZeppOsFileUploadService extends AbstractZeppOsService {
write(builder, new byte[]{CMD_CAPABILITIES_REQUEST});
}
private void handleFileTransferRequest(final byte[] payload) {
// File transfer request initialized from watch
int pos = 1;
final byte session = payload[pos++];
final String url = StringUtils.untilNullTerminator(payload, pos);
if (url == null) {
LOG.error("Unable to parse url from transfer request");
return;
}
pos += url.length() + 1;
final String filename = StringUtils.untilNullTerminator(payload, pos);
if (filename == null) {
LOG.error("Unable to parse filename from transfer request");
return;
}
pos += filename.length() + 1;
final int length = BLETypeConversions.toUint32(payload, pos);
pos += 4;
final int crc32 = BLETypeConversions.toUint32(payload, pos);
LOG.info("Got transfer request: session={}, url={}, filename={}, length={}", session, url, filename, length);
final FileTransferRequest request = new FileTransferRequest(url, filename, new byte[length], getSupport());
request.setCrc32(crc32);
final ByteBuffer buf = ByteBuffer.allocate(7).order(ByteOrder.LITTLE_ENDIAN);
buf.order(ByteOrder.LITTLE_ENDIAN);
buf.put(CMD_TRANSFER_RESPONSE);
buf.put(session);
buf.put((byte) 0x00);
buf.putInt(0);
mSessionRequests.put(session, request);
write("send file transfer response", buf.array());
}
private void handleFileTransferData(final byte[] payload) {
final ByteBuffer buf = ByteBuffer.wrap(payload).order(ByteOrder.LITTLE_ENDIAN);
buf.get(); // Discard first byte
final byte secondByte = buf.get();
final boolean firstPacket = (secondByte == 1);
final boolean lastPacket = (secondByte == 2);
final byte session = buf.get();
final byte index = buf.get();
final short size = buf.getShort();
final FileTransferRequest request = mSessionRequests.get(session);
if (request == null) {
LOG.error("No request found for session {}", session);
return;
}
if (index != request.index) {
LOG.warn("Unexpected index {}, expected {}", index, request.index);
return;
}
if (firstPacket && request.getProgress() != 0) {
LOG.warn("Got first packet, but progress is {}", request.getProgress());
return;
}
buf.get(request.getBytes(), request.getProgress(), size);
request.setIndex((byte) (index + 1));
request.setProgress(request.getProgress() + size);
LOG.debug("Got data for session={}, progress={}/{}", session, request.getProgress(), request.getSize());
if (lastPacket) {
mSessionRequests.remove(session);
if (request.getProgress() != request.getSize()) {
LOG.warn("Request not finished: {}/{}", request.getProgress(), request.getSize());
return;
}
final int checksum = CheckSums.getCRC32(request.getBytes());
if (checksum != request.getCrc32()) {
LOG.warn("Checksum mismatch: expected {}, got {}", request.getCrc32(), checksum);
return;
}
request.getCallback().onFileDownloadFinish(request.getUrl(), request.getFilename(), request.getBytes());
}
}
public void sendFile(final String url, final String filename, final byte[] bytes, final Callback callback) {
if (mChunkSize < 0) {
LOG.error("Service not initialized, refusing to send {}", url);
@ -136,20 +229,23 @@ public class ZeppOsFileUploadService extends AbstractZeppOsService {
LOG.info("Sending {} bytes to {}", bytes.length, url);
final FileSendRequest request = new FileSendRequest(url, filename, bytes, callback);
final FileTransferRequest request = new FileTransferRequest(url, filename, bytes, callback);
final byte session = (byte) mSessionRequests.size();
byte session = (byte) mSessionRequests.size();
while (mSessionRequests.containsKey(session)) {
session++;
}
final ByteBuffer buf = ByteBuffer.allocate(2 + url.length() + 1 + filename.length() + 1 + 4 + 4);
buf.order(ByteOrder.LITTLE_ENDIAN);
buf.put(CMD_UPLOAD_REQUEST);
buf.put(CMD_TRANSFER_REQUEST);
buf.put(session);
buf.put(url.getBytes(StandardCharsets.UTF_8));
buf.put((byte) 0x00);
buf.put(filename.getBytes(StandardCharsets.UTF_8));
buf.put((byte) 0x00);
buf.putInt(bytes.length);
buf.putInt(CheckSums.getCRC32(bytes));
buf.putInt(request.getCrc32());
write("send file upload request", buf.array());
@ -157,7 +253,7 @@ public class ZeppOsFileUploadService extends AbstractZeppOsService {
}
private void sendNextQueuedData(final byte session) {
final FileSendRequest request = mSessionRequests.get(session);
final FileTransferRequest request = mSessionRequests.get(session);
if (request == null) {
LOG.error("No request found for session {}", session);
return;
@ -165,7 +261,7 @@ public class ZeppOsFileUploadService extends AbstractZeppOsService {
if (request.getProgress() >= request.getSize()) {
LOG.info("Sending {} finished", request.getUrl());
onFinish(session, true);
onUploadFinish(session, true);
return;
}
@ -209,8 +305,8 @@ public class ZeppOsFileUploadService extends AbstractZeppOsService {
write("send file data", buf.array());
}
private void onFinish(final byte session, final boolean success) {
final FileSendRequest request = mSessionRequests.get(session);
private void onUploadFinish(final byte session, final boolean success) {
final FileTransferRequest request = mSessionRequests.get(session);
if (request == null) {
LOG.error("No request found for session {}", session);
return;
@ -224,19 +320,21 @@ public class ZeppOsFileUploadService extends AbstractZeppOsService {
/**
* Wrapper class to keep track of ongoing file send requests and their progress.
*/
public static class FileSendRequest {
public static class FileTransferRequest {
private final String url;
private final String filename;
private final byte[] bytes;
private final Callback callback;
private int progress = 0;
private byte index = 0;
private int crc32;
public FileSendRequest(final String url, final String filename, final byte[] bytes, final Callback callback) {
public FileTransferRequest(final String url, final String filename, final byte[] bytes, final Callback callback) {
this.url = url;
this.filename = filename;
this.bytes = bytes;
this.callback = callback;
this.crc32 = CheckSums.getCRC32(bytes);
}
public String getUrl() {
@ -274,11 +372,21 @@ public class ZeppOsFileUploadService extends AbstractZeppOsService {
public void setIndex(final byte index) {
this.index = index;
}
public int getCrc32() {
return crc32;
}
public void setCrc32(final int crc32) {
this.crc32 = crc32;
}
}
public interface Callback {
void onFileUploadFinish(boolean success);
void onFileUploadProgress(int progress);
void onFileDownloadFinish(String url, String filename, byte[] data);
}
}

View File

@ -71,11 +71,11 @@ public class ZeppOsNotificationService extends AbstractZeppOsService {
// This needs to be simplified.
private final LimitedQueue mNotificationReplyAction = new LimitedQueue(16);
private final ZeppOsFileUploadService fileUploadService;
private final ZeppOsFileTransferService fileTransferService;
public ZeppOsNotificationService(final Huami2021Support support, final ZeppOsFileUploadService fileUploadService) {
public ZeppOsNotificationService(final Huami2021Support support, final ZeppOsFileTransferService fileTransferService) {
super(support);
this.fileUploadService = fileUploadService;
this.fileTransferService = fileTransferService;
}
@Override
@ -380,11 +380,11 @@ public class ZeppOsNotificationService extends AbstractZeppOsService {
);
final String filename = String.format("logo_%s.tga", packageName.replace(".", "_"));
fileUploadService.sendFile(
fileTransferService.sendFile(
url,
filename,
tga565,
new ZeppOsFileUploadService.Callback() {
new ZeppOsFileTransferService.Callback() {
@Override
public void onFileUploadFinish(final boolean success) {
LOG.info("Finished sending icon, success={}", success);
@ -397,6 +397,11 @@ public class ZeppOsNotificationService extends AbstractZeppOsService {
public void onFileUploadProgress(final int progress) {
LOG.trace("Icon send progress: {}", 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);
}
}
);