mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge
synced 2024-11-23 18:36:50 +01:00
Xiaomi: extract watch face preview image
This commit is contained in:
parent
b2cf83d002
commit
0a9da03618
@ -17,6 +17,7 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.xiaomi;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.net.Uri;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
@ -33,6 +34,7 @@ import java.util.regex.Pattern;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.XiaomiBitmapUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.ArrayUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
|
||||
@ -258,6 +260,63 @@ public class XiaomiFWHelper {
|
||||
return new String(localizationBytes, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
public Bitmap getWatchfacePreview() {
|
||||
if (!isWatchface() || fw == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final ByteBuffer bb = ByteBuffer.wrap(fw).order(ByteOrder.LITTLE_ENDIAN);
|
||||
final int previewOffset = bb.getInt(0x20);
|
||||
if (previewOffset == 0) {
|
||||
LOG.debug("No preview available (at offset 0)");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (previewOffset + 12 > fw.length) {
|
||||
LOG.debug("No preview available (header out-of-bounds)");
|
||||
return null;
|
||||
}
|
||||
|
||||
bb.position(previewOffset);
|
||||
final int bitmapType = bb.get() & 0xff;
|
||||
final int compressionType = bb.get() & 0xff;
|
||||
bb.getShort(); // ignore
|
||||
final int width = bb.getShort() & 0xffff;
|
||||
final int height = bb.getShort() & 0xffff;
|
||||
final int bitmapSize = bb.getInt();
|
||||
|
||||
byte[] bitmapData = new byte[bitmapSize];
|
||||
bb.get(bitmapData);
|
||||
|
||||
if (compressionType != 0) {
|
||||
LOG.debug("Preview image compression type: {}", compressionType);
|
||||
switch (compressionType) {
|
||||
case 4:
|
||||
bitmapData = XiaomiBitmapUtils.decompressLvglRleV2(bitmapData);
|
||||
break;
|
||||
case 8:
|
||||
bitmapData = XiaomiBitmapUtils.decompressLvglRleV1(bitmapData);
|
||||
break;
|
||||
default:
|
||||
LOG.error("unknown compression type {}", compressionType);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (bitmapData == null) {
|
||||
LOG.error("decompression returned null");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return XiaomiBitmapUtils.decodeWatchfaceImage(
|
||||
bitmapData,
|
||||
bitmapType,
|
||||
compressionType == 8,
|
||||
width,
|
||||
height
|
||||
);
|
||||
}
|
||||
|
||||
private boolean parseAsWatchface() {
|
||||
if (fw[0] != (byte) 0x5A || fw[1] != (byte) 0xA5) {
|
||||
LOG.warn("File header not a watchface");
|
||||
|
@ -17,6 +17,7 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.xiaomi;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.net.Uri;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
@ -65,6 +66,11 @@ public class XiaomiInstallHandler implements InstallHandler {
|
||||
if (helper.isWatchface()) {
|
||||
installItem.setIcon(R.drawable.ic_watchface);
|
||||
installItem.setName(mContext.getString(R.string.kind_watchface));
|
||||
|
||||
final Bitmap preview = helper.getWatchfacePreview();
|
||||
if (preview != null) {
|
||||
installItem.setPreview(preview);
|
||||
}
|
||||
} else if (helper.isFirmware()) {
|
||||
installItem.setIcon(R.drawable.ic_firmware);
|
||||
installItem.setName(mContext.getString(R.string.kind_firmware));
|
||||
|
@ -26,9 +26,15 @@ import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.ArrayUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
|
||||
public class XiaomiBitmapUtils {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(XiaomiBitmapUtils.class);
|
||||
private static final byte[] LVGL_RLE_HEADER = new byte[] {(byte) 0xe0, 0x21, (byte) 0xa5, 0x5a };
|
||||
|
||||
public static final int PIXEL_FORMAT_RGB_565_LE = 0;
|
||||
public static final int PIXEL_FORMAT_RGB_565_BE = 1;
|
||||
@ -167,4 +173,190 @@ public class XiaomiBitmapUtils {
|
||||
LOG.error("Unknown pixel format {}", pixelFormat);
|
||||
return null;
|
||||
}
|
||||
|
||||
public static byte[] decompressLvglRleV1(final byte[] bitmapData) {
|
||||
if (!ArrayUtils.equals(bitmapData, LVGL_RLE_HEADER, 0)) {
|
||||
LOG.debug("Compressed data does not start with expected LVGL RLE header (found {})",
|
||||
GB.hexdump(bitmapData, 0, 4));
|
||||
return null;
|
||||
}
|
||||
|
||||
int chunkSize = bitmapData[4] & 0xf;
|
||||
if (chunkSize == 0) {
|
||||
chunkSize = 1;
|
||||
}
|
||||
|
||||
final ByteBuffer bb = ByteBuffer.wrap(bitmapData).order(ByteOrder.LITTLE_ENDIAN);
|
||||
bb.getInt(); // magic
|
||||
final int decompressedSize = (bb.getInt() >> 4) & 0xfffffff;
|
||||
|
||||
final byte[] out = new byte[decompressedSize];
|
||||
int outOff = 0;
|
||||
|
||||
while (bb.hasRemaining()) {
|
||||
byte control = bb.get();
|
||||
int n = control & 0x7f;
|
||||
|
||||
if (outOff + chunkSize * (n+1) > out.length) {
|
||||
LOG.error("decompression overflow");
|
||||
return null;
|
||||
}
|
||||
|
||||
if ((control & 0x80) != 0) {
|
||||
// copy next chunk n+1 times to out
|
||||
if (bb.remaining() < chunkSize) {
|
||||
LOG.error("not enough data to decompress");
|
||||
return null;
|
||||
}
|
||||
|
||||
final byte[] chunk = new byte[chunkSize];
|
||||
bb.get(chunk);
|
||||
|
||||
for (int i = 0; i < n + 1; i++) {
|
||||
System.arraycopy(chunk, 0, out, outOff, chunk.length);
|
||||
outOff += chunk.length;
|
||||
}
|
||||
} else {
|
||||
// copy next n+1 chunks to out
|
||||
if (bb.remaining() < chunkSize * (n + 1)) {
|
||||
LOG.error("not enough data to decompress");
|
||||
return null;
|
||||
}
|
||||
|
||||
final byte[] chunk = new byte[chunkSize * (n+1)];
|
||||
bb.get(chunk);
|
||||
System.arraycopy(chunk, 0, out, outOff, chunk.length);
|
||||
outOff += chunk.length;
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
public static byte[] decompressLvglRleV2(final byte[] bitmapData) {
|
||||
if (!ArrayUtils.equals(bitmapData, LVGL_RLE_HEADER, 0)) {
|
||||
LOG.debug("Compressed data does not start with expected LVGL RLE header (found {})",
|
||||
GB.hexdump(bitmapData, 0, 4));
|
||||
return null;
|
||||
}
|
||||
|
||||
int chunkSize = bitmapData[4] & 0xf;
|
||||
if (chunkSize == 0) {
|
||||
chunkSize = 1;
|
||||
}
|
||||
|
||||
LOG.debug("Chunk size: {}", chunkSize);
|
||||
final ByteBuffer bb = ByteBuffer.wrap(bitmapData).order(ByteOrder.LITTLE_ENDIAN);
|
||||
bb.getInt(); // magic
|
||||
final int decompressedSize = (bb.getInt() >> 4) & 0xfffffff;
|
||||
LOG.debug("Compressed size: {}, decompressed size: {}", bb.remaining(), decompressedSize);
|
||||
|
||||
final byte[] out = new byte[decompressedSize];
|
||||
int outOff = 0;
|
||||
|
||||
while (bb.hasRemaining()) {
|
||||
byte control = bb.get();
|
||||
int n = control & 0x7f;
|
||||
|
||||
if (outOff + chunkSize * n > out.length) {
|
||||
LOG.error("decompression overflow");
|
||||
return null;
|
||||
}
|
||||
|
||||
if ((control & 0x80) != 0) {
|
||||
// copy next n+1 chunks to out
|
||||
if (bb.remaining() < chunkSize * n) {
|
||||
LOG.error("not enough data to decompress");
|
||||
return null;
|
||||
}
|
||||
|
||||
final byte[] chunk = new byte[chunkSize * n];
|
||||
bb.get(chunk);
|
||||
System.arraycopy(chunk, 0, out, outOff, chunk.length);
|
||||
outOff += chunk.length;
|
||||
} else {
|
||||
// copy next chunk n+1 times to out
|
||||
if (bb.remaining() < chunkSize) {
|
||||
LOG.error("not enough data to decompress");
|
||||
return null;
|
||||
}
|
||||
|
||||
final byte[] chunk = new byte[chunkSize];
|
||||
bb.get(chunk);
|
||||
|
||||
for (int i = 0; i < n; i++) {
|
||||
System.arraycopy(chunk, 0, out, outOff, chunk.length);
|
||||
outOff += chunk.length;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
public static Bitmap decodeWatchfaceImage(final byte[] bitmapData, final int bitmapFormat, final boolean swapRedBlueChannel, final int width, final int height) {
|
||||
final int expectedInputSize;
|
||||
switch (bitmapFormat) {
|
||||
case 0:
|
||||
expectedInputSize = width * height * 4;
|
||||
break;
|
||||
case 1:
|
||||
case 4:
|
||||
case 7:
|
||||
expectedInputSize = width * height * 2;
|
||||
break;
|
||||
case 16:
|
||||
expectedInputSize = 256 * 4 + width * height;
|
||||
break;
|
||||
default:
|
||||
LOG.warn("bitmap format {} unknown", bitmapFormat);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (expectedInputSize > bitmapData.length) {
|
||||
LOG.error("Not enough pixel data (expected {} bytes, got {})",
|
||||
expectedInputSize,
|
||||
bitmapData.length);
|
||||
return null;
|
||||
}
|
||||
|
||||
final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
|
||||
final ByteBuffer bb = ByteBuffer.wrap(bitmapData);
|
||||
bb.order(bitmapFormat == 7 ? ByteOrder.BIG_ENDIAN : ByteOrder.LITTLE_ENDIAN);
|
||||
|
||||
|
||||
int[] palette = new int[0];
|
||||
if (bitmapFormat == 16) {
|
||||
palette = new int[256];
|
||||
for (int i = 0; i < palette.length; i++) {
|
||||
palette[i] = bb.getInt();
|
||||
}
|
||||
}
|
||||
|
||||
for (int y = 0; y < height; y++) {
|
||||
for (int x = 0; x < width; x++) {
|
||||
switch (bitmapFormat) {
|
||||
case 0x00:
|
||||
bitmap.setPixel(x, y, bb.getInt());
|
||||
break;
|
||||
case 0x01:
|
||||
case 0x04:
|
||||
case 0x07:
|
||||
final int c565 = bb.getShort() & 0xffff;
|
||||
final int pixel = 0xff000000 |
|
||||
((c565 & 0xf800) << 8) |
|
||||
((c565 & 0x07e0) << 5) |
|
||||
((c565 & 0x001f) << 3);
|
||||
bitmap.setPixel(x, y, pixel);
|
||||
break;
|
||||
case 0x10:
|
||||
final int paletteId = bb.get() & 0xff;
|
||||
bitmap.setPixel(x, y, palette[paletteId]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return bitmap;
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user