1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-08-25 08:40:43 +02:00

Xiaomi: Manage widgets

This commit is contained in:
José Rebelo 2023-12-11 22:00:16 +00:00
parent 108307c711
commit 6de7af62e3
26 changed files with 1949 additions and 4 deletions

View File

@ -593,6 +593,10 @@
android:name=".activities.ConfigureContacts"
android:label="@string/title_activity_set_contacts"
android:parentActivityName=".activities.ControlCenterv2" />
<activity
android:name=".activities.widgets.WidgetScreensListActivity"
android:label="@string/menuitem_widgets"
android:parentActivityName=".activities.ControlCenterv2" />
<activity
android:name=".activities.ConfigureWorldClocks"
android:label="@string/pref_world_clocks_title"
@ -618,6 +622,12 @@
android:parentActivityName=".activities.ConfigureReminders"
android:screenOrientation="portrait"
android:windowSoftInputMode="adjustResize" />
<activity
android:name=".activities.widgets.WidgetScreenDetailsActivity"
android:label="@string/widget_screen"
android:parentActivityName=".activities.widgets.WidgetScreensListActivity"
android:screenOrientation="portrait"
android:windowSoftInputMode="adjustResize" />
<activity
android:name=".activities.WorldClockDetails"
android:label="@string/title_activity_world_clock_details"

View File

@ -243,6 +243,7 @@ public class DeviceSettingsPreferenceConst {
public static final String PREF_WORLD_CLOCKS = "pref_world_clocks";
public static final String PREF_CONTACTS = "pref_contacts";
public static final String PREF_WIDGETS = "pref_widgets";
public static final String PREF_ANTILOST_ENABLED = "pref_antilost_enabled";
public static final String PREF_HYDRATION_SWITCH = "pref_hydration_switch";

View File

@ -46,6 +46,7 @@ import nodomain.freeyourgadget.gadgetbridge.activities.AbstractPreferenceFragmen
import nodomain.freeyourgadget.gadgetbridge.activities.app_specific_notifications.AppSpecificNotificationSettingsActivity;
import nodomain.freeyourgadget.gadgetbridge.activities.loyaltycards.LoyaltyCardsSettingsActivity;
import nodomain.freeyourgadget.gadgetbridge.activities.loyaltycards.LoyaltyCardsSettingsConst;
import nodomain.freeyourgadget.gadgetbridge.activities.widgets.WidgetScreensListActivity;
import nodomain.freeyourgadget.gadgetbridge.capabilities.HeartRateCapability;
import nodomain.freeyourgadget.gadgetbridge.capabilities.password.PasswordCapabilityImpl;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
@ -57,7 +58,6 @@ import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
import static nodomain.freeyourgadget.gadgetbridge.GBApplication.getContext;
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.*;
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_CONTROL_CENTER_SORTABLE;
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_DEVICE_ACTION_FELL_SLEEP_BROADCAST;
@ -774,6 +774,16 @@ public class DeviceSpecificSettingsFragment extends AbstractPreferenceFragment i
});
}
final Preference widgets = findPreference(PREF_WIDGETS);
if (widgets != null) {
widgets.setOnPreferenceClickListener(preference -> {
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() {

View File

@ -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 <http://www.gnu.org/licenses/>. */
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<WidgetLayout> 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<String> 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<WidgetPart> 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<WidgetPart> 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<String> 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<WidgetPartSubtype> 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<String> 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();
});
}
}

View File

@ -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 <http://www.gnu.org/licenses/>. */
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<WidgetScreenListAdapter.ViewHolder> {
private final Context mContext;
private ArrayList<WidgetScreen> widgetScreenList;
public WidgetScreenListAdapter(Context context) {
this.mContext = context;
}
public void setWidgetScreenList(List<WidgetScreen> widgetScreens) {
this.widgetScreenList = new ArrayList<>(widgetScreens);
}
public ArrayList<WidgetScreen> 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<String> 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);
}
}
}

View File

@ -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 <http://www.gnu.org/licenses/>. */
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<Intent> configureWidgetScreenLauncher;
private final ActivityResultCallback<ActivityResult> 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<WidgetPart> 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<WidgetScreen> 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();
}
}
}

View File

@ -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 <http://www.gnu.org/licenses/>. */
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;
}
}

View File

@ -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 <http://www.gnu.org/licenses/>. */
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<WidgetLayout> getSupportedWidgetLayouts();
/**
* The widget parts that can be used to build a widget screen, for a specific widget type.x
*/
List<WidgetPart> getSupportedWidgetParts(WidgetType targetWidgetType);
/**
* The currently configured widget screens on this device.
*/
List<WidgetScreen> 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();
}

View File

@ -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 <http://www.gnu.org/licenses/>. */
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<WidgetPartSubtype> 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<WidgetPartSubtype> getSupportedSubtypes() {
return supportedSubtypes;
}
}

View File

@ -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 <http://www.gnu.org/licenses/>. */
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;
}
}

View File

@ -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 <http://www.gnu.org/licenses/>. */
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<WidgetPart> parts;
public WidgetScreen(@Nullable final String id, final WidgetLayout layout, final List<WidgetPart> 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<WidgetPart> getParts() {
return parts;
}
public void setParts(final List<WidgetPart> parts) {
this.parts = parts;
}
}

View File

@ -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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.capabilities.widgets;
public enum WidgetType {
SMALL, // 1x1
TALL, // 1x2
WIDE, // 2x1
;
}

View File

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

View File

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

View File

@ -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<HeartRateCapability.MeasurementInterval> 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();

View File

@ -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()));
}

View File

@ -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 <http://www.gnu.org/licenses/>. */
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<WidgetLayout> getSupportedWidgetLayouts() {
final List<WidgetLayout> layouts = new ArrayList<>();
final Set<WidgetType> 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<WidgetPart> getSupportedWidgetParts(final WidgetType targetWidgetType) {
final List<WidgetPart> 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<XiaomiWorkoutType> 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<WidgetScreen> getWidgetScreens() {
final XiaomiProto.WidgetScreens rawWidgetScreens = getRawWidgetScreens();
final List<WidgetScreen> ret = new ArrayList<>(rawWidgetScreens.getWidgetScreenCount());
final List<XiaomiWorkoutType> workoutTypes = XiaomiPreferences.getWorkoutTypes(getDevice());
for (final XiaomiProto.WidgetScreen widgetScreen : rawWidgetScreens.getWidgetScreenList()) {
final WidgetLayout layout = fromRawLayout(widgetScreen.getLayout());
final List<WidgetPart> 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<XiaomiWorkoutType> 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();
}
}
}

View File

@ -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 <http://www.gnu.org/licenses/>. */
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;
}
}

View File

@ -16,13 +16,18 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. */
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<XiaomiWorkoutType> getWorkoutTypes(final GBDevice gbDevice) {
final Prefs prefs = new Prefs(GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()));
final List<String> codes = prefs.getList(PREF_WORKOUT_TYPES, Collections.emptyList());
final List<XiaomiWorkoutType> 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;
}
}

View File

@ -16,6 +16,8 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. */
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<String> 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);

View File

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

View File

@ -0,0 +1,254 @@
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:card_view="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/constraintLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context="nodomain.freeyourgadget.gadgetbridge.activities.widgets.WidgetScreenDetailsActivity">
<com.google.android.material.card.MaterialCardView
android:id="@+id/card_layout"
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:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
card_view:cardElevation="3dp"
card_view:contentPadding="4dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp">
<TextView
android:id="@+id/label_layout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/widget_layout"
android:textAppearance="?android:attr/textAppearance"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/widget_screen_layout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:text="2x1"
android:textAppearance="?android:attr/textAppearanceLarge"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/label_layout" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
<com.google.android.material.card.MaterialCardView
android:id="@+id/card_widget_top_left"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:foreground="?android:attr/selectableItemBackground"
app:cardBackgroundColor="?attr/cardview_background_color"
app:layout_constraintEnd_toStartOf="@id/card_widget_top_right"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/card_layout"
card_view:cardElevation="3dp"
card_view:contentPadding="4dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp">
<TextView
android:id="@+id/label_widget_top_left"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/widget"
android:textAppearance="?android:attr/textAppearance"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/widget_top_left"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:text="TOP LEFT"
android:textAppearance="?android:attr/textAppearanceLarge"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/label_widget_top_left" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
<com.google.android.material.card.MaterialCardView
android:id="@+id/card_widget_top_right"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:foreground="?android:attr/selectableItemBackground"
app:cardBackgroundColor="?attr/cardview_background_color"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/card_widget_top_left"
app:layout_constraintTop_toBottomOf="@id/card_layout"
card_view:cardElevation="3dp"
card_view:contentPadding="4dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp">
<TextView
android:id="@+id/label_widget_top_right"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/widget"
android:textAppearance="?android:attr/textAppearance"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/widget_top_right"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:text="TOP RIGHT"
android:textAppearance="?android:attr/textAppearanceLarge"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/label_widget_top_right" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
<com.google.android.material.card.MaterialCardView
android:id="@+id/card_widget_center"
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:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/card_widget_top_right"
card_view:cardElevation="3dp"
card_view:contentPadding="4dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp">
<TextView
android:id="@+id/label_widget_center"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/widget"
android:textAppearance="?android:attr/textAppearance"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/widget_center"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:text="CENTER"
android:textAppearance="?android:attr/textAppearanceLarge"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/label_widget_center" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
<com.google.android.material.card.MaterialCardView
android:id="@+id/card_widget_bottom_left"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:foreground="?android:attr/selectableItemBackground"
app:cardBackgroundColor="?attr/cardview_background_color"
app:layout_constraintEnd_toStartOf="@id/card_widget_bottom_right"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/card_widget_center"
card_view:cardElevation="3dp"
card_view:contentPadding="4dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp">
<TextView
android:id="@+id/label_widget_bottom_left"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/widget"
android:textAppearance="?android:attr/textAppearance"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/widget_bottom_left"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:text="BOT LEFT"
android:textAppearance="?android:attr/textAppearanceLarge"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/label_widget_bottom_left" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
<com.google.android.material.card.MaterialCardView
android:id="@+id/card_widget_bottom_right"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:foreground="?android:attr/selectableItemBackground"
app:cardBackgroundColor="?attr/cardview_background_color"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/card_widget_bottom_left"
app:layout_constraintTop_toBottomOf="@id/card_widget_center"
card_view:cardElevation="3dp"
card_view:contentPadding="4dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp">
<TextView
android:id="@+id/label_widget_bottom_right"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/widget"
android:textAppearance="?android:attr/textAppearance"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/widget_bottom_right"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:text="BOT RIGHT"
android:textAppearance="?android:attr/textAppearanceLarge"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/label_widget_bottom_right" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab_save"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:srcCompat="@drawable/ic_save" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,27 @@
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:context="nodomain.freeyourgadget.gadgetbridge.activities.widgets.WidgetScreensListActivity">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/widget_screens_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_alignParentTop="true"
android:layout_centerHorizontal="true"
android:divider="@null" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentBottom="true"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
app:srcCompat="@drawable/ic_add" />
</RelativeLayout>

View File

@ -0,0 +1,52 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:card_view="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.card.MaterialCardView
android:id="@+id/card_widget_screen"
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"
card_view:cardElevation="3dp"
card_view:contentPadding="4dp">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp">
<TextView
android:id="@+id/widget_screen_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:layout_marginStart="3dp"
android:layout_marginTop="0dp"
android:layout_marginEnd="0dp"
android:layout_marginBottom="4dp"
android:text="Screen 1"
android:textAppearance="?android:attr/textAppearance" />
<TextView
android:id="@+id/widget_screen_description"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:layout_marginStart="3dp"
android:layout_marginTop="25dp"
android:layout_marginEnd="0dp"
android:layout_marginBottom="0dp"
android:text="x widgets"
android:textAppearance="?android:attr/textAppearanceSmall" />
</RelativeLayout>
</com.google.android.material.card.MaterialCardView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -1280,6 +1280,9 @@
<string name="activity_type_iceskating">Ice Skating</string>
<string name="activity_type_golf">Golfing</string>
<string name="activity_type_other">Other</string>
<string name="activity_type_trekking">Trekking</string>
<string name="activity_type_trail_run">Trail run</string>
<string name="activity_type_wrestling">Wrestling</string>
<string name="activity_type_unknown">Unknown activity</string>
<string name="activity_summaries">Sport Activities</string>
<string name="activity_summary_detail">Sport Activity Detail</string>
@ -2457,4 +2460,22 @@
<string name="pref_vitality_score_7_day_summary">Get a notification when your vitality score reaches 30, 60 or 100 in the past 7 days</string>
<string name="pref_vitality_score_daily_title">Daily progress</string>
<string name="pref_vitality_score_daily_summary">Get a notification when you reached the maximum number of vitality points for the day</string>
<string name="widget">Widget</string>
<string name="widget_screen">Widget Screen</string>
<string name="widget_screen_delete_confirm_title">Delete widget screen</string>
<string name="widget_screen_delete_confirm_description">Are you sure you want to delete \'%1$s\'?</string>
<string name="widget_screen_no_free_slots_description">The device has no free slots for widget screens (total slots: %1$s)</string>
<string name="widget_screen_min_screens">There must be a minimum of %1$s screens</string>
<string name="widget_layout_top_1_bot_2">1 top, 2 bottom</string>
<string name="widget_layout_top_2_bot_1">2 top, 1 bottom</string>
<string name="widget_layout_top_2_bot_2">2 top, 2 bottom</string>
<string name="widget_layout">Widget layout</string>
<string name="widget_subtype">Widget Subtype</string>
<string name="widget_screen_x">Screen %s</string>
<string name="widget_layout_single">1 widget</string>
<string name="widget_layout_two">2 widgets</string>
<string name="widget_move_up">Move up</string>
<string name="widget_move_down">Move down</string>
<string name="widget_missing_parts">Please select all widgets</string>
<string name="widget_unknown_workout">Unknown workout - %s</string>
</resources>

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<Preference
android:icon="@drawable/ic_widgets"
android:key="pref_widgets"
android:title="@string/menuitem_widgets" />
</androidx.preference.PreferenceScreen>