1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-11-27 20:36:51 +01:00

Fossil Hybrid HR: Edit existing watchfaces from app manager cache

This commit is contained in:
Arjan Schrijver 2021-06-23 20:54:16 +02:00 committed by Gitea
parent c77d3e49b1
commit fa89df562a
7 changed files with 177 additions and 16 deletions

View File

@ -75,6 +75,7 @@ public abstract class AbstractAppManagerFragment extends Fragment {
private GBDeviceAppAdapter mGBDeviceAppAdapter;
protected GBDevice mGBDevice = null;
protected DeviceCoordinator mCoordinator = null;
private Class<? extends Activity> watchfaceDesignerActivity;
protected abstract List<GBDeviceApp> 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<? extends Activity> 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);
}

View File

@ -99,8 +99,7 @@ public class GBDeviceAppAdapter extends RecyclerView.Adapter<GBDeviceAppAdapter.
holder.itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
UUID uuid = deviceApp.getUUID();
GBApplication.deviceService().onAppStart(uuid, true);
mParentFragment.onItemClick(view, deviceApp);
}
});

View File

@ -21,10 +21,12 @@ import android.net.Uri;
import org.json.JSONException;
import org.json.JSONObject;
import org.json.JSONTokener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.BufferedInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
@ -54,6 +56,15 @@ public class FossilFileReader {
private GBDeviceApp app;
private JSONObject mAppKeys;
private int jerryStart;
private int appIconStart;
private int layout_start;
private int display_name_start;
private int display_name_start_2;
private int config_start;
private int file_end;
public FossilFileReader(Uri uri, Context context) throws IOException {
this.uri = uri;
uriHelper = UriHelper.get(uri, context);
@ -127,13 +138,13 @@ public class FossilFileReader {
foundVersion = (int)buf.get() + "." + (int)buf.get() + "." + (int)buf.get() + "." + (int)buf.get();
mAppKeys.put("version", foundVersion);
buf.position(buf.position() + 8); // skip null bytes
int jerryStart = buf.getInt();
int appIconStart = buf.getInt();
int layout_start = buf.getInt();
int display_name_start = buf.getInt();
int display_name_start_2 = buf.getInt();
int config_start = buf.getInt();
int file_end = buf.getInt();
jerryStart = buf.getInt();
appIconStart = buf.getInt();
layout_start = buf.getInt();
display_name_start = buf.getInt();
display_name_start_2 = buf.getInt();
config_start = buf.getInt();
file_end = buf.getInt();
buf.position(jerryStart);
ArrayList<String> filenamesCode = parseAppFilenames(buf, appIconStart,false);
@ -145,6 +156,7 @@ public class FossilFileReader {
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);
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;
}

View File

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

View File

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

View File

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

View File

@ -1,5 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android" >
<item
android:id="@+id/appmanager_app_edit"
android:title="Edit"/>
<item
android:id="@+id/appmanager_app_reinstall"
android:title="@string/appmananger_app_reinstall"/>