diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/Huami2021Service.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/Huami2021Service.java index 3a21c3311..3b9bb69bd 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/Huami2021Service.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/Huami2021Service.java @@ -23,7 +23,6 @@ public class Huami2021Service { public static final short CHUNKED2021_ENDPOINT_HTTP = 0x0001; public static final short CHUNKED2021_ENDPOINT_CALENDAR = 0x0007; public static final short CHUNKED2021_ENDPOINT_CONFIG = 0x000a; - public static final short CHUNKED2021_ENDPOINT_ICONS = 0x000d; public static final short CHUNKED2021_ENDPOINT_WEATHER = 0x000e; public static final short CHUNKED2021_ENDPOINT_ALARMS = 0x000f; public static final short CHUNKED2021_ENDPOINT_CANNED_MESSAGES = 0x0013; @@ -232,16 +231,6 @@ public class Huami2021Service { public static final byte CONFIG_CMD_SET = 0x05; public static final byte CONFIG_CMD_ACK = 0x06; - /** - * Config, for {@link Huami2021Service#CHUNKED2021_ENDPOINT_ICONS}. - */ - public static final byte ICONS_CMD_CAPABILITIES_REQUEST = 0x01; - public static final byte ICONS_CMD_CAPABILITIES_RESPONSE = 0x02; - public static final byte ICONS_CMD_SEND_REQUEST = 0x03; - public static final byte ICONS_CMD_SEND_RESPONSE = 0x04; - public static final byte ICONS_CMD_DATA_SEND = 0x10; - public static final byte ICONS_CMD_DATA_ACK = 0x11; - /** * Reminders, for {@link Huami2021Service#CHUNKED2021_ENDPOINT_REMINDERS}. */ diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/Huami2021Support.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/Huami2021Support.java index 8ce257037..687ad1496 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/Huami2021Support.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/Huami2021Support.java @@ -27,7 +27,6 @@ import static nodomain.freeyourgadget.gadgetbridge.model.ActivityUser.PREF_USER_ import static nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions.fromUint16; import static nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions.fromUint8; import static nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions.mapTimeZone; -import static nodomain.freeyourgadget.gadgetbridge.service.devices.huami.Huami2021Config.ConfigArg.DATE_FORMAT; import static nodomain.freeyourgadget.gadgetbridge.service.devices.huami.Huami2021Config.ConfigArg.FITNESS_GOAL_CALORIES; import static nodomain.freeyourgadget.gadgetbridge.service.devices.huami.Huami2021Config.ConfigArg.FITNESS_GOAL_FAT_BURN_TIME; import static nodomain.freeyourgadget.gadgetbridge.service.devices.huami.Huami2021Config.ConfigArg.FITNESS_GOAL_SLEEP; @@ -121,9 +120,9 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.Fet 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.services.ZeppOsFileUploadService; import nodomain.freeyourgadget.gadgetbridge.util.AlarmUtils; import nodomain.freeyourgadget.gadgetbridge.util.BitmapUtil; -import nodomain.freeyourgadget.gadgetbridge.util.CheckSums; import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper; import nodomain.freeyourgadget.gadgetbridge.util.GB; import nodomain.freeyourgadget.gadgetbridge.util.GBPrefs; @@ -143,6 +142,9 @@ public abstract class Huami2021Support extends HuamiSupport { // send CONTINUE commands private boolean heartRateRealtimeStarted; + // Services + private final ZeppOsFileUploadService fileUploadService = new ZeppOsFileUploadService(this); + public Huami2021Support() { this(LOG); } @@ -1366,6 +1368,7 @@ public abstract class Huami2021Support extends HuamiSupport { } requestCapabilityReminders(builder); + fileUploadService.requestCapability(builder); for (final HuamiVibrationPatternNotificationType type : coordinator.getVibrationPatternNotificationTypes(gbDevice)) { // FIXME: Can we read these from the band? @@ -1437,8 +1440,8 @@ public abstract class Huami2021Support extends HuamiSupport { case CHUNKED2021_ENDPOINT_CONFIG: handle2021Config(payload); return; - case CHUNKED2021_ENDPOINT_ICONS: - handle2021Icons(payload); + case ZeppOsFileUploadService.ENDPOINT: + fileUploadService.handlePayload(payload); return; case CHUNKED2021_ENDPOINT_WEATHER: handle2021Weather(payload); @@ -2151,12 +2154,12 @@ public abstract class Huami2021Support extends HuamiSupport { return; } int pos = 1 + packageName.length() + 1; - // payload[pos] = 0x08? + final byte iconFormat = payload[pos]; pos++; int width = BLETypeConversions.toUint16(subarray(payload, pos, pos + 2)); pos += 2; int height = BLETypeConversions.toUint16(subarray(payload, pos, pos + 2)); - sendIconForPackage(packageName, width, height); + sendIconForPackage(packageName, iconFormat, width, height); return; default: LOG.warn("Unexpected notification byte {}", String.format("0x%02x", payload[0])); @@ -2176,33 +2179,6 @@ public abstract class Huami2021Support extends HuamiSupport { writeToChunked2021("ack notification reply", CHUNKED2021_ENDPOINT_NOTIFICATIONS, buf.array(), true); } - // Package names for which icon is being sent - // FIXME: This only handles 1 icon at a time - private String queuedIconPackage; - // Encoded TGA565 bytes - private byte[] queuedIconBytes; - // Keep track of the last time we queued an icon, as a failsafe. If somehow we didn't get the ack - // after 10 seconds, we'll allow another icon to be sent - private long queuedIconTimeMillis = 0; - - protected void handle2021Icons(final byte[] payload) { - switch (payload[0]) { - case ICONS_CMD_SEND_RESPONSE: - LOG.info("Band acknowledged icon send request: {}", GB.hexdump(payload)); - // FIXME: The bytes probably mean something.. - sendNextQueuedIconData(); - return; - case ICONS_CMD_DATA_ACK: - LOG.info("Band acknowledged icon icon data: {}", GB.hexdump(payload)); - // After the icon is sent to the band, we need to ACK it on the notifications - // FIXME: The bytes probably mean something.. - ackNotificationAfterIconSent(); - return; - default: - LOG.warn("Unexpected icons byte {}", String.format("0x%02x", payload[0])); - } - } - protected void handle2021Weather(final byte[] payload) { switch (payload[0]) { case WEATHER_CMD_DEFAULT_LOCATION_ACK: @@ -2213,43 +2189,7 @@ public abstract class Huami2021Support extends HuamiSupport { } } - private void sendNextQueuedIconData() { - if (queuedIconPackage == null) { - LOG.error("No queued icon to send"); - return; - } - - if (queuedIconBytes == null) { - LOG.error("No icon bytes for {}", queuedIconPackage); - return; - } - - LOG.info("Sending icon data for {}", queuedIconPackage); - - // The band always sends a full 8192 chunk, with zeroes at the end if bytes < 8192 - final ByteBuffer buf = ByteBuffer.allocate(10 + queuedIconBytes.length); - buf.order(ByteOrder.LITTLE_ENDIAN); - buf.put(ICONS_CMD_DATA_SEND); - buf.put((byte) 0x03); // ? - buf.put((byte) 0x00); // ? - buf.put((byte) 0x00); // ? - buf.put((byte) 0x00); // ? - buf.put((byte) 0x00); // ? - buf.put((byte) 0x00); // ? - buf.put((byte) 0x00); // ? - buf.put((byte) 0x08); // ? - buf.put((byte) 0x17); // ? - buf.put(queuedIconBytes); - - writeToChunked2021("send icon data", CHUNKED2021_ENDPOINT_ICONS, buf.array(), false); - } - - private void ackNotificationAfterIconSent() { - if (queuedIconPackage == null) { - LOG.error("No queued icon to ack"); - return; - } - + private void ackNotificationAfterIconSent(final String queuedIconPackage) { LOG.info("Acknowledging icon send for {}", queuedIconPackage); final ByteBuffer buf = ByteBuffer.allocate(1 + queuedIconPackage.length() + 1 + 1); @@ -2259,21 +2199,31 @@ public abstract class Huami2021Support extends HuamiSupport { buf.put((byte) 0x00); buf.put((byte) 0x01); - queuedIconPackage = null; - queuedIconBytes = null; - writeToChunked2021("ack icon send", CHUNKED2021_ENDPOINT_NOTIFICATIONS, buf.array(), true); } - private void sendIconForPackage(final String packageName, final int width, final int height) { + private void sendIconForPackage(final String packageName, final byte iconFormat, final int width, final int height) { if (getMTU() < 247) { LOG.warn("Sending icons requires high MTU, current MTU is {}", getMTU()); return; } - if (queuedIconPackage != null && System.currentTimeMillis() - queuedIconTimeMillis < 10_000L) { - LOG.warn("Icon for {} already queued, not sending icon for {}", queuedIconPackage, packageName); - return; + // Without the expected tga id and format string they seem to get corrupted, + // but the encoding seems to actually be the same...? + final String format; + final String tgaId; + switch (iconFormat) { + case 0x04: + format = "TGA_RGB565_GCNANOLITE"; + tgaId = "SOMHP"; + break; + case 0x08: + format = "TGA_RGB565_DAVE2D"; + tgaId = "SOMH6"; + break; + default: + LOG.error("Unknown icon format {}", String.format("0x%02x", iconFormat)); + return; } final Drawable icon; @@ -2287,19 +2237,11 @@ public abstract class Huami2021Support extends HuamiSupport { final Bitmap bmp = BitmapUtil.toBitmap(icon); // The TGA needs to have this ID, or the band does not accept it - final byte[] tgaId = new byte[46]; - System.arraycopy("SOMH6".getBytes(StandardCharsets.UTF_8), 0, tgaId, 0, 5); + final byte[] tgaIdBytes = new byte[46]; + System.arraycopy(tgaId.getBytes(StandardCharsets.UTF_8), 0, tgaIdBytes, 0, 5); - final byte[] tga565 = BitmapUtil.convertToTgaRGB565(bmp, width, height, tgaId); + final byte[] tga565 = BitmapUtil.convertToTgaRGB565(bmp, width, height, tgaIdBytes); - if (tga565.length > 8192) { - // FIXME: Pretty sure we can't send more than 8KB in a single request, - // but don't know how it's supposed to be encoded - LOG.error("TGA output is too large: {}", tga565.length); - return; - } - - final String format = "TGA_RGB565_DAVE2D"; final String url = String.format( Locale.ROOT, "notification://logo?app_id=%s&width=%d&height=%d&format=%s", @@ -2310,23 +2252,27 @@ public abstract class Huami2021Support extends HuamiSupport { ); final String filename = String.format("logo_%s.tga", packageName.replace(".", "_")); - final ByteBuffer buf = ByteBuffer.allocate(2 + url.length() + 1 + filename.length() + 1 + 4 + 4); - buf.order(ByteOrder.LITTLE_ENDIAN); - buf.put(ICONS_CMD_SEND_REQUEST); - buf.put((byte) 0x00); - buf.put(url.getBytes(StandardCharsets.UTF_8)); - buf.put((byte) 0x00); - buf.put(filename.getBytes(StandardCharsets.UTF_8)); - buf.put((byte) 0x00); - buf.putInt(tga565.length); - buf.putInt(CheckSums.getCRC32(tga565)); + fileUploadService.sendFile( + url, + filename, + tga565, + new ZeppOsFileUploadService.Callback() { + @Override + public void onFinish(final boolean success) { + LOG.info("Finished sending icon, success={}", success); + if (success) { + ackNotificationAfterIconSent(packageName); + } + } + + @Override + public void onProgress(final int progress) { + LOG.trace("Icon send progress: {}", progress); + } + } + ); LOG.info("Queueing icon for {}", packageName); - queuedIconPackage = packageName; - queuedIconBytes = tga565; - queuedIconTimeMillis = System.currentTimeMillis(); - - writeToChunked2021("send icon send request", CHUNKED2021_ENDPOINT_ICONS, buf.array(), false); } protected void handle2021Reminders(final byte[] payload) { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiSupport.java index af58bb1d8..c11091049 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiSupport.java @@ -3931,19 +3931,19 @@ public abstract class HuamiSupport extends AbstractBTLEDeviceSupport implements } } - protected void writeToChunked2021(TransactionBuilder builder, short type, byte data, boolean encrypt) { + public void writeToChunked2021(TransactionBuilder builder, short type, byte data, boolean encrypt) { writeToChunked2021(builder, type, new byte[]{data}, encrypt); } - protected void writeToChunked2021(TransactionBuilder builder, short type, byte[] data, boolean encrypt) { + public void writeToChunked2021(TransactionBuilder builder, short type, byte[] data, boolean encrypt) { huami2021ChunkedEncoder.write(builder, type, data, force2021Protocol(), encrypt); } - protected void writeToChunked2021(final String taskName, short type, byte data, boolean encrypt) { + public void writeToChunked2021(final String taskName, short type, byte data, boolean encrypt) { writeToChunked2021(taskName, type, new byte[]{data}, encrypt); } - protected void writeToChunked2021(final String taskName, short type, byte[] data, boolean encrypt) { + public void writeToChunked2021(final String taskName, short type, byte[] data, boolean encrypt) { try { final TransactionBuilder builder = performInitialized(taskName); writeToChunked2021(builder, type, data, encrypt); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/zeppos/AbstractZeppOsService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/zeppos/AbstractZeppOsService.java new file mode 100644 index 000000000..1c11886ee --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/zeppos/AbstractZeppOsService.java @@ -0,0 +1,40 @@ +/* Copyright (C) 2022 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.huami.zeppos; + +import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.Huami2021Support; + +public abstract class AbstractZeppOsService { + private final Huami2021Support mSupport; + + public AbstractZeppOsService(final Huami2021Support support) { + this.mSupport = support; + } + + public abstract short getEndpoint(); + public abstract boolean isEncrypted(); + public abstract void handlePayload(final byte[] payload); + + protected void write(final String taskName, final byte[] data) { + this.mSupport.writeToChunked2021(taskName, getEndpoint(), data, isEncrypted()); + } + + protected void write(final TransactionBuilder builder, final byte[] data) { + this.mSupport.writeToChunked2021(builder, getEndpoint(), data, isEncrypted()); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/zeppos/services/ZeppOsFileUploadService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/zeppos/services/ZeppOsFileUploadService.java new file mode 100644 index 000000000..d635ac3db --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/zeppos/services/ZeppOsFileUploadService.java @@ -0,0 +1,278 @@ +/* Copyright (C) 2022 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.huami.zeppos.services; + +import org.apache.commons.lang3.ArrayUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; + +import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions; +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; + +public class ZeppOsFileUploadService extends AbstractZeppOsService { + private static final Logger LOG = LoggerFactory.getLogger(ZeppOsFileUploadService.class); + + public static final short ENDPOINT = 0x000d; + + public static final byte CMD_CAPABILITIES_REQUEST = 0x01; + public static final byte CMD_CAPABILITIES_RESPONSE = 0x02; + public static final byte CMD_UPLOAD_REQUEST = 0x03; + public static final byte CMD_UPLOAD_RESPONSE = 0x04; + public static final byte CMD_DATA_SEND = 0x10; + public static final byte CMD_DATA_ACK = 0x11; + + public static final byte FLAG_FIRST_CHUNK = 0x01; + public static final byte FLAG_LAST_CHUNK = 0x02; + + private final Map mSessionRequests = new HashMap<>(); + + private int mChunkSize = -1; + + public ZeppOsFileUploadService(final Huami2021Support support) { + super(support); + } + + @Override + public short getEndpoint() { + return ENDPOINT; + } + + @Override + public boolean isEncrypted() { + return false; + } + + @Override + public void handlePayload(final byte[] payload) { + byte session; + byte status; + + switch (payload[0]) { + case CMD_CAPABILITIES_RESPONSE: + final int version = payload[1] & 0xff; + if (version != 1 && version != 2) { + LOG.error("Unsupported file upload service version: {}", version); + return; + } + mChunkSize = BLETypeConversions.toUint16(payload, 2); + LOG.info("Got file upload service: version={}, chunkSize={}", version, mChunkSize); + return; + case CMD_UPLOAD_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); + if (status != 0) { + LOG.error("Unexpected status from band for session {}, aborting", session); + onFinish(session, false); + return; + } + if (existingProgress != 0) { + LOG.info("Updating existing progress for session {} to {}", session, existingProgress); + final FileSendRequest request = mSessionRequests.get(session); + if (request == null) { + LOG.error("No request found for session {}", session); + return; + } + request.setProgress(existingProgress); + } + sendNextQueuedData(session); + return; + case CMD_DATA_ACK: + session = payload[1]; + status = payload[2]; + LOG.info("Band acknowledged file upload data: session={}, status={}", session, status); + if (status != 0) { + LOG.error("Unexpected status from band, aborting session {}", session); + onFinish(session, false); + return; + } + sendNextQueuedData(session); + return; + default: + LOG.warn("Unexpected file upload byte {}", String.format("0x%02x", payload[0])); + } + } + + public void requestCapability(final TransactionBuilder builder) { + write(builder, new byte[]{CMD_CAPABILITIES_REQUEST}); + } + + 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); + callback.onFinish(false); + return; + } + + LOG.info("Sending {} bytes to {}", bytes.length, url); + + final FileSendRequest request = new FileSendRequest(url, filename, bytes, callback); + + final byte session = (byte) mSessionRequests.size(); + + 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(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)); + + write("send file upload request", buf.array()); + + mSessionRequests.put(session, request); + } + + private void sendNextQueuedData(final byte session) { + final FileSendRequest request = mSessionRequests.get(session); + if (request == null) { + LOG.error("No request found for session {}", session); + return; + } + + if (request.getProgress() >= request.getSize()) { + LOG.info("Sending {} finished", request.getUrl()); + onFinish(session, true); + return; + } + + LOG.debug("Sending file data for session={}, progress={}, index={}", session, request.getProgress(), request.getIndex()); + + final ByteBuffer buf = ByteBuffer.allocate(10 + mChunkSize); + buf.order(ByteOrder.LITTLE_ENDIAN); + buf.put(CMD_DATA_SEND); + + byte flags = 0; + if (request.getProgress() == 0) { + flags |= FLAG_FIRST_CHUNK; + } + if (request.getProgress() + mChunkSize >= request.getSize()) { + flags |= FLAG_LAST_CHUNK; + } + + buf.put(flags); + buf.put(session); + buf.put(request.getIndex()); + if ((flags & FLAG_FIRST_CHUNK) > 0) { + buf.put((byte) 0x00); // ? + buf.put((byte) 0x00); // ? + buf.put((byte) 0x00); // ? + buf.put((byte) 0x00); // ? + } + + final byte[] payload = ArrayUtils.subarray( + request.getBytes(), + request.getProgress(), + request.getProgress() + mChunkSize + ); + + buf.putShort((short) payload.length); + buf.put(payload); + + request.setProgress(request.getProgress() + payload.length); + request.setIndex((byte) (request.getIndex() + 1)); + request.getCallback().onProgress(request.getProgress()); + + write("send file data", buf.array()); + } + + private void onFinish(final byte session, final boolean success) { + final FileSendRequest request = mSessionRequests.get(session); + if (request == null) { + LOG.error("No request found for session {}", session); + return; + } + + mSessionRequests.remove(session); + + request.getCallback().onFinish(success); + } + + /** + * Wrapper class to keep track of ongoing file send requests and their progress. + */ + public static class FileSendRequest { + private final String url; + private final String filename; + private final byte[] bytes; + private final Callback callback; + private int progress = 0; + private byte index = 0; + + public FileSendRequest(final String url, final String filename, final byte[] bytes, final Callback callback) { + this.url = url; + this.filename = filename; + this.bytes = bytes; + this.callback = callback; + } + + public String getUrl() { + return url; + } + + public String getFilename() { + return filename; + } + + public byte[] getBytes() { + return bytes; + } + + public int getSize() { + return bytes.length; + } + + public Callback getCallback() { + return callback; + } + + public int getProgress() { + return progress; + } + + public void setProgress(final int progress) { + this.progress = progress; + } + + public byte getIndex() { + return index; + } + + public void setIndex(final byte index) { + this.index = index; + } + } + + public interface Callback { + void onFinish(boolean success); + + void onProgress(int progress); + } +}