From fa89df562a89995f25feaeb193bb238c09aa2943 Mon Sep 17 00:00:00 2001 From: Arjan Schrijver Date: Wed, 23 Jun 2021 20:54:16 +0200 Subject: [PATCH] Fossil Hybrid HR: Edit existing watchfaces from app manager cache --- .../AbstractAppManagerFragment.java | 23 ++++- .../adapter/GBDeviceAppAdapter.java | 3 +- .../devices/qhybrid/FossilFileReader.java | 69 ++++++++++++-- .../HybridHRWatchfaceDesignerActivity.java | 92 ++++++++++++++++++- .../gadgetbridge/impl/GBDevice.java | 1 + .../fossil_hr/image/ImageConverter.java | 2 +- app/src/main/res/menu/appmanager_context.xml | 3 + 7 files changed, 177 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/appmanager/AbstractAppManagerFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/appmanager/AbstractAppManagerFragment.java index 5c5d1f4b0..a73ccf11d 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/appmanager/AbstractAppManagerFragment.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/appmanager/AbstractAppManagerFragment.java @@ -75,6 +75,7 @@ public abstract class AbstractAppManagerFragment extends Fragment { private GBDeviceAppAdapter mGBDeviceAppAdapter; protected GBDevice mGBDevice = null; protected DeviceCoordinator mCoordinator = null; + private Class watchfaceDesignerActivity; protected abstract List getSystemAppsInCategory(); @@ -273,7 +274,7 @@ public abstract class AbstractAppManagerFragment extends Fragment { final FloatingActionButton appListFab = ((FloatingActionButton) getActivity().findViewById(R.id.fab)); final FloatingActionButton appListFabNew = ((FloatingActionButton) getActivity().findViewById(R.id.fab_new)); - final Class watchfaceDesignerActivity = mCoordinator.getWatchfaceDesignerActivity(); + watchfaceDesignerActivity = mCoordinator.getWatchfaceDesignerActivity(); View rootView = inflater.inflate(R.layout.activity_appmanager, container, false); RecyclerView appListView = (RecyclerView) (rootView.findViewById(R.id.appListView)); @@ -328,6 +329,15 @@ public abstract class AbstractAppManagerFragment extends Fragment { GBApplication.deviceService().onAppReorder(uuids.toArray(new UUID[uuids.size()])); } + public void onItemClick(View view, GBDeviceApp deviceApp) { + if (isCacheManager()) { + openPopupMenu(view, deviceApp); + } else { + UUID uuid = deviceApp.getUUID(); + GBApplication.deviceService().onAppStart(uuid, true); + } + } + public boolean openPopupMenu(View view, GBDeviceApp deviceApp) { PopupMenu popupMenu = new PopupMenu(getContext(), view); popupMenu.getMenuInflater().inflate(R.menu.appmanager_context, popupMenu.getMenu()); @@ -335,6 +345,7 @@ public abstract class AbstractAppManagerFragment extends Fragment { final GBDeviceApp selectedApp = deviceApp; if (!selectedApp.isInCache()) { + menu.removeItem(R.id.appmanager_app_edit); menu.removeItem(R.id.appmanager_app_reinstall); menu.removeItem(R.id.appmanager_app_delete_cache); } @@ -369,6 +380,10 @@ public abstract class AbstractAppManagerFragment extends Fragment { } } + if ((mGBDevice.getType() != DeviceType.FOSSILQHYBRID) || (selectedApp.getType() != GBDeviceApp.Type.WATCHFACE)) { + menu.removeItem(R.id.appmanager_app_edit); + } + if (mGBDevice.getType() == DeviceType.PEBBLE) { switch (selectedApp.getType()) { case WATCHFACE: @@ -463,6 +478,12 @@ public abstract class AbstractAppManagerFragment extends Fragment { intent.setData(Uri.parse(url)); startActivity(intent); return true; + case R.id.appmanager_app_edit: + Intent editWatchfaceIntent = new Intent(getContext(), watchfaceDesignerActivity); + editWatchfaceIntent.putExtra(GBDevice.EXTRA_DEVICE, mGBDevice); + editWatchfaceIntent.putExtra(GBDevice.EXTRA_UUID, selectedApp.getUUID().toString()); + getContext().startActivity(editWatchfaceIntent); + return true; default: return super.onContextItemSelected(item); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/GBDeviceAppAdapter.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/GBDeviceAppAdapter.java index 6ba233a5f..f93577635 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/GBDeviceAppAdapter.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/GBDeviceAppAdapter.java @@ -99,8 +99,7 @@ public class GBDeviceAppAdapter extends RecyclerView.Adapter filenamesCode = parseAppFilenames(buf, appIconStart,false); @@ -145,6 +156,7 @@ public class FossilFileReader { 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); if (filenamesDisplayName.contains("theme_class")) { isApp = false; @@ -176,6 +188,49 @@ public class FossilFileReader { return list; } + public JSONObject getConfigJSON(String filename) throws IOException, JSONException { + byte[] fileBytes = getFileContentsByName(filename, config_start, file_end, true); + String fileString = new String(fileBytes, StandardCharsets.UTF_8); + JSONTokener jsonTokener = new JSONTokener(fileString); + return new JSONObject(jsonTokener); + } + + private byte[] getImageFileContents(String filename) throws IOException { + return getFileContentsByName(filename, appIconStart, layout_start, false); + } + + private byte[] getFileContentsByName(String filename, int startPos, int endPos, boolean cutTrailingNull) throws IOException { + InputStream in = new BufferedInputStream(uriHelper.openInputStream()); + byte[] bytes = new byte[in.available()]; + in.read(bytes); + in.close(); + ByteBuffer buf = ByteBuffer.wrap(bytes); + buf.order(ByteOrder.LITTLE_ENDIAN); + buf.position(startPos); + while (buf.position() < endPos) { + int filenameLength = (int)buf.get(); + byte[] filenameBytes = new byte[filenameLength - 1]; + buf.get(filenameBytes); + buf.get(); + String foundFilename = new String(filenameBytes, StandardCharsets.UTF_8); + int filesize = buf.getShort(); + if (cutTrailingNull) { + filesize -= 1; + } + if (foundFilename.equals(filename)) { + byte[] fileBytes = new byte[filesize]; + buf.get(fileBytes); + return fileBytes; + } else { + buf.position(buf.position() + filesize); + } + if (cutTrailingNull) { + buf.get(); + } + } + throw new FileNotFoundException(); + } + public boolean isValid() { return isValid; } 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 70554f166..b52d296b5 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 @@ -25,6 +25,7 @@ import android.content.DialogInterface; import android.content.Intent; import android.database.Cursor; import android.graphics.Bitmap; +import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Matrix; @@ -45,15 +46,20 @@ import android.widget.RadioGroup; import android.widget.RelativeLayout; import android.widget.TextView; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.BufferedOutputStream; import java.io.File; +import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.Iterator; import java.util.UUID; import nodomain.freeyourgadget.gadgetbridge.GBApplication; @@ -86,10 +92,16 @@ public class HybridHRWatchfaceDesignerActivity extends AppCompatActivity impleme } else { throw new IllegalArgumentException("Must provide a device when invoking this activity"); } - mCoordinator = DeviceHelper.getInstance().getCoordinator(mGBDevice); + mCoordinator = DeviceHelper.getInstance().getCoordinator(mGBDevice); calculateDisplayImageSize(); backgroundImageView = findViewById(R.id.hybridhr_background_image); + + if (bundle.containsKey(GBDevice.EXTRA_UUID)) { + String appUUID = bundle.getString(GBDevice.EXTRA_UUID); + loadConfigurationFromApp(appUUID); + } + renderWatchfacePreview(); findViewById(R.id.button_edit_name).setOnClickListener(this); @@ -135,9 +147,7 @@ public class HybridHRWatchfaceDesignerActivity extends AppCompatActivity impleme .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { - watchfaceName = input.getText().toString(); - TextView watchfaceNameView = findViewById(R.id.watchface_name); - watchfaceNameView.setText(watchfaceName); + setWatchfaceName(input.getText().toString()); } }) .setTitle("Set watchface name") @@ -156,6 +166,78 @@ public class HybridHRWatchfaceDesignerActivity extends AppCompatActivity impleme } } + private void loadConfigurationFromApp(String appUUID) { + File appCacheDir; + try { + appCacheDir = mCoordinator.getAppCacheDir(); + } catch (IOException e) { + LOG.warn("Could not get external dir while trying to access app cache.", e); + return; + } + File backgroundFile = new File(appCacheDir, appUUID + ".png"); + try { + Bitmap cachedBackground = BitmapFactory.decodeStream(new FileInputStream(backgroundFile)); + selectedBackgroundImage = BitmapUtil.convertToGrayscale(BitmapUtil.getCircularBitmap(cachedBackground)); + } catch (IOException e) { + LOG.warn("Error loading cached background image", e); + } + File cachedFile = new File(appCacheDir, appUUID + mCoordinator.getAppFileExtension()); + FossilFileReader fileReader; + try { + fileReader = new FossilFileReader(Uri.fromFile(cachedFile), this); + } catch (IOException e) { + LOG.warn("Could not open cached app file", e); + return; + } + setWatchfaceName(fileReader.getName()); + JSONObject configJSON; + try { + configJSON = fileReader.getConfigJSON("customWatchFace"); + } catch (IOException e) { + LOG.warn("Could not read config from cached app file", e); + return; + } catch (JSONException e) { + LOG.warn("JSON parsing error", e); + return; + } + if (configJSON == null) { + return; + } + for (Iterator it = configJSON.keys(); it.hasNext(); ) { + String key = it.next(); + if (key.equals("layout")) { + try { + JSONArray layout = configJSON.getJSONArray(key); + for (int i = 0; i < layout.length(); i++) { + JSONObject layoutItem = layout.getJSONObject(i); + if (layoutItem.getString("type").equals("comp")) { + String widgetName = layoutItem.getString("name"); + switch (widgetName) { + case "dateSSE": + widgetName = "widgetDate"; + break; + case "weatherSSE": + widgetName = "widgetWeather"; + break; + } + widgets.add(new HybridHRWatchfaceWidget(widgetName, + layoutItem.getJSONObject("pos").getInt("x"), + layoutItem.getJSONObject("pos").getInt("y"))); + } + } + } catch (JSONException e) { + LOG.warn("JSON parsing error", e); + } + } + } + } + + private void setWatchfaceName(String name) { + watchfaceName = name; + TextView watchfaceNameView = findViewById(R.id.watchface_name); + watchfaceNameView.setText(watchfaceName); + } + private void renderWatchfacePreview() { int widgetSize = 50; if (selectedBackgroundImage == null) { @@ -169,7 +251,7 @@ public class HybridHRWatchfaceDesignerActivity extends AppCompatActivity impleme circlePaint.setStyle(Paint.Style.STROKE); backgroundImageCanvas.drawCircle(displayImageSize/2f + 2, displayImageSize/2f + 2, displayImageSize/2f - 5, circlePaint); } else { - processedBackgroundImage = Bitmap.createScaledBitmap(selectedBackgroundImage, displayImageSize, displayImageSize, false); + processedBackgroundImage = Bitmap.createScaledBitmap(selectedBackgroundImage, displayImageSize, displayImageSize, true); } // Remove existing widget ImageViews RelativeLayout imageContainer = this.findViewById(R.id.watchface_preview_image); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDevice.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDevice.java index a704bfd9d..96426f4f5 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDevice.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDevice.java @@ -60,6 +60,7 @@ public class GBDevice implements Parcelable { public static final short BATTERY_UNKNOWN = -1; private static final short BATTERY_THRESHOLD_PERCENT = 10; public static final String EXTRA_DEVICE = "device"; + public static final String EXTRA_UUID = "extraUUID"; private static final String DEVINFO_HW_VER = "HW: "; private static final String DEVINFO_FW_VER = "FW: "; private static final String DEVINFO_FW2_VER = "FW2: "; 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 adfffe633..6b9f32763 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 @@ -88,7 +88,7 @@ public class ImageConverter { return result; } - public static @ColorInt int convertToMonochrome(@ColorInt int color){ + public static int convertToMonochrome(@ColorInt int color){ int sum = Color.red(color) + Color.green(color) + Color.blue(color); sum /= 3; return sum; diff --git a/app/src/main/res/menu/appmanager_context.xml b/app/src/main/res/menu/appmanager_context.xml index 2f281ce82..aa0055e9a 100644 --- a/app/src/main/res/menu/appmanager_context.xml +++ b/app/src/main/res/menu/appmanager_context.xml @@ -1,5 +1,8 @@ +