diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/AbstractItemAdapter.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/AbstractItemAdapter.java index e001c9e30..2511dd5f9 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/AbstractItemAdapter.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/AbstractItemAdapter.java @@ -18,6 +18,7 @@ package nodomain.freeyourgadget.gadgetbridge.adapter; import android.content.Context; +import android.graphics.Bitmap; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -87,11 +88,13 @@ public abstract class AbstractItemAdapter extends ArrayAdapter { ImageView iconView = (ImageView) view.findViewById(R.id.item_image); TextView nameView = (TextView) view.findViewById(R.id.item_name); TextView detailsView = (TextView) view.findViewById(R.id.item_details); + ImageView previewView = (ImageView) view.findViewById(R.id.item_preview); nameView.setText(getName(item)); detailsView.setText(getDetails(item)); iconView.setImageResource(getIcon(item)); iconView.setBackgroundColor(backgroundColor); + previewView.setImageBitmap(getPreview(item)); return view; } @@ -103,6 +106,8 @@ public abstract class AbstractItemAdapter extends ArrayAdapter { @DrawableRes protected abstract int getIcon(T item); + protected abstract Bitmap getPreview(T item); + public void setSize(int size) { this.size = size; } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/ItemWithDetailsAdapter.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/ItemWithDetailsAdapter.java index 0d83ef89f..2b81bed00 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/ItemWithDetailsAdapter.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/ItemWithDetailsAdapter.java @@ -17,6 +17,7 @@ package nodomain.freeyourgadget.gadgetbridge.adapter; import android.content.Context; +import android.graphics.Bitmap; import java.util.List; @@ -46,4 +47,8 @@ public class ItemWithDetailsAdapter extends AbstractItemAdapter return item.getIcon(); } + @Override + protected Bitmap getPreview(ItemWithDetails item) { + return item.getPreview(); + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/FossilFileReader.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/FossilFileReader.java index 903fe5160..1f16faa6b 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/FossilFileReader.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/FossilFileReader.java @@ -17,6 +17,7 @@ package nodomain.freeyourgadget.gadgetbridge.devices.qhybrid; import android.content.Context; +import android.graphics.Bitmap; import android.net.Uri; import org.json.JSONException; @@ -37,6 +38,8 @@ import java.util.ArrayList; import java.util.UUID; import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceApp; +import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.image.ImageConverter; +import nodomain.freeyourgadget.gadgetbridge.util.BitmapUtil; import nodomain.freeyourgadget.gadgetbridge.util.UriHelper; /** @@ -64,6 +67,11 @@ public class FossilFileReader { private int config_start; private int file_end; + ArrayList filenamesIcons; + ArrayList filenamesLayout; + ArrayList filenamesDisplayName; + ArrayList filenamesConfig; + public FossilFileReader(Uri uri, Context context) throws IOException { this.uri = uri; @@ -155,10 +163,10 @@ public class FossilFileReader { mAppKeys.put("name", foundName); mAppKeys.put("uuid", UUID.nameUUIDFromBytes(foundName.getBytes(StandardCharsets.UTF_8))); } - ArrayList filenamesIcons = parseAppFilenames(buf, layout_start,false); - ArrayList filenamesLayout = parseAppFilenames(buf, display_name_start,true); - ArrayList filenamesDisplayName = parseAppFilenames(buf, config_start,true); - ArrayList filenamesConfig = parseAppFilenames(buf, file_end,true); + filenamesIcons = parseAppFilenames(buf, layout_start,false); + filenamesLayout = parseAppFilenames(buf, display_name_start,true); + filenamesDisplayName = parseAppFilenames(buf, config_start,true); + filenamesConfig = parseAppFilenames(buf, file_end,true); if (filenamesDisplayName.contains("theme_class")) { isApp = false; @@ -197,6 +205,22 @@ public class FossilFileReader { return new JSONObject(jsonTokener); } + public Bitmap getPreview() { + try { + if ((filenamesIcons != null) && (filenamesIcons.contains("!preview.rle"))) { + return BitmapUtil.getCircularBitmap(ImageConverter.decodeFromRLEImage(getImageFileContents("!preview.rle"))); + } + if ((filenamesIcons != null) && (filenamesIcons.contains("!preview"))) { + return BitmapUtil.getCircularBitmap(ImageConverter.decodeFromRLEImage(getImageFileContents("!preview"))); + } + } catch (IOException e) { + LOG.warn("Couldn't read preview image from wapp file: ", e); + return null; + } + LOG.warn("No preview image found in wapp file"); + return null; + } + private byte[] getImageFileContents(String filename) throws IOException { return getFileContentsByName(filename, appIconStart, layout_start, false); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/FossilHRInstallHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/FossilHRInstallHandler.java index 261ca53a3..5dc65eb14 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/FossilHRInstallHandler.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/FossilHRInstallHandler.java @@ -79,6 +79,11 @@ public class FossilHRInstallHandler implements InstallHandler { GenericItem installItem = new GenericItem(); installItem.setName(fossilFile.getName()); installItem.setDetails(fossilFile.getVersion()); + Bitmap preview = fossilFile.getPreview(); + if (preview != null) { + preview = Bitmap.createScaledBitmap(preview, preview.getWidth() * 3, preview.getHeight() * 3, false); + } + installItem.setPreview(preview); if (fossilFile.isFirmware()) { installItem.setIcon(R.drawable.ic_firmware); installActivity.setInfoText(mContext.getString(R.string.firmware_install_warning, "(unknown)")); @@ -106,12 +111,12 @@ public class FossilHRInstallHandler implements InstallHandler { if (fossilFile.isFirmware()) { return; } - saveAppInCache(fossilFile, null, mCoordinator, mContext); + saveAppInCache(fossilFile, null, fossilFile.getPreview(), mCoordinator, mContext); // refresh list manager.sendBroadcast(new Intent(AbstractAppManagerFragment.ACTION_REFRESH_APPLIST)); } - public static void saveAppInCache(FossilFileReader fossilFile, Bitmap backgroundImg, DeviceCoordinator mCoordinator, Context mContext) { + public static void saveAppInCache(FossilFileReader fossilFile, Bitmap backgroundImg, Bitmap previewImg, DeviceCoordinator mCoordinator, Context mContext) { GBDeviceApp app; File destDir; // write app file @@ -150,7 +155,7 @@ public class FossilHRInstallHandler implements InstallHandler { } // write watchface background image if (backgroundImg != null) { - outputFile = new File(destDir, app.getUUID().toString() + ".png"); + outputFile = new File(destDir, app.getUUID().toString() + "_bg.png"); try { FileOutputStream fos = new FileOutputStream(outputFile); backgroundImg.compress(Bitmap.CompressFormat.PNG, 9, fos); @@ -159,6 +164,17 @@ public class FossilHRInstallHandler implements InstallHandler { LOG.error("Failed to write to output file: " + e.getMessage(), e); } } + // write watchface preview image + if (previewImg != null) { + outputFile = new File(destDir, app.getUUID().toString() + "_preview.png"); + try { + FileOutputStream fos = new FileOutputStream(outputFile); + previewImg.compress(Bitmap.CompressFormat.PNG, 9, fos); + fos.close(); + } catch (IOException e) { + LOG.error("Failed to write to output file: " + e.getMessage(), e); + } + } } @Override diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/HybridHRWatchfaceDesignerActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/HybridHRWatchfaceDesignerActivity.java index deef83016..79671c859 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/HybridHRWatchfaceDesignerActivity.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/HybridHRWatchfaceDesignerActivity.java @@ -329,7 +329,10 @@ public class HybridHRWatchfaceDesignerActivity extends AbstractGBActivity implem LOG.warn("Could not get external dir while trying to access app cache.", e); return; } - File backgroundFile = new File(appCacheDir, appUUID + ".png"); + File backgroundFile = new File(appCacheDir, appUUID + "_bg.png"); + if (!backgroundFile.exists()) { + backgroundFile = new File(appCacheDir, appUUID + ".png"); + } try { Bitmap cachedBackground = BitmapFactory.decodeStream(new FileInputStream(backgroundFile)); selectedBackgroundImage = BitmapUtil.convertToGrayscale(BitmapUtil.getCircularBitmap(cachedBackground)); @@ -561,7 +564,8 @@ public class HybridHRWatchfaceDesignerActivity extends AbstractGBActivity implem } private void sendToWatch(boolean preview) { - HybridHRWatchfaceFactory wfFactory; + final Context mContext = this; + final HybridHRWatchfaceFactory wfFactory; if (preview) { wfFactory = new HybridHRWatchfaceFactory("previewWatchface"); } else { @@ -603,20 +607,15 @@ public class HybridHRWatchfaceDesignerActivity extends AbstractGBActivity implem public void onClick(DialogInterface dialog, int which) { findViewById(R.id.watchface_upload_progress_bar).setVisibility(View.VISIBLE); GBApplication.deviceService().onInstallApp(tempAppFileUri); - FossilHRInstallHandler.saveAppInCache(fossilFile, processedBackgroundImage, mCoordinator, HybridHRWatchfaceDesignerActivity.this); + FossilHRInstallHandler.saveAppInCache(fossilFile, processedBackgroundImage, wfFactory.getPreviewImage(mContext), mCoordinator, HybridHRWatchfaceDesignerActivity.this); } }) .show(); } else { findViewById(R.id.watchface_upload_progress_bar).setVisibility(View.VISIBLE); GBApplication.deviceService().onInstallApp(tempAppFileUri); - FossilHRInstallHandler.saveAppInCache(fossilFile, processedBackgroundImage, mCoordinator, HybridHRWatchfaceDesignerActivity.this); + FossilHRInstallHandler.saveAppInCache(fossilFile, processedBackgroundImage, wfFactory.getPreviewImage(mContext), mCoordinator, HybridHRWatchfaceDesignerActivity.this); } - Bitmap previewImage = wfFactory.getPreviewImage(this); - File previewFile = new File(cacheDir, app.getUUID().toString() + "_preview.png"); - FileOutputStream previewFOS = new FileOutputStream(previewFile); - previewImage.compress(Bitmap.CompressFormat.PNG, 9, previewFOS); - previewFOS.close(); } } catch (IOException e) { LOG.warn("Error while creating and uploading watchface", e); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/GenericItem.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/GenericItem.java index 687fcae9c..5b9eb0d8a 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/GenericItem.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/GenericItem.java @@ -16,6 +16,7 @@ along with this program. If not, see . */ package nodomain.freeyourgadget.gadgetbridge.model; +import android.graphics.Bitmap; import android.os.Parcel; import android.os.Parcelable; @@ -27,6 +28,7 @@ public class GenericItem implements ItemWithDetails { private String details; private int icon; private boolean warning = false; + private Bitmap preview; public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { @Override @@ -106,6 +108,14 @@ public class GenericItem implements ItemWithDetails { return 0; } + public void setPreview(Bitmap preview) { + this.preview = preview; + } + + public Bitmap getPreview() { + return preview; + } + @Override public boolean equals(Object o) { if (o == null || getClass() != o.getClass()) { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ItemWithDetails.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ItemWithDetails.java index 8902d4bf8..01bbd761d 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ItemWithDetails.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ItemWithDetails.java @@ -16,6 +16,7 @@ along with this program. If not, see . */ package nodomain.freeyourgadget.gadgetbridge.model; +import android.graphics.Bitmap; import android.os.Parcelable; public interface ItemWithDetails extends Parcelable, Comparable { @@ -25,6 +26,8 @@ public interface ItemWithDetails extends Parcelable, Comparable int getIcon(); + Bitmap getPreview(); + /** * Equality is based on #getName() only. * diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/fossil_hr/image/ImageConverter.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/fossil_hr/image/ImageConverter.java index 6b9f32763..aa8b0a401 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/fossil_hr/image/ImageConverter.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/fossil_hr/image/ImageConverter.java @@ -21,12 +21,20 @@ import android.graphics.Color; import androidx.annotation.ColorInt; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.FossilFileReader; import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.encoder.RLEEncoder; public class ImageConverter { + private static final Logger LOG = LoggerFactory.getLogger(ImageConverter.class); + public static byte[] get2BitsRLEImageBytes(Bitmap bitmap) { int[] pixels = new int[bitmap.getWidth() * bitmap.getHeight()]; bitmap.getPixels(pixels, 0, bitmap.getWidth(), 0, 0, bitmap.getWidth(), bitmap.getHeight()); @@ -93,4 +101,58 @@ public class ImageConverter { sum /= 3; return sum; } + + public static Bitmap decodeFromRLEImage(byte[] rleImage) { + ByteBuffer buf = ByteBuffer.wrap(rleImage); + buf.order(ByteOrder.LITTLE_ENDIAN); + int width = Byte.toUnsignedInt(buf.get()); + int height = Byte.toUnsignedInt(buf.get()); + Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + int posX = 0; + int posY = 0; + while (buf.remaining() > 2) { + int repetitions = Byte.toUnsignedInt(buf.get()); + int pixel = Byte.toUnsignedInt(buf.get()); + int color = pixel << 6; + int combinedColor = Color.rgb(color, color, color); + for (int i=0; i= width) { + posX = 0; + posY++; + } + } + } + return bitmap; + } + + public static Bitmap decodeFromRAWImage(byte[] rawImage, int width, int height) { + int imageSize = rawImage.length; + if (imageSize * 4 != width * height) { + // imageSize is multiplied by 4 because there are 2-bit pixels stored in every byte + LOG.warn("decodeFromRAWImage: provided pixels (" + imageSize * 4 + ") not equal to resolution " + width + "*" + height); + return null; + } + ByteBuffer buf = ByteBuffer.wrap(rawImage); + buf.order(ByteOrder.LITTLE_ENDIAN); + Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + int posX = 0; + int posY = 0; + while (buf.remaining() > 0) { + int currentPixels = Byte.toUnsignedInt(buf.get()); + for (int shift=0; shift<=6; shift+=2) { + //for (int shift=6; shift>=0; shift-=2) { + int color = ((currentPixels >> shift) & 0b00000011) << 6; + int combinedColor = Color.rgb(color, color, color); + bitmap.setPixel(posX, posY, combinedColor); + posX++; + if (posX >= width) { + posX = 0; + posY++; + } + } + } + return bitmap; + } } diff --git a/app/src/main/res/layout/item_with_details.xml b/app/src/main/res/layout/item_with_details.xml index bc946372b..213dbb249 100644 --- a/app/src/main/res/layout/item_with_details.xml +++ b/app/src/main/res/layout/item_with_details.xml @@ -4,11 +4,20 @@ android:layout_height="wrap_content" android:background="?android:attr/activatedBackgroundIndicator"> + + @@ -17,6 +26,7 @@ android:layout_height="wrap_content" android:layout_centerVertical="true" android:layout_toEndOf="@+id/item_image" + android:layout_below="@+id/item_preview" android:orientation="vertical" android:paddingStart="8dp" android:paddingEnd="8dp"> diff --git a/app/src/main/res/layout/item_with_details_horizontal.xml b/app/src/main/res/layout/item_with_details_horizontal.xml index 3ebd3f37b..ac2d5373e 100644 --- a/app/src/main/res/layout/item_with_details_horizontal.xml +++ b/app/src/main/res/layout/item_with_details_horizontal.xml @@ -5,11 +5,19 @@ android:background="?android:attr/activatedBackgroundIndicator" android:paddingStart="1dp"> + + + +