mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge
synced 2024-11-04 01:09:47 +01:00
Xiaomi: Manage widgets
This commit is contained in:
parent
108307c711
commit
6de7af62e3
@ -593,6 +593,10 @@
|
|||||||
android:name=".activities.ConfigureContacts"
|
android:name=".activities.ConfigureContacts"
|
||||||
android:label="@string/title_activity_set_contacts"
|
android:label="@string/title_activity_set_contacts"
|
||||||
android:parentActivityName=".activities.ControlCenterv2" />
|
android:parentActivityName=".activities.ControlCenterv2" />
|
||||||
|
<activity
|
||||||
|
android:name=".activities.widgets.WidgetScreensListActivity"
|
||||||
|
android:label="@string/menuitem_widgets"
|
||||||
|
android:parentActivityName=".activities.ControlCenterv2" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.ConfigureWorldClocks"
|
android:name=".activities.ConfigureWorldClocks"
|
||||||
android:label="@string/pref_world_clocks_title"
|
android:label="@string/pref_world_clocks_title"
|
||||||
@ -618,6 +622,12 @@
|
|||||||
android:parentActivityName=".activities.ConfigureReminders"
|
android:parentActivityName=".activities.ConfigureReminders"
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="portrait"
|
||||||
android:windowSoftInputMode="adjustResize" />
|
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
|
<activity
|
||||||
android:name=".activities.WorldClockDetails"
|
android:name=".activities.WorldClockDetails"
|
||||||
android:label="@string/title_activity_world_clock_details"
|
android:label="@string/title_activity_world_clock_details"
|
||||||
|
@ -243,6 +243,7 @@ public class DeviceSettingsPreferenceConst {
|
|||||||
|
|
||||||
public static final String PREF_WORLD_CLOCKS = "pref_world_clocks";
|
public static final String PREF_WORLD_CLOCKS = "pref_world_clocks";
|
||||||
public static final String PREF_CONTACTS = "pref_contacts";
|
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_ANTILOST_ENABLED = "pref_antilost_enabled";
|
||||||
public static final String PREF_HYDRATION_SWITCH = "pref_hydration_switch";
|
public static final String PREF_HYDRATION_SWITCH = "pref_hydration_switch";
|
||||||
|
@ -46,6 +46,7 @@ import nodomain.freeyourgadget.gadgetbridge.activities.AbstractPreferenceFragmen
|
|||||||
import nodomain.freeyourgadget.gadgetbridge.activities.app_specific_notifications.AppSpecificNotificationSettingsActivity;
|
import nodomain.freeyourgadget.gadgetbridge.activities.app_specific_notifications.AppSpecificNotificationSettingsActivity;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.activities.loyaltycards.LoyaltyCardsSettingsActivity;
|
import nodomain.freeyourgadget.gadgetbridge.activities.loyaltycards.LoyaltyCardsSettingsActivity;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.activities.loyaltycards.LoyaltyCardsSettingsConst;
|
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.HeartRateCapability;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.capabilities.password.PasswordCapabilityImpl;
|
import nodomain.freeyourgadget.gadgetbridge.capabilities.password.PasswordCapabilityImpl;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
|
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.model.DeviceType;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
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.activities.devicesettings.DeviceSettingsPreferenceConst.*;
|
||||||
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_CONTROL_CENTER_SORTABLE;
|
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;
|
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");
|
final Preference calendarBlacklist = findPreference("blacklist_calendars");
|
||||||
if (calendarBlacklist != null) {
|
if (calendarBlacklist != null) {
|
||||||
calendarBlacklist.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
|
calendarBlacklist.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
|
||||||
|
@ -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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
;
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
package nodomain.freeyourgadget.gadgetbridge.devices;
|
package nodomain.freeyourgadget.gadgetbridge.devices;
|
||||||
|
|
||||||
public abstract class AbstractBLEDeviceCoordinator extends AbstractDeviceCoordinator{
|
public abstract class AbstractBLEDeviceCoordinator extends AbstractDeviceCoordinator {
|
||||||
@Override
|
@Override
|
||||||
public ConnectionType getConnectionType() {
|
public ConnectionType getConnectionType() {
|
||||||
return ConnectionType.BLE;
|
return ConnectionType.BLE;
|
||||||
|
@ -51,6 +51,7 @@ import nodomain.freeyourgadget.gadgetbridge.R;
|
|||||||
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsCustomizer;
|
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsCustomizer;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.capabilities.HeartRateCapability;
|
import nodomain.freeyourgadget.gadgetbridge.capabilities.HeartRateCapability;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.capabilities.password.PasswordCapabilityImpl;
|
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.DBHandler;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
|
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst;
|
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() {
|
public boolean supportsNavigation() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -38,6 +38,7 @@ import nodomain.freeyourgadget.gadgetbridge.GBException;
|
|||||||
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsCustomizer;
|
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsCustomizer;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.capabilities.HeartRateCapability;
|
import nodomain.freeyourgadget.gadgetbridge.capabilities.HeartRateCapability;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.capabilities.password.PasswordCapabilityImpl;
|
import nodomain.freeyourgadget.gadgetbridge.capabilities.password.PasswordCapabilityImpl;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.capabilities.widgets.WidgetManager;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
|
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
|
||||||
@ -550,6 +551,17 @@ public interface DeviceCoordinator {
|
|||||||
|
|
||||||
List<HeartRateCapability.MeasurementInterval> getHeartRateMeasurementIntervals();
|
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();
|
boolean supportsNavigation();
|
||||||
|
|
||||||
int getOrderPriority();
|
int getOrderPriority();
|
||||||
|
@ -42,6 +42,7 @@ import nodomain.freeyourgadget.gadgetbridge.activities.appmanager.AppManagerActi
|
|||||||
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsCustomizer;
|
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsCustomizer;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.capabilities.HeartRateCapability;
|
import nodomain.freeyourgadget.gadgetbridge.capabilities.HeartRateCapability;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.capabilities.password.PasswordCapabilityImpl;
|
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.AbstractBLEDeviceCoordinator;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
|
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.devices.TimeSampleProvider;
|
import nodomain.freeyourgadget.gadgetbridge.devices.TimeSampleProvider;
|
||||||
@ -371,6 +372,9 @@ public abstract class XiaomiCoordinator extends AbstractBLEDeviceCoordinator {
|
|||||||
if (supports(device, FEAT_DISPLAY_ITEMS)) {
|
if (supports(device, FEAT_DISPLAY_ITEMS)) {
|
||||||
settings.add(R.xml.devicesettings_xiaomi_displayitems);
|
settings.add(R.xml.devicesettings_xiaomi_displayitems);
|
||||||
}
|
}
|
||||||
|
if (this.supportsWidgets(device)) {
|
||||||
|
settings.add(R.xml.devicesettings_widgets);
|
||||||
|
}
|
||||||
if (supports(device, FEAT_PASSWORD)) {
|
if (supports(device, FEAT_PASSWORD)) {
|
||||||
settings.add(R.xml.devicesettings_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) {
|
protected static Prefs getPrefs(final GBDevice device) {
|
||||||
return new Prefs(GBApplication.getDeviceSpecificSharedPrefs(device.getAddress()));
|
return new Prefs(GBApplication.getDeviceSpecificSharedPrefs(device.getAddress()));
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -16,13 +16,18 @@
|
|||||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi;
|
package nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Calendar;
|
import java.util.Calendar;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.GregorianCalendar;
|
import java.util.GregorianCalendar;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.TimeZone;
|
import java.util.TimeZone;
|
||||||
|
|
||||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
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.impl.GBDevice;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.proto.xiaomi.XiaomiProto;
|
import nodomain.freeyourgadget.gadgetbridge.proto.xiaomi.XiaomiProto;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
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_REMINDER_SLOTS = "reminder_slots";
|
||||||
public static final String PREF_CANNED_MESSAGES_MIN = "canned_messages_min";
|
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_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_WEAR_MODE = "feat_wear_mode";
|
||||||
public static final String FEAT_DEVICE_ACTIONS = "feat_device_actions";
|
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_VITALITY_SCORE = "feat_vitality_score";
|
||||||
public static final String FEAT_SCREEN_ON_ON_NOTIFICATIONS = "feat_screen_on_on_notifications";
|
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_CAMERA_REMOTE = "feat_camera_remote";
|
||||||
|
public static final String FEAT_WIDGETS = "feat_widgets";
|
||||||
|
|
||||||
private XiaomiPreferences() {
|
private XiaomiPreferences() {
|
||||||
// util class
|
// util class
|
||||||
@ -80,4 +89,22 @@ public final class XiaomiPreferences {
|
|||||||
final Prefs prefs = new Prefs(GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()));
|
final Prefs prefs = new Prefs(GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()));
|
||||||
return prefs.getBoolean("keep_activity_data_on_device", false);
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,8 @@
|
|||||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.services;
|
package nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.services;
|
||||||
|
|
||||||
|
import com.google.protobuf.InvalidProtocolBufferException;
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
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_PASSWORD_SET = 21;
|
||||||
public static final int CMD_DISPLAY_ITEMS_GET = 29;
|
public static final int CMD_DISPLAY_ITEMS_GET = 29;
|
||||||
public static final int CMD_DISPLAY_ITEMS_SET = 30;
|
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_MISC_SETTING_SET_FROM_BAND = 42;
|
||||||
public static final int CMD_SILENT_MODE_GET = 43;
|
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_PHONE = 44;
|
||||||
public static final int CMD_SILENT_MODE_SET_FROM_WATCH = 45;
|
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_GET = 78;
|
||||||
public static final int CMD_DEVICE_STATE = 79;
|
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 password", COMMAND_TYPE, CMD_PASSWORD_GET);
|
||||||
getSupport().sendCommand("get display items", COMMAND_TYPE, CMD_DISPLAY_ITEMS_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 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
|
@Override
|
||||||
@ -159,6 +168,9 @@ public class XiaomiSystemService extends AbstractXiaomiService implements Xiaomi
|
|||||||
case CMD_DISPLAY_ITEMS_GET:
|
case CMD_DISPLAY_ITEMS_GET:
|
||||||
handleDisplayItems(cmd.getSystem().getDisplayItems());
|
handleDisplayItems(cmd.getSystem().getDisplayItems());
|
||||||
return;
|
return;
|
||||||
|
case CMD_WORKOUT_TYPES_GET:
|
||||||
|
handleWorkoutTypes(cmd.getSystem().getWorkoutTypes());
|
||||||
|
return;
|
||||||
case CMD_MISC_SETTING_SET_FROM_BAND:
|
case CMD_MISC_SETTING_SET_FROM_BAND:
|
||||||
handleMiscSettingSet(cmd.getSystem().getMiscSettingSet());
|
handleMiscSettingSet(cmd.getSystem().getMiscSettingSet());
|
||||||
return;
|
return;
|
||||||
@ -168,6 +180,15 @@ public class XiaomiSystemService extends AbstractXiaomiService implements Xiaomi
|
|||||||
case CMD_SILENT_MODE_SET_FROM_WATCH:
|
case CMD_SILENT_MODE_SET_FROM_WATCH:
|
||||||
handlePhoneSilentModeSet(cmd.getSystem().getPhoneSilentModeSet());
|
handlePhoneSilentModeSet(cmd.getSystem().getPhoneSilentModeSet());
|
||||||
return;
|
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:
|
case CMD_DEVICE_STATE_GET:
|
||||||
handleBasicDeviceState(cmd.getSystem().hasBasicDeviceState()
|
handleBasicDeviceState(cmd.getSystem().hasBasicDeviceState()
|
||||||
? cmd.getSystem().getBasicDeviceState()
|
? cmd.getSystem().getBasicDeviceState()
|
||||||
@ -205,6 +226,9 @@ public class XiaomiSystemService extends AbstractXiaomiService implements Xiaomi
|
|||||||
case HuamiConst.PREF_DISPLAY_ITEMS_SORTABLE:
|
case HuamiConst.PREF_DISPLAY_ITEMS_SORTABLE:
|
||||||
setDisplayItems();
|
setDisplayItems();
|
||||||
return true;
|
return true;
|
||||||
|
case DeviceSettingsPreferenceConst.PREF_WIDGETS:
|
||||||
|
setWidgets();
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return super.onSendConfiguration(config, prefs);
|
return super.onSendConfiguration(config, prefs);
|
||||||
@ -592,6 +616,20 @@ public class XiaomiSystemService extends AbstractXiaomiService implements Xiaomi
|
|||||||
getSupport().evaluateGBDeviceEvent(eventUpdatePreferences);
|
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) {
|
private void handleWearingState(int newStateValue) {
|
||||||
WearingState newState;
|
WearingState newState;
|
||||||
|
|
||||||
@ -775,6 +813,52 @@ public class XiaomiSystemService extends AbstractXiaomiService implements Xiaomi
|
|||||||
getSupport().sendCommand("request battery state", COMMAND_TYPE, CMD_BATTERY);
|
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) {
|
public void onFindPhone(final boolean start) {
|
||||||
LOG.debug("Find phone: {}", start);
|
LOG.debug("Find phone: {}", start);
|
||||||
|
|
||||||
|
@ -111,7 +111,7 @@ message System {
|
|||||||
optional Language language = 20;
|
optional Language language = 20;
|
||||||
|
|
||||||
// 2, 51 get | 2, 52 create
|
// 2, 51 get | 2, 52 create
|
||||||
optional WidgetsScreens widgetScreens = 28;
|
optional WidgetScreens widgetScreens = 28;
|
||||||
// 2, 53
|
// 2, 53
|
||||||
optional WidgetParts widgetParts = 29;
|
optional WidgetParts widgetParts = 29;
|
||||||
|
|
||||||
@ -228,7 +228,7 @@ message WorkoutType {
|
|||||||
optional uint32 unknown2 = 2; // 1
|
optional uint32 unknown2 = 2; // 1
|
||||||
}
|
}
|
||||||
|
|
||||||
message WidgetsScreens {
|
message WidgetScreens {
|
||||||
repeated WidgetScreen widgetScreen = 1;
|
repeated WidgetScreen widgetScreen = 1;
|
||||||
optional uint32 isFullList = 2; // 1 to overwrite the full list
|
optional uint32 isFullList = 2; // 1 to overwrite the full list
|
||||||
optional WidgetsCapabilities widgetsCapabilities = 3; // only in response
|
optional WidgetsCapabilities widgetsCapabilities = 3; // only in response
|
||||||
|
254
app/src/main/res/layout/activity_widget_screen_details.xml
Normal file
254
app/src/main/res/layout/activity_widget_screen_details.xml
Normal 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>
|
27
app/src/main/res/layout/activity_widget_screens_list.xml
Normal file
27
app/src/main/res/layout/activity_widget_screens_list.xml
Normal 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>
|
52
app/src/main/res/layout/item_widget_screen.xml
Normal file
52
app/src/main/res/layout/item_widget_screen.xml
Normal 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>
|
@ -1280,6 +1280,9 @@
|
|||||||
<string name="activity_type_iceskating">Ice Skating</string>
|
<string name="activity_type_iceskating">Ice Skating</string>
|
||||||
<string name="activity_type_golf">Golfing</string>
|
<string name="activity_type_golf">Golfing</string>
|
||||||
<string name="activity_type_other">Other</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_type_unknown">Unknown activity</string>
|
||||||
<string name="activity_summaries">Sport Activities</string>
|
<string name="activity_summaries">Sport Activities</string>
|
||||||
<string name="activity_summary_detail">Sport Activity Detail</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_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_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="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>
|
</resources>
|
||||||
|
7
app/src/main/res/xml/devicesettings_widgets.xml
Normal file
7
app/src/main/res/xml/devicesettings_widgets.xml
Normal 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>
|
Loading…
Reference in New Issue
Block a user