1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-08-24 08:10:52 +02:00

Fossil Hybrid HR: Use embedded preview image from .wapp file during import

This commit is contained in:
Arjan Schrijver 2022-07-29 15:53:48 +02:00
parent c490b4050d
commit 75dd5f1863
11 changed files with 171 additions and 20 deletions

View File

@ -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<T> extends ArrayAdapter<T> {
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<T> extends ArrayAdapter<T> {
@DrawableRes
protected abstract int getIcon(T item);
protected abstract Bitmap getPreview(T item);
public void setSize(int size) {
this.size = size;
}

View File

@ -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<ItemWithDetails>
return item.getIcon();
}
@Override
protected Bitmap getPreview(ItemWithDetails item) {
return item.getPreview();
}
}

View File

@ -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<String> filenamesIcons;
ArrayList<String> filenamesLayout;
ArrayList<String> filenamesDisplayName;
ArrayList<String> 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<String> filenamesIcons = parseAppFilenames(buf, layout_start,false);
ArrayList<String> filenamesLayout = parseAppFilenames(buf, display_name_start,true);
ArrayList<String> filenamesDisplayName = parseAppFilenames(buf, config_start,true);
ArrayList<String> 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);
}

View File

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

View File

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

View File

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

View File

@ -16,6 +16,7 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.model;
import android.graphics.Bitmap;
import android.os.Parcelable;
public interface ItemWithDetails extends Parcelable, Comparable<ItemWithDetails> {
@ -25,6 +26,8 @@ public interface ItemWithDetails extends Parcelable, Comparable<ItemWithDetails>
int getIcon();
Bitmap getPreview();
/**
* Equality is based on #getName() only.
*

View File

@ -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<repetitions; i++) {
bitmap.setPixel(posX, posY, combinedColor);
posX++;
if (posX >= 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;
}
}

View File

@ -4,11 +4,20 @@
android:layout_height="wrap_content"
android:background="?android:attr/activatedBackgroundIndicator">
<ImageView
android:id="@+id/item_preview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="false"
android:layout_alignParentTop="true"
android:layout_centerHorizontal="true"
android:contentDescription="Item preview" />
<ImageView
android:id="@+id/item_image"
android:layout_width="56dp"
android:layout_height="56dp"
android:layout_alignParentStart="true"
android:layout_below="@+id/item_preview"
android:contentDescription="@string/candidate_item_device_image"
android:padding="8dp" />
@ -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">

View File

@ -5,11 +5,19 @@
android:background="?android:attr/activatedBackgroundIndicator"
android:paddingStart="1dp">
<ImageView
android:id="@+id/item_preview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_centerHorizontal="true"
android:contentDescription="Item preview" />
<ImageView
android:id="@+id/item_image"
android:layout_width="8dp"
android:layout_height="8dp"
android:layout_alignParentStart="true"
android:layout_toEndOf="@+id/item_preview"
android:contentDescription="@string/candidate_item_device_image" />
<LinearLayout

View File

@ -5,18 +5,27 @@
android:background="?android:attr/activatedBackgroundIndicator"
android:padding="4dp">
<ImageView
android:id="@+id/item_preview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="false"
android:layout_alignParentTop="true"
android:layout_centerHorizontal="true"
android:contentDescription="Item preview" />
<ImageView
android:id="@+id/item_image"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_alignParentStart="true"
android:layout_below="@+id/item_preview"
android:contentDescription="@string/candidate_item_device_image" />
<LinearLayout
android:layout_width="fill_parent"
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="4dp"
android:paddingEnd="4dp">