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:
parent
c77d3e49b1
commit
fa89df562a
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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: ";
|
||||
|
@ -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;
|
||||
|
@ -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"/>
|
||||
|
Loading…
Reference in New Issue
Block a user