diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index dcda90c38..41bdf43b2 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -593,6 +593,10 @@
android:name=".activities.ConfigureContacts"
android:label="@string/title_activity_set_contacts"
android:parentActivityName=".activities.ControlCenterv2" />
+
+
{
+ final Intent intent = new Intent(getContext(), WidgetScreensListActivity.class);
+ intent.putExtra(GBDevice.EXTRA_DEVICE, device);
+ startActivity(intent);
+ return true;
+ });
+ }
+
final Preference calendarBlacklist = findPreference("blacklist_calendars");
if (calendarBlacklist != null) {
calendarBlacklist.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/widgets/WidgetScreenDetailsActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/widgets/WidgetScreenDetailsActivity.java
new file mode 100644
index 000000000..4dcff2f89
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/widgets/WidgetScreenDetailsActivity.java
@@ -0,0 +1,274 @@
+/* Copyright (C) 2023 José Rebelo
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.activities.widgets;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.ArrayAdapter;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+
+import com.google.android.material.dialog.MaterialAlertDialogBuilder;
+import com.google.android.material.floatingactionbutton.FloatingActionButton;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import nodomain.freeyourgadget.gadgetbridge.R;
+import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBActivity;
+import nodomain.freeyourgadget.gadgetbridge.capabilities.widgets.WidgetLayout;
+import nodomain.freeyourgadget.gadgetbridge.capabilities.widgets.WidgetManager;
+import nodomain.freeyourgadget.gadgetbridge.capabilities.widgets.WidgetPart;
+import nodomain.freeyourgadget.gadgetbridge.capabilities.widgets.WidgetPartSubtype;
+import nodomain.freeyourgadget.gadgetbridge.capabilities.widgets.WidgetScreen;
+import nodomain.freeyourgadget.gadgetbridge.capabilities.widgets.WidgetType;
+import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
+import nodomain.freeyourgadget.gadgetbridge.util.GB;
+
+public class WidgetScreenDetailsActivity extends AbstractGBActivity {
+ private static final Logger LOG = LoggerFactory.getLogger(WidgetScreenDetailsActivity.class);
+
+ private WidgetScreen widgetScreen;
+ private WidgetManager widgetManager;
+
+ private View cardWidgetTopLeft;
+ private View cardWidgetTopRight;
+ private View cardWidgetCenter;
+ private View cardWidgetBotLeft;
+ private View cardWidgetBotRight;
+
+ private TextView labelWidgetScreenLayout;
+ private TextView labelWidgetTopLeft;
+ private TextView labelWidgetTopRight;
+ private TextView labelWidgetCenter;
+ private TextView labelWidgetBotLeft;
+ private TextView labelWidgetBotRight;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_widget_screen_details);
+
+ final GBDevice device = getIntent().getParcelableExtra(GBDevice.EXTRA_DEVICE);
+ if (device == null) {
+ LOG.error("device must not be null");
+ finish();
+ return;
+ }
+
+ widgetScreen = (WidgetScreen) getIntent().getSerializableExtra(WidgetScreen.EXTRA_WIDGET_SCREEN);
+ if (widgetScreen == null) {
+ GB.toast("No widget screen provided to WidgetScreenDetailsActivity", Toast.LENGTH_LONG, GB.ERROR);
+ finish();
+ return;
+ }
+
+ widgetManager = device.getDeviceCoordinator().getWidgetManager(device);
+
+ // Save button
+ final FloatingActionButton fab = findViewById(R.id.fab_save);
+ fab.setOnClickListener(view -> {
+ for (final WidgetPart part : widgetScreen.getParts()) {
+ if (part.getId() == null) {
+ GB.toast(getBaseContext().getString(R.string.widget_missing_parts), Toast.LENGTH_LONG, GB.WARN);
+ return;
+ }
+ }
+
+ updateWidgetScreen();
+ WidgetScreenDetailsActivity.this.setResult(Activity.RESULT_OK);
+ finish();
+ });
+
+ // Layouts
+ final List supportedLayouts = widgetManager.getSupportedWidgetLayouts();
+ final String[] layoutStrings = new String[supportedLayouts.size()];
+ for (int i = 0; i < supportedLayouts.size(); i++) {
+ layoutStrings[i] = getBaseContext().getString(supportedLayouts.get(i).getName());
+ }
+ final ArrayAdapter layoutAdapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, layoutStrings);
+
+ final View cardLayout = findViewById(R.id.card_layout);
+ cardLayout.setOnClickListener(view -> {
+ new MaterialAlertDialogBuilder(WidgetScreenDetailsActivity.this).setAdapter(layoutAdapter, (dialogInterface, i) -> {
+ if (widgetScreen.getLayout() != supportedLayouts.get(i)) {
+ final ArrayList defaultParts = new ArrayList<>();
+ for (final WidgetType widgetType : supportedLayouts.get(i).getWidgetTypes()) {
+ defaultParts.add(new WidgetPart(null, "", widgetType));
+ }
+ widgetScreen.setParts(defaultParts);
+ }
+ widgetScreen.setLayout(supportedLayouts.get(i));
+ updateUiFromWidget();
+ }).setTitle(R.string.widget_layout).create().show();
+ });
+
+ cardWidgetTopLeft = findViewById(R.id.card_widget_top_left);
+ cardWidgetTopRight = findViewById(R.id.card_widget_top_right);
+ cardWidgetCenter = findViewById(R.id.card_widget_center);
+ cardWidgetBotLeft = findViewById(R.id.card_widget_bottom_left);
+ cardWidgetBotRight = findViewById(R.id.card_widget_bottom_right);
+
+ labelWidgetScreenLayout = findViewById(R.id.widget_screen_layout);
+ labelWidgetTopLeft = findViewById(R.id.widget_top_left);
+ labelWidgetTopRight = findViewById(R.id.widget_top_right);
+ labelWidgetCenter = findViewById(R.id.widget_center);
+ labelWidgetBotLeft = findViewById(R.id.widget_bottom_left);
+ labelWidgetBotRight = findViewById(R.id.widget_bottom_right);
+
+ updateUiFromWidget();
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(final MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ // back button
+ // TODO confirm when exiting without saving
+ finish();
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ private void updateWidgetScreen() {
+ widgetManager.saveScreen(widgetScreen);
+ }
+
+ @Override
+ protected void onSaveInstanceState(@NonNull final Bundle state) {
+ super.onSaveInstanceState(state);
+ state.putSerializable("widgetScreen", widgetScreen);
+ }
+
+ @Override
+ protected void onRestoreInstanceState(@NonNull final Bundle savedInstanceState) {
+ super.onRestoreInstanceState(savedInstanceState);
+ widgetScreen = (WidgetScreen) savedInstanceState.getSerializable("widgetScreen");
+ updateUiFromWidget();
+ }
+
+ private void updateUiFromWidget() {
+ labelWidgetScreenLayout.setText(widgetScreen.getLayout().getName());
+
+ switch (widgetScreen.getLayout()) {
+ case TOP_1_BOT_2:
+ updateWidget(cardWidgetTopLeft, labelWidgetTopLeft, -1);
+ updateWidget(cardWidgetTopRight, labelWidgetTopRight, -1);
+ updateWidget(cardWidgetCenter, labelWidgetCenter, 0);
+ updateWidget(cardWidgetBotLeft, labelWidgetBotLeft, 1);
+ updateWidget(cardWidgetBotRight, labelWidgetBotRight, 2);
+ break;
+ case TOP_2_BOT_1:
+ updateWidget(cardWidgetTopLeft, labelWidgetTopLeft, 0);
+ updateWidget(cardWidgetTopRight, labelWidgetTopRight, 1);
+ updateWidget(cardWidgetCenter, labelWidgetCenter, 2);
+ updateWidget(cardWidgetBotLeft, labelWidgetBotLeft, -1);
+ updateWidget(cardWidgetBotRight, labelWidgetBotRight, -1);
+ break;
+ case TOP_2_BOT_2:
+ updateWidget(cardWidgetTopLeft, labelWidgetTopLeft, 0);
+ updateWidget(cardWidgetTopRight, labelWidgetTopRight, 1);
+ updateWidget(cardWidgetCenter, labelWidgetCenter, -1);
+ updateWidget(cardWidgetBotLeft, labelWidgetBotLeft, 2);
+ updateWidget(cardWidgetBotRight, labelWidgetBotRight, 3);
+ break;
+ case SINGLE:
+ updateWidget(cardWidgetTopLeft, labelWidgetTopLeft, -1);
+ updateWidget(cardWidgetTopRight, labelWidgetTopRight, -1);
+ updateWidget(cardWidgetCenter, labelWidgetCenter, 0);
+ updateWidget(cardWidgetBotLeft, labelWidgetBotLeft, -1);
+ updateWidget(cardWidgetBotRight, labelWidgetBotRight, -1);
+ break;
+ case TWO:
+ updateWidget(cardWidgetTopLeft, labelWidgetTopLeft, 0);
+ updateWidget(cardWidgetTopRight, labelWidgetTopRight, 1);
+ updateWidget(cardWidgetCenter, labelWidgetCenter, -1);
+ updateWidget(cardWidgetBotLeft, labelWidgetBotLeft, -1);
+ updateWidget(cardWidgetBotRight, labelWidgetBotRight, -1);
+ break;
+ default:
+ throw new IllegalStateException("Unknown layout " + widgetScreen.getLayout());
+ }
+ }
+
+ private void updateWidget(final View card, final TextView label, final int partIdx) {
+ final boolean validPart = partIdx >= 0 && partIdx < widgetScreen.getParts().size();
+
+ card.setVisibility(validPart ? View.VISIBLE : View.GONE);
+
+ if (!validPart) {
+ card.setOnClickListener(null);
+ label.setText(R.string.not_set);
+ return;
+ }
+
+ final WidgetPart widgetPart = widgetScreen.getParts().get(partIdx);
+
+ if (widgetPart.getId() == null) {
+ label.setText(R.string.not_set);
+ } else {
+ label.setText(widgetPart.getFullName());
+ }
+
+ // Select widget part
+
+ final List supportedParts = widgetManager.getSupportedWidgetParts(widgetPart.getType());
+ final String[] layoutStrings = new String[supportedParts.size()];
+ for (int i = 0; i < supportedParts.size(); i++) {
+ layoutStrings[i] = supportedParts.get(i).getName();
+ }
+ final ArrayAdapter partAdapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, layoutStrings);
+
+ card.setOnClickListener(view -> {
+ new MaterialAlertDialogBuilder(WidgetScreenDetailsActivity.this).setAdapter(partAdapter, (dialogInterface, i) -> {
+ final WidgetPart selectedPart = supportedParts.get(i);
+
+ final List supportedSubtypes = selectedPart.getSupportedSubtypes();
+ if (supportedSubtypes.isEmpty()) {
+ // No subtypes selected
+
+ widgetScreen.getParts().set(partIdx, selectedPart);
+ updateUiFromWidget();
+ return;
+ }
+
+ // If the selected part supports subtypes, the user must select a subtype
+
+ final String[] subtypeStrings = new String[supportedSubtypes.size()];
+ for (int j = 0; j < supportedSubtypes.size(); j++) {
+ subtypeStrings[j] = supportedSubtypes.get(j).getName();
+ }
+ final ArrayAdapter subtypesAdapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, subtypeStrings);
+
+ new MaterialAlertDialogBuilder(WidgetScreenDetailsActivity.this).setAdapter(subtypesAdapter, (dialogInterface1, j) -> {
+ final WidgetPartSubtype selectedSubtype = supportedSubtypes.get(j);
+ selectedPart.setSubtype(selectedSubtype);
+ widgetScreen.getParts().set(partIdx, selectedPart);
+ updateUiFromWidget();
+ }).setTitle(R.string.widget_subtype).create().show();
+ }).setTitle(R.string.widget).create().show();
+ });
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/widgets/WidgetScreenListAdapter.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/widgets/WidgetScreenListAdapter.java
new file mode 100644
index 000000000..18403ec8c
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/widgets/WidgetScreenListAdapter.java
@@ -0,0 +1,123 @@
+/* Copyright (C) 2023 José Rebelo
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.activities.widgets;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.google.android.material.card.MaterialCardView;
+import com.google.android.material.dialog.MaterialAlertDialogBuilder;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import nodomain.freeyourgadget.gadgetbridge.R;
+import nodomain.freeyourgadget.gadgetbridge.capabilities.widgets.WidgetPart;
+import nodomain.freeyourgadget.gadgetbridge.capabilities.widgets.WidgetScreen;
+
+public class WidgetScreenListAdapter extends RecyclerView.Adapter {
+
+ private final Context mContext;
+ private ArrayList widgetScreenList;
+
+ public WidgetScreenListAdapter(Context context) {
+ this.mContext = context;
+ }
+
+ public void setWidgetScreenList(List widgetScreens) {
+ this.widgetScreenList = new ArrayList<>(widgetScreens);
+ }
+
+ public ArrayList getWidgetScreenList() {
+ return widgetScreenList;
+ }
+
+ @NonNull
+ @Override
+ public WidgetScreenListAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ final View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_widget_screen, parent, false);
+ return new ViewHolder(view);
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull ViewHolder holder, final int position) {
+ final WidgetScreen widgetScreen = widgetScreenList.get(position);
+
+ holder.container.setOnClickListener(v -> ((WidgetScreensListActivity) mContext).configureWidgetScreen(widgetScreen));
+
+ holder.container.setOnLongClickListener(v -> {
+ // TODO move up
+ // TODO move down
+ new MaterialAlertDialogBuilder(v.getContext())
+ .setTitle(R.string.widget_screen_delete_confirm_title)
+ .setMessage(mContext.getString(
+ R.string.widget_screen_delete_confirm_description,
+ mContext.getString(R.string.widget_screen_x, widgetScreen.getId())
+ ))
+ .setIcon(R.drawable.ic_warning)
+ .setPositiveButton(android.R.string.yes, (dialog, whichButton) -> {
+ ((WidgetScreensListActivity) mContext).deleteWidgetScreen(widgetScreen);
+ })
+ .setNegativeButton(android.R.string.no, null)
+ .show();
+
+ return true;
+ });
+
+ holder.widgetScreenName.setText(mContext.getString(R.string.widget_screen_x, widgetScreen.getId()));
+ final List widgetNames = new ArrayList<>();
+
+ for (final WidgetPart part : widgetScreen.getParts()) {
+ if (part.getId() != null) {
+ widgetNames.add(part.getFullName());
+ }
+ }
+
+ if (!widgetNames.isEmpty()) {
+ holder.widgetScreenDescription.setText(String.join(", ", widgetNames));
+ } else {
+ holder.widgetScreenDescription.setText(R.string.unknown);
+ }
+ }
+
+ @Override
+ public int getItemCount() {
+ return widgetScreenList.size();
+ }
+
+ static class ViewHolder extends RecyclerView.ViewHolder {
+ final MaterialCardView container;
+
+ final TextView widgetScreenName;
+ final TextView widgetScreenDescription;
+
+ ViewHolder(View view) {
+ super(view);
+
+ container = view.findViewById(R.id.card_widget_screen);
+
+ widgetScreenName = view.findViewById(R.id.widget_screen_name);
+ widgetScreenDescription = view.findViewById(R.id.widget_screen_description);
+ }
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/widgets/WidgetScreensListActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/widgets/WidgetScreensListActivity.java
new file mode 100644
index 000000000..f8f1b17b3
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/widgets/WidgetScreensListActivity.java
@@ -0,0 +1,183 @@
+/* Copyright (C) 2023 José Rebelo
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.activities.widgets;
+
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.MenuItem;
+
+import androidx.activity.result.ActivityResult;
+import androidx.activity.result.ActivityResultCallback;
+import androidx.activity.result.ActivityResultLauncher;
+import androidx.activity.result.contract.ActivityResultContracts;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.google.android.material.dialog.MaterialAlertDialogBuilder;
+import com.google.android.material.floatingactionbutton.FloatingActionButton;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+
+import nodomain.freeyourgadget.gadgetbridge.R;
+import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBActivity;
+import nodomain.freeyourgadget.gadgetbridge.capabilities.widgets.WidgetLayout;
+import nodomain.freeyourgadget.gadgetbridge.capabilities.widgets.WidgetManager;
+import nodomain.freeyourgadget.gadgetbridge.capabilities.widgets.WidgetPart;
+import nodomain.freeyourgadget.gadgetbridge.capabilities.widgets.WidgetScreen;
+import nodomain.freeyourgadget.gadgetbridge.capabilities.widgets.WidgetType;
+import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
+
+public class WidgetScreensListActivity extends AbstractGBActivity {
+ private static final Logger LOG = LoggerFactory.getLogger(WidgetScreensListActivity.class);
+
+ private WidgetScreenListAdapter mGBWidgetScreenListAdapter;
+ private GBDevice gbDevice;
+ private WidgetManager widgetManager;
+
+ private ActivityResultLauncher configureWidgetScreenLauncher;
+ private final ActivityResultCallback configureWidgetCallback = result -> {
+ if (result.getResultCode() == Activity.RESULT_OK) {
+ updateWidgetScreensFromManager();
+ sendWidgetsToDevice();
+ }
+ };
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.activity_widget_screens_list);
+
+ gbDevice = getIntent().getParcelableExtra(GBDevice.EXTRA_DEVICE);
+ if (gbDevice == null) {
+ LOG.error("gbDevice must not be null");
+ finish();
+ return;
+ }
+
+ widgetManager = gbDevice.getDeviceCoordinator().getWidgetManager(gbDevice);
+ if (widgetManager == null) {
+ LOG.error("widgetManager must not be null");
+ finish();
+ return;
+ }
+
+ configureWidgetScreenLauncher = registerForActivityResult(
+ new ActivityResultContracts.StartActivityForResult(),
+ configureWidgetCallback
+ );
+
+ mGBWidgetScreenListAdapter = new WidgetScreenListAdapter(this);
+
+ final RecyclerView widgetsRecyclerView = findViewById(R.id.widget_screens_list);
+ widgetsRecyclerView.setHasFixedSize(true);
+ widgetsRecyclerView.setLayoutManager(new LinearLayoutManager(this));
+ widgetsRecyclerView.setAdapter(mGBWidgetScreenListAdapter);
+ updateWidgetScreensFromManager();
+
+ final FloatingActionButton fab = findViewById(R.id.fab);
+ fab.setOnClickListener(v -> {
+ int deviceSlots = widgetManager.getMaxScreens();
+
+ if (mGBWidgetScreenListAdapter.getItemCount() >= deviceSlots) {
+ // No more free slots
+ new MaterialAlertDialogBuilder(v.getContext())
+ .setTitle(R.string.reminder_no_free_slots_title)
+ .setMessage(getBaseContext().getString(R.string.widget_screen_no_free_slots_description, String.format(Locale.getDefault(), "%d", deviceSlots)))
+ .setIcon(R.drawable.ic_warning)
+ .setPositiveButton(android.R.string.ok, (dialog, whichButton) -> {
+ })
+ .show();
+ return;
+ }
+
+ final WidgetLayout defaultLayout = widgetManager.getSupportedWidgetLayouts().get(0);
+ final ArrayList defaultParts = new ArrayList<>();
+ for (final WidgetType widgetType : defaultLayout.getWidgetTypes()) {
+ defaultParts.add(new WidgetPart(null, "", widgetType));
+ }
+ final WidgetScreen widgetScreen = new WidgetScreen(
+ null,
+ defaultLayout,
+ defaultParts
+ );
+
+ configureWidgetScreen(widgetScreen);
+ });
+ }
+
+ /**
+ * Reads the available widgets from the database and updates the view afterwards.
+ */
+ @SuppressLint("NotifyDataSetChanged")
+ private void updateWidgetScreensFromManager() {
+ final List widgetScreens = widgetManager.getWidgetScreens();
+
+ mGBWidgetScreenListAdapter.setWidgetScreenList(widgetScreens);
+ mGBWidgetScreenListAdapter.notifyDataSetChanged();
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ // back button
+ finish();
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ public void configureWidgetScreen(final WidgetScreen widgetScreen) {
+ final Intent startIntent = new Intent(getApplicationContext(), WidgetScreenDetailsActivity.class);
+ startIntent.putExtra(GBDevice.EXTRA_DEVICE, gbDevice);
+ startIntent.putExtra(WidgetScreen.EXTRA_WIDGET_SCREEN, widgetScreen);
+ configureWidgetScreenLauncher.launch(startIntent);
+ }
+
+ public void deleteWidgetScreen(final WidgetScreen widgetScreen) {
+ if (mGBWidgetScreenListAdapter.getItemCount() - 1 < widgetManager.getMinScreens()) {
+ // Under minimum slots
+ new MaterialAlertDialogBuilder(this.getBaseContext())
+ .setTitle(R.string.widget_screen_delete_confirm_title)
+ .setMessage(getBaseContext().getString(R.string.widget_screen_min_screens, String.format(Locale.getDefault(), "%d", widgetManager.getMinScreens())))
+ .setIcon(R.drawable.ic_warning)
+ .setPositiveButton(android.R.string.ok, (dialog, whichButton) -> {
+ })
+ .show();
+ return;
+ }
+
+ widgetManager.deleteScreen(widgetScreen);
+
+ updateWidgetScreensFromManager();
+ sendWidgetsToDevice();
+ }
+
+ private void sendWidgetsToDevice() {
+ if (gbDevice.isInitialized()) {
+ widgetManager.sendToDevice();
+ }
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/capabilities/widgets/WidgetLayout.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/capabilities/widgets/WidgetLayout.java
new file mode 100644
index 000000000..d10331d38
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/capabilities/widgets/WidgetLayout.java
@@ -0,0 +1,48 @@
+/* Copyright (C) 2023 José Rebelo
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.capabilities.widgets;
+
+import androidx.annotation.StringRes;
+
+import nodomain.freeyourgadget.gadgetbridge.R;
+
+public enum WidgetLayout {
+ TOP_1_BOT_2(R.string.widget_layout_top_1_bot_2, WidgetType.WIDE, WidgetType.SMALL, WidgetType.SMALL),
+ TOP_2_BOT_1(R.string.widget_layout_top_2_bot_1, WidgetType.SMALL, WidgetType.SMALL, WidgetType.SMALL),
+ TOP_2_BOT_2(R.string.widget_layout_top_2_bot_2, WidgetType.SMALL, WidgetType.SMALL, WidgetType.SMALL, WidgetType.SMALL),
+ SINGLE(R.string.widget_layout_single, WidgetType.TALL),
+ TWO(R.string.widget_layout_two, WidgetType.SMALL, WidgetType.SMALL),
+ ;
+
+ @StringRes
+ private final int name;
+ private final WidgetType[] widgetTypes;
+
+ WidgetLayout(final int name, final WidgetType... widgetTypes) {
+ this.name = name;
+ this.widgetTypes = widgetTypes;
+ }
+
+ @StringRes
+ public int getName() {
+ return name;
+ }
+
+ public WidgetType[] getWidgetTypes() {
+ return widgetTypes;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/capabilities/widgets/WidgetManager.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/capabilities/widgets/WidgetManager.java
new file mode 100644
index 000000000..1ac23da96
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/capabilities/widgets/WidgetManager.java
@@ -0,0 +1,70 @@
+/* Copyright (C) 2023 José Rebelo
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.capabilities.widgets;
+
+import java.util.List;
+
+import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
+
+/**
+ * Provide an interface to manage a device's widgets in a device-independent manner.
+ */
+public interface WidgetManager {
+ /**
+ * The widget layouts supported by this device.
+ */
+ List getSupportedWidgetLayouts();
+
+ /**
+ * The widget parts that can be used to build a widget screen, for a specific widget type.x
+ */
+ List getSupportedWidgetParts(WidgetType targetWidgetType);
+
+ /**
+ * The currently configured widget screens on this device.
+ */
+ List getWidgetScreens();
+
+ GBDevice getDevice();
+
+ /**
+ * The minimum number of screens that must be present - deleting screens won't be allowed
+ * past this number.
+ */
+ int getMinScreens();
+
+ /**
+ * The maximum number of screens that can be created.
+ */
+ int getMaxScreens();
+
+ /**
+ * Saves a screen. If the screen is new, the ID will be null, otherwise it matches the
+ * screen that is being updated.
+ */
+ void saveScreen(WidgetScreen widgetScreen);
+
+ /**
+ * Deletes a screen.
+ */
+ void deleteScreen(WidgetScreen widgetScreen);
+
+ /**
+ * Send the currently configured screens to the device.
+ */
+ void sendToDevice();
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/capabilities/widgets/WidgetPart.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/capabilities/widgets/WidgetPart.java
new file mode 100644
index 000000000..54c10ae8c
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/capabilities/widgets/WidgetPart.java
@@ -0,0 +1,97 @@
+/* Copyright (C) 2023 José Rebelo
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.capabilities.widgets;
+
+import androidx.annotation.Nullable;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * A widget part is a single widget in a widget screen.
+ */
+public class WidgetPart implements Serializable {
+ // Null when not selected
+ @Nullable
+ private String id;
+
+ // The human-readable part name
+ private String name;
+
+ private WidgetType type;
+
+ // Null if it has no specific subtype
+ @Nullable
+ private WidgetPartSubtype subtype;
+
+ // The list of subtypes supported by this part, if any
+ private final List supportedSubtypes = new ArrayList<>();
+
+ public WidgetPart(@Nullable final String id, final String name, final WidgetType type) {
+ this.id = id;
+ this.name = name;
+ this.type = type;
+ }
+
+ @Nullable
+ public String getId() {
+ return id;
+ }
+
+ public void setId(@Nullable final String id) {
+ this.id = id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String getFullName() {
+ if (subtype != null) {
+ return String.format(Locale.ROOT, "%s (%s)", name, subtype.getName());
+ }
+
+ return name;
+ }
+
+ public void setName(final String name) {
+ this.name = name;
+ }
+
+ public WidgetType getType() {
+ return type;
+ }
+
+ public void setType(final WidgetType type) {
+ this.type = type;
+ }
+
+ @Nullable
+ public WidgetPartSubtype getSubtype() {
+ return subtype;
+ }
+
+ public void setSubtype(@Nullable final WidgetPartSubtype subtype) {
+ this.subtype = subtype;
+ }
+
+ public List getSupportedSubtypes() {
+ return supportedSubtypes;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/capabilities/widgets/WidgetPartSubtype.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/capabilities/widgets/WidgetPartSubtype.java
new file mode 100644
index 000000000..7655ed020
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/capabilities/widgets/WidgetPartSubtype.java
@@ -0,0 +1,48 @@
+/* Copyright (C) 2023 José Rebelo
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.capabilities.widgets;
+
+import java.io.Serializable;
+
+/**
+ * A widget part can have an optional subtype (eg. a workout type).
+ */
+public class WidgetPartSubtype implements Serializable {
+ private String id;
+ private String name;
+
+ public WidgetPartSubtype(final String id, final String name) {
+ this.id = id;
+ this.name = name;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(final String id) {
+ this.id = id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(final String name) {
+ this.name = name;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/capabilities/widgets/WidgetScreen.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/capabilities/widgets/WidgetScreen.java
new file mode 100644
index 000000000..c7394d8fc
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/capabilities/widgets/WidgetScreen.java
@@ -0,0 +1,65 @@
+/* Copyright (C) 2023 José Rebelo
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.capabilities.widgets;
+
+import androidx.annotation.Nullable;
+
+import java.io.Serializable;
+import java.util.List;
+
+public class WidgetScreen implements Serializable {
+ public static final String EXTRA_WIDGET_SCREEN = "widget_screen";
+
+ // Null when creating a new screen
+ @Nullable
+ private String id;
+ private WidgetLayout layout;
+
+ // The list of parts must match what the WidgetLayout expects
+ private List parts;
+
+ public WidgetScreen(@Nullable final String id, final WidgetLayout layout, final List parts) {
+ this.id = id;
+ this.layout = layout;
+ this.parts = parts;
+ }
+
+ @Nullable
+ public String getId() {
+ return id;
+ }
+
+ public void setId(@Nullable final String id) {
+ this.id = id;
+ }
+
+ public WidgetLayout getLayout() {
+ return layout;
+ }
+
+ public void setLayout(final WidgetLayout layout) {
+ this.layout = layout;
+ }
+
+ public List getParts() {
+ return parts;
+ }
+
+ public void setParts(final List parts) {
+ this.parts = parts;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/capabilities/widgets/WidgetType.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/capabilities/widgets/WidgetType.java
new file mode 100644
index 000000000..14be6f4f8
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/capabilities/widgets/WidgetType.java
@@ -0,0 +1,24 @@
+/* Copyright (C) 2022 José Rebelo
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.capabilities.widgets;
+
+public enum WidgetType {
+ SMALL, // 1x1
+ TALL, // 1x2
+ WIDE, // 2x1
+ ;
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractBLEDeviceCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractBLEDeviceCoordinator.java
index 9ea31e016..18500a4b0 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractBLEDeviceCoordinator.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractBLEDeviceCoordinator.java
@@ -1,6 +1,6 @@
package nodomain.freeyourgadget.gadgetbridge.devices;
-public abstract class AbstractBLEDeviceCoordinator extends AbstractDeviceCoordinator{
+public abstract class AbstractBLEDeviceCoordinator extends AbstractDeviceCoordinator {
@Override
public ConnectionType getConnectionType() {
return ConnectionType.BLE;
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractDeviceCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractDeviceCoordinator.java
index e5d58e595..66ca3dfb2 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractDeviceCoordinator.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractDeviceCoordinator.java
@@ -51,6 +51,7 @@ import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsCustomizer;
import nodomain.freeyourgadget.gadgetbridge.capabilities.HeartRateCapability;
import nodomain.freeyourgadget.gadgetbridge.capabilities.password.PasswordCapabilityImpl;
+import nodomain.freeyourgadget.gadgetbridge.capabilities.widgets.WidgetManager;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst;
@@ -586,6 +587,17 @@ public abstract class AbstractDeviceCoordinator implements DeviceCoordinator {
);
}
+ @Override
+ public boolean supportsWidgets(final GBDevice device) {
+ return false;
+ }
+
+ @Nullable
+ @Override
+ public WidgetManager getWidgetManager(final GBDevice device) {
+ return null;
+ }
+
public boolean supportsNavigation() {
return false;
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceCoordinator.java
index 618e5fa6f..d39090e32 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceCoordinator.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceCoordinator.java
@@ -38,6 +38,7 @@ import nodomain.freeyourgadget.gadgetbridge.GBException;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsCustomizer;
import nodomain.freeyourgadget.gadgetbridge.capabilities.HeartRateCapability;
import nodomain.freeyourgadget.gadgetbridge.capabilities.password.PasswordCapabilityImpl;
+import nodomain.freeyourgadget.gadgetbridge.capabilities.widgets.WidgetManager;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
@@ -550,6 +551,17 @@ public interface DeviceCoordinator {
List getHeartRateMeasurementIntervals();
+ /**
+ * Whether the device supports screens with configurable widgets.
+ */
+ boolean supportsWidgets(GBDevice device);
+
+ /**
+ * Gets the {@link WidgetManager} for this device. Must not be null if supportsWidgets is true.
+ */
+ @Nullable
+ WidgetManager getWidgetManager(GBDevice device);
+
boolean supportsNavigation();
int getOrderPriority();
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/xiaomi/XiaomiCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/xiaomi/XiaomiCoordinator.java
index 5617ce1a0..d3164cb49 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/xiaomi/XiaomiCoordinator.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/xiaomi/XiaomiCoordinator.java
@@ -42,6 +42,7 @@ import nodomain.freeyourgadget.gadgetbridge.activities.appmanager.AppManagerActi
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsCustomizer;
import nodomain.freeyourgadget.gadgetbridge.capabilities.HeartRateCapability;
import nodomain.freeyourgadget.gadgetbridge.capabilities.password.PasswordCapabilityImpl;
+import nodomain.freeyourgadget.gadgetbridge.capabilities.widgets.WidgetManager;
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractBLEDeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.TimeSampleProvider;
@@ -371,6 +372,9 @@ public abstract class XiaomiCoordinator extends AbstractBLEDeviceCoordinator {
if (supports(device, FEAT_DISPLAY_ITEMS)) {
settings.add(R.xml.devicesettings_xiaomi_displayitems);
}
+ if (this.supportsWidgets(device)) {
+ settings.add(R.xml.devicesettings_widgets);
+ }
if (supports(device, FEAT_PASSWORD)) {
settings.add(R.xml.devicesettings_password);
}
@@ -514,6 +518,16 @@ public abstract class XiaomiCoordinator extends AbstractBLEDeviceCoordinator {
);
}
+ @Override
+ public boolean supportsWidgets(final GBDevice device) {
+ return getPrefs(device).getBoolean(XiaomiPreferences.FEAT_WIDGETS, false);
+ }
+
+ @Override
+ public WidgetManager getWidgetManager(final GBDevice device) {
+ return new XiaomiWidgetManager(device);
+ }
+
protected static Prefs getPrefs(final GBDevice device) {
return new Prefs(GBApplication.getDeviceSpecificSharedPrefs(device.getAddress()));
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/xiaomi/XiaomiWidgetManager.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/xiaomi/XiaomiWidgetManager.java
new file mode 100644
index 000000000..f0d87862c
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/xiaomi/XiaomiWidgetManager.java
@@ -0,0 +1,421 @@
+/* Copyright (C) 2023 José Rebelo
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.devices.xiaomi;
+
+import androidx.annotation.Nullable;
+
+import com.google.protobuf.InvalidProtocolBufferException;
+
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+import nodomain.freeyourgadget.gadgetbridge.GBApplication;
+import nodomain.freeyourgadget.gadgetbridge.R;
+import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst;
+import nodomain.freeyourgadget.gadgetbridge.capabilities.widgets.WidgetLayout;
+import nodomain.freeyourgadget.gadgetbridge.capabilities.widgets.WidgetManager;
+import nodomain.freeyourgadget.gadgetbridge.capabilities.widgets.WidgetPart;
+import nodomain.freeyourgadget.gadgetbridge.capabilities.widgets.WidgetPartSubtype;
+import nodomain.freeyourgadget.gadgetbridge.capabilities.widgets.WidgetScreen;
+import nodomain.freeyourgadget.gadgetbridge.capabilities.widgets.WidgetType;
+import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
+import nodomain.freeyourgadget.gadgetbridge.proto.xiaomi.XiaomiProto;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.XiaomiPreferences;
+import nodomain.freeyourgadget.gadgetbridge.util.GB;
+import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
+
+public class XiaomiWidgetManager implements WidgetManager {
+ private static final Logger LOG = LoggerFactory.getLogger(XiaomiWidgetManager.class);
+
+ private final GBDevice device;
+
+ public XiaomiWidgetManager(final GBDevice device) {
+ this.device = device;
+ }
+
+ @Override
+ public List getSupportedWidgetLayouts() {
+ final List layouts = new ArrayList<>();
+ final Set partTypes = new HashSet<>();
+
+ final XiaomiProto.WidgetParts rawWidgetParts = getRawWidgetParts();
+
+ for (final XiaomiProto.WidgetPart widgetPart : rawWidgetParts.getWidgetPartList()) {
+ partTypes.add(fromRawWidgetType(widgetPart.getType()));
+ }
+
+ if (partTypes.contains(WidgetType.WIDE) && partTypes.contains(WidgetType.SMALL)) {
+ layouts.add(WidgetLayout.TOP_1_BOT_2);
+ layouts.add(WidgetLayout.TOP_2_BOT_1);
+ layouts.add(WidgetLayout.TOP_2_BOT_2);
+ }
+
+ if (partTypes.contains(WidgetType.TALL)) {
+ layouts.add(WidgetLayout.SINGLE);
+
+ if (partTypes.contains(WidgetType.SMALL)) {
+ layouts.add(WidgetLayout.TWO);
+ }
+ }
+
+ return layouts;
+ }
+
+ @Override
+ public List getSupportedWidgetParts(final WidgetType targetWidgetType) {
+ final List parts = new LinkedList<>();
+
+ final XiaomiProto.WidgetParts rawWidgetParts = getRawWidgetParts();
+
+ for (final XiaomiProto.WidgetPart widgetPart : rawWidgetParts.getWidgetPartList()) {
+ final WidgetType type = fromRawWidgetType(widgetPart.getType());
+
+ if (type != null && type.equals(targetWidgetType)) {
+ final WidgetPart newPart = new WidgetPart(
+ String.valueOf(widgetPart.getId()),
+ widgetPart.getTitle(),
+ type
+ );
+
+ // FIXME are there others?
+ if (widgetPart.getId() == 2321) {
+ if (StringUtils.isBlank(newPart.getName())) {
+ newPart.setName(GBApplication.getContext().getString(R.string.menuitem_workout));
+ }
+
+ final List workoutTypes = XiaomiPreferences.getWorkoutTypes(getDevice());
+ for (final XiaomiWorkoutType workoutType : workoutTypes) {
+ newPart.getSupportedSubtypes().add(
+ new WidgetPartSubtype(
+ String.valueOf(workoutType.getCode()),
+ workoutType.getName()
+ )
+ );
+ Collections.sort(newPart.getSupportedSubtypes(), (p1, p2) -> p1.getName().compareToIgnoreCase(p2.getName()));
+ }
+ }
+
+ parts.add(newPart);
+ }
+ }
+
+ return parts;
+ }
+
+ @Override
+ public List getWidgetScreens() {
+ final XiaomiProto.WidgetScreens rawWidgetScreens = getRawWidgetScreens();
+
+ final List ret = new ArrayList<>(rawWidgetScreens.getWidgetScreenCount());
+
+ final List workoutTypes = XiaomiPreferences.getWorkoutTypes(getDevice());
+
+ for (final XiaomiProto.WidgetScreen widgetScreen : rawWidgetScreens.getWidgetScreenList()) {
+ final WidgetLayout layout = fromRawLayout(widgetScreen.getLayout());
+
+ final List parts = new ArrayList<>(widgetScreen.getWidgetPartCount());
+
+ for (final XiaomiProto.WidgetPart widgetPart : widgetScreen.getWidgetPartList()) {
+ final WidgetType type = fromRawWidgetType(widgetPart.getType());
+
+ final WidgetPart newPart = new WidgetPart(
+ String.valueOf(widgetPart.getId()),
+ "Unknown (" + widgetPart.getId() + ")",
+ type
+ );
+
+ // Find the name
+ final XiaomiProto.WidgetPart rawPart1 = findRawPart(widgetPart.getType(), widgetPart.getId());
+ if (rawPart1 != null) {
+ newPart.setName(rawPart1.getTitle());
+ }
+
+ // FIXME are there others?
+ if (widgetPart.getId() == 2321) {
+ if (StringUtils.isBlank(newPart.getName())) {
+ newPart.setName(GBApplication.getContext().getString(R.string.menuitem_workout));
+ }
+ }
+
+ // Get the proper subtype, if any
+ if (widgetPart.getSubType() != 0) {
+ for (final XiaomiWorkoutType workoutType : workoutTypes) {
+ if (workoutType.getCode() == widgetPart.getSubType()) {
+ newPart.setSubtype(new WidgetPartSubtype(
+ String.valueOf(workoutType.getCode()),
+ workoutType.getName()
+ ));
+ }
+ }
+ }
+
+ parts.add(newPart);
+ }
+
+ ret.add(new WidgetScreen(
+ String.valueOf(widgetScreen.getId()),
+ layout,
+ parts
+ ));
+ }
+
+ return ret;
+ }
+
+ @Override
+ public GBDevice getDevice() {
+ return device;
+ }
+
+ @Override
+ public int getMinScreens() {
+ return getRawWidgetScreens().getWidgetsCapabilities().getMinWidgets();
+ }
+
+ @Override
+ public int getMaxScreens() {
+ return getRawWidgetScreens().getWidgetsCapabilities().getMaxWidgets();
+ }
+
+ @Override
+ public void saveScreen(final WidgetScreen widgetScreen) {
+ final XiaomiProto.WidgetScreens rawWidgetScreens = getRawWidgetScreens();
+
+ final int layoutNum;
+ switch (widgetScreen.getLayout()) {
+ case TOP_2_BOT_2:
+ layoutNum = 1;
+ break;
+ case TOP_1_BOT_2:
+ layoutNum = 2;
+ break;
+ case TOP_2_BOT_1:
+ layoutNum = 4;
+ break;
+ case TWO:
+ layoutNum = 256;
+ break;
+ case SINGLE:
+ layoutNum = 512;
+ break;
+ default:
+ LOG.warn("Unknown widget screens layout {}", widgetScreen.getLayout());
+ return;
+ }
+
+ XiaomiProto.WidgetScreen.Builder rawScreen = null;
+ if (widgetScreen.getId() == null) {
+ // new screen
+ rawScreen = XiaomiProto.WidgetScreen.newBuilder()
+ .setId(rawWidgetScreens.getWidgetScreenCount() + 1); // ids start at 1
+ } else {
+ for (final XiaomiProto.WidgetScreen screen : rawWidgetScreens.getWidgetScreenList()) {
+ if (String.valueOf(screen.getId()).equals(widgetScreen.getId())) {
+ rawScreen = XiaomiProto.WidgetScreen.newBuilder(screen);
+ break;
+ }
+
+ LOG.warn("Failed to find original screen for {}", widgetScreen.getId());
+ }
+
+ if (rawScreen == null) {
+ rawScreen = XiaomiProto.WidgetScreen.newBuilder()
+ .setId(rawWidgetScreens.getWidgetScreenCount() + 1);
+ }
+ }
+
+ rawScreen.setLayout(layoutNum);
+ rawScreen.clearWidgetPart();
+
+ for (final WidgetPart newPart : widgetScreen.getParts()) {
+ // Find the existing raw part
+ final XiaomiProto.WidgetPart knownRawPart = findRawPart(
+ toRawWidgetType(newPart.getType()),
+ Integer.parseInt(Objects.requireNonNull(newPart.getId()))
+ );
+
+ final XiaomiProto.WidgetPart.Builder newRawPartBuilder = XiaomiProto.WidgetPart.newBuilder(knownRawPart);
+
+ if (newPart.getSubtype() != null) {
+ // Get the workout type as subtype
+ final List workoutTypes = XiaomiPreferences.getWorkoutTypes(getDevice());
+ for (final XiaomiWorkoutType workoutType : workoutTypes) {
+ if (newPart.getSubtype().getId().equals(String.valueOf(workoutType.getCode()))) {
+ newRawPartBuilder.setSubType(workoutType.getCode());
+ break;
+ }
+ }
+ }
+
+ rawScreen.addWidgetPart(newRawPartBuilder);
+ }
+
+ final XiaomiProto.WidgetScreens.Builder builder = XiaomiProto.WidgetScreens.newBuilder(rawWidgetScreens);
+ if (rawScreen.getId() == rawWidgetScreens.getWidgetScreenCount() + 1) {
+ // Append at the end
+ builder.addWidgetScreen(rawScreen);
+ } else {
+ // Replace existing
+ builder.clearWidgetScreen();
+
+ for (final XiaomiProto.WidgetScreen screen : rawWidgetScreens.getWidgetScreenList()) {
+ if (screen.getId() == rawScreen.getId()) {
+ builder.addWidgetScreen(rawScreen);
+ } else {
+ builder.addWidgetScreen(screen);
+ }
+ }
+ }
+
+ getPrefs().getPreferences().edit()
+ .putString(XiaomiPreferences.PREF_WIDGET_SCREENS, GB.hexdump(builder.build().toByteArray()))
+ .apply();
+ }
+
+ @Override
+ public void deleteScreen(final WidgetScreen widgetScreen) {
+ if (widgetScreen.getId() == null) {
+ LOG.warn("Can't delete screen without id");
+ return;
+ }
+
+ final XiaomiProto.WidgetScreens rawWidgetScreens = getRawWidgetScreens();
+
+ final XiaomiProto.WidgetScreens.Builder builder = XiaomiProto.WidgetScreens.newBuilder(rawWidgetScreens)
+ .clearWidgetScreen();
+
+ for (final XiaomiProto.WidgetScreen screen : rawWidgetScreens.getWidgetScreenList()) {
+ if (String.valueOf(screen.getId()).equals(widgetScreen.getId())) {
+ continue;
+ }
+
+ builder.addWidgetScreen(screen);
+ }
+
+ getPrefs().getPreferences().edit()
+ .putString(XiaomiPreferences.PREF_WIDGET_SCREENS, GB.hexdump(builder.build().toByteArray()))
+ .apply();
+ }
+
+ @Override
+ public void sendToDevice() {
+ GBApplication.deviceService(getDevice()).onSendConfiguration(DeviceSettingsPreferenceConst.PREF_WIDGETS);
+ }
+
+ private Prefs getPrefs() {
+ return new Prefs(GBApplication.getDeviceSpecificSharedPrefs(getDevice().getAddress()));
+ }
+
+ @Nullable
+ private WidgetType fromRawWidgetType(final int rawType) {
+ switch (rawType) {
+ case 1:
+ return WidgetType.SMALL;
+ case 2:
+ return WidgetType.WIDE;
+ case 3:
+ return WidgetType.TALL;
+ default:
+ LOG.warn("Unknown widget type {}", rawType);
+ return null;
+ }
+ }
+
+ private int toRawWidgetType(final WidgetType widgetType) {
+ switch (widgetType) {
+ case SMALL:
+ return 1;
+ case WIDE:
+ return 2;
+ case TALL:
+ return 3;
+ default:
+ throw new IllegalArgumentException("Unknown widget type " + widgetType);
+ }
+ }
+
+ @Nullable
+ private WidgetLayout fromRawLayout(final int rawLayout) {
+ switch (rawLayout) {
+ case 1:
+ return WidgetLayout.TOP_2_BOT_2;
+ case 2:
+ return WidgetLayout.TOP_1_BOT_2;
+ case 4:
+ return WidgetLayout.TOP_2_BOT_1;
+ case 256:
+ return WidgetLayout.TWO;
+ case 512:
+ return WidgetLayout.SINGLE;
+ default:
+ LOG.warn("Unknown widget screens layout {}", rawLayout);
+ return null;
+ }
+ }
+
+ @Nullable
+ private XiaomiProto.WidgetPart findRawPart(final int type, final int id) {
+ final XiaomiProto.WidgetParts rawWidgetParts = getRawWidgetParts();
+
+ for (final XiaomiProto.WidgetPart rawPart : rawWidgetParts.getWidgetPartList()) {
+ if (rawPart.getType() == type && rawPart.getId() == id) {
+ return rawPart;
+ }
+ }
+
+ return null;
+ }
+
+ private XiaomiProto.WidgetScreens getRawWidgetScreens() {
+ final String hex = getPrefs().getString(XiaomiPreferences.PREF_WIDGET_SCREENS, null);
+ if (hex == null) {
+ LOG.warn("raw widget screens hex is null");
+ return XiaomiProto.WidgetScreens.newBuilder().build();
+ }
+
+ try {
+ return XiaomiProto.WidgetScreens.parseFrom(GB.hexStringToByteArray(hex));
+ } catch (final InvalidProtocolBufferException e) {
+ LOG.warn("failed to parse raw widget screns hex");
+ return XiaomiProto.WidgetScreens.newBuilder().build();
+ }
+ }
+
+ private XiaomiProto.WidgetParts getRawWidgetParts() {
+ final String hex = getPrefs().getString(XiaomiPreferences.PREF_WIDGET_PARTS, null);
+ if (hex == null) {
+ LOG.warn("raw widget parts hex is null");
+ return XiaomiProto.WidgetParts.newBuilder().build();
+ }
+
+ try {
+ return XiaomiProto.WidgetParts.parseFrom(GB.hexStringToByteArray(hex));
+ } catch (final InvalidProtocolBufferException e) {
+ LOG.warn("failed to parse raw widget parts hex");
+ return XiaomiProto.WidgetParts.newBuilder().build();
+ }
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/xiaomi/XiaomiWorkoutType.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/xiaomi/XiaomiWorkoutType.java
new file mode 100644
index 000000000..f9aa663d4
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/xiaomi/XiaomiWorkoutType.java
@@ -0,0 +1,61 @@
+/* Copyright (C) 2023 José Rebelo
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.devices.xiaomi;
+
+import androidx.annotation.StringRes;
+
+import nodomain.freeyourgadget.gadgetbridge.R;
+
+public class XiaomiWorkoutType {
+ private final int code;
+ private final String name;
+
+ public XiaomiWorkoutType(final int code, final String name) {
+ this.code = code;
+ this.name = name;
+ }
+
+ public int getCode() {
+ return code;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ @StringRes
+ public static int mapWorkoutName(final int code) {
+ switch (code) {
+ case 1:
+ return R.string.activity_type_outdoor_running;
+ case 2:
+ return R.string.activity_type_walking;
+ case 3:
+ return R.string.activity_type_hiking;
+ case 4:
+ return R.string.activity_type_trekking;
+ case 5:
+ return R.string.activity_type_trail_run;
+ case 6:
+ return R.string.activity_type_outdoor_cycling;
+ case 501:
+ return R.string.activity_type_wrestling;
+ }
+
+ return -1;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiPreferences.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiPreferences.java
index 6ef16b89c..0324716ce 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiPreferences.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiPreferences.java
@@ -16,13 +16,18 @@
along with this program. If not, see . */
package nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi;
+import java.util.ArrayList;
import java.util.Calendar;
+import java.util.Collections;
import java.util.Date;
import java.util.GregorianCalendar;
+import java.util.List;
import java.util.Locale;
import java.util.TimeZone;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
+import nodomain.freeyourgadget.gadgetbridge.R;
+import nodomain.freeyourgadget.gadgetbridge.devices.xiaomi.XiaomiWorkoutType;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.proto.xiaomi.XiaomiProto;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
@@ -32,6 +37,9 @@ public final class XiaomiPreferences {
public static final String PREF_REMINDER_SLOTS = "reminder_slots";
public static final String PREF_CANNED_MESSAGES_MIN = "canned_messages_min";
public static final String PREF_CANNED_MESSAGES_MAX = "canned_messages_max";
+ public static final String PREF_WORKOUT_TYPES = "workout_types";
+ public static final String PREF_WIDGET_SCREENS = "widget_screens"; // FIXME the value is a protobuf hex string
+ public static final String PREF_WIDGET_PARTS = "widget_parts"; // FIXME the value is a protobuf hex string
public static final String FEAT_WEAR_MODE = "feat_wear_mode";
public static final String FEAT_DEVICE_ACTIONS = "feat_device_actions";
@@ -45,6 +53,7 @@ public final class XiaomiPreferences {
public static final String FEAT_VITALITY_SCORE = "feat_vitality_score";
public static final String FEAT_SCREEN_ON_ON_NOTIFICATIONS = "feat_screen_on_on_notifications";
public static final String FEAT_CAMERA_REMOTE = "feat_camera_remote";
+ public static final String FEAT_WIDGETS = "feat_widgets";
private XiaomiPreferences() {
// util class
@@ -80,4 +89,22 @@ public final class XiaomiPreferences {
final Prefs prefs = new Prefs(GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()));
return prefs.getBoolean("keep_activity_data_on_device", false);
}
+
+ // FIXME this function should not be here
+ public static List getWorkoutTypes(final GBDevice gbDevice) {
+ final Prefs prefs = new Prefs(GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()));
+ final List codes = prefs.getList(PREF_WORKOUT_TYPES, Collections.emptyList());
+ final List ret = new ArrayList<>(codes.size());
+ for (final String code : codes) {
+ final int codeInt = Integer.parseInt(code);
+ final int codeNameStringRes = XiaomiWorkoutType.mapWorkoutName(codeInt);
+ ret.add(new XiaomiWorkoutType(
+ codeInt,
+ codeNameStringRes != -1 ?
+ GBApplication.getContext().getString(codeNameStringRes) :
+ GBApplication.getContext().getString(R.string.widget_unknown_workout, code)
+ ));
+ }
+ return ret;
+ }
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/services/XiaomiSystemService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/services/XiaomiSystemService.java
index 2be56f7d0..9b88806f3 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/services/XiaomiSystemService.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/services/XiaomiSystemService.java
@@ -16,6 +16,8 @@
along with this program. If not, see . */
package nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.services;
+import com.google.protobuf.InvalidProtocolBufferException;
+
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -83,10 +85,14 @@ public class XiaomiSystemService extends AbstractXiaomiService implements Xiaomi
public static final int CMD_PASSWORD_SET = 21;
public static final int CMD_DISPLAY_ITEMS_GET = 29;
public static final int CMD_DISPLAY_ITEMS_SET = 30;
+ public static final int CMD_WORKOUT_TYPES_GET = 39;
public static final int CMD_MISC_SETTING_SET_FROM_BAND = 42;
public static final int CMD_SILENT_MODE_GET = 43;
public static final int CMD_SILENT_MODE_SET_FROM_PHONE = 44;
public static final int CMD_SILENT_MODE_SET_FROM_WATCH = 45;
+ public static final int CMD_WIDGET_SCREENS_GET = 51;
+ public static final int CMD_WIDGET_SCREENS_SET = 52;
+ public static final int CMD_WIDGET_PARTS_GET = 53;
public static final int CMD_DEVICE_STATE_GET = 78;
public static final int CMD_DEVICE_STATE = 79;
@@ -111,6 +117,9 @@ public class XiaomiSystemService extends AbstractXiaomiService implements Xiaomi
getSupport().sendCommand("get password", COMMAND_TYPE, CMD_PASSWORD_GET);
getSupport().sendCommand("get display items", COMMAND_TYPE, CMD_DISPLAY_ITEMS_GET);
getSupport().sendCommand("get camera remote", COMMAND_TYPE, CMD_CAMERA_REMOTE_GET);
+ getSupport().sendCommand("get widgets", COMMAND_TYPE, CMD_WIDGET_SCREENS_GET);
+ getSupport().sendCommand("get widget parts", COMMAND_TYPE, CMD_WIDGET_PARTS_GET);
+ getSupport().sendCommand("get workout types", COMMAND_TYPE, CMD_WORKOUT_TYPES_GET);
}
@Override
@@ -159,6 +168,9 @@ public class XiaomiSystemService extends AbstractXiaomiService implements Xiaomi
case CMD_DISPLAY_ITEMS_GET:
handleDisplayItems(cmd.getSystem().getDisplayItems());
return;
+ case CMD_WORKOUT_TYPES_GET:
+ handleWorkoutTypes(cmd.getSystem().getWorkoutTypes());
+ return;
case CMD_MISC_SETTING_SET_FROM_BAND:
handleMiscSettingSet(cmd.getSystem().getMiscSettingSet());
return;
@@ -168,6 +180,15 @@ public class XiaomiSystemService extends AbstractXiaomiService implements Xiaomi
case CMD_SILENT_MODE_SET_FROM_WATCH:
handlePhoneSilentModeSet(cmd.getSystem().getPhoneSilentModeSet());
return;
+ case CMD_WIDGET_SCREENS_GET:
+ handleWidgetScreens(cmd.getSystem().getWidgetScreens());
+ return;
+ case CMD_WIDGET_SCREENS_SET:
+ LOG.debug("Got widget screens set ack, status={}", cmd.getStatus());
+ return;
+ case CMD_WIDGET_PARTS_GET:
+ handleWidgetParts(cmd.getSystem().getWidgetParts());
+ return;
case CMD_DEVICE_STATE_GET:
handleBasicDeviceState(cmd.getSystem().hasBasicDeviceState()
? cmd.getSystem().getBasicDeviceState()
@@ -205,6 +226,9 @@ public class XiaomiSystemService extends AbstractXiaomiService implements Xiaomi
case HuamiConst.PREF_DISPLAY_ITEMS_SORTABLE:
setDisplayItems();
return true;
+ case DeviceSettingsPreferenceConst.PREF_WIDGETS:
+ setWidgets();
+ return true;
}
return super.onSendConfiguration(config, prefs);
@@ -592,6 +616,20 @@ public class XiaomiSystemService extends AbstractXiaomiService implements Xiaomi
getSupport().evaluateGBDeviceEvent(eventUpdatePreferences);
}
+ private void handleWorkoutTypes(final XiaomiProto.WorkoutTypes workoutTypes) {
+ LOG.debug("Got {} workout types", workoutTypes.getWorkoutTypeCount());
+
+ final List codes = new ArrayList<>(workoutTypes.getWorkoutTypeCount());
+ for (final XiaomiProto.WorkoutType workoutType : workoutTypes.getWorkoutTypeList()) {
+ codes.add(String.valueOf(workoutType.getType()));
+ }
+
+ final GBDeviceEventUpdatePreferences eventUpdatePreferences = new GBDeviceEventUpdatePreferences()
+ .withPreference(XiaomiPreferences.PREF_WORKOUT_TYPES, String.join(",", codes));
+
+ getSupport().evaluateGBDeviceEvent(eventUpdatePreferences);
+ }
+
private void handleWearingState(int newStateValue) {
WearingState newState;
@@ -775,6 +813,52 @@ public class XiaomiSystemService extends AbstractXiaomiService implements Xiaomi
getSupport().sendCommand("request battery state", COMMAND_TYPE, CMD_BATTERY);
}
+ private void handleWidgetScreens(final XiaomiProto.WidgetScreens widgetScreens) {
+ LOG.debug("Got {} widget screens", widgetScreens.getWidgetScreenCount());
+ final GBDeviceEventUpdatePreferences eventUpdatePreferences = new GBDeviceEventUpdatePreferences()
+ // FIXME we're just persisting the protobuf bytes - probably not a good idea
+ .withPreference(XiaomiPreferences.PREF_WIDGET_SCREENS, GB.hexdump(widgetScreens.toByteArray()));
+
+ getSupport().evaluateGBDeviceEvent(eventUpdatePreferences);
+ }
+
+ private void handleWidgetParts(final XiaomiProto.WidgetParts widgetParts) {
+ LOG.debug("Got {} widget parts", widgetParts.getWidgetPartCount());
+ final GBDeviceEventUpdatePreferences eventUpdatePreferences = new GBDeviceEventUpdatePreferences()
+ .withPreference(XiaomiPreferences.FEAT_WIDGETS, widgetParts.getWidgetPartCount() > 0)
+ // FIXME we're just persisting the protobuf bytes - probably not a good idea
+ .withPreference(XiaomiPreferences.PREF_WIDGET_PARTS, GB.hexdump(widgetParts.toByteArray()));
+
+ getSupport().evaluateGBDeviceEvent(eventUpdatePreferences);
+ }
+
+ private void setWidgets() {
+ final String hex = getDevicePrefs().getString(XiaomiPreferences.PREF_WIDGET_SCREENS, null);
+ if (hex == null) {
+ LOG.warn("raw widget screens hex is null");
+ return;
+ }
+
+ final XiaomiProto.WidgetScreens widgetScreens;
+ try {
+ widgetScreens = XiaomiProto.WidgetScreens.parseFrom(GB.hexStringToByteArray(hex));
+ } catch (final InvalidProtocolBufferException e) {
+ LOG.warn("failed to parse raw widget screns hex");
+ return;
+ }
+
+ LOG.debug("Setting {} widget screens", widgetScreens.getWidgetScreenCount());
+
+ getSupport().sendCommand(
+ "set widgets",
+ XiaomiProto.Command.newBuilder()
+ .setType(COMMAND_TYPE)
+ .setSubtype(CMD_WIDGET_SCREENS_SET)
+ .setSystem(XiaomiProto.System.newBuilder().setWidgetScreens(widgetScreens))
+ .build()
+ );
+ }
+
public void onFindPhone(final boolean start) {
LOG.debug("Find phone: {}", start);
diff --git a/app/src/main/proto/xiaomi.proto b/app/src/main/proto/xiaomi.proto
index 7db812883..9bf2ef411 100644
--- a/app/src/main/proto/xiaomi.proto
+++ b/app/src/main/proto/xiaomi.proto
@@ -111,7 +111,7 @@ message System {
optional Language language = 20;
// 2, 51 get | 2, 52 create
- optional WidgetsScreens widgetScreens = 28;
+ optional WidgetScreens widgetScreens = 28;
// 2, 53
optional WidgetParts widgetParts = 29;
@@ -228,7 +228,7 @@ message WorkoutType {
optional uint32 unknown2 = 2; // 1
}
-message WidgetsScreens {
+message WidgetScreens {
repeated WidgetScreen widgetScreen = 1;
optional uint32 isFullList = 2; // 1 to overwrite the full list
optional WidgetsCapabilities widgetsCapabilities = 3; // only in response
diff --git a/app/src/main/res/layout/activity_widget_screen_details.xml b/app/src/main/res/layout/activity_widget_screen_details.xml
new file mode 100644
index 000000000..5b6929dfe
--- /dev/null
+++ b/app/src/main/res/layout/activity_widget_screen_details.xml
@@ -0,0 +1,254 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_widget_screens_list.xml b/app/src/main/res/layout/activity_widget_screens_list.xml
new file mode 100644
index 000000000..0c642fba7
--- /dev/null
+++ b/app/src/main/res/layout/activity_widget_screens_list.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/item_widget_screen.xml b/app/src/main/res/layout/item_widget_screen.xml
new file mode 100644
index 000000000..0adc57d8d
--- /dev/null
+++ b/app/src/main/res/layout/item_widget_screen.xml
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 36a27eb71..d6862b3e2 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1280,6 +1280,9 @@
Ice Skating
Golfing
Other
+ Trekking
+ Trail run
+ Wrestling
Unknown activity
Sport Activities
Sport Activity Detail
@@ -2457,4 +2460,22 @@
Get a notification when your vitality score reaches 30, 60 or 100 in the past 7 days
Daily progress
Get a notification when you reached the maximum number of vitality points for the day
+ Widget
+ Widget Screen
+ Delete widget screen
+ Are you sure you want to delete \'%1$s\'?
+ The device has no free slots for widget screens (total slots: %1$s)
+ There must be a minimum of %1$s screens
+ 1 top, 2 bottom
+ 2 top, 1 bottom
+ 2 top, 2 bottom
+ Widget layout
+ Widget Subtype
+ Screen %s
+ 1 widget
+ 2 widgets
+ Move up
+ Move down
+ Please select all widgets
+ Unknown workout - %s
diff --git a/app/src/main/res/xml/devicesettings_widgets.xml b/app/src/main/res/xml/devicesettings_widgets.xml
new file mode 100644
index 000000000..5c137f993
--- /dev/null
+++ b/app/src/main/res/xml/devicesettings_widgets.xml
@@ -0,0 +1,7 @@
+
+
+
+