mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge
synced 2024-08-25 08:40:43 +02:00
Xiaomi: Manage widgets
This commit is contained in:
parent
108307c711
commit
6de7af62e3
@ -593,6 +593,10 @@
|
||||
android:name=".activities.ConfigureContacts"
|
||||
android:label="@string/title_activity_set_contacts"
|
||||
android:parentActivityName=".activities.ControlCenterv2" />
|
||||
<activity
|
||||
android:name=".activities.widgets.WidgetScreensListActivity"
|
||||
android:label="@string/menuitem_widgets"
|
||||
android:parentActivityName=".activities.ControlCenterv2" />
|
||||
<activity
|
||||
android:name=".activities.ConfigureWorldClocks"
|
||||
android:label="@string/pref_world_clocks_title"
|
||||
@ -618,6 +622,12 @@
|
||||
android:parentActivityName=".activities.ConfigureReminders"
|
||||
android:screenOrientation="portrait"
|
||||
android:windowSoftInputMode="adjustResize" />
|
||||
<activity
|
||||
android:name=".activities.widgets.WidgetScreenDetailsActivity"
|
||||
android:label="@string/widget_screen"
|
||||
android:parentActivityName=".activities.widgets.WidgetScreensListActivity"
|
||||
android:screenOrientation="portrait"
|
||||
android:windowSoftInputMode="adjustResize" />
|
||||
<activity
|
||||
android:name=".activities.WorldClockDetails"
|
||||
android:label="@string/title_activity_world_clock_details"
|
||||
|
@ -243,6 +243,7 @@ public class DeviceSettingsPreferenceConst {
|
||||
|
||||
public static final String PREF_WORLD_CLOCKS = "pref_world_clocks";
|
||||
public static final String PREF_CONTACTS = "pref_contacts";
|
||||
public static final String PREF_WIDGETS = "pref_widgets";
|
||||
|
||||
public static final String PREF_ANTILOST_ENABLED = "pref_antilost_enabled";
|
||||
public static final String PREF_HYDRATION_SWITCH = "pref_hydration_switch";
|
||||
|
@ -46,6 +46,7 @@ import nodomain.freeyourgadget.gadgetbridge.activities.AbstractPreferenceFragmen
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.app_specific_notifications.AppSpecificNotificationSettingsActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.loyaltycards.LoyaltyCardsSettingsActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.loyaltycards.LoyaltyCardsSettingsConst;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.widgets.WidgetScreensListActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.capabilities.HeartRateCapability;
|
||||
import nodomain.freeyourgadget.gadgetbridge.capabilities.password.PasswordCapabilityImpl;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
|
||||
@ -57,7 +58,6 @@ import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
||||
|
||||
import static nodomain.freeyourgadget.gadgetbridge.GBApplication.getContext;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.*;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_CONTROL_CENTER_SORTABLE;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_DEVICE_ACTION_FELL_SLEEP_BROADCAST;
|
||||
@ -774,6 +774,16 @@ public class DeviceSpecificSettingsFragment extends AbstractPreferenceFragment i
|
||||
});
|
||||
}
|
||||
|
||||
final Preference widgets = findPreference(PREF_WIDGETS);
|
||||
if (widgets != null) {
|
||||
widgets.setOnPreferenceClickListener(preference -> {
|
||||
final Intent intent = new Intent(getContext(), WidgetScreensListActivity.class);
|
||||
intent.putExtra(GBDevice.EXTRA_DEVICE, device);
|
||||
startActivity(intent);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
final Preference calendarBlacklist = findPreference("blacklist_calendars");
|
||||
if (calendarBlacklist != null) {
|
||||
calendarBlacklist.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
|
||||
|
@ -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;
|
||||
|
||||
public abstract class AbstractBLEDeviceCoordinator extends AbstractDeviceCoordinator{
|
||||
public abstract class AbstractBLEDeviceCoordinator extends AbstractDeviceCoordinator {
|
||||
@Override
|
||||
public ConnectionType getConnectionType() {
|
||||
return ConnectionType.BLE;
|
||||
|
@ -51,6 +51,7 @@ import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsCustomizer;
|
||||
import nodomain.freeyourgadget.gadgetbridge.capabilities.HeartRateCapability;
|
||||
import nodomain.freeyourgadget.gadgetbridge.capabilities.password.PasswordCapabilityImpl;
|
||||
import nodomain.freeyourgadget.gadgetbridge.capabilities.widgets.WidgetManager;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst;
|
||||
@ -586,6 +587,17 @@ public abstract class AbstractDeviceCoordinator implements DeviceCoordinator {
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsWidgets(final GBDevice device) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public WidgetManager getWidgetManager(final GBDevice device) {
|
||||
return null;
|
||||
}
|
||||
|
||||
public boolean supportsNavigation() {
|
||||
return false;
|
||||
}
|
||||
|
@ -38,6 +38,7 @@ import nodomain.freeyourgadget.gadgetbridge.GBException;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsCustomizer;
|
||||
import nodomain.freeyourgadget.gadgetbridge.capabilities.HeartRateCapability;
|
||||
import nodomain.freeyourgadget.gadgetbridge.capabilities.password.PasswordCapabilityImpl;
|
||||
import nodomain.freeyourgadget.gadgetbridge.capabilities.widgets.WidgetManager;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
|
||||
@ -550,6 +551,17 @@ public interface DeviceCoordinator {
|
||||
|
||||
List<HeartRateCapability.MeasurementInterval> getHeartRateMeasurementIntervals();
|
||||
|
||||
/**
|
||||
* Whether the device supports screens with configurable widgets.
|
||||
*/
|
||||
boolean supportsWidgets(GBDevice device);
|
||||
|
||||
/**
|
||||
* Gets the {@link WidgetManager} for this device. Must not be null if supportsWidgets is true.
|
||||
*/
|
||||
@Nullable
|
||||
WidgetManager getWidgetManager(GBDevice device);
|
||||
|
||||
boolean supportsNavigation();
|
||||
|
||||
int getOrderPriority();
|
||||
|
@ -42,6 +42,7 @@ import nodomain.freeyourgadget.gadgetbridge.activities.appmanager.AppManagerActi
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsCustomizer;
|
||||
import nodomain.freeyourgadget.gadgetbridge.capabilities.HeartRateCapability;
|
||||
import nodomain.freeyourgadget.gadgetbridge.capabilities.password.PasswordCapabilityImpl;
|
||||
import nodomain.freeyourgadget.gadgetbridge.capabilities.widgets.WidgetManager;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractBLEDeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.TimeSampleProvider;
|
||||
@ -371,6 +372,9 @@ public abstract class XiaomiCoordinator extends AbstractBLEDeviceCoordinator {
|
||||
if (supports(device, FEAT_DISPLAY_ITEMS)) {
|
||||
settings.add(R.xml.devicesettings_xiaomi_displayitems);
|
||||
}
|
||||
if (this.supportsWidgets(device)) {
|
||||
settings.add(R.xml.devicesettings_widgets);
|
||||
}
|
||||
if (supports(device, FEAT_PASSWORD)) {
|
||||
settings.add(R.xml.devicesettings_password);
|
||||
}
|
||||
@ -514,6 +518,16 @@ public abstract class XiaomiCoordinator extends AbstractBLEDeviceCoordinator {
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsWidgets(final GBDevice device) {
|
||||
return getPrefs(device).getBoolean(XiaomiPreferences.FEAT_WIDGETS, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public WidgetManager getWidgetManager(final GBDevice device) {
|
||||
return new XiaomiWidgetManager(device);
|
||||
}
|
||||
|
||||
protected static Prefs getPrefs(final GBDevice device) {
|
||||
return new Prefs(GBApplication.getDeviceSpecificSharedPrefs(device.getAddress()));
|
||||
}
|
||||
|
@ -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/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Calendar;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.GregorianCalendar;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.TimeZone;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.xiaomi.XiaomiWorkoutType;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.proto.xiaomi.XiaomiProto;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
||||
@ -32,6 +37,9 @@ public final class XiaomiPreferences {
|
||||
public static final String PREF_REMINDER_SLOTS = "reminder_slots";
|
||||
public static final String PREF_CANNED_MESSAGES_MIN = "canned_messages_min";
|
||||
public static final String PREF_CANNED_MESSAGES_MAX = "canned_messages_max";
|
||||
public static final String PREF_WORKOUT_TYPES = "workout_types";
|
||||
public static final String PREF_WIDGET_SCREENS = "widget_screens"; // FIXME the value is a protobuf hex string
|
||||
public static final String PREF_WIDGET_PARTS = "widget_parts"; // FIXME the value is a protobuf hex string
|
||||
|
||||
public static final String FEAT_WEAR_MODE = "feat_wear_mode";
|
||||
public static final String FEAT_DEVICE_ACTIONS = "feat_device_actions";
|
||||
@ -45,6 +53,7 @@ public final class XiaomiPreferences {
|
||||
public static final String FEAT_VITALITY_SCORE = "feat_vitality_score";
|
||||
public static final String FEAT_SCREEN_ON_ON_NOTIFICATIONS = "feat_screen_on_on_notifications";
|
||||
public static final String FEAT_CAMERA_REMOTE = "feat_camera_remote";
|
||||
public static final String FEAT_WIDGETS = "feat_widgets";
|
||||
|
||||
private XiaomiPreferences() {
|
||||
// util class
|
||||
@ -80,4 +89,22 @@ public final class XiaomiPreferences {
|
||||
final Prefs prefs = new Prefs(GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()));
|
||||
return prefs.getBoolean("keep_activity_data_on_device", false);
|
||||
}
|
||||
|
||||
// FIXME this function should not be here
|
||||
public static List<XiaomiWorkoutType> getWorkoutTypes(final GBDevice gbDevice) {
|
||||
final Prefs prefs = new Prefs(GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()));
|
||||
final List<String> codes = prefs.getList(PREF_WORKOUT_TYPES, Collections.emptyList());
|
||||
final List<XiaomiWorkoutType> ret = new ArrayList<>(codes.size());
|
||||
for (final String code : codes) {
|
||||
final int codeInt = Integer.parseInt(code);
|
||||
final int codeNameStringRes = XiaomiWorkoutType.mapWorkoutName(codeInt);
|
||||
ret.add(new XiaomiWorkoutType(
|
||||
codeInt,
|
||||
codeNameStringRes != -1 ?
|
||||
GBApplication.getContext().getString(codeNameStringRes) :
|
||||
GBApplication.getContext().getString(R.string.widget_unknown_workout, code)
|
||||
));
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
@ -16,6 +16,8 @@
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.services;
|
||||
|
||||
import com.google.protobuf.InvalidProtocolBufferException;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@ -83,10 +85,14 @@ public class XiaomiSystemService extends AbstractXiaomiService implements Xiaomi
|
||||
public static final int CMD_PASSWORD_SET = 21;
|
||||
public static final int CMD_DISPLAY_ITEMS_GET = 29;
|
||||
public static final int CMD_DISPLAY_ITEMS_SET = 30;
|
||||
public static final int CMD_WORKOUT_TYPES_GET = 39;
|
||||
public static final int CMD_MISC_SETTING_SET_FROM_BAND = 42;
|
||||
public static final int CMD_SILENT_MODE_GET = 43;
|
||||
public static final int CMD_SILENT_MODE_SET_FROM_PHONE = 44;
|
||||
public static final int CMD_SILENT_MODE_SET_FROM_WATCH = 45;
|
||||
public static final int CMD_WIDGET_SCREENS_GET = 51;
|
||||
public static final int CMD_WIDGET_SCREENS_SET = 52;
|
||||
public static final int CMD_WIDGET_PARTS_GET = 53;
|
||||
public static final int CMD_DEVICE_STATE_GET = 78;
|
||||
public static final int CMD_DEVICE_STATE = 79;
|
||||
|
||||
@ -111,6 +117,9 @@ public class XiaomiSystemService extends AbstractXiaomiService implements Xiaomi
|
||||
getSupport().sendCommand("get password", COMMAND_TYPE, CMD_PASSWORD_GET);
|
||||
getSupport().sendCommand("get display items", COMMAND_TYPE, CMD_DISPLAY_ITEMS_GET);
|
||||
getSupport().sendCommand("get camera remote", COMMAND_TYPE, CMD_CAMERA_REMOTE_GET);
|
||||
getSupport().sendCommand("get widgets", COMMAND_TYPE, CMD_WIDGET_SCREENS_GET);
|
||||
getSupport().sendCommand("get widget parts", COMMAND_TYPE, CMD_WIDGET_PARTS_GET);
|
||||
getSupport().sendCommand("get workout types", COMMAND_TYPE, CMD_WORKOUT_TYPES_GET);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -159,6 +168,9 @@ public class XiaomiSystemService extends AbstractXiaomiService implements Xiaomi
|
||||
case CMD_DISPLAY_ITEMS_GET:
|
||||
handleDisplayItems(cmd.getSystem().getDisplayItems());
|
||||
return;
|
||||
case CMD_WORKOUT_TYPES_GET:
|
||||
handleWorkoutTypes(cmd.getSystem().getWorkoutTypes());
|
||||
return;
|
||||
case CMD_MISC_SETTING_SET_FROM_BAND:
|
||||
handleMiscSettingSet(cmd.getSystem().getMiscSettingSet());
|
||||
return;
|
||||
@ -168,6 +180,15 @@ public class XiaomiSystemService extends AbstractXiaomiService implements Xiaomi
|
||||
case CMD_SILENT_MODE_SET_FROM_WATCH:
|
||||
handlePhoneSilentModeSet(cmd.getSystem().getPhoneSilentModeSet());
|
||||
return;
|
||||
case CMD_WIDGET_SCREENS_GET:
|
||||
handleWidgetScreens(cmd.getSystem().getWidgetScreens());
|
||||
return;
|
||||
case CMD_WIDGET_SCREENS_SET:
|
||||
LOG.debug("Got widget screens set ack, status={}", cmd.getStatus());
|
||||
return;
|
||||
case CMD_WIDGET_PARTS_GET:
|
||||
handleWidgetParts(cmd.getSystem().getWidgetParts());
|
||||
return;
|
||||
case CMD_DEVICE_STATE_GET:
|
||||
handleBasicDeviceState(cmd.getSystem().hasBasicDeviceState()
|
||||
? cmd.getSystem().getBasicDeviceState()
|
||||
@ -205,6 +226,9 @@ public class XiaomiSystemService extends AbstractXiaomiService implements Xiaomi
|
||||
case HuamiConst.PREF_DISPLAY_ITEMS_SORTABLE:
|
||||
setDisplayItems();
|
||||
return true;
|
||||
case DeviceSettingsPreferenceConst.PREF_WIDGETS:
|
||||
setWidgets();
|
||||
return true;
|
||||
}
|
||||
|
||||
return super.onSendConfiguration(config, prefs);
|
||||
@ -592,6 +616,20 @@ public class XiaomiSystemService extends AbstractXiaomiService implements Xiaomi
|
||||
getSupport().evaluateGBDeviceEvent(eventUpdatePreferences);
|
||||
}
|
||||
|
||||
private void handleWorkoutTypes(final XiaomiProto.WorkoutTypes workoutTypes) {
|
||||
LOG.debug("Got {} workout types", workoutTypes.getWorkoutTypeCount());
|
||||
|
||||
final List<String> codes = new ArrayList<>(workoutTypes.getWorkoutTypeCount());
|
||||
for (final XiaomiProto.WorkoutType workoutType : workoutTypes.getWorkoutTypeList()) {
|
||||
codes.add(String.valueOf(workoutType.getType()));
|
||||
}
|
||||
|
||||
final GBDeviceEventUpdatePreferences eventUpdatePreferences = new GBDeviceEventUpdatePreferences()
|
||||
.withPreference(XiaomiPreferences.PREF_WORKOUT_TYPES, String.join(",", codes));
|
||||
|
||||
getSupport().evaluateGBDeviceEvent(eventUpdatePreferences);
|
||||
}
|
||||
|
||||
private void handleWearingState(int newStateValue) {
|
||||
WearingState newState;
|
||||
|
||||
@ -775,6 +813,52 @@ public class XiaomiSystemService extends AbstractXiaomiService implements Xiaomi
|
||||
getSupport().sendCommand("request battery state", COMMAND_TYPE, CMD_BATTERY);
|
||||
}
|
||||
|
||||
private void handleWidgetScreens(final XiaomiProto.WidgetScreens widgetScreens) {
|
||||
LOG.debug("Got {} widget screens", widgetScreens.getWidgetScreenCount());
|
||||
final GBDeviceEventUpdatePreferences eventUpdatePreferences = new GBDeviceEventUpdatePreferences()
|
||||
// FIXME we're just persisting the protobuf bytes - probably not a good idea
|
||||
.withPreference(XiaomiPreferences.PREF_WIDGET_SCREENS, GB.hexdump(widgetScreens.toByteArray()));
|
||||
|
||||
getSupport().evaluateGBDeviceEvent(eventUpdatePreferences);
|
||||
}
|
||||
|
||||
private void handleWidgetParts(final XiaomiProto.WidgetParts widgetParts) {
|
||||
LOG.debug("Got {} widget parts", widgetParts.getWidgetPartCount());
|
||||
final GBDeviceEventUpdatePreferences eventUpdatePreferences = new GBDeviceEventUpdatePreferences()
|
||||
.withPreference(XiaomiPreferences.FEAT_WIDGETS, widgetParts.getWidgetPartCount() > 0)
|
||||
// FIXME we're just persisting the protobuf bytes - probably not a good idea
|
||||
.withPreference(XiaomiPreferences.PREF_WIDGET_PARTS, GB.hexdump(widgetParts.toByteArray()));
|
||||
|
||||
getSupport().evaluateGBDeviceEvent(eventUpdatePreferences);
|
||||
}
|
||||
|
||||
private void setWidgets() {
|
||||
final String hex = getDevicePrefs().getString(XiaomiPreferences.PREF_WIDGET_SCREENS, null);
|
||||
if (hex == null) {
|
||||
LOG.warn("raw widget screens hex is null");
|
||||
return;
|
||||
}
|
||||
|
||||
final XiaomiProto.WidgetScreens widgetScreens;
|
||||
try {
|
||||
widgetScreens = XiaomiProto.WidgetScreens.parseFrom(GB.hexStringToByteArray(hex));
|
||||
} catch (final InvalidProtocolBufferException e) {
|
||||
LOG.warn("failed to parse raw widget screns hex");
|
||||
return;
|
||||
}
|
||||
|
||||
LOG.debug("Setting {} widget screens", widgetScreens.getWidgetScreenCount());
|
||||
|
||||
getSupport().sendCommand(
|
||||
"set widgets",
|
||||
XiaomiProto.Command.newBuilder()
|
||||
.setType(COMMAND_TYPE)
|
||||
.setSubtype(CMD_WIDGET_SCREENS_SET)
|
||||
.setSystem(XiaomiProto.System.newBuilder().setWidgetScreens(widgetScreens))
|
||||
.build()
|
||||
);
|
||||
}
|
||||
|
||||
public void onFindPhone(final boolean start) {
|
||||
LOG.debug("Find phone: {}", start);
|
||||
|
||||
|
@ -111,7 +111,7 @@ message System {
|
||||
optional Language language = 20;
|
||||
|
||||
// 2, 51 get | 2, 52 create
|
||||
optional WidgetsScreens widgetScreens = 28;
|
||||
optional WidgetScreens widgetScreens = 28;
|
||||
// 2, 53
|
||||
optional WidgetParts widgetParts = 29;
|
||||
|
||||
@ -228,7 +228,7 @@ message WorkoutType {
|
||||
optional uint32 unknown2 = 2; // 1
|
||||
}
|
||||
|
||||
message WidgetsScreens {
|
||||
message WidgetScreens {
|
||||
repeated WidgetScreen widgetScreen = 1;
|
||||
optional uint32 isFullList = 2; // 1 to overwrite the full list
|
||||
optional WidgetsCapabilities widgetsCapabilities = 3; // only in response
|
||||
|
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_golf">Golfing</string>
|
||||
<string name="activity_type_other">Other</string>
|
||||
<string name="activity_type_trekking">Trekking</string>
|
||||
<string name="activity_type_trail_run">Trail run</string>
|
||||
<string name="activity_type_wrestling">Wrestling</string>
|
||||
<string name="activity_type_unknown">Unknown activity</string>
|
||||
<string name="activity_summaries">Sport Activities</string>
|
||||
<string name="activity_summary_detail">Sport Activity Detail</string>
|
||||
@ -2457,4 +2460,22 @@
|
||||
<string name="pref_vitality_score_7_day_summary">Get a notification when your vitality score reaches 30, 60 or 100 in the past 7 days</string>
|
||||
<string name="pref_vitality_score_daily_title">Daily progress</string>
|
||||
<string name="pref_vitality_score_daily_summary">Get a notification when you reached the maximum number of vitality points for the day</string>
|
||||
<string name="widget">Widget</string>
|
||||
<string name="widget_screen">Widget Screen</string>
|
||||
<string name="widget_screen_delete_confirm_title">Delete widget screen</string>
|
||||
<string name="widget_screen_delete_confirm_description">Are you sure you want to delete \'%1$s\'?</string>
|
||||
<string name="widget_screen_no_free_slots_description">The device has no free slots for widget screens (total slots: %1$s)</string>
|
||||
<string name="widget_screen_min_screens">There must be a minimum of %1$s screens</string>
|
||||
<string name="widget_layout_top_1_bot_2">1 top, 2 bottom</string>
|
||||
<string name="widget_layout_top_2_bot_1">2 top, 1 bottom</string>
|
||||
<string name="widget_layout_top_2_bot_2">2 top, 2 bottom</string>
|
||||
<string name="widget_layout">Widget layout</string>
|
||||
<string name="widget_subtype">Widget Subtype</string>
|
||||
<string name="widget_screen_x">Screen %s</string>
|
||||
<string name="widget_layout_single">1 widget</string>
|
||||
<string name="widget_layout_two">2 widgets</string>
|
||||
<string name="widget_move_up">Move up</string>
|
||||
<string name="widget_move_down">Move down</string>
|
||||
<string name="widget_missing_parts">Please select all widgets</string>
|
||||
<string name="widget_unknown_workout">Unknown workout - %s</string>
|
||||
</resources>
|
||||
|
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