Fossil/Skagen Hybrids: Embed menu_structure in watchface apps (#3245)

This PR aims to optimize the method of synchronizing the menu_structure for the openSourceWatchface by making the menu_structure request from the watch obsolete.

Instead, when a new menu_structure is sent to GB via the Intent `nodomain.freeyourgadget.gadgetbridge.Q_SET_MENU_STRUCTURE`, GB remembers that JSON.

Next time a watchface is built, the JSON is embedded into that app package so the watch doesn't forget the structure.

This requires a full GB rebuild to work properky.

Reviewed-on: https://codeberg.org/Freeyourgadget/Gadgetbridge/pulls/3245
Co-authored-by: Daniel Dakhno <dakhnod@gmail.com>
Co-committed-by: Daniel Dakhno <dakhnod@gmail.com>
This commit is contained in:
Daniel Dakhno 2023-08-19 20:48:53 +00:00 committed by Arjan Schrijver
parent b029aa252d
commit 73d67d4093
11 changed files with 196 additions and 23 deletions

View File

@ -23,6 +23,7 @@ import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
@ -39,6 +40,7 @@ import android.view.DragEvent;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.LinearLayout;
@ -73,6 +75,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceApp;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.QHybridSupport;
import nodomain.freeyourgadget.gadgetbridge.util.AndroidUtils;
import nodomain.freeyourgadget.gadgetbridge.util.BitmapUtil;
import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
@ -154,6 +157,14 @@ public class HybridHRWatchfaceDesignerActivity extends AbstractGBActivity implem
findViewById(R.id.watchface_rotate_left).setOnClickListener(this);
findViewById(R.id.watchface_rotate_right).setOnClickListener(this);
findViewById(R.id.watchface_remove_image).setOnClickListener(this);
findViewById(R.id.button_watchface_open_menu_companion).setOnClickListener(this);
findViewById(R.id.button_watchface_reset_menu_structure).setOnClickListener(this);
}
@Override
protected void onResume() {
super.onResume();
reloadMenuStructureIndicator();
}
@Override
@ -207,7 +218,8 @@ public class HybridHRWatchfaceDesignerActivity extends AbstractGBActivity implem
@Override
public void onClick(View v) {
if (v.getId() == R.id.button_edit_name) {
int buttonId = v.getId();
if (buttonId == R.id.button_edit_name) {
final EditText input = new EditText(this);
input.setText(watchfaceName);
input.setId(0);
@ -226,7 +238,7 @@ public class HybridHRWatchfaceDesignerActivity extends AbstractGBActivity implem
})
.setTitle(R.string.watchface_dialog_title_set_name)
.show();
} else if (v.getId() == R.id.watchface_invert_colors) {
} else if (buttonId == R.id.watchface_invert_colors) {
if (selectedBackgroundImage != null) {
selectedBackgroundImage = BitmapUtil.invertBitmapColors(selectedBackgroundImage);
for (int i=0; i<widgets.size(); i++) {
@ -241,28 +253,57 @@ public class HybridHRWatchfaceDesignerActivity extends AbstractGBActivity implem
defaultWidgetColor = HybridHRWatchfaceWidget.COLOR_WHITE;
}
}
} else if (v.getId() == R.id.button_set_background) {
} else if (buttonId == R.id.button_set_background) {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("image/*");
startActivityForResult(intent, CHILD_ACTIVITY_IMAGE_CHOOSER);
} else if (v.getId() == R.id.button_add_widget) {
} else if (buttonId == R.id.button_add_widget) {
showWidgetEditPopup(-1);
} else if (v.getId() == R.id.button_watchface_settings) {
} else if (buttonId == R.id.button_watchface_settings) {
showWatchfaceSettingsPopup();
} else if (v.getId() == R.id.watchface_rotate_left) {
} else if (buttonId == R.id.watchface_rotate_left) {
if (selectedBackgroundImage != null) {
selectedBackgroundImage = BitmapUtil.rotateImage(selectedBackgroundImage, -90);
renderWatchfacePreview();
}
} else if (v.getId() == R.id.watchface_rotate_right) {
} else if (buttonId== R.id.watchface_rotate_right) {
if (selectedBackgroundImage != null) {
selectedBackgroundImage = BitmapUtil.rotateImage(selectedBackgroundImage, 90);
renderWatchfacePreview();
}
} else if (v.getId() == R.id.watchface_remove_image) {
} else if (buttonId == R.id.watchface_remove_image) {
deleteWatchfaceBackground();
renderWatchfacePreview();
} else if(buttonId == R.id.button_watchface_open_menu_companion){
try {
AndroidUtils.openApp("d.d.hrmenucompanion");
} catch (Exception e) {
GB.toast(getString(R.string.error_menu_companion_not_installed), Toast.LENGTH_SHORT, GB.INFO);
AndroidUtils.openWebsite("https://github.com/dakhnod/Fossil-HR-Menu-Companion/releases/latest");
}
} else if(buttonId == R.id.button_watchface_reset_menu_structure) {
SharedPreferences prefs = GBApplication.getDeviceSpecificSharedPrefs(mGBDevice.getAddress());
prefs.edit().remove("MENU_STRUCTURE_JSON").apply();
GB.toast(getString(R.string.info_menu_structure_removed), Toast.LENGTH_SHORT, GB.INFO);
reloadMenuStructureIndicator();
}
}
private void reloadMenuStructureIndicator(){
SharedPreferences prefs = GBApplication.getDeviceSpecificSharedPrefs(mGBDevice.getAddress());
String menuStructureJson = prefs.getString("MENU_STRUCTURE_JSON", "");
boolean active = !menuStructureJson.isEmpty();
((Button) findViewById(R.id.button_watchface_reset_menu_structure))
.setEnabled(active);
findViewById(R.id.fossil_menu_structure_hint_container).setVisibility(active ? View.VISIBLE : View.GONE);
if(active){
((TextView)findViewById(R.id.text_watchface_menu_structure))
.setText(getString(R.string.info_menu_structure_contents, menuStructureJson));
}
}
@ -572,6 +613,18 @@ public class HybridHRWatchfaceDesignerActivity extends AbstractGBActivity implem
wfFactory.setSettings(watchfaceSettings);
wfFactory.setBackground(selectedBackgroundImage);
wfFactory.addWidgets(widgets);
SharedPreferences prefs = GBApplication.getDeviceSpecificSharedPrefs(mGBDevice.getAddress());
String menuStructureJson = prefs.getString("MENU_STRUCTURE_JSON", "");
if(!menuStructureJson.isEmpty()){
try {
JSONObject menuStructure = new JSONObject(menuStructureJson);
wfFactory.setMenuStructure(menuStructure);
} catch (JSONException e) {
LOG.error("Error loading menu structure", e);
}
}
try {
File tempFile = File.createTempFile("tmpWatchfaceFile", null);
tempFile.deleteOnExit();

View File

@ -47,6 +47,7 @@ public class HybridHRWatchfaceFactory {
private static final int PREVIEW_WIDTH = 192;
private static final int PREVIEW_HEIGHT = 192;
private ArrayList<JSONObject> widgets = new ArrayList<>();
private JSONObject menuStructure = new JSONObject();
public HybridHRWatchfaceFactory(String name) {
watchfaceName = name.replaceAll("[^-A-Za-z0-9]", "");
@ -130,6 +131,10 @@ public class HybridHRWatchfaceFactory {
}
}
public void setMenuStructure(JSONObject menuStructure){
this.menuStructure = menuStructure;
}
public void addWidgets(ArrayList<HybridHRWatchfaceWidget> widgets) {
for (HybridHRWatchfaceWidget widget : widgets) {
addWidget(widget);
@ -311,6 +316,8 @@ public class HybridHRWatchfaceFactory {
config.put("light_up_on_notification", settings.getLightUpOnNotification());
configuration.put("config", config);
configuration.put("menu_structure", menuStructure);
return configuration.toString();
}

View File

@ -22,7 +22,7 @@ import java.util.Map;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
public final class QHybridConstants {
public static final String HYBRIDHR_WATCHFACE_VERSION = "1.10";
public static final String HYBRIDHR_WATCHFACE_VERSION = "1.11";
public static final int HYBRID_HR_WATCHFACE_WIDGET_SIZE = 76;
public static Map<String, String> KNOWN_WAPP_VERSIONS = new HashMap<String, String>() {

View File

@ -30,6 +30,8 @@ import android.widget.Toast;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import org.json.JSONException;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -99,6 +101,8 @@ public class QHybridSupport extends QHybridBaseSupport {
public static final String QHYBRID_COMMAND_DOWNLOAD_FILE = "nodomain.freeyourgadget.gadgetbridge.Q_DOWNLOAD_FILE";
public static final String QHYBRID_COMMAND_UPLOAD_FILE = "nodomain.freeyourgadget.gadgetbridge.Q_UPLOAD_FILE";
public static final String QHYBRID_COMMAND_SET_MENU_STRUCTURE = "nodomain.freeyourgadget.gadgetbridge.Q_SET_MENU_STRUCTURE";
public static final String QHYBRID_ACTION_DOWNLOADED_FILE = "nodomain.freeyourgadget.gadgetbridge.Q_DOWNLOADED_FILE";
public static final String QHYBRID_ACTION_UPLOADED_FILE = "nodomain.freeyourgadget.gadgetbridge.Q_UPLOADED_FILE";
@ -305,6 +309,7 @@ public class QHybridSupport extends QHybridBaseSupport {
globalFilter.addAction(QHYBRID_COMMAND_UPLOAD_FILE);
globalFilter.addAction(QHYBRID_COMMAND_PUSH_CONFIG);
globalFilter.addAction(QHYBRID_COMMAND_SWITCH_WATCHFACE);
globalFilter.addAction(QHYBRID_COMMAND_SET_MENU_STRUCTURE);
globalCommandReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
@ -380,6 +385,10 @@ public class QHybridSupport extends QHybridBaseSupport {
handleSwitchWatchfaceIntent(intent);
break;
}
case QHYBRID_COMMAND_SET_MENU_STRUCTURE:{
handleSetMenuStructure(intent);
break;
}
}
}
};
@ -398,6 +407,30 @@ public class QHybridSupport extends QHybridBaseSupport {
}
}
private void handleSetMenuStructure(Intent intent){
if(intent == null){
logger.error("intent null");
return;
}
String menuStructureJson = intent.getStringExtra("EXTRA_MENU_STRUCTURE_JSON");
if(menuStructureJson == null){
logger.error("Menu structure json null");
return;
}
if(menuStructureJson.isEmpty()){
logger.error("Menu structure json empty");
return;
}
try {
JSONObject menuStructure = new JSONObject(menuStructureJson);
watchAdapter.handleSetMenuStructure(menuStructure);
GB.toast(getContext().getString(R.string.info_menu_structure_set), Toast.LENGTH_SHORT, GB.INFO);
} catch (JSONException e) {
logger.error("Menu structure json empty");
GB.toast(getContext().getString(R.string.error_invalid_menu_structure), Toast.LENGTH_SHORT, GB.ERROR);
}
}
private boolean dangerousIntentsAllowed(){
return GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getBoolean(DeviceSettingsPreferenceConst.PREF_HYBRID_HR_DANGEROUS_EXTERNAL_INTENTS, true);
}

View File

@ -21,6 +21,8 @@ import android.bluetooth.BluetoothGattCharacteristic;
import android.content.Context;
import android.net.Uri;
import org.json.JSONObject;
import java.util.ArrayList;
import nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.NotificationConfiguration;
@ -165,4 +167,8 @@ public abstract class WatchAdapter {
public void pushConfigJson(String configJson){
}
public void handleSetMenuStructure(JSONObject menuStructure) {
}
}

View File

@ -1068,6 +1068,28 @@ public class FossilHRWatchAdapter extends FossilWatchAdapter {
}
}
@Override
public void handleSetMenuStructure(JSONObject menuStructure) {
String serialized = menuStructure.toString();
getDeviceSpecificPreferences()
.edit()
.putString("MENU_STRUCTURE_JSON", serialized)
.apply();
try {
String payload = new JSONObject()
.put("push", new JSONObject()
.put("set", new JSONObject()
.put("customWatchFace._.config.menu_structure", menuStructure)
)
).toString();
pushConfigJson(payload);
} catch (JSONException e) {
throw new RuntimeException(e);
}
}
@Override
public void setWidgetContent(String widgetID, String content, boolean renderOnWatch) {
boolean update = false;
@ -1898,16 +1920,12 @@ public class FossilHRWatchAdapter extends FossilWatchAdapter {
queueWrite(new JsonPutRequest(responseObject, this));
}
} else if (request.optString("custom_menu").equals("request_config")) {
// watchface requests custom menu data to be initialized
LOG.info("Got custom_menu config request, sending intent to HR Menu Companion app...");
Intent intent = new Intent();
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setClassName("d.d.hrmenucompanion", "d.d.hrmenucompanion.MainActivity");
intent.putExtra("SEND_CONFIG", true);
try {
getContext().startActivity(intent);
} catch (Exception e) {
LOG.info("Couldn't send intent to Fossil-HR-Menu-Companion app, is it installed?");
PackageManager manager = getContext().getPackageManager();
try{
// only show toast when companion app is installed
manager.getApplicationInfo("d.d.hrmenucompanion", 0);
GB.toast(getContext().getString(R.string.info_fossil_rebuild_watchface_custom_menu), Toast.LENGTH_SHORT, GB.INFO);
}catch (PackageManager.NameNotFoundException e){
}
} else {
LOG.warn("Unhandled request from watch: " + requestJson.toString());

View File

@ -317,4 +317,20 @@ public class AndroidUtils {
Toast.makeText(context, R.string.activity_error_share_failed, Toast.LENGTH_LONG).show();
}
}
public static void openWebsite(String url){
Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
i.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
GBApplication.getContext().startActivity(i);
}
public static void openApp(String packageName) throws ClassNotFoundException {
Context context = GBApplication.getContext();
Intent launchIntent = context.getPackageManager().getLaunchIntentForPackage(packageName);
if(launchIntent == null){
throw new ClassNotFoundException("App " + packageName + " cannot be found");
}
GBApplication.getContext().startActivity(launchIntent);
}
}

View File

@ -112,21 +112,53 @@
<Button
android:id="@+id/button_set_background"
android:layout_width="wrap_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/button_watchface_select_image" />
<Button
android:id="@+id/button_add_widget"
android:layout_width="wrap_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/button_watchface_add_widget" />
<Button
android:id="@+id/button_watchface_settings"
android:layout_width="wrap_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/button_watchface_settings" />
<Button
android:id="@+id/button_watchface_open_menu_companion"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/button_open_menu_companion" />
<Button
android:id="@+id/button_watchface_reset_menu_structure"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/button_reset_menu_structure" />
<com.google.android.material.card.MaterialCardView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/fossil_menu_structure_hint_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:foreground="?android:attr/selectableItemBackground"
app:cardBackgroundColor="?attr/cardview_background_color"
app:cardElevation="3dp"
app:contentPadding="8dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/text_watchface_menu_structure"
android:lines="1"
android:ellipsize="end" />
</com.google.android.material.card.MaterialCardView>
</LinearLayout>
</ScrollView>

View File

@ -2242,4 +2242,12 @@
<string name="pref_activity_full_sync_trigger_warning">This will trigger a full sync of all activity data from the device. It may take a few minutes to complete.</string>
<string name="pref_theme_dynamic_colors_not_available_warning">Dynamic colors are not available on your device, only Android 12+ supports this functionality. Gadgetbridge will use the default Material 3 colors.</string>
<string name="pref_theme_dynamic_colors_explanation">Note: for the dynamic colors theme you need to enable Wallpaper Colors or Color Palette in your Android 12+ appearance settings. If you don\'t, Gadgetbridge will use the default Material 3 colors.</string>
<string name="info_menu_structure_set">Menu structure JSON set in GB</string>
<string name="error_invalid_menu_structure">Invalid menu structure JSON</string>
<string name="button_open_menu_companion">Open menu companion app</string>
<string name="button_reset_menu_structure">Reset menu structure</string>
<string name="info_menu_structure_removed">Menu structure removed</string>
<string name="error_menu_companion_not_installed">\'HR Menu Companion\' probably not installed</string>
<string name="info_menu_structure_contents">Menu structure: %s</string>
<string name="info_fossil_rebuild_watchface_custom_menu">Please rebuild your watchface for custom menu</string>
</resources>

@ -1 +1 @@
Subproject commit d19c3b84acc03cd7f5dc03c59374920a63aa998f
Subproject commit 24247ae23e1b903ddcc1e4e9cfb4ad7280a77db2