From 42c37c04a0b4a051999057ede0cac268202c74b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Rebelo?= Date: Sat, 10 Jun 2023 13:34:48 +0100 Subject: [PATCH] Zepp OS: Display watchface and app preview on install --- .../activities/FwAppInstallerActivity.java | 15 +++ .../activities/InstallActivity.java | 6 + .../devices/huami/HuamiFWHelper.java | 10 ++ .../miband/AbstractMiBandFWHelper.java | 5 + .../AbstractMiBandFWInstallHandler.java | 5 +- .../huami/AbstractHuamiFirmwareInfo.java | 9 ++ .../devices/huami/Huami2021FirmwareInfo.java | 72 +++++++++++- .../devices/huami/HuamiFirmwareType.java | 8 ++ .../gadgetbridge/util/BitmapUtil.java | 109 ++++++++++++++++++ .../main/res/layout/activity_appinstaller.xml | 9 ++ 10 files changed, 244 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/FwAppInstallerActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/FwAppInstallerActivity.java index 1e0eebe44..c617b525a 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/FwAppInstallerActivity.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/FwAppInstallerActivity.java @@ -21,16 +21,19 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.graphics.Bitmap; import android.net.Uri; import android.os.Bundle; import android.view.MenuItem; import android.view.View; import android.widget.Button; +import android.widget.ImageView; import android.widget.ListView; import android.widget.ProgressBar; import android.widget.TextView; import android.widget.Toast; +import androidx.annotation.Nullable; import androidx.core.app.NavUtils; import androidx.localbroadcastmanager.content.LocalBroadcastManager; @@ -59,6 +62,7 @@ public class FwAppInstallerActivity extends AbstractGBActivity implements Instal private static final String ITEM_DETAILS = "details"; private TextView fwAppInstallTextView; + private ImageView previewImage; private Button installButton; private Uri uri; private GBDevice device; @@ -178,6 +182,7 @@ public class FwAppInstallerActivity extends AbstractGBActivity implements Instal itemAdapter = new ItemWithDetailsAdapter(this, items); itemListView.setAdapter(itemAdapter); fwAppInstallTextView = findViewById(R.id.infoTextView); + previewImage = findViewById(R.id.previewImage); installButton = findViewById(R.id.installButton); progressBar = findViewById(R.id.installProgressBar); progressText = findViewById(R.id.installProgressText); @@ -300,6 +305,16 @@ public class FwAppInstallerActivity extends AbstractGBActivity implements Instal fwAppInstallTextView.setText(text); } + @Override + public void setPreview(@Nullable final Bitmap bitmap) { + previewImage.setImageBitmap(bitmap); + if (previewImage == null) { + previewImage.setVisibility(View.GONE); + } else { + previewImage.setVisibility(View.VISIBLE); + } + } + @Override public CharSequence getInfoText() { return fwAppInstallTextView.getText(); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/InstallActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/InstallActivity.java index bd6e40257..c3699cce6 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/InstallActivity.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/InstallActivity.java @@ -16,6 +16,10 @@ along with this program. If not, see . */ package nodomain.freeyourgadget.gadgetbridge.activities; +import android.graphics.Bitmap; + +import androidx.annotation.Nullable; + import nodomain.freeyourgadget.gadgetbridge.model.ItemWithDetails; public interface InstallActivity { @@ -23,6 +27,8 @@ public interface InstallActivity { void setInfoText(String text); + void setPreview(@Nullable Bitmap bitmap); + void setInstallEnabled(boolean enable); void clearInstallItems(); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/HuamiFWHelper.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/HuamiFWHelper.java index c089c41ea..2a50623de 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/HuamiFWHelper.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/HuamiFWHelper.java @@ -18,6 +18,7 @@ package nodomain.freeyourgadget.gadgetbridge.devices.huami; import android.content.Context; +import android.graphics.Bitmap; import android.net.Uri; import java.io.IOException; @@ -132,4 +133,13 @@ public abstract class HuamiFWHelper extends AbstractMiBandFWHelper { public AbstractHuamiFirmwareInfo getFirmwareInfo() { return firmwareInfo; } + + @Override + public Bitmap getPreview() { + if (firmwareInfo != null) { + return firmwareInfo.getPreview(); + } + + return null; + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/AbstractMiBandFWHelper.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/AbstractMiBandFWHelper.java index a43ad98c1..fdd5f94cf 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/AbstractMiBandFWHelper.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/AbstractMiBandFWHelper.java @@ -18,6 +18,7 @@ package nodomain.freeyourgadget.gadgetbridge.devices.miband; import android.content.Context; +import android.graphics.Bitmap; import android.net.Uri; import androidx.annotation.NonNull; @@ -147,4 +148,8 @@ public abstract class AbstractMiBandFWHelper { public abstract void checkValid() throws IllegalArgumentException; public abstract HuamiFirmwareType getFirmwareType(); + + public Bitmap getPreview() { + return null; + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/AbstractMiBandFWInstallHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/AbstractMiBandFWInstallHandler.java index 3dd7347c5..976210b70 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/AbstractMiBandFWInstallHandler.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/AbstractMiBandFWInstallHandler.java @@ -101,7 +101,7 @@ public abstract class AbstractMiBandFWInstallHandler implements InstallHandler { return; } StringBuilder builder = new StringBuilder(); - if (helper.getFirmwareType() != WATCHFACE && helper.getFirmwareType() != AGPS_UIHH) { + if (!helper.getFirmwareType().isWatchface() && !helper.getFirmwareType().isApp() && helper.getFirmwareType() != AGPS_UIHH) { if (helper.isSingleFirmware()) { builder.append(getFwUpgradeNotice()); } else { @@ -120,6 +120,9 @@ public abstract class AbstractMiBandFWInstallHandler implements InstallHandler { // TODO: set a UNKNOWN (question mark) button } } + + installActivity.setPreview(helper.getPreview()); + installActivity.setInfoText(builder.toString()); installActivity.setInstallItem(fwItem); installActivity.setInstallEnabled(true); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/AbstractHuamiFirmwareInfo.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/AbstractHuamiFirmwareInfo.java index 99f04cbb2..b768c0472 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/AbstractHuamiFirmwareInfo.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/AbstractHuamiFirmwareInfo.java @@ -17,6 +17,10 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.huami; +import android.graphics.Bitmap; + +import androidx.annotation.Nullable; + import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.Map; @@ -86,6 +90,11 @@ public abstract class AbstractHuamiFirmwareInfo { this.bytes = null; } + @Nullable + public Bitmap getPreview() { + return null; + } + public abstract String toVersion(int crc16); public abstract boolean isGenerallyCompatibleWith(GBDevice device); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/Huami2021FirmwareInfo.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/Huami2021FirmwareInfo.java index b6798e03d..b097a6dd8 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/Huami2021FirmwareInfo.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/Huami2021FirmwareInfo.java @@ -17,20 +17,29 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.huami; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; + +import androidx.annotation.Nullable; + import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.File; import java.io.IOException; import java.lang.reflect.Constructor; import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.Set; +import java.util.UUID; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceApp; import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions; import nodomain.freeyourgadget.gadgetbridge.util.ArrayUtils; +import nodomain.freeyourgadget.gadgetbridge.util.BitmapUtil; import nodomain.freeyourgadget.gadgetbridge.util.ZipFile; import nodomain.freeyourgadget.gadgetbridge.util.ZipFileException; @@ -39,6 +48,7 @@ public abstract class Huami2021FirmwareInfo extends AbstractHuamiFirmwareInfo { private static final Logger LOG = LoggerFactory.getLogger(Huami2021FirmwareInfo.class); private final String preComputedVersion; + private GBDeviceApp gbDeviceApp; public Huami2021FirmwareInfo(final byte[] bytes) { super(bytes); @@ -125,22 +135,64 @@ public abstract class Huami2021FirmwareInfo extends AbstractHuamiFirmwareInfo { // Attempt to handle as an app / watchface final JSONObject appJson = getJson(zipFile, "app.json"); if (appJson != null) { + final int appId; + final String appName; + final String appVersion; final String appType; + final String appCreator; + final String appIconPath; + final JSONObject appJsonApp; try { - appType = appJson.getJSONObject("app").getString("appType"); + appJsonApp = appJson.getJSONObject("app"); + appId = appJsonApp.getInt("appId"); + appName = appJsonApp.getString("appName"); + appVersion = appJsonApp.getJSONObject("version").getString("name"); + appType = appJsonApp.getString("appType"); + appCreator = appJsonApp.getString("vender"); + appIconPath = appJsonApp.getString("icon"); } catch (final Exception e) { LOG.error("Failed to get appType from app.json", e); return HuamiFirmwareType.INVALID; } + final HuamiFirmwareType huamiFirmwareType; + final GBDeviceApp.Type gbDeviceAppType; switch (appType) { case "watchface": - return HuamiFirmwareType.WATCHFACE; + huamiFirmwareType = HuamiFirmwareType.WATCHFACE; + gbDeviceAppType = GBDeviceApp.Type.WATCHFACE; + break; case "app": - return HuamiFirmwareType.APP; + huamiFirmwareType = HuamiFirmwareType.APP; + gbDeviceAppType = GBDeviceApp.Type.APP_GENERIC; + break; default: LOG.warn("Unknown app type {}", appType); + return HuamiFirmwareType.INVALID; } + + Bitmap icon = null; + try { + final byte[] iconBytes = zipFile.getFileFromZip("assets/" + appIconPath); + if (BitmapUtil.isPng(iconBytes)) { + icon = BitmapFactory.decodeByteArray(iconBytes, 0, iconBytes.length); + } else { + icon = BitmapUtil.decodeTga(iconBytes); + } + } catch (final ZipFileException e) { + LOG.error("Failed to get app icon from zip", e); + } + + gbDeviceApp = new GBDeviceApp( + UUID.fromString(String.format("%08x-0000-0000-0000-000000000000", appId)), + appName, + appCreator, + appVersion, + gbDeviceAppType, + icon + ); + + return huamiFirmwareType; } // Attempt to handle as a zab file @@ -254,6 +306,20 @@ public abstract class Huami2021FirmwareInfo extends AbstractHuamiFirmwareInfo { return null; } + public GBDeviceApp getAppInfo() { + return gbDeviceApp; + } + + @Nullable + @Override + public Bitmap getPreview() { + if (gbDeviceApp != null) { + return gbDeviceApp.getPreviewImage(); + } + + return null; + } + public Huami2021FirmwareInfo repackFirmwareInUIHH() throws IOException { if (!firmwareType.equals(HuamiFirmwareType.FIRMWARE)) { throw new IllegalStateException("Can only repack FIRMWARE"); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiFirmwareType.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiFirmwareType.java index 0ede411d5..ec830c98c 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiFirmwareType.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiFirmwareType.java @@ -44,4 +44,12 @@ public enum HuamiFirmwareType { public byte getValue() { return value; } + + public boolean isApp() { + return this == APP || this == ZEPPOS_APP; + } + + public boolean isWatchface() { + return this == WATCHFACE; + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/BitmapUtil.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/BitmapUtil.java index 09345c9f5..8ecb85909 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/BitmapUtil.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/BitmapUtil.java @@ -30,9 +30,17 @@ import android.graphics.RectF; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; +import androidx.annotation.Nullable; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.nio.ByteBuffer; +import java.nio.ByteOrder; public class BitmapUtil { + private static final Logger LOG = LoggerFactory.getLogger(BitmapUtil.class); + /** * Downscale a bitmap to a maximum resolution. Doesn't scale if the bitmap is already smaller than the max resolution. * @@ -284,4 +292,105 @@ public class BitmapUtil { return tga565buf.array(); } + + public static boolean isPng(final byte[] data) { + return ArrayUtils.equals(data, new byte[] {(byte) 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a}, 0); + } + + @Nullable + public static Bitmap decodeTga(final byte[] bytes) { + final ByteBuffer buf = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN); + final byte idLength = buf.get(); + final byte colorMapType = buf.get(); + final byte imageType = buf.get(); + final short colorMapFirstEntryIndex = buf.getShort(); + final short colorMapLength = buf.getShort(); + final byte colorMapEntrySize = buf.get(); + final short xOrigin = buf.getShort(); + final short yOrigin = buf.getShort(); + final short width = buf.getShort(); + final short height = buf.getShort(); + final byte bitsPerPixel = buf.get(); + final byte imageDescriptor = buf.get(); + + final byte[] id = new byte[idLength]; + buf.get(id); + + if (imageType== 2 && colorMapType == 0) { + // Parse true-color uncompressed image data as RGB565 + final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565); + bitmap.copyPixelsFromBuffer(buf); + + return bitmap; + } + + if (colorMapType != 0x01) { + LOG.warn("Unknown color map type {}", colorMapType); + return null; + } + + if (colorMapEntrySize != 32) { + LOG.warn("Color map entry size {} not supported", colorMapEntrySize); + return null; + } + + final int[] colorMap = new int[colorMapLength]; + for (int i = 0; i < colorMapLength; i++) { + colorMap[i] = buf.getInt(); + } + + final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + + switch (imageType) { + case 1: + if (bitsPerPixel != 8) { + LOG.warn("Unsupported bits per pixel {} for imageType 1", bitsPerPixel); + return null; + } + + // uncompressed color-mapped image + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + bitmap.setPixel(x, y, colorMap[buf.get() & 0xff]); + } + } + break; + case 9: + // run-length encoded color-mapped image + int i = 0; + while (buf.position() < buf.limit()) { + final byte b = buf.get(); + final int count = (b & 127) + 1; + if ((b & 128) != 0) { + // msb 1 - run-length encoded + final int val = buf.get() & 0xff; + for (int j = 0; j < count; j++) { + int y = i / width; + int x = i % width; + bitmap.setPixel(x, y, colorMap[val]); + i++; + } + } else { + // msb 0 - raw pixels + for (int j = 0; j < count; j++) { + int y = i / width; + int x = i % width; + bitmap.setPixel(x, y, colorMap[buf.get() & 0xff]); + i++; + } + } + } + break; + default: + LOG.warn("Image type {} not supported", imageType); + return null; + } + + final int remainingBytes = buf.limit() - buf.position(); + if (remainingBytes != 0) { + LOG.warn("There are {} bytes remaining in the buffer", remainingBytes); + } + + return bitmap; + } } diff --git a/app/src/main/res/layout/activity_appinstaller.xml b/app/src/main/res/layout/activity_appinstaller.xml index 26378125a..a23b73869 100644 --- a/app/src/main/res/layout/activity_appinstaller.xml +++ b/app/src/main/res/layout/activity_appinstaller.xml @@ -15,10 +15,19 @@ tools:context="nodomain.freeyourgadget.gadgetbridge.activities.FwAppInstallerActivity" tools:ignore="ScrollViewSize"> + +