1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-11-29 05:16:51 +01: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.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();

View File

@ -16,6 +16,10 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. */
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();

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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);

View File

@ -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);

View File

@ -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");

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -15,10 +15,19 @@
tools:context="nodomain.freeyourgadget.gadgetbridge.activities.FwAppInstallerActivity"
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
android:id="@+id/itemListView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/previewImage"
android:layout_alignParentEnd="false" />
<TextView