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">
+
+