1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-07-12 00:14:04 +02:00

Zepp OS: Display watchface and app preview on install

This commit is contained in:
José Rebelo 2023-06-10 13:34:48 +01:00
parent 003246ae1c
commit 42c37c04a0
10 changed files with 244 additions and 4 deletions

View File

@ -21,16 +21,19 @@ import android.content.BroadcastReceiver;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.IntentFilter; import android.content.IntentFilter;
import android.graphics.Bitmap;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.widget.Button; import android.widget.Button;
import android.widget.ImageView;
import android.widget.ListView; import android.widget.ListView;
import android.widget.ProgressBar; import android.widget.ProgressBar;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import androidx.annotation.Nullable;
import androidx.core.app.NavUtils; import androidx.core.app.NavUtils;
import androidx.localbroadcastmanager.content.LocalBroadcastManager; import androidx.localbroadcastmanager.content.LocalBroadcastManager;
@ -59,6 +62,7 @@ public class FwAppInstallerActivity extends AbstractGBActivity implements Instal
private static final String ITEM_DETAILS = "details"; private static final String ITEM_DETAILS = "details";
private TextView fwAppInstallTextView; private TextView fwAppInstallTextView;
private ImageView previewImage;
private Button installButton; private Button installButton;
private Uri uri; private Uri uri;
private GBDevice device; private GBDevice device;
@ -178,6 +182,7 @@ public class FwAppInstallerActivity extends AbstractGBActivity implements Instal
itemAdapter = new ItemWithDetailsAdapter(this, items); itemAdapter = new ItemWithDetailsAdapter(this, items);
itemListView.setAdapter(itemAdapter); itemListView.setAdapter(itemAdapter);
fwAppInstallTextView = findViewById(R.id.infoTextView); fwAppInstallTextView = findViewById(R.id.infoTextView);
previewImage = findViewById(R.id.previewImage);
installButton = findViewById(R.id.installButton); installButton = findViewById(R.id.installButton);
progressBar = findViewById(R.id.installProgressBar); progressBar = findViewById(R.id.installProgressBar);
progressText = findViewById(R.id.installProgressText); progressText = findViewById(R.id.installProgressText);
@ -300,6 +305,16 @@ public class FwAppInstallerActivity extends AbstractGBActivity implements Instal
fwAppInstallTextView.setText(text); 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 @Override
public CharSequence getInfoText() { public CharSequence getInfoText() {
return fwAppInstallTextView.getText(); return fwAppInstallTextView.getText();

View File

@ -16,6 +16,10 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. */ along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.activities; package nodomain.freeyourgadget.gadgetbridge.activities;
import android.graphics.Bitmap;
import androidx.annotation.Nullable;
import nodomain.freeyourgadget.gadgetbridge.model.ItemWithDetails; import nodomain.freeyourgadget.gadgetbridge.model.ItemWithDetails;
public interface InstallActivity { public interface InstallActivity {
@ -23,6 +27,8 @@ public interface InstallActivity {
void setInfoText(String text); void setInfoText(String text);
void setPreview(@Nullable Bitmap bitmap);
void setInstallEnabled(boolean enable); void setInstallEnabled(boolean enable);
void clearInstallItems(); void clearInstallItems();

View File

@ -18,6 +18,7 @@
package nodomain.freeyourgadget.gadgetbridge.devices.huami; package nodomain.freeyourgadget.gadgetbridge.devices.huami;
import android.content.Context; import android.content.Context;
import android.graphics.Bitmap;
import android.net.Uri; import android.net.Uri;
import java.io.IOException; import java.io.IOException;
@ -132,4 +133,13 @@ public abstract class HuamiFWHelper extends AbstractMiBandFWHelper {
public AbstractHuamiFirmwareInfo getFirmwareInfo() { public AbstractHuamiFirmwareInfo getFirmwareInfo() {
return firmwareInfo; return firmwareInfo;
} }
@Override
public Bitmap getPreview() {
if (firmwareInfo != null) {
return firmwareInfo.getPreview();
}
return null;
}
} }

View File

@ -18,6 +18,7 @@
package nodomain.freeyourgadget.gadgetbridge.devices.miband; package nodomain.freeyourgadget.gadgetbridge.devices.miband;
import android.content.Context; import android.content.Context;
import android.graphics.Bitmap;
import android.net.Uri; import android.net.Uri;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
@ -147,4 +148,8 @@ public abstract class AbstractMiBandFWHelper {
public abstract void checkValid() throws IllegalArgumentException; public abstract void checkValid() throws IllegalArgumentException;
public abstract HuamiFirmwareType getFirmwareType(); public abstract HuamiFirmwareType getFirmwareType();
public Bitmap getPreview() {
return null;
}
} }

View File

@ -101,7 +101,7 @@ public abstract class AbstractMiBandFWInstallHandler implements InstallHandler {
return; return;
} }
StringBuilder builder = new StringBuilder(); 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()) { if (helper.isSingleFirmware()) {
builder.append(getFwUpgradeNotice()); builder.append(getFwUpgradeNotice());
} else { } else {
@ -120,6 +120,9 @@ public abstract class AbstractMiBandFWInstallHandler implements InstallHandler {
// TODO: set a UNKNOWN (question mark) button // TODO: set a UNKNOWN (question mark) button
} }
} }
installActivity.setPreview(helper.getPreview());
installActivity.setInfoText(builder.toString()); installActivity.setInfoText(builder.toString());
installActivity.setInstallItem(fwItem); installActivity.setInstallItem(fwItem);
installActivity.setInstallEnabled(true); installActivity.setInstallEnabled(true);

View File

@ -17,6 +17,10 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.huami; package nodomain.freeyourgadget.gadgetbridge.service.devices.huami;
import android.graphics.Bitmap;
import androidx.annotation.Nullable;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.ByteOrder; import java.nio.ByteOrder;
import java.util.Map; import java.util.Map;
@ -86,6 +90,11 @@ public abstract class AbstractHuamiFirmwareInfo {
this.bytes = null; this.bytes = null;
} }
@Nullable
public Bitmap getPreview() {
return null;
}
public abstract String toVersion(int crc16); public abstract String toVersion(int crc16);
public abstract boolean isGenerallyCompatibleWith(GBDevice device); public abstract boolean isGenerallyCompatibleWith(GBDevice device);

View File

@ -17,20 +17,29 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.huami; 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.JSONArray;
import org.json.JSONException; import org.json.JSONException;
import org.json.JSONObject; import org.json.JSONObject;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.lang.reflect.Constructor; import java.lang.reflect.Constructor;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.Arrays; import java.util.Arrays;
import java.util.Set; 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.service.btle.BLETypeConversions;
import nodomain.freeyourgadget.gadgetbridge.util.ArrayUtils; import nodomain.freeyourgadget.gadgetbridge.util.ArrayUtils;
import nodomain.freeyourgadget.gadgetbridge.util.BitmapUtil;
import nodomain.freeyourgadget.gadgetbridge.util.ZipFile; import nodomain.freeyourgadget.gadgetbridge.util.ZipFile;
import nodomain.freeyourgadget.gadgetbridge.util.ZipFileException; 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 static final Logger LOG = LoggerFactory.getLogger(Huami2021FirmwareInfo.class);
private final String preComputedVersion; private final String preComputedVersion;
private GBDeviceApp gbDeviceApp;
public Huami2021FirmwareInfo(final byte[] bytes) { public Huami2021FirmwareInfo(final byte[] bytes) {
super(bytes); super(bytes);
@ -125,22 +135,64 @@ public abstract class Huami2021FirmwareInfo extends AbstractHuamiFirmwareInfo {
// Attempt to handle as an app / watchface // Attempt to handle as an app / watchface
final JSONObject appJson = getJson(zipFile, "app.json"); final JSONObject appJson = getJson(zipFile, "app.json");
if (appJson != null) { if (appJson != null) {
final int appId;
final String appName;
final String appVersion;
final String appType; final String appType;
final String appCreator;
final String appIconPath;
final JSONObject appJsonApp;
try { 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) { } catch (final Exception e) {
LOG.error("Failed to get appType from app.json", e); LOG.error("Failed to get appType from app.json", e);
return HuamiFirmwareType.INVALID; return HuamiFirmwareType.INVALID;
} }
final HuamiFirmwareType huamiFirmwareType;
final GBDeviceApp.Type gbDeviceAppType;
switch (appType) { switch (appType) {
case "watchface": case "watchface":
return HuamiFirmwareType.WATCHFACE; huamiFirmwareType = HuamiFirmwareType.WATCHFACE;
gbDeviceAppType = GBDeviceApp.Type.WATCHFACE;
break;
case "app": case "app":
return HuamiFirmwareType.APP; huamiFirmwareType = HuamiFirmwareType.APP;
gbDeviceAppType = GBDeviceApp.Type.APP_GENERIC;
break;
default: default:
LOG.warn("Unknown app type {}", appType); 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 // Attempt to handle as a zab file
@ -254,6 +306,20 @@ public abstract class Huami2021FirmwareInfo extends AbstractHuamiFirmwareInfo {
return null; 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 { public Huami2021FirmwareInfo repackFirmwareInUIHH() throws IOException {
if (!firmwareType.equals(HuamiFirmwareType.FIRMWARE)) { if (!firmwareType.equals(HuamiFirmwareType.FIRMWARE)) {
throw new IllegalStateException("Can only repack FIRMWARE"); throw new IllegalStateException("Can only repack FIRMWARE");

View File

@ -44,4 +44,12 @@ public enum HuamiFirmwareType {
public byte getValue() { public byte getValue() {
return value; return value;
} }
public boolean isApp() {
return this == APP || this == ZEPPOS_APP;
}
public boolean isWatchface() {
return this == WATCHFACE;
}
} }

View File

@ -30,9 +30,17 @@ import android.graphics.RectF;
import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import androidx.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.ByteOrder;
public class BitmapUtil { 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. * 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(); 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;
}
} }

View File

@ -15,10 +15,19 @@
tools:context="nodomain.freeyourgadget.gadgetbridge.activities.FwAppInstallerActivity" tools:context="nodomain.freeyourgadget.gadgetbridge.activities.FwAppInstallerActivity"
tools:ignore="ScrollViewSize"> tools:ignore="ScrollViewSize">
<ImageView
android:id="@+id/previewImage"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:contentDescription="preview image"
android:visibility="gone" />
<ListView <ListView
android:id="@+id/itemListView" android:id="@+id/itemListView"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_below="@+id/previewImage"
android:layout_alignParentEnd="false" /> android:layout_alignParentEnd="false" />
<TextView <TextView