mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge
synced 2024-11-24 02:46: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;
|
package nodomain.freeyourgadget.gadgetbridge.devices.xiaomi;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.graphics.Bitmap;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
@ -33,6 +34,7 @@ import java.util.regex.Pattern;
|
|||||||
|
|
||||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
|
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.ArrayUtils;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
|
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
|
import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
|
||||||
@ -258,6 +260,63 @@ public class XiaomiFWHelper {
|
|||||||
return new String(localizationBytes, StandardCharsets.UTF_8);
|
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() {
|
private boolean parseAsWatchface() {
|
||||||
if (fw[0] != (byte) 0x5A || fw[1] != (byte) 0xA5) {
|
if (fw[0] != (byte) 0x5A || fw[1] != (byte) 0xA5) {
|
||||||
LOG.warn("File header not a watchface");
|
LOG.warn("File header not a watchface");
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
package nodomain.freeyourgadget.gadgetbridge.devices.xiaomi;
|
package nodomain.freeyourgadget.gadgetbridge.devices.xiaomi;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.graphics.Bitmap;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
|
||||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||||
@ -65,6 +66,11 @@ public class XiaomiInstallHandler implements InstallHandler {
|
|||||||
if (helper.isWatchface()) {
|
if (helper.isWatchface()) {
|
||||||
installItem.setIcon(R.drawable.ic_watchface);
|
installItem.setIcon(R.drawable.ic_watchface);
|
||||||
installItem.setName(mContext.getString(R.string.kind_watchface));
|
installItem.setName(mContext.getString(R.string.kind_watchface));
|
||||||
|
|
||||||
|
final Bitmap preview = helper.getWatchfacePreview();
|
||||||
|
if (preview != null) {
|
||||||
|
installItem.setPreview(preview);
|
||||||
|
}
|
||||||
} else if (helper.isFirmware()) {
|
} else if (helper.isFirmware()) {
|
||||||
installItem.setIcon(R.drawable.ic_firmware);
|
installItem.setIcon(R.drawable.ic_firmware);
|
||||||
installItem.setName(mContext.getString(R.string.kind_firmware));
|
installItem.setName(mContext.getString(R.string.kind_firmware));
|
||||||
|
@ -26,9 +26,15 @@ import org.slf4j.LoggerFactory;
|
|||||||
|
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.nio.ByteOrder;
|
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 {
|
public class XiaomiBitmapUtils {
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(XiaomiBitmapUtils.class);
|
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_LE = 0;
|
||||||
public static final int PIXEL_FORMAT_RGB_565_BE = 1;
|
public static final int PIXEL_FORMAT_RGB_565_BE = 1;
|
||||||
@ -167,4 +173,190 @@ public class XiaomiBitmapUtils {
|
|||||||
LOG.error("Unknown pixel format {}", pixelFormat);
|
LOG.error("Unknown pixel format {}", pixelFormat);
|
||||||
return null;
|
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