1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-12-28 11:35:48 +01:00

Zepp OS: Send notification pictures

This commit is contained in:
José Rebelo 2024-11-23 22:04:42 +00:00
parent 4aa145560a
commit 9174d95894
5 changed files with 286 additions and 58 deletions

View File

@ -152,7 +152,6 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.service
import nodomain.freeyourgadget.gadgetbridge.util.AlarmUtils; import nodomain.freeyourgadget.gadgetbridge.util.AlarmUtils;
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils; import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
import nodomain.freeyourgadget.gadgetbridge.util.GB; import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.util.GBPrefs;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs; import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
import nodomain.freeyourgadget.gadgetbridge.util.SilentMode; import nodomain.freeyourgadget.gadgetbridge.util.SilentMode;

View File

@ -111,7 +111,7 @@ public class ZeppOsAgpsUpdateOperation extends AbstractBTLEOperation<ZeppOsSuppo
return; return;
} }
fileTransferService.sendFile(AGPS_UPDATE_URL, AGPS_UPDATE_FILE, fileBytes, this); fileTransferService.sendFile(AGPS_UPDATE_URL, AGPS_UPDATE_FILE, fileBytes, false, this);
} }
@Override @Override

View File

@ -54,6 +54,7 @@ public class ZeppOsGpxRouteUploadOperation extends AbstractBTLEOperation<ZeppOsS
"sport://file_transfer?appId=7073283073&params={}", "sport://file_transfer?appId=7073283073&params={}",
"track_" + file.getTimestamp() + ".dat", "track_" + file.getTimestamp() + ".dat",
fileBytes, fileBytes,
false,
this this
); );
} }

View File

@ -20,12 +20,14 @@ import org.apache.commons.lang3.ArrayUtils;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.io.ByteArrayOutputStream;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.ByteOrder; import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.zip.DataFormatException; import java.util.zip.DataFormatException;
import java.util.zip.Deflater;
import java.util.zip.Inflater; import java.util.zip.Inflater;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiService; import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiService;
@ -58,6 +60,7 @@ public class ZeppOsFileTransferService extends AbstractZeppOsService {
private int mVersion = -1; private int mVersion = -1;
private int mChunkSize = -1; private int mChunkSize = -1;
private int mCompressedChunkSize = -1;
public ZeppOsFileTransferService(final ZeppOsSupport support) { public ZeppOsFileTransferService(final ZeppOsSupport support) {
super(support, false); super(support, false);
@ -81,13 +84,19 @@ public class ZeppOsFileTransferService extends AbstractZeppOsService {
return; return;
} }
mChunkSize = BLETypeConversions.toUint16(payload, 2); mChunkSize = BLETypeConversions.toUint16(payload, 2);
// TODO parse the rest for v3
LOG.info("Got file transfer service: version={}, chunkSize={}", mVersion, mChunkSize);
if (mVersion == 3) { if (mVersion == 3) {
// TODO parse the rest for v3
mCompressedChunkSize = BLETypeConversions.toUint32(payload, 4);
final TransactionBuilder builder = getSupport().createTransactionBuilder("enable file transfer v3 notifications"); final TransactionBuilder builder = getSupport().createTransactionBuilder("enable file transfer v3 notifications");
builder.notify(getSupport().getCharacteristic(HuamiService.UUID_CHARACTERISTIC_ZEPP_OS_FILE_TRANSFER_V3), true); builder.notify(getSupport().getCharacteristic(HuamiService.UUID_CHARACTERISTIC_ZEPP_OS_FILE_TRANSFER_V3), true);
builder.queue(getSupport().getQueue()); builder.queue(getSupport().getQueue());
} }
LOG.info(
"Got file transfer service: version={}, chunkSize={}, compressedChunkSize={}",
mVersion,
mChunkSize,
mCompressedChunkSize
);
return; return;
case CMD_TRANSFER_REQUEST: case CMD_TRANSFER_REQUEST:
handleFileTransferRequest(payload); handleFileTransferRequest(payload);
@ -176,7 +185,14 @@ public class ZeppOsFileTransferService extends AbstractZeppOsService {
LOG.info("Got transfer request: session={}, url={}, filename={}, length={}, compressed={}", session, url, filename, length, compressed); LOG.info("Got transfer request: session={}, url={}, filename={}, length={}, compressed={}", session, url, filename, length, compressed);
final FileTransferRequest request = new FileTransferRequest(url, filename, new byte[length], compressed, getSupport()); final FileTransferRequest request = new FileTransferRequest(
url,
filename,
new byte[length],
compressed,
compressed ? mCompressedChunkSize : mChunkSize,
getSupport()
);
request.setCrc32(crc32); request.setCrc32(crc32);
if (mVersion < 3) { if (mVersion < 3) {
@ -260,6 +276,20 @@ public class ZeppOsFileTransferService extends AbstractZeppOsService {
} }
} }
public static byte[] compress(final byte[] data) {
final Deflater deflater = new Deflater();
deflater.setInput(data);
deflater.finish();
final ByteArrayOutputStream baos = new ByteArrayOutputStream(data.length);
final byte[] buf = new byte[8096];
int read;
while ((read = deflater.deflate(buf)) > 0) {
baos.write(buf, 0, read);
}
return baos.toByteArray();
}
public static byte[] decompress(final byte[] data) { public static byte[] decompress(final byte[] data) {
final Inflater inflater = new Inflater(); final Inflater inflater = new Inflater();
final byte[] output = new byte[data.length]; final byte[] output = new byte[data.length];
@ -276,16 +306,23 @@ public class ZeppOsFileTransferService extends AbstractZeppOsService {
return output; return output;
} }
public void sendFile(final String url, final String filename, final byte[] bytes, final Callback callback) { public void sendFile(final String url, final String filename, final byte[] bytes, final boolean compress, final Callback callback) {
if (mChunkSize < 0) { if (mChunkSize < 0) {
LOG.error("Service not initialized, refusing to send {}", url); LOG.error("Service not initialized, refusing to send {}", url);
callback.onFileUploadFinish(false); callback.onFileUploadFinish(false);
return; return;
} }
LOG.info("Sending {} bytes to {}", bytes.length, url); LOG.info("Sending {} bytes to {} in {}", bytes.length, filename, url);
final FileTransferRequest request = new FileTransferRequest(url, filename, bytes, false, callback); final FileTransferRequest request = new FileTransferRequest(
url,
filename,
bytes,
compress && mCompressedChunkSize > 0,
compress && mCompressedChunkSize > 0 ? mCompressedChunkSize : mChunkSize,
callback
);
if (mVersion == 3 && !mSessionRequests.isEmpty()) { if (mVersion == 3 && !mSessionRequests.isEmpty()) {
// FIXME non-zero session on v3 // FIXME non-zero session on v3
@ -302,6 +339,9 @@ public class ZeppOsFileTransferService extends AbstractZeppOsService {
int payloadSize = 2 + url.length() + 1 + filename.length() + 1 + 4 + 4; int payloadSize = 2 + url.length() + 1 + filename.length() + 1 + 4 + 4;
if (mVersion == 3) { if (mVersion == 3) {
payloadSize += 2; payloadSize += 2;
if (compress) {
payloadSize += 4;
}
} }
final ByteBuffer buf = ByteBuffer.allocate(payloadSize); final ByteBuffer buf = ByteBuffer.allocate(payloadSize);
@ -315,8 +355,10 @@ public class ZeppOsFileTransferService extends AbstractZeppOsService {
buf.putInt(bytes.length); buf.putInt(bytes.length);
buf.putInt(request.getCrc32()); buf.putInt(request.getCrc32());
if (mVersion == 3) { if (mVersion == 3) {
// compression ? buf.put((byte) (compress ? 1 : 0));
buf.put((byte) 0); if (compress) {
buf.putInt(mCompressedChunkSize);
}
buf.put((byte) 0); buf.put((byte) 0);
} }
@ -333,7 +375,7 @@ public class ZeppOsFileTransferService extends AbstractZeppOsService {
} }
if (request.getProgress() >= request.getSize()) { if (request.getProgress() >= request.getSize()) {
LOG.info("Sending {} finished", request.getUrl()); LOG.info("Finished sending {}", request.getUrl());
onUploadFinish(session, true); onUploadFinish(session, true);
return; return;
} }
@ -354,7 +396,7 @@ public class ZeppOsFileTransferService extends AbstractZeppOsService {
} }
private void writeChunkV1(final FileTransferRequest request, final byte session) { private void writeChunkV1(final FileTransferRequest request, final byte session) {
final ByteBuffer buf = ByteBuffer.allocate(10 + mChunkSize); final ByteBuffer buf = ByteBuffer.allocate(10 + request.getChunkSize());
buf.order(ByteOrder.LITTLE_ENDIAN); buf.order(ByteOrder.LITTLE_ENDIAN);
buf.put(CMD_DATA_SEND); buf.put(CMD_DATA_SEND);
@ -362,7 +404,7 @@ public class ZeppOsFileTransferService extends AbstractZeppOsService {
if (request.getProgress() == 0) { if (request.getProgress() == 0) {
flags |= FLAG_FIRST_CHUNK; flags |= FLAG_FIRST_CHUNK;
} }
if (request.getProgress() + mChunkSize >= request.getSize()) { if (request.getProgress() + request.getChunkSize() >= request.getSize()) {
flags |= FLAG_LAST_CHUNK; flags |= FLAG_LAST_CHUNK;
} }
@ -379,7 +421,7 @@ public class ZeppOsFileTransferService extends AbstractZeppOsService {
final byte[] payload = ArrayUtils.subarray( final byte[] payload = ArrayUtils.subarray(
request.getBytes(), request.getBytes(),
request.getProgress(), request.getProgress(),
request.getProgress() + mChunkSize request.getProgress() + request.getChunkSize()
); );
buf.putShort((short) payload.length); buf.putShort((short) payload.length);
@ -396,14 +438,14 @@ public class ZeppOsFileTransferService extends AbstractZeppOsService {
final byte[] chunk = ArrayUtils.subarray( final byte[] chunk = ArrayUtils.subarray(
request.getBytes(), request.getBytes(),
request.getProgress(), request.getProgress(),
request.getProgress() + mChunkSize request.getProgress() + request.getChunkSize()
); );
byte flags = 0; byte flags = 0;
if (request.getProgress() == 0) { if (request.getProgress() == 0) {
flags |= FLAG_FIRST_CHUNK; flags |= FLAG_FIRST_CHUNK;
} }
if (request.getProgress() + mChunkSize >= request.getSize()) { if (request.getProgress() + request.getChunkSize() >= request.getSize()) {
flags |= FLAG_LAST_CHUNK; flags |= FLAG_LAST_CHUNK;
} }
@ -494,16 +536,18 @@ public class ZeppOsFileTransferService extends AbstractZeppOsService {
private final String filename; private final String filename;
private final byte[] bytes; private final byte[] bytes;
private final boolean compressed; private final boolean compressed;
private final int chunkSize;
private final Callback callback; private final Callback callback;
private int progress = 0; private int progress = 0;
private byte index = 0; private byte index = 0;
private int crc32; private int crc32;
public FileTransferRequest(final String url, final String filename, final byte[] bytes, boolean compressed, final Callback callback) { public FileTransferRequest(final String url, final String filename, final byte[] bytes, boolean compressed, int chunkSize, final Callback callback) {
this.url = url; this.url = url;
this.filename = filename; this.filename = filename;
this.bytes = bytes; this.bytes = compressed ? compress(bytes) : bytes;
this.compressed = compressed; this.compressed = compressed;
this.chunkSize = chunkSize;
this.callback = callback; this.callback = callback;
this.crc32 = CheckSums.getCRC32(bytes); this.crc32 = CheckSums.getCRC32(bytes);
} }
@ -528,6 +572,10 @@ public class ZeppOsFileTransferService extends AbstractZeppOsService {
return compressed; return compressed;
} }
public int getChunkSize() {
return chunkSize;
}
public Callback getCallback() { public Callback getCallback() {
return callback; return callback;
} }

View File

@ -19,8 +19,11 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.servic
import static org.apache.commons.lang3.ArrayUtils.subarray; import static org.apache.commons.lang3.ArrayUtils.subarray;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import androidx.annotation.Nullable;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -29,6 +32,7 @@ import java.nio.ByteBuffer;
import java.nio.ByteOrder; import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.Locale; import java.util.Locale;
import java.util.function.Consumer;
import nodomain.freeyourgadget.gadgetbridge.BuildConfig; import nodomain.freeyourgadget.gadgetbridge.BuildConfig;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst; import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst;
@ -42,6 +46,7 @@ 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;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.AbstractZeppOsService; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.AbstractZeppOsService;
import nodomain.freeyourgadget.gadgetbridge.util.BitmapUtil; import nodomain.freeyourgadget.gadgetbridge.util.BitmapUtil;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.util.LimitedQueue; import nodomain.freeyourgadget.gadgetbridge.util.LimitedQueue;
import nodomain.freeyourgadget.gadgetbridge.util.NotificationUtils; import nodomain.freeyourgadget.gadgetbridge.util.NotificationUtils;
import nodomain.freeyourgadget.gadgetbridge.util.StringUtils; import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
@ -51,12 +56,16 @@ public class ZeppOsNotificationService extends AbstractZeppOsService {
public static final short ENDPOINT = 0x001e; public static final short ENDPOINT = 0x001e;
public static final byte NOTIFICATION_CMD_CAPABILITIES_REQUEST = 0x01;
public static final byte NOTIFICATION_CMD_CAPABILITIES_RESPONSE = 0x02;
public static final byte NOTIFICATION_CMD_SEND = 0x03; public static final byte NOTIFICATION_CMD_SEND = 0x03;
public static final byte NOTIFICATION_CMD_REPLY = 0x04; public static final byte NOTIFICATION_CMD_REPLY = 0x04;
public static final byte NOTIFICATION_CMD_DISMISS = 0x05; public static final byte NOTIFICATION_CMD_DISMISS = 0x05;
public static final byte NOTIFICATION_CMD_REPLY_ACK = 0x06; public static final byte NOTIFICATION_CMD_REPLY_ACK = 0x06;
public static final byte NOTIFICATION_CMD_ICON_REQUEST = 0x10; public static final byte NOTIFICATION_CMD_ICON_REQUEST = 0x10;
public static final byte NOTIFICATION_CMD_ICON_REQUEST_ACK = 0x11; public static final byte NOTIFICATION_CMD_ICON_REQUEST_ACK = 0x11;
public static final byte NOTIFICATION_CMD_PICTURE_REQUEST = 0x19;
public static final byte NOTIFICATION_CMD_PICTURE_REQUEST_ACK = 0x1a;
public static final byte NOTIFICATION_TYPE_NORMAL = (byte) 0xfa; public static final byte NOTIFICATION_TYPE_NORMAL = (byte) 0xfa;
public static final byte NOTIFICATION_TYPE_CALL = 0x03; public static final byte NOTIFICATION_TYPE_CALL = 0x03;
public static final byte NOTIFICATION_TYPE_SMS = (byte) 0x05; public static final byte NOTIFICATION_TYPE_SMS = (byte) 0x05;
@ -68,10 +77,17 @@ public class ZeppOsNotificationService extends AbstractZeppOsService {
public static final byte NOTIFICATION_CALL_STATE_START = 0x00; public static final byte NOTIFICATION_CALL_STATE_START = 0x00;
public static final byte NOTIFICATION_CALL_STATE_END = 0x02; public static final byte NOTIFICATION_CALL_STATE_END = 0x02;
private int version = -1;
private boolean supportsPictures = false;
private boolean supportsNotificationKey = false;
// Keep track of Notification ID -> action handle, as BangleJSDeviceSupport. // Keep track of Notification ID -> action handle, as BangleJSDeviceSupport.
// This needs to be simplified. // This needs to be simplified.
private final LimitedQueue<Integer, Long> mNotificationReplyAction = new LimitedQueue<>(16); private final LimitedQueue<Integer, Long> mNotificationReplyAction = new LimitedQueue<>(16);
// Keep track of notification pictures
private final LimitedQueue<Integer, String> mNotificationPictures = new LimitedQueue<>(16);
private final ZeppOsFileTransferService fileTransferService; private final ZeppOsFileTransferService fileTransferService;
public ZeppOsNotificationService(final ZeppOsSupport support, final ZeppOsFileTransferService fileTransferService) { public ZeppOsNotificationService(final ZeppOsSupport support, final ZeppOsFileTransferService fileTransferService) {
@ -84,13 +100,45 @@ public class ZeppOsNotificationService extends AbstractZeppOsService {
return ENDPOINT; return ENDPOINT;
} }
@Override
public void initialize(final TransactionBuilder builder) {
requestCapabilities(builder);
}
public void requestCapabilities(final TransactionBuilder builder) {
write(builder, NOTIFICATION_CMD_CAPABILITIES_REQUEST);
}
@Override @Override
public void handlePayload(final byte[] payload) { public void handlePayload(final byte[] payload) {
final ByteBuffer buf = ByteBuffer.wrap(payload).order(ByteOrder.LITTLE_ENDIAN);
final byte cmd = buf.get();
final GBDeviceEventNotificationControl deviceEvtNotificationControl = new GBDeviceEventNotificationControl(); final GBDeviceEventNotificationControl deviceEvtNotificationControl = new GBDeviceEventNotificationControl();
final GBDeviceEventCallControl deviceEvtCallControl = new GBDeviceEventCallControl(); final GBDeviceEventCallControl deviceEvtCallControl = new GBDeviceEventCallControl();
switch (payload[0]) { switch (cmd) {
case NOTIFICATION_CMD_REPLY: case NOTIFICATION_CMD_CAPABILITIES_RESPONSE: {
version = buf.get() & 0xff;
if (version < 4 || version > 5) {
// Untested, might work, might not..
LOG.warn("Unsupported notification service version {}", version);
}
if (version >= 4) {
final short unk1 = buf.getShort(); // 100
final byte unk2 = buf.get(); // 1
final byte unk3 = buf.get(); // 1
final short unk4count = buf.getShort();
buf.get(new byte[unk4count]);
}
if (version >= 5) {
supportsPictures = buf.get() != 0;
supportsNotificationKey = buf.get() != 0;
}
LOG.info("Notification service version={}, supportsPictures={}", version, supportsPictures);
break;
}
case NOTIFICATION_CMD_REPLY: {
// TODO make this configurable? // TODO make this configurable?
final int notificationId = BLETypeConversions.toUint32(subarray(payload, 1, 5)); final int notificationId = BLETypeConversions.toUint32(subarray(payload, 1, 5));
final Long replyHandle = mNotificationReplyAction.lookup(notificationId); final Long replyHandle = mNotificationReplyAction.lookup(notificationId);
@ -114,6 +162,7 @@ public class ZeppOsNotificationService extends AbstractZeppOsService {
ackNotificationReply(notificationId); // FIXME: premature? ackNotificationReply(notificationId); // FIXME: premature?
deleteNotification(notificationId); // FIXME: premature? deleteNotification(notificationId); // FIXME: premature?
return; return;
}
case NOTIFICATION_CMD_DISMISS: case NOTIFICATION_CMD_DISMISS:
switch (payload[1]) { switch (payload[1]) {
case NOTIFICATION_DISMISS_NOTIFICATION: case NOTIFICATION_DISMISS_NOTIFICATION:
@ -138,7 +187,7 @@ public class ZeppOsNotificationService extends AbstractZeppOsService {
LOG.warn("Unexpected notification dismiss byte {}", String.format("0x%02x", payload[1])); LOG.warn("Unexpected notification dismiss byte {}", String.format("0x%02x", payload[1]));
return; return;
} }
case NOTIFICATION_CMD_ICON_REQUEST: case NOTIFICATION_CMD_ICON_REQUEST: {
final String packageName = StringUtils.untilNullTerminator(payload, 1); final String packageName = StringUtils.untilNullTerminator(payload, 1);
if (packageName == null) { if (packageName == null) {
LOG.error("Failed to decode package name from payload"); LOG.error("Failed to decode package name from payload");
@ -159,6 +208,31 @@ public class ZeppOsNotificationService extends AbstractZeppOsService {
int height = BLETypeConversions.toUint16(subarray(payload, pos, pos + 2)); int height = BLETypeConversions.toUint16(subarray(payload, pos, pos + 2));
sendIconForPackage(packageName, iconFormat, width, height); sendIconForPackage(packageName, iconFormat, width, height);
return; return;
}
case NOTIFICATION_CMD_PICTURE_REQUEST: {
final String packageName = StringUtils.untilNullTerminator(buf);
if (packageName == null) {
LOG.error("Failed to decode package name for picture from payload");
return;
}
final int notificationId = buf.getInt();
final byte pictureFormat = buf.get();
final int width = buf.getShort();
final int height = buf.getShort();
LOG.info(
"Got notification picture request for {}, {}, {}, {}x{}",
packageName,
notificationId,
pictureFormat,
width,
height
);
sendNotificationPicture(packageName, notificationId, pictureFormat, width);
return;
}
default: default:
LOG.warn("Unexpected notification byte {}", String.format("0x%02x", payload[0])); LOG.warn("Unexpected notification byte {}", String.format("0x%02x", payload[0]));
} }
@ -264,7 +338,7 @@ public class ZeppOsNotificationService extends AbstractZeppOsService {
// reply // reply
boolean hasReply = false; boolean hasReply = false;
if (notificationSpec.attachedActions != null && notificationSpec.attachedActions.size() > 0) { if (notificationSpec.attachedActions != null && !notificationSpec.attachedActions.isEmpty()) {
for (int i = 0; i < notificationSpec.attachedActions.size(); i++) { for (int i = 0; i < notificationSpec.attachedActions.size(); i++) {
final NotificationSpec.Action action = notificationSpec.attachedActions.get(i); final NotificationSpec.Action action = notificationSpec.attachedActions.get(i);
@ -281,6 +355,19 @@ public class ZeppOsNotificationService extends AbstractZeppOsService {
} }
baos.write((byte) (hasReply ? 1 : 0)); baos.write((byte) (hasReply ? 1 : 0));
if (version >= 5) {
baos.write(1); // ?
}
if (supportsPictures) {
baos.write((byte) (notificationSpec.picturePath != null ? 1 : 0));
if (notificationSpec.picturePath != null) {
mNotificationPictures.add(notificationSpec.getId(), notificationSpec.picturePath);
}
}
if (supportsNotificationKey) {
baos.write(notificationSpec.key.getBytes(StandardCharsets.UTF_8));
baos.write(0);
}
write(builder, baos.toByteArray()); write(builder, baos.toByteArray());
builder.queue(getSupport().getQueue()); builder.queue(getSupport().getQueue());
@ -325,7 +412,11 @@ public class ZeppOsNotificationService extends AbstractZeppOsService {
write("ack notification reply", buf.array()); write("ack notification reply", buf.array());
} }
private void ackNotificationAfterIconSent(final String queuedIconPackage) { private void ackNotificationAfterIconSent(final String queuedIconPackage, final boolean success) {
if (!success) {
return;
}
LOG.info("Acknowledging icon send for {}", queuedIconPackage); LOG.info("Acknowledging icon send for {}", queuedIconPackage);
final ByteBuffer buf = ByteBuffer.allocate(1 + queuedIconPackage.length() + 1 + 1); final ByteBuffer buf = ByteBuffer.allocate(1 + queuedIconPackage.length() + 1 + 1);
@ -333,33 +424,29 @@ public class ZeppOsNotificationService extends AbstractZeppOsService {
buf.put(NOTIFICATION_CMD_ICON_REQUEST_ACK); buf.put(NOTIFICATION_CMD_ICON_REQUEST_ACK);
buf.put(queuedIconPackage.getBytes(StandardCharsets.UTF_8)); buf.put(queuedIconPackage.getBytes(StandardCharsets.UTF_8));
buf.put((byte) 0x00); buf.put((byte) 0x00);
buf.put((byte) 0x01); buf.put((byte) 0x01); // TODO !success?
write("ack icon send", buf.array()); write("ack icon send", buf.array());
} }
private void sendIconForPackage(final String packageName, final byte iconFormat, final int width, final int height) { private void ackNotificationAfterPictureSent(final String packageName, final int notificationId, final boolean success) {
if (getSupport().getMTU() < 247) { LOG.info("Acknowledging picture send for {}", packageName);
LOG.warn("Sending icons requires high MTU, current MTU is {}", getSupport().getMTU());
return;
}
// Without the expected tga id and format string they seem to get corrupted, final ByteBuffer buf = ByteBuffer.allocate(1 + packageName.length() + 1 + 4 + 1).order(ByteOrder.LITTLE_ENDIAN);
// but the encoding seems to actually be the same...? buf.put(NOTIFICATION_CMD_PICTURE_REQUEST_ACK);
final String format; buf.put(packageName.getBytes(StandardCharsets.UTF_8));
final String tgaId; buf.put((byte) 0x00);
switch (iconFormat) { buf.putInt(notificationId);
case 0x04: buf.put((byte) (success ? 0x01 : 0x02));
format = "TGA_RGB565_GCNANOLITE";
tgaId = "SOMHP"; write("ack picture send", buf.array());
break; }
case 0x08:
format = "TGA_RGB565_DAVE2D"; private void sendIconForPackage(final String packageName, final byte iconFormat, final int width, final int height) {
tgaId = "SOMH6"; final BitmapFormat format = BitmapFormat.fromCode(iconFormat);
break; if (format == null) {
default: LOG.error("Unknown icon bitmap format code {}", String.format("0x%02x", iconFormat));
LOG.error("Unknown icon format {}", String.format("0x%02x", iconFormat)); return;
return;
} }
final Drawable icon = NotificationUtils.getAppIcon(getContext(), packageName); final Drawable icon = NotificationUtils.getAppIcon(getContext(), packageName);
@ -369,12 +456,7 @@ public class ZeppOsNotificationService extends AbstractZeppOsService {
} }
final Bitmap bmp = BitmapUtil.toBitmap(icon); final Bitmap bmp = BitmapUtil.toBitmap(icon);
final byte[] tga = encodeBitmap(bmp, format, width, height);
// The TGA needs to have this ID, or the band does not accept it
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, tgaIdBytes);
final String url = String.format( final String url = String.format(
Locale.ROOT, Locale.ROOT,
@ -386,22 +468,76 @@ public class ZeppOsNotificationService extends AbstractZeppOsService {
); );
final String filename = String.format("logo_%s.tga", packageName.replace(".", "_")); final String filename = String.format("logo_%s.tga", packageName.replace(".", "_"));
sendFile(url, filename, tga, false, success -> ackNotificationAfterIconSent(packageName, success));
}
private void sendNotificationPicture(final String packageName, final int notificationId, final byte pictureFormat, final int width) {
final BitmapFormat format = BitmapFormat.fromCode(pictureFormat);
if (format == null) {
LOG.error("Unknown picture bitmap format code {}", String.format("0x%02x", pictureFormat));
ackNotificationAfterPictureSent(packageName, notificationId, false);
return;
}
final String picturePath = mNotificationPictures.lookup(notificationId);
if (picturePath == null) {
LOG.warn("Failed to find picture path for {}", notificationId);
ackNotificationAfterPictureSent(packageName, notificationId, false);
return;
}
final Bitmap bmp = BitmapFactory.decodeFile(picturePath);
if (bmp == null) {
LOG.warn("Failed to decode bitmap from {}", picturePath);
ackNotificationAfterPictureSent(packageName, notificationId, false);
return;
}
// FIXME: On the GTR 4, the band sends 358 on the url, but the actual image has 368 width
// if sent as requested, it gets all corrupted...
final int targetWidth = width + 10;
final int targetHeight = (int) Math.round(bmp.getHeight() * ((double) targetWidth / bmp.getWidth()));
final byte[] tga = encodeBitmap(bmp, format, targetWidth, targetHeight);
final String url = String.format(
Locale.ROOT,
"notification://content_image?app_id=%s&uid=%d&width=%d&height=%d&format=%s",
packageName,
notificationId,
width,
targetHeight,
format
);
final String filename = String.format(Locale.ROOT, "picture_%d.tga", notificationId);
sendFile(url, filename, tga, true, success -> ackNotificationAfterPictureSent(packageName, notificationId, success));
}
private void sendFile(final String url,
final String filename,
final byte[] bytes,
final boolean compress,
final Consumer<Boolean> uploadFinishCallback) {
if (getSupport().getMTU() < 247) {
LOG.warn("Sending files requires high MTU, current MTU is {}", getSupport().getMTU());
return;
}
fileTransferService.sendFile( fileTransferService.sendFile(
url, url,
filename, filename,
tga565, bytes,
true,
new ZeppOsFileTransferService.Callback() { new ZeppOsFileTransferService.Callback() {
@Override @Override
public void onFileUploadFinish(final boolean success) { public void onFileUploadFinish(final boolean success) {
LOG.info("Finished sending icon, success={}", success); LOG.info("Finished sending '{}' to '{}', success={}", filename, url, success);
if (success) { uploadFinishCallback.accept(success);
ackNotificationAfterIconSent(packageName);
}
} }
@Override @Override
public void onFileUploadProgress(final int progress) { public void onFileUploadProgress(final int progress) {
LOG.trace("Icon send progress: {}", progress); LOG.trace("File send progress: {}", progress);
} }
@Override @Override
@ -411,6 +547,50 @@ public class ZeppOsNotificationService extends AbstractZeppOsService {
} }
); );
LOG.info("Queueing icon for {}", packageName); LOG.info("Queueing file send '{}' to '{}'", filename, url);
}
private static byte[] encodeBitmap(final Bitmap bmp, final BitmapFormat format, final int width, final int height) {
// Without the expected tga id and format string they seem to get corrupted,
// but the encoding seems to actually be the same...?
// The TGA needs to have this ID, or the band does not accept it
final byte[] tgaIdBytes = new byte[46];
//System.arraycopy(format.getTgaId().getBytes(StandardCharsets.UTF_8), 0, tgaIdBytes, 0, 5);
System.arraycopy(GB.hexStringToByteArray("534F4D486601"), 0, tgaIdBytes, 0, 6);
return BitmapUtil.convertToTgaRGB565(bmp, width, height, tgaIdBytes);
}
private enum BitmapFormat {
TGA_RGB565_GCNANOLITE(0x04, "SOMHP"),
TGA_RGB565_DAVE2D(0x08, "SOMH6"),
;
private final byte code;
private final String tgaId;
BitmapFormat(final int code, final String tgaId) {
this.code = (byte) code;
this.tgaId = tgaId;
}
public byte getCode() {
return code;
}
public String getTgaId() {
return tgaId;
}
@Nullable
public static BitmapFormat fromCode(final byte code) {
for (final BitmapFormat format : BitmapFormat.values()) {
if (format.code == code) {
return format;
}
}
return null;
}
} }
} }