mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge
synced 2024-09-01 03:55:47 +02:00
Dashboard: Add new widgets, make them clickable
Add 3 new widget types: - Body energy - Stress (simple, segmented, breakdown) - HRV Make widgets clickable, opening the corresponding charts page.
This commit is contained in:
parent
d4df00ccbf
commit
f76180c4bd
@ -123,7 +123,7 @@ public class GBApplication extends Application {
|
|||||||
private static SharedPreferences sharedPrefs;
|
private static SharedPreferences sharedPrefs;
|
||||||
private static final String PREFS_VERSION = "shared_preferences_version";
|
private static final String PREFS_VERSION = "shared_preferences_version";
|
||||||
//if preferences have to be migrated, increment the following and add the migration logic in migratePrefs below; see http://stackoverflow.com/questions/16397848/how-can-i-migrate-android-preferences-with-a-new-version
|
//if preferences have to be migrated, increment the following and add the migration logic in migratePrefs below; see http://stackoverflow.com/questions/16397848/how-can-i-migrate-android-preferences-with-a-new-version
|
||||||
private static final int CURRENT_PREFS_VERSION = 36;
|
private static final int CURRENT_PREFS_VERSION = 37;
|
||||||
|
|
||||||
private static final LimitedQueue<Integer, String> mIDSenderLookup = new LimitedQueue<>(16);
|
private static final LimitedQueue<Integer, String> mIDSenderLookup = new LimitedQueue<>(16);
|
||||||
private static GBPrefs prefs;
|
private static GBPrefs prefs;
|
||||||
@ -1718,6 +1718,13 @@ public class GBApplication extends Application {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (oldVersion < 37) {
|
||||||
|
// Add new dashboard widgets
|
||||||
|
final String dashboardWidgetsOrder = sharedPrefs.getString("pref_dashboard_widgets_order", null);
|
||||||
|
if (!StringUtils.isBlank(dashboardWidgetsOrder) && !dashboardWidgetsOrder.contains("bodyenergy")) {
|
||||||
|
editor.putString("pref_dashboard_widgets_order", dashboardWidgetsOrder + ",bodyenergy,stress_segmented,hrv");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
editor.putString(PREFS_VERSION, Integer.toString(CURRENT_PREFS_VERSION));
|
editor.putString(PREFS_VERSION, Integer.toString(CURRENT_PREFS_VERSION));
|
||||||
editor.apply();
|
editor.apply();
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
/* Copyright (C) 2023-2024 Arjan Schrijver
|
/* Copyright (C) 2023-2024 Arjan Schrijver, José Rebelo
|
||||||
|
|
||||||
This file is part of Gadgetbridge.
|
This file is part of Gadgetbridge.
|
||||||
|
|
||||||
@ -16,6 +16,7 @@
|
|||||||
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.activities;
|
package nodomain.freeyourgadget.gadgetbridge.activities;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
import android.content.BroadcastReceiver;
|
import android.content.BroadcastReceiver;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
@ -30,8 +31,12 @@ import android.view.View;
|
|||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import androidx.activity.result.ActivityResult;
|
||||||
|
import androidx.activity.result.ActivityResultCallback;
|
||||||
|
import androidx.activity.result.ActivityResultLauncher;
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.core.view.MenuProvider;
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
import androidx.fragment.app.FragmentContainerView;
|
import androidx.fragment.app.FragmentContainerView;
|
||||||
import androidx.gridlayout.widget.GridLayout;
|
import androidx.gridlayout.widget.GridLayout;
|
||||||
@ -44,23 +49,31 @@ import org.slf4j.LoggerFactory;
|
|||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Calendar;
|
import java.util.Calendar;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.GregorianCalendar;
|
import java.util.GregorianCalendar;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.activities.dashboard.AbstractDashboardWidget;
|
import nodomain.freeyourgadget.gadgetbridge.activities.dashboard.AbstractDashboardWidget;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.activities.dashboard.DashboardActiveTimeWidget;
|
import nodomain.freeyourgadget.gadgetbridge.activities.dashboard.DashboardActiveTimeWidget;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.activities.dashboard.DashboardBodyEnergyWidget;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.activities.dashboard.DashboardCalendarActivity;
|
import nodomain.freeyourgadget.gadgetbridge.activities.dashboard.DashboardCalendarActivity;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.activities.dashboard.DashboardDistanceWidget;
|
import nodomain.freeyourgadget.gadgetbridge.activities.dashboard.DashboardDistanceWidget;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.activities.dashboard.DashboardGoalsWidget;
|
import nodomain.freeyourgadget.gadgetbridge.activities.dashboard.DashboardGoalsWidget;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.activities.dashboard.DashboardHrvWidget;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.activities.dashboard.DashboardSleepWidget;
|
import nodomain.freeyourgadget.gadgetbridge.activities.dashboard.DashboardSleepWidget;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.activities.dashboard.DashboardStepsWidget;
|
import nodomain.freeyourgadget.gadgetbridge.activities.dashboard.DashboardStepsWidget;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.activities.dashboard.DashboardStressBreakdownWidget;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.activities.dashboard.DashboardStressSegmentedWidget;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.activities.dashboard.DashboardStressSimpleWidget;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.activities.dashboard.DashboardTodayWidget;
|
import nodomain.freeyourgadget.gadgetbridge.activities.dashboard.DashboardTodayWidget;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
|
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
|
||||||
@ -68,23 +81,28 @@ import nodomain.freeyourgadget.gadgetbridge.util.DashboardUtils;
|
|||||||
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
|
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
||||||
|
|
||||||
public class DashboardFragment extends Fragment {
|
public class DashboardFragment extends Fragment implements MenuProvider {
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(DashboardFragment.class);
|
private static final Logger LOG = LoggerFactory.getLogger(DashboardFragment.class);
|
||||||
|
|
||||||
private Calendar day = GregorianCalendar.getInstance();
|
private final Calendar day = GregorianCalendar.getInstance();
|
||||||
private TextView textViewDate;
|
private TextView textViewDate;
|
||||||
private TextView arrowLeft;
|
|
||||||
private TextView arrowRight;
|
private TextView arrowRight;
|
||||||
private GridLayout gridLayout;
|
private GridLayout gridLayout;
|
||||||
private DashboardTodayWidget todayWidget;
|
private final Map<String, AbstractDashboardWidget> widgetMap = new HashMap<>();
|
||||||
private DashboardGoalsWidget goalsWidget;
|
|
||||||
private DashboardStepsWidget stepsWidget;
|
|
||||||
private DashboardDistanceWidget distanceWidget;
|
|
||||||
private DashboardActiveTimeWidget activeTimeWidget;
|
|
||||||
private DashboardSleepWidget sleepWidget;
|
|
||||||
private DashboardData dashboardData = new DashboardData();
|
private DashboardData dashboardData = new DashboardData();
|
||||||
private boolean isConfigChanged = false;
|
private boolean isConfigChanged = false;
|
||||||
|
|
||||||
|
private ActivityResultLauncher<Intent> calendarLauncher;
|
||||||
|
private final ActivityResultCallback<ActivityResult> calendarCallback = result -> {
|
||||||
|
if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) {
|
||||||
|
long timeMillis = result.getData().getLongExtra(DashboardCalendarActivity.EXTRA_TIMESTAMP, 0);
|
||||||
|
if (timeMillis != 0) {
|
||||||
|
day.setTimeInMillis(timeMillis);
|
||||||
|
fullRefresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
public static final String ACTION_CONFIG_CHANGE = "nodomain.freeyourgadget.gadgetbridge.activities.dashboardfragment.action.config_change";
|
public static final String ACTION_CONFIG_CHANGE = "nodomain.freeyourgadget.gadgetbridge.activities.dashboardfragment.action.config_change";
|
||||||
|
|
||||||
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
|
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
|
||||||
@ -95,7 +113,7 @@ public class DashboardFragment extends Fragment {
|
|||||||
switch (action) {
|
switch (action) {
|
||||||
case GBApplication.ACTION_NEW_DATA:
|
case GBApplication.ACTION_NEW_DATA:
|
||||||
final GBDevice dev = intent.getParcelableExtra(GBDevice.EXTRA_DEVICE);
|
final GBDevice dev = intent.getParcelableExtra(GBDevice.EXTRA_DEVICE);
|
||||||
if (dev != null && !dev.isBusy()) {
|
if (dev != null) {
|
||||||
if (dashboardData.showAllDevices || dashboardData.showDeviceList.contains(dev.getAddress())) {
|
if (dashboardData.showAllDevices || dashboardData.showDeviceList.contains(dev.getAddress())) {
|
||||||
refresh();
|
refresh();
|
||||||
}
|
}
|
||||||
@ -109,20 +127,25 @@ public class DashboardFragment extends Fragment {
|
|||||||
};
|
};
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||||
super.onCreateView(inflater, container, savedInstanceState);
|
super.onCreateView(inflater, container, savedInstanceState);
|
||||||
View dashboardView = inflater.inflate(R.layout.fragment_dashboard, container, false);
|
View dashboardView = inflater.inflate(R.layout.fragment_dashboard, container, false);
|
||||||
setHasOptionsMenu(true);
|
requireActivity().addMenuProvider(this);
|
||||||
textViewDate = dashboardView.findViewById(R.id.dashboard_date);
|
textViewDate = dashboardView.findViewById(R.id.dashboard_date);
|
||||||
gridLayout = dashboardView.findViewById(R.id.dashboard_gridlayout);
|
gridLayout = dashboardView.findViewById(R.id.dashboard_gridlayout);
|
||||||
|
|
||||||
|
calendarLauncher = registerForActivityResult(
|
||||||
|
new ActivityResultContracts.StartActivityForResult(),
|
||||||
|
calendarCallback
|
||||||
|
);
|
||||||
|
|
||||||
// Increase column count on landscape, tablets and open foldables
|
// Increase column count on landscape, tablets and open foldables
|
||||||
DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
|
DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
|
||||||
if (displayMetrics.widthPixels / displayMetrics.density >= 600) {
|
if (displayMetrics.widthPixels / displayMetrics.density >= 600) {
|
||||||
gridLayout.setColumnCount(4);
|
gridLayout.setColumnCount(4);
|
||||||
}
|
}
|
||||||
|
|
||||||
arrowLeft = dashboardView.findViewById(R.id.arrow_left);
|
final TextView arrowLeft = dashboardView.findViewById(R.id.arrow_left);
|
||||||
arrowLeft.setOnClickListener(v -> {
|
arrowLeft.setOnClickListener(v -> {
|
||||||
day.add(Calendar.DAY_OF_MONTH, -1);
|
day.add(Calendar.DAY_OF_MONTH, -1);
|
||||||
refresh();
|
refresh();
|
||||||
@ -155,7 +178,7 @@ public class DashboardFragment extends Fragment {
|
|||||||
if (isConfigChanged) {
|
if (isConfigChanged) {
|
||||||
isConfigChanged = false;
|
isConfigChanged = false;
|
||||||
fullRefresh();
|
fullRefresh();
|
||||||
} else if (dashboardData.isEmpty() || todayWidget == null) {
|
} else if (dashboardData.isEmpty() || !widgetMap.containsKey("today")) {
|
||||||
refresh();
|
refresh();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -173,43 +196,29 @@ public class DashboardFragment extends Fragment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
|
public void onCreateMenu(@NonNull final Menu menu, @NonNull final MenuInflater inflater) {
|
||||||
super.onCreateOptionsMenu(menu, inflater);
|
|
||||||
inflater.inflate(R.menu.dashboard_menu, menu);
|
inflater.inflate(R.menu.dashboard_menu, menu);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onOptionsItemSelected(@NonNull final MenuItem item) {
|
public boolean onMenuItemSelected(@NonNull final MenuItem item) {
|
||||||
final int itemId = item.getItemId();
|
final int itemId = item.getItemId();
|
||||||
if (itemId == R.id.dashboard_show_calendar) {
|
if (itemId == R.id.dashboard_show_calendar) {
|
||||||
final Intent intent = new Intent(requireActivity(), DashboardCalendarActivity.class);
|
final Intent intent = new Intent(requireActivity(), DashboardCalendarActivity.class);
|
||||||
intent.putExtra(DashboardCalendarActivity.EXTRA_TIMESTAMP, day.getTimeInMillis());
|
intent.putExtra(DashboardCalendarActivity.EXTRA_TIMESTAMP, day.getTimeInMillis());
|
||||||
startActivityForResult(intent, 0);
|
calendarLauncher.launch(intent);
|
||||||
|
return true;
|
||||||
|
} else if (itemId == R.id.dashboard_settings) {
|
||||||
|
final Intent intent = new Intent(requireActivity(), DashboardPreferencesActivity.class);
|
||||||
|
startActivity(intent);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return super.onOptionsItemSelected(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
|
|
||||||
super.onActivityResult(requestCode, resultCode, data);
|
|
||||||
if (requestCode == 0 && resultCode == DashboardCalendarActivity.RESULT_OK && data != null) {
|
|
||||||
long timeMillis = data.getLongExtra(DashboardCalendarActivity.EXTRA_TIMESTAMP, 0);
|
|
||||||
if (timeMillis != 0) {
|
|
||||||
day.setTimeInMillis(timeMillis);
|
|
||||||
fullRefresh();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void fullRefresh() {
|
private void fullRefresh() {
|
||||||
gridLayout.removeAllViews();
|
gridLayout.removeAllViews();
|
||||||
todayWidget = null;
|
widgetMap.clear();
|
||||||
goalsWidget = null;
|
|
||||||
stepsWidget = null;
|
|
||||||
distanceWidget = null;
|
|
||||||
activeTimeWidget = null;
|
|
||||||
sleepWidget = null;
|
|
||||||
refresh();
|
refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -229,13 +238,13 @@ public class DashboardFragment extends Fragment {
|
|||||||
|
|
||||||
private void draw() {
|
private void draw() {
|
||||||
Prefs prefs = GBApplication.getPrefs();
|
Prefs prefs = GBApplication.getPrefs();
|
||||||
String defaultWidgetsOrder = String.join(",", getResources().getStringArray(R.array.pref_dashboard_widgets_order_values));
|
String defaultWidgetsOrder = String.join(",", getResources().getStringArray(R.array.pref_dashboard_widgets_order_default));
|
||||||
String widgetsOrderPref = prefs.getString("pref_dashboard_widgets_order", defaultWidgetsOrder);
|
String widgetsOrderPref = prefs.getString("pref_dashboard_widgets_order", defaultWidgetsOrder);
|
||||||
List<String> widgetsOrder = Arrays.asList(widgetsOrderPref.split(","));
|
String[] widgetsOrder = widgetsOrderPref.split(",");
|
||||||
|
|
||||||
Calendar today = GregorianCalendar.getInstance();
|
Calendar today = GregorianCalendar.getInstance();
|
||||||
if (DateTimeUtils.isSameDay(today, day)) {
|
if (DateTimeUtils.isSameDay(today, day)) {
|
||||||
textViewDate.setText(getContext().getString(R.string.activity_summary_today));
|
textViewDate.setText(requireContext().getString(R.string.activity_summary_today));
|
||||||
arrowRight.setAlpha(0.5f);
|
arrowRight.setAlpha(0.5f);
|
||||||
} else {
|
} else {
|
||||||
textViewDate.setText(DateTimeUtils.formatDate(day.getTime()));
|
textViewDate.setText(DateTimeUtils.formatDate(day.getTime()));
|
||||||
@ -245,55 +254,55 @@ public class DashboardFragment extends Fragment {
|
|||||||
boolean cardsEnabled = prefs.getBoolean("dashboard_cards_enabled", true);
|
boolean cardsEnabled = prefs.getBoolean("dashboard_cards_enabled", true);
|
||||||
|
|
||||||
for (String widgetName : widgetsOrder) {
|
for (String widgetName : widgetsOrder) {
|
||||||
|
AbstractDashboardWidget widget = widgetMap.get(widgetName);
|
||||||
|
if (widget == null) {
|
||||||
|
int columnSpan = 1;
|
||||||
switch (widgetName) {
|
switch (widgetName) {
|
||||||
case "today":
|
case "today":
|
||||||
if (todayWidget == null) {
|
widget = DashboardTodayWidget.newInstance(dashboardData);
|
||||||
todayWidget = DashboardTodayWidget.newInstance(dashboardData);
|
columnSpan = prefs.getBoolean("dashboard_widget_today_2columns", true) ? 2 : 1;
|
||||||
createWidget(todayWidget, cardsEnabled, prefs.getBoolean("dashboard_widget_today_2columns", true) ? 2 : 1);
|
|
||||||
} else {
|
|
||||||
todayWidget.update();
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
case "goals":
|
case "goals":
|
||||||
if (goalsWidget == null) {
|
widget = DashboardGoalsWidget.newInstance(dashboardData);
|
||||||
goalsWidget = DashboardGoalsWidget.newInstance(dashboardData);
|
columnSpan = prefs.getBoolean("dashboard_widget_goals_2columns", true) ? 2 : 1;
|
||||||
createWidget(goalsWidget, cardsEnabled, prefs.getBoolean("dashboard_widget_goals_2columns", true) ? 2 : 1);
|
|
||||||
} else {
|
|
||||||
goalsWidget.update();
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
case "steps":
|
case "steps":
|
||||||
if (stepsWidget == null) {
|
widget = DashboardStepsWidget.newInstance(dashboardData);
|
||||||
stepsWidget = DashboardStepsWidget.newInstance(dashboardData);
|
|
||||||
createWidget(stepsWidget, cardsEnabled, 1);
|
|
||||||
} else {
|
|
||||||
stepsWidget.update();
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
case "distance":
|
case "distance":
|
||||||
if (distanceWidget == null) {
|
widget = DashboardDistanceWidget.newInstance(dashboardData);
|
||||||
distanceWidget = DashboardDistanceWidget.newInstance(dashboardData);
|
|
||||||
createWidget(distanceWidget, cardsEnabled, 1);
|
|
||||||
} else {
|
|
||||||
distanceWidget.update();
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
case "activetime":
|
case "activetime":
|
||||||
if (activeTimeWidget == null) {
|
widget = DashboardActiveTimeWidget.newInstance(dashboardData);
|
||||||
activeTimeWidget = DashboardActiveTimeWidget.newInstance(dashboardData);
|
|
||||||
createWidget(activeTimeWidget, cardsEnabled, 1);
|
|
||||||
} else {
|
|
||||||
activeTimeWidget.update();
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
case "sleep":
|
case "sleep":
|
||||||
if (sleepWidget == null) {
|
widget = DashboardSleepWidget.newInstance(dashboardData);
|
||||||
sleepWidget = DashboardSleepWidget.newInstance(dashboardData);
|
|
||||||
createWidget(sleepWidget, cardsEnabled, 1);
|
|
||||||
} else {
|
|
||||||
sleepWidget.update();
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
|
case "stress_simple":
|
||||||
|
widget = DashboardStressSimpleWidget.newInstance(dashboardData);
|
||||||
|
break;
|
||||||
|
case "stress_segmented":
|
||||||
|
widget = DashboardStressSegmentedWidget.newInstance(dashboardData);
|
||||||
|
break;
|
||||||
|
case "stress_breakdown":
|
||||||
|
widget = DashboardStressBreakdownWidget.newInstance(dashboardData);
|
||||||
|
break;
|
||||||
|
case "bodyenergy":
|
||||||
|
widget = DashboardBodyEnergyWidget.newInstance(dashboardData);
|
||||||
|
break;
|
||||||
|
case "hrv":
|
||||||
|
widget = DashboardHrvWidget.newInstance(dashboardData);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
LOG.error("Unknown dashboard widget {}", widgetName);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
createWidget(widget, cardsEnabled, columnSpan);
|
||||||
|
|
||||||
|
widgetMap.put(widgetName, widget);
|
||||||
|
} else {
|
||||||
|
widget.update();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -309,8 +318,8 @@ public class DashboardFragment extends Fragment {
|
|||||||
.commit();
|
.commit();
|
||||||
|
|
||||||
GridLayout.LayoutParams layoutParams = new GridLayout.LayoutParams(
|
GridLayout.LayoutParams layoutParams = new GridLayout.LayoutParams(
|
||||||
GridLayout.spec(GridLayout.UNDEFINED, GridLayout.FILL,1f),
|
GridLayout.spec(GridLayout.UNDEFINED, GridLayout.FILL, 1f),
|
||||||
GridLayout.spec(GridLayout.UNDEFINED, columnSpan, GridLayout.FILL,1f)
|
GridLayout.spec(GridLayout.UNDEFINED, columnSpan, GridLayout.FILL, 1f)
|
||||||
);
|
);
|
||||||
layoutParams.width = 0;
|
layoutParams.width = 0;
|
||||||
int pixels_8dp = (int) (8 * scale + 0.5f);
|
int pixels_8dp = (int) (8 * scale + 0.5f);
|
||||||
@ -352,6 +361,7 @@ public class DashboardFragment extends Fragment {
|
|||||||
private float distanceGoalFactor;
|
private float distanceGoalFactor;
|
||||||
private long activeMinutesTotal;
|
private long activeMinutesTotal;
|
||||||
private float activeMinutesGoalFactor;
|
private float activeMinutesGoalFactor;
|
||||||
|
private final Map<String, Serializable> genericData = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
public void clear() {
|
public void clear() {
|
||||||
stepsTotal = 0;
|
stepsTotal = 0;
|
||||||
@ -363,6 +373,7 @@ public class DashboardFragment extends Fragment {
|
|||||||
activeMinutesTotal = 0;
|
activeMinutesTotal = 0;
|
||||||
activeMinutesGoalFactor = 0;
|
activeMinutesGoalFactor = 0;
|
||||||
generalizedActivities.clear();
|
generalizedActivities.clear();
|
||||||
|
genericData.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isEmpty() {
|
public boolean isEmpty() {
|
||||||
@ -374,6 +385,7 @@ public class DashboardFragment extends Fragment {
|
|||||||
distanceGoalFactor == 0 &&
|
distanceGoalFactor == 0 &&
|
||||||
activeMinutesTotal == 0 &&
|
activeMinutesTotal == 0 &&
|
||||||
activeMinutesGoalFactor == 0 &&
|
activeMinutesGoalFactor == 0 &&
|
||||||
|
genericData.isEmpty() &&
|
||||||
generalizedActivities.isEmpty());
|
generalizedActivities.isEmpty());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -425,6 +437,21 @@ public class DashboardFragment extends Fragment {
|
|||||||
return sleepGoalFactor;
|
return sleepGoalFactor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void put(final String key, final Serializable value) {
|
||||||
|
genericData.put(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Serializable get(final String key) {
|
||||||
|
return genericData.get(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @noinspection UnusedReturnValue
|
||||||
|
*/
|
||||||
|
public Serializable computeIfAbsent(final String key, final Supplier<Serializable> supplier) {
|
||||||
|
return genericData.computeIfAbsent(key, absent -> supplier.get());
|
||||||
|
}
|
||||||
|
|
||||||
public static class GeneralizedActivity implements Serializable {
|
public static class GeneralizedActivity implements Serializable {
|
||||||
public ActivityKind activityKind;
|
public ActivityKind activityKind;
|
||||||
public long timeFrom;
|
public long timeFrom;
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||||
package nodomain.freeyourgadget.gadgetbridge.activities.charts;
|
package nodomain.freeyourgadget.gadgetbridge.activities.charts;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
import android.app.DatePickerDialog;
|
import android.app.DatePickerDialog;
|
||||||
import android.content.BroadcastReceiver;
|
import android.content.BroadcastReceiver;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
@ -29,18 +30,27 @@ import android.widget.Button;
|
|||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import androidx.activity.result.ActivityResult;
|
||||||
|
import androidx.activity.result.ActivityResultCallback;
|
||||||
|
import androidx.activity.result.ActivityResultLauncher;
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.appcompat.app.ActionBar;
|
||||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
||||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
|
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
|
||||||
import androidx.viewpager.widget.ViewPager;
|
import androidx.viewpager.widget.ViewPager;
|
||||||
|
|
||||||
|
import com.google.android.material.tabs.TabLayout;
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
import java.util.Calendar;
|
import java.util.Calendar;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||||
@ -56,8 +66,10 @@ public abstract class AbstractChartsActivity extends AbstractGBFragmentActivity
|
|||||||
public static final String STATE_START_DATE = "stateStartDate";
|
public static final String STATE_START_DATE = "stateStartDate";
|
||||||
public static final String STATE_END_DATE = "stateEndDate";
|
public static final String STATE_END_DATE = "stateEndDate";
|
||||||
|
|
||||||
public static final String EXTRA_FRAGMENT_ID = "fragment";
|
public static final String EXTRA_FRAGMENT_ID = "fragmentId";
|
||||||
public static final int REQUEST_CODE_PREFERENCES = 1;
|
public static final String EXTRA_SINGLE_FRAGMENT_NAME = "singleFragmentName";
|
||||||
|
public static final String EXTRA_ACTIONBAR_TITLE = "actionbarTitle";
|
||||||
|
public static final String EXTRA_TIMESTAMP = "timestamp";
|
||||||
|
|
||||||
private TextView mDateControl;
|
private TextView mDateControl;
|
||||||
|
|
||||||
@ -70,13 +82,19 @@ public abstract class AbstractChartsActivity extends AbstractGBFragmentActivity
|
|||||||
private GBDevice mGBDevice;
|
private GBDevice mGBDevice;
|
||||||
private ViewGroup dateBar;
|
private ViewGroup dateBar;
|
||||||
|
|
||||||
|
private ActivityResultLauncher<Intent> chartsPreferencesLauncher;
|
||||||
|
private final ActivityResultCallback<ActivityResult> chartsPreferencesCallback = result -> {
|
||||||
|
recreate();
|
||||||
|
};
|
||||||
|
|
||||||
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
|
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
|
||||||
@Override
|
@Override
|
||||||
public void onReceive(Context context, Intent intent) {
|
public void onReceive(final Context context, final Intent intent) {
|
||||||
String action = intent.getAction();
|
final String action = intent.getAction();
|
||||||
|
//noinspection SwitchStatementWithTooFewBranches
|
||||||
switch (Objects.requireNonNull(action)) {
|
switch (Objects.requireNonNull(action)) {
|
||||||
case GBDevice.ACTION_DEVICE_CHANGED:
|
case GBDevice.ACTION_DEVICE_CHANGED:
|
||||||
GBDevice dev = intent.getParcelableExtra(GBDevice.EXTRA_DEVICE);
|
final GBDevice dev = intent.getParcelableExtra(GBDevice.EXTRA_DEVICE);
|
||||||
if (dev != null) {
|
if (dev != null) {
|
||||||
refreshBusyState(dev);
|
refreshBusyState(dev);
|
||||||
}
|
}
|
||||||
@ -85,11 +103,11 @@ public abstract class AbstractChartsActivity extends AbstractGBFragmentActivity
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private void refreshBusyState(GBDevice dev) {
|
private void refreshBusyState(final GBDevice dev) {
|
||||||
if (dev.isBusy()) {
|
if (dev.isBusy()) {
|
||||||
swipeLayout.setRefreshing(true);
|
swipeLayout.setRefreshing(true);
|
||||||
} else {
|
} else {
|
||||||
boolean wasBusy = swipeLayout.isRefreshing();
|
final boolean wasBusy = swipeLayout.isRefreshing();
|
||||||
swipeLayout.setRefreshing(false);
|
swipeLayout.setRefreshing(false);
|
||||||
if (wasBusy) {
|
if (wasBusy) {
|
||||||
LocalBroadcastManager.getInstance(this).sendBroadcast(new Intent(REFRESH));
|
LocalBroadcastManager.getInstance(this).sendBroadcast(new Intent(REFRESH));
|
||||||
@ -99,31 +117,51 @@ public abstract class AbstractChartsActivity extends AbstractGBFragmentActivity
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(final Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
setContentView(R.layout.activity_charts);
|
setContentView(R.layout.activity_charts);
|
||||||
int tabFragmentToOpen = -1;
|
|
||||||
|
|
||||||
|
final Bundle extras = getIntent().getExtras();
|
||||||
|
if (extras == null) {
|
||||||
|
throw new IllegalArgumentException("Must provide a device when invoking this activity");
|
||||||
|
}
|
||||||
|
|
||||||
|
mGBDevice = extras.getParcelable(GBDevice.EXTRA_DEVICE);
|
||||||
|
|
||||||
|
chartsPreferencesLauncher = registerForActivityResult(
|
||||||
|
new ActivityResultContracts.StartActivityForResult(),
|
||||||
|
chartsPreferencesCallback
|
||||||
|
);
|
||||||
|
|
||||||
|
// Set start and end date
|
||||||
if (savedInstanceState != null) {
|
if (savedInstanceState != null) {
|
||||||
setEndDate(new Date(savedInstanceState.getLong(STATE_END_DATE, System.currentTimeMillis())));
|
setEndDate(new Date(savedInstanceState.getLong(STATE_END_DATE, System.currentTimeMillis())));
|
||||||
setStartDate(new Date(savedInstanceState.getLong(STATE_START_DATE, DateTimeUtils.shiftByDays(getEndDate(), -1).getTime())));
|
} else if (extras.containsKey(EXTRA_TIMESTAMP)) {
|
||||||
|
final int endTimestamp = extras.getInt(EXTRA_TIMESTAMP, 0);
|
||||||
|
setEndDate(new Date(endTimestamp * 1000L));
|
||||||
} else {
|
} else {
|
||||||
setEndDate(new Date());
|
setEndDate(new Date());
|
||||||
setStartDate(DateTimeUtils.shiftByDays(getEndDate(), -1));
|
|
||||||
}
|
}
|
||||||
|
setStartDate(DateTimeUtils.shiftByDays(getEndDate(), -1));
|
||||||
|
|
||||||
final IntentFilter filterLocal = new IntentFilter();
|
final IntentFilter filterLocal = new IntentFilter();
|
||||||
filterLocal.addAction(GBDevice.ACTION_DEVICE_CHANGED);
|
filterLocal.addAction(GBDevice.ACTION_DEVICE_CHANGED);
|
||||||
LocalBroadcastManager.getInstance(this).registerReceiver(mReceiver, filterLocal);
|
LocalBroadcastManager.getInstance(this).registerReceiver(mReceiver, filterLocal);
|
||||||
|
|
||||||
final Bundle extras = getIntent().getExtras();
|
// Open the specified fragment, if any, and setup single page view if specified
|
||||||
if (extras != null) {
|
final int tabFragmentIdToOpen = extras.getInt(EXTRA_FRAGMENT_ID, -1);
|
||||||
mGBDevice = extras.getParcelable(GBDevice.EXTRA_DEVICE);
|
final String singleFragmentName = extras.getString(EXTRA_SINGLE_FRAGMENT_NAME, null);
|
||||||
tabFragmentToOpen = extras.getInt(EXTRA_FRAGMENT_ID);
|
final int actionbarTitle = extras.getInt(EXTRA_ACTIONBAR_TITLE, 0);
|
||||||
} else {
|
|
||||||
throw new IllegalArgumentException("Must provide a device when invoking this activity");
|
if (tabFragmentIdToOpen >= 0 && singleFragmentName != null) {
|
||||||
|
throw new IllegalArgumentException("Must specify either fragment ID or single fragment name, not both");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (singleFragmentName != null) {
|
||||||
|
enabledTabsList = Collections.singletonList(singleFragmentName);
|
||||||
|
} else {
|
||||||
enabledTabsList = fillChartsTabsList();
|
enabledTabsList = fillChartsTabsList();
|
||||||
|
}
|
||||||
|
|
||||||
swipeLayout = findViewById(R.id.activity_swipe_layout);
|
swipeLayout = findViewById(R.id.activity_swipe_layout);
|
||||||
swipeLayout.setOnRefreshListener(this::fetchRecordedData);
|
swipeLayout.setOnRefreshListener(this::fetchRecordedData);
|
||||||
@ -132,8 +170,23 @@ public abstract class AbstractChartsActivity extends AbstractGBFragmentActivity
|
|||||||
// Set up the ViewPager with the sections adapter.
|
// Set up the ViewPager with the sections adapter.
|
||||||
final NonSwipeableViewPager viewPager = findViewById(R.id.charts_pager);
|
final NonSwipeableViewPager viewPager = findViewById(R.id.charts_pager);
|
||||||
viewPager.setAdapter(getPagerAdapter());
|
viewPager.setAdapter(getPagerAdapter());
|
||||||
if (tabFragmentToOpen > -1) {
|
if (tabFragmentIdToOpen > -1) {
|
||||||
viewPager.setCurrentItem(tabFragmentToOpen); // open the tab as specified in the intent
|
viewPager.setCurrentItem(tabFragmentIdToOpen); // open the tab as specified in the intent
|
||||||
|
}
|
||||||
|
|
||||||
|
viewPager.setAllowSwipe(singleFragmentName == null && GBApplication.getPrefs().getBoolean("charts_allow_swipe", true));
|
||||||
|
|
||||||
|
if (singleFragmentName != null) {
|
||||||
|
final TabLayout tabLayout = findViewById(R.id.charts_pagerTabStrip);
|
||||||
|
tabLayout.setVisibility(TextView.GONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (actionbarTitle != 0) {
|
||||||
|
final ActionBar actionBar = getSupportActionBar();
|
||||||
|
|
||||||
|
if (actionBar != null) {
|
||||||
|
actionBar.setTitle(actionbarTitle);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
|
viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
|
||||||
@ -158,19 +211,19 @@ public abstract class AbstractChartsActivity extends AbstractGBFragmentActivity
|
|||||||
new ShowDurationDialog(detailedDuration, AbstractChartsActivity.this).show();
|
new ShowDurationDialog(detailedDuration, AbstractChartsActivity.this).show();
|
||||||
});
|
});
|
||||||
|
|
||||||
Button mPrevButton = findViewById(R.id.charts_previous_day);
|
final Button mPrevButton = findViewById(R.id.charts_previous_day);
|
||||||
mPrevButton.setOnClickListener(v -> handleButtonClicked(DATE_PREV_DAY));
|
mPrevButton.setOnClickListener(v -> handleButtonClicked(DATE_PREV_DAY));
|
||||||
Button mNextButton = findViewById(R.id.charts_next_day);
|
final Button mNextButton = findViewById(R.id.charts_next_day);
|
||||||
mNextButton.setOnClickListener(v -> handleButtonClicked(DATE_NEXT_DAY));
|
mNextButton.setOnClickListener(v -> handleButtonClicked(DATE_NEXT_DAY));
|
||||||
|
|
||||||
Button mPrevWeekButton = findViewById(R.id.charts_previous_week);
|
final Button mPrevWeekButton = findViewById(R.id.charts_previous_week);
|
||||||
mPrevWeekButton.setOnClickListener(v -> handleButtonClicked(DATE_PREV_WEEK));
|
mPrevWeekButton.setOnClickListener(v -> handleButtonClicked(DATE_PREV_WEEK));
|
||||||
Button mNextWeekButton = findViewById(R.id.charts_next_week);
|
final Button mNextWeekButton = findViewById(R.id.charts_next_week);
|
||||||
mNextWeekButton.setOnClickListener(v -> handleButtonClicked(DATE_NEXT_WEEK));
|
mNextWeekButton.setOnClickListener(v -> handleButtonClicked(DATE_NEXT_WEEK));
|
||||||
|
|
||||||
Button mPrevMonthButton = findViewById(R.id.charts_previous_month);
|
final Button mPrevMonthButton = findViewById(R.id.charts_previous_month);
|
||||||
mPrevMonthButton.setOnClickListener(v -> handleButtonClicked(DATE_PREV_MONTH));
|
mPrevMonthButton.setOnClickListener(v -> handleButtonClicked(DATE_PREV_MONTH));
|
||||||
Button mNextMonthButton = findViewById(R.id.charts_next_month);
|
final Button mNextMonthButton = findViewById(R.id.charts_next_month);
|
||||||
mNextMonthButton.setOnClickListener(v -> handleButtonClicked(DATE_NEXT_MONTH));
|
mNextMonthButton.setOnClickListener(v -> handleButtonClicked(DATE_NEXT_MONTH));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -193,7 +246,7 @@ public abstract class AbstractChartsActivity extends AbstractGBFragmentActivity
|
|||||||
protected abstract List<String> fillChartsTabsList();
|
protected abstract List<String> fillChartsTabsList();
|
||||||
|
|
||||||
private String formatDetailedDuration() {
|
private String formatDetailedDuration() {
|
||||||
final SimpleDateFormat dateFormat = new SimpleDateFormat("dd.MM.yyyy HH:mm");
|
final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.getDefault());
|
||||||
final String dateStringFrom = dateFormat.format(getStartDate());
|
final String dateStringFrom = dateFormat.format(getStartDate());
|
||||||
final String dateStringTo = dateFormat.format(getEndDate());
|
final String dateStringTo = dateFormat.format(getEndDate());
|
||||||
|
|
||||||
@ -262,15 +315,7 @@ public abstract class AbstractChartsActivity extends AbstractGBFragmentActivity
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
public boolean onOptionsItemSelected(final MenuItem item) {
|
||||||
super.onActivityResult(requestCode, resultCode, data);
|
|
||||||
if (requestCode == REQUEST_CODE_PREFERENCES) {
|
|
||||||
this.recreate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onOptionsItemSelected(MenuItem item) {
|
|
||||||
final int itemId = item.getItemId();
|
final int itemId = item.getItemId();
|
||||||
if (itemId == R.id.charts_fetch_activity_data) {
|
if (itemId == R.id.charts_fetch_activity_data) {
|
||||||
fetchRecordedData();
|
fetchRecordedData();
|
||||||
@ -285,8 +330,8 @@ public abstract class AbstractChartsActivity extends AbstractGBFragmentActivity
|
|||||||
LocalBroadcastManager.getInstance(this).sendBroadcast(new Intent(REFRESH));
|
LocalBroadcastManager.getInstance(this).sendBroadcast(new Intent(REFRESH));
|
||||||
}, currentDate.get(Calendar.YEAR), currentDate.get(Calendar.MONTH), currentDate.get(Calendar.DATE)).show();
|
}, currentDate.get(Calendar.YEAR), currentDate.get(Calendar.MONTH), currentDate.get(Calendar.DATE)).show();
|
||||||
} else if (itemId == R.id.prefs_charts_menu) {
|
} else if (itemId == R.id.prefs_charts_menu) {
|
||||||
Intent settingsIntent = new Intent(this, ChartsPreferencesActivity.class);
|
final Intent settingsIntent = new Intent(this, ChartsPreferencesActivity.class);
|
||||||
startActivityForResult(settingsIntent, REQUEST_CODE_PREFERENCES);
|
chartsPreferencesLauncher.launch(settingsIntent);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -294,7 +339,7 @@ public abstract class AbstractChartsActivity extends AbstractGBFragmentActivity
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void enableSwipeRefresh(boolean enable) {
|
public void enableSwipeRefresh(final boolean enable) {
|
||||||
swipeLayout.setEnabled(enable && allowRefresh());
|
swipeLayout.setEnabled(enable && allowRefresh());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,120 @@
|
|||||||
|
/* Copyright (C) 2024 a0z, 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 <https://www.gnu.org/licenses/>. */
|
||||||
|
package nodomain.freeyourgadget.gadgetbridge.activities.charts;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.fragment.app.FragmentManager;
|
||||||
|
import androidx.viewpager2.widget.ViewPager2;
|
||||||
|
|
||||||
|
import com.google.android.material.tabs.TabLayout;
|
||||||
|
import com.google.android.material.tabs.TabLayoutMediator;
|
||||||
|
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBFragment;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.adapter.NestedFragmentAdapter;
|
||||||
|
|
||||||
|
public abstract class AbstractCollectionFragment extends AbstractGBFragment {
|
||||||
|
protected static final String ARG_ALLOW_SWIPE = "allow_swipe";
|
||||||
|
|
||||||
|
protected NestedFragmentAdapter nestedFragmentsAdapter;
|
||||||
|
protected ViewPager2 viewPager;
|
||||||
|
private int last_position = 0;
|
||||||
|
private boolean allowSwipe;
|
||||||
|
|
||||||
|
public abstract NestedFragmentAdapter getNestedFragmentAdapter(final AbstractGBFragment fragment,
|
||||||
|
final FragmentManager childFragmentManager);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(final Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
if (getArguments() != null) {
|
||||||
|
allowSwipe = getArguments().getBoolean(ARG_ALLOW_SWIPE, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onMadeVisibleInActivity() {
|
||||||
|
super.onMadeVisibleInActivity();
|
||||||
|
nestedFragmentsAdapter.updateFragments(last_position);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onMadeInvisibleInActivity() {
|
||||||
|
if (nestedFragmentsAdapter != null) {
|
||||||
|
nestedFragmentsAdapter.updateFragments(-1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
|
||||||
|
@Nullable Bundle savedInstanceState) {
|
||||||
|
View rootView = inflater.inflate(R.layout.fragment_nested_tabs, container, false);
|
||||||
|
nestedFragmentsAdapter = getNestedFragmentAdapter(this, getChildFragmentManager());
|
||||||
|
viewPager = rootView.findViewById(R.id.pager);
|
||||||
|
viewPager.setAdapter(nestedFragmentsAdapter);
|
||||||
|
if (!allowSwipe) {
|
||||||
|
viewPager.setOrientation(ViewPager2.ORIENTATION_VERTICAL);
|
||||||
|
viewPager.setUserInputEnabled(false);
|
||||||
|
}
|
||||||
|
viewPager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {
|
||||||
|
@Override
|
||||||
|
public void onPageSelected(int position) {
|
||||||
|
super.onPageSelected(position);
|
||||||
|
last_position = position;
|
||||||
|
viewPager.post(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
if (isVisibleInActivity()) {
|
||||||
|
nestedFragmentsAdapter.updateFragments(position);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return rootView;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||||
|
TabLayout tabLayout = view.findViewById(R.id.tab_layout);
|
||||||
|
new TabLayoutMediator(tabLayout, viewPager, (tab, position) -> {
|
||||||
|
switch (position) {
|
||||||
|
case 0:
|
||||||
|
tab.setText(getString(R.string.calendar_day));
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
tab.setText(getString(R.string.calendar_week));
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
tab.setText(getString(R.string.calendar_month));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}).attach();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
protected CharSequence getTitle() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -145,7 +145,7 @@ public class ActivityChartsActivity extends AbstractChartsActivity {
|
|||||||
case "activitylist":
|
case "activitylist":
|
||||||
return new ActivityListingChartFragment();
|
return new ActivityListingChartFragment();
|
||||||
case "sleep":
|
case "sleep":
|
||||||
return new SleepCollectionFragment();
|
return SleepCollectionFragment.newInstance(enabledTabsList.size() == 1);
|
||||||
case "hrvstatus":
|
case "hrvstatus":
|
||||||
return new HRVStatusFragment();
|
return new HRVStatusFragment();
|
||||||
case "bodyenergy":
|
case "bodyenergy":
|
||||||
@ -155,7 +155,7 @@ public class ActivityChartsActivity extends AbstractChartsActivity {
|
|||||||
case "pai":
|
case "pai":
|
||||||
return new PaiChartFragment();
|
return new PaiChartFragment();
|
||||||
case "stepsweek":
|
case "stepsweek":
|
||||||
return new StepsCollectionFragment();
|
return StepsCollectionFragment.newInstance(enabledTabsList.size() == 1);
|
||||||
case "speedzones":
|
case "speedzones":
|
||||||
return new SpeedZonesFragment();
|
return new SpeedZonesFragment();
|
||||||
case "livestats":
|
case "livestats":
|
||||||
@ -177,14 +177,6 @@ public class ActivityChartsActivity extends AbstractChartsActivity {
|
|||||||
return enabledTabsList.toArray().length;
|
return enabledTabsList.toArray().length;
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getSleepTitle() {
|
|
||||||
if (GBApplication.getPrefs().getBoolean("charts_range", true)) {
|
|
||||||
return getString(R.string.weeksleepchart_sleep_a_month);
|
|
||||||
} else {
|
|
||||||
return getString(R.string.weeksleepchart_sleep_a_week);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CharSequence getPageTitle(int position) {
|
public CharSequence getPageTitle(int position) {
|
||||||
switch (enabledTabsList.get(position)) {
|
switch (enabledTabsList.get(position)) {
|
||||||
|
@ -25,14 +25,19 @@ import androidx.viewpager.widget.ViewPager;
|
|||||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||||
|
|
||||||
public class NonSwipeableViewPager extends ViewPager {
|
public class NonSwipeableViewPager extends ViewPager {
|
||||||
|
private boolean allowSwipe = true;
|
||||||
|
|
||||||
public NonSwipeableViewPager(final Context context, final AttributeSet attrs) {
|
public NonSwipeableViewPager(final Context context, final AttributeSet attrs) {
|
||||||
super(context, attrs);
|
super(context, attrs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setAllowSwipe(final boolean allowSwipe) {
|
||||||
|
this.allowSwipe = allowSwipe;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onInterceptTouchEvent(final MotionEvent ev) {
|
public boolean onInterceptTouchEvent(final MotionEvent ev) {
|
||||||
if (GBApplication.getPrefs().getBoolean("charts_allow_swipe", true)) {
|
if (allowSwipe) {
|
||||||
return super.onInterceptTouchEvent(ev);
|
return super.onInterceptTouchEvent(ev);
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@ -40,7 +45,7 @@ public class NonSwipeableViewPager extends ViewPager {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onTouchEvent(final MotionEvent ev) {
|
public boolean onTouchEvent(final MotionEvent ev) {
|
||||||
if (GBApplication.getPrefs().getBoolean("charts_allow_swipe", true)) {
|
if (allowSwipe) {
|
||||||
return super.onTouchEvent(ev);
|
return super.onTouchEvent(ev);
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
@ -1,87 +1,44 @@
|
|||||||
|
/* Copyright (C) 2024 a0z, 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 <https://www.gnu.org/licenses/>. */
|
||||||
package nodomain.freeyourgadget.gadgetbridge.activities.charts;
|
package nodomain.freeyourgadget.gadgetbridge.activities.charts;
|
||||||
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.fragment.app.FragmentManager;
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.viewpager2.widget.ViewPager2;
|
|
||||||
|
|
||||||
import com.google.android.material.tabs.TabLayout;
|
|
||||||
import com.google.android.material.tabs.TabLayoutMediator;
|
|
||||||
|
|
||||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
|
||||||
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBFragment;
|
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBFragment;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.adapter.NestedFragmentAdapter;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.adapter.SleepFragmentAdapter;
|
import nodomain.freeyourgadget.gadgetbridge.adapter.SleepFragmentAdapter;
|
||||||
|
|
||||||
public class SleepCollectionFragment extends AbstractGBFragment {
|
public class SleepCollectionFragment extends AbstractCollectionFragment {
|
||||||
protected SleepFragmentAdapter nestedFragmentsAdapter;
|
public SleepCollectionFragment() {
|
||||||
protected ViewPager2 viewPager;
|
|
||||||
private int last_position = 0;
|
|
||||||
|
|
||||||
@Override
|
}
|
||||||
protected void onMadeVisibleInActivity() {
|
|
||||||
super.onMadeVisibleInActivity();
|
public static SleepCollectionFragment newInstance(final boolean allowSwipe) {
|
||||||
nestedFragmentsAdapter.updateFragments(last_position);
|
final SleepCollectionFragment fragment = new SleepCollectionFragment();
|
||||||
|
final Bundle args = new Bundle();
|
||||||
|
args.putBoolean(ARG_ALLOW_SWIPE, allowSwipe);
|
||||||
|
fragment.setArguments(args);
|
||||||
|
return fragment;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onMadeInvisibleInActivity() {
|
public NestedFragmentAdapter getNestedFragmentAdapter(AbstractGBFragment fragment, FragmentManager childFragmentManager) {
|
||||||
if (nestedFragmentsAdapter != null) {
|
return new SleepFragmentAdapter(this, getChildFragmentManager());
|
||||||
nestedFragmentsAdapter.updateFragments(-1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
|
|
||||||
@Nullable Bundle savedInstanceState) {
|
|
||||||
View rootView = inflater.inflate(R.layout.fragment_nested_tabs, container, false);
|
|
||||||
nestedFragmentsAdapter = new SleepFragmentAdapter(this, getChildFragmentManager());
|
|
||||||
viewPager = rootView.findViewById(R.id.pager);
|
|
||||||
viewPager.setAdapter(nestedFragmentsAdapter);
|
|
||||||
viewPager.setOrientation(ViewPager2.ORIENTATION_VERTICAL);
|
|
||||||
viewPager.setUserInputEnabled(false);
|
|
||||||
viewPager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {
|
|
||||||
@Override
|
|
||||||
public void onPageSelected(int position) {
|
|
||||||
super.onPageSelected(position);
|
|
||||||
last_position = position;
|
|
||||||
viewPager.post(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
if (isVisibleInActivity()) {
|
|
||||||
nestedFragmentsAdapter.updateFragments(position);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return rootView;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
|
||||||
TabLayout tabLayout = view.findViewById(R.id.tab_layout);
|
|
||||||
new TabLayoutMediator(tabLayout, viewPager, (tab, position) -> {
|
|
||||||
switch (position) {
|
|
||||||
case 0:
|
|
||||||
tab.setText(getString(R.string.calendar_day));
|
|
||||||
break;
|
|
||||||
case 1:
|
|
||||||
tab.setText(getString(R.string.calendar_week));
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
tab.setText(getString(R.string.calendar_month));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}).attach();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
@Override
|
|
||||||
protected CharSequence getTitle() {
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,87 +1,45 @@
|
|||||||
|
/* Copyright (C) 2024 a0z, 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 <https://www.gnu.org/licenses/>. */
|
||||||
package nodomain.freeyourgadget.gadgetbridge.activities.charts;
|
package nodomain.freeyourgadget.gadgetbridge.activities.charts;
|
||||||
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.fragment.app.FragmentManager;
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.viewpager2.widget.ViewPager2;
|
|
||||||
|
|
||||||
import com.google.android.material.tabs.TabLayout;
|
|
||||||
import com.google.android.material.tabs.TabLayoutMediator;
|
|
||||||
|
|
||||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
|
||||||
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBFragment;
|
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBFragment;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.adapter.NestedFragmentAdapter;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.adapter.StepsFragmentAdapter;
|
import nodomain.freeyourgadget.gadgetbridge.adapter.StepsFragmentAdapter;
|
||||||
|
|
||||||
public class StepsCollectionFragment extends AbstractGBFragment {
|
public class StepsCollectionFragment extends AbstractCollectionFragment {
|
||||||
protected StepsFragmentAdapter nestedFragmentsAdapter;
|
public StepsCollectionFragment() {
|
||||||
protected ViewPager2 viewPager;
|
|
||||||
private int last_position = 0;
|
|
||||||
|
|
||||||
@Override
|
}
|
||||||
protected void onMadeVisibleInActivity() {
|
|
||||||
super.onMadeVisibleInActivity();
|
public static StepsCollectionFragment newInstance(final boolean allowSwipe) {
|
||||||
nestedFragmentsAdapter.updateFragments(last_position);
|
final StepsCollectionFragment fragment = new StepsCollectionFragment();
|
||||||
|
final Bundle args = new Bundle();
|
||||||
|
args.putBoolean(ARG_ALLOW_SWIPE, allowSwipe);
|
||||||
|
fragment.setArguments(args);
|
||||||
|
return fragment;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onMadeInvisibleInActivity() {
|
public NestedFragmentAdapter getNestedFragmentAdapter(AbstractGBFragment fragment, FragmentManager childFragmentManager) {
|
||||||
if (nestedFragmentsAdapter != null) {
|
return new StepsFragmentAdapter(this, getChildFragmentManager());
|
||||||
nestedFragmentsAdapter.updateFragments(-1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
|
|
||||||
@Nullable Bundle savedInstanceState) {
|
|
||||||
View rootView = inflater.inflate(R.layout.fragment_nested_tabs, container, false);
|
|
||||||
nestedFragmentsAdapter = new StepsFragmentAdapter(this, getChildFragmentManager());
|
|
||||||
viewPager = rootView.findViewById(R.id.pager);
|
|
||||||
viewPager.setAdapter(nestedFragmentsAdapter);
|
|
||||||
viewPager.setOrientation(ViewPager2.ORIENTATION_VERTICAL);
|
|
||||||
viewPager.setUserInputEnabled(false);
|
|
||||||
viewPager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {
|
|
||||||
@Override
|
|
||||||
public void onPageSelected(int position) {
|
|
||||||
super.onPageSelected(position);
|
|
||||||
last_position = position;
|
|
||||||
viewPager.post(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
if (isVisibleInActivity()) {
|
|
||||||
nestedFragmentsAdapter.updateFragments(position);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return rootView;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
|
||||||
TabLayout tabLayout = view.findViewById(R.id.tab_layout);
|
|
||||||
new TabLayoutMediator(tabLayout, viewPager, (tab, position) -> {
|
|
||||||
switch (position) {
|
|
||||||
case 0:
|
|
||||||
tab.setText(getString(R.string.calendar_day));
|
|
||||||
break;
|
|
||||||
case 1:
|
|
||||||
tab.setText(getString(R.string.calendar_week));
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
tab.setText(getString(R.string.calendar_month));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}).attach();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
@Override
|
|
||||||
protected CharSequence getTitle() {
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -520,7 +520,7 @@ public class StressChartFragment extends AbstractChartFragment<StressChartFragme
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected enum StressType {
|
public enum StressType {
|
||||||
UNKNOWN(R.string.unknown, R.color.chart_stress_unknown),
|
UNKNOWN(R.string.unknown, R.color.chart_stress_unknown),
|
||||||
RELAXED(R.string.stress_relaxed, R.color.chart_stress_relaxed),
|
RELAXED(R.string.stress_relaxed, R.color.chart_stress_relaxed),
|
||||||
MILD(R.string.stress_mild, R.color.chart_stress_mild),
|
MILD(R.string.stress_mild, R.color.chart_stress_mild),
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
/* Copyright (C) 2023-2024 Arjan Schrijver
|
/* Copyright (C) 2023-2024 Arjan Schrijver, José Rebelo
|
||||||
|
|
||||||
This file is part of Gadgetbridge.
|
This file is part of Gadgetbridge.
|
||||||
|
|
||||||
@ -16,19 +16,31 @@
|
|||||||
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.activities.dashboard;
|
package nodomain.freeyourgadget.gadgetbridge.activities.dashboard;
|
||||||
|
|
||||||
import android.graphics.Bitmap;
|
import android.content.Context;
|
||||||
import android.graphics.Canvas;
|
import android.content.Intent;
|
||||||
import android.graphics.Color;
|
import android.graphics.Color;
|
||||||
import android.graphics.Paint;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
import androidx.annotation.ColorInt;
|
import androidx.annotation.ColorInt;
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
|
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.activities.DashboardFragment;
|
import nodomain.freeyourgadget.gadgetbridge.activities.DashboardFragment;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.activities.charts.ActivityChartsActivity;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||||
|
|
||||||
public abstract class AbstractDashboardWidget extends Fragment {
|
public abstract class AbstractDashboardWidget extends Fragment {
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(AbstractDashboardWidget.class);
|
private static final Logger LOG = LoggerFactory.getLogger(AbstractDashboardWidget.class);
|
||||||
@ -57,37 +69,67 @@ public abstract class AbstractDashboardWidget extends Fragment {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public void update() {
|
public void update() {
|
||||||
fillData();
|
fillData();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected abstract void fillData();
|
protected abstract void fillData();
|
||||||
|
|
||||||
/**
|
protected boolean isSupportedBy(final GBDevice device) {
|
||||||
* @param width Bitmap width in pixels
|
return device.getDeviceCoordinator().supportsActivityTracking();
|
||||||
* @param barWidth Gauge bar width in pixels
|
}
|
||||||
* @param filledColor Color of the filled part of the gauge
|
|
||||||
* @param filledFactor Factor between 0 and 1 that determines the amount of the gauge that should be filled
|
|
||||||
* @return Bitmap containing the gauge
|
|
||||||
*/
|
|
||||||
Bitmap drawGauge(int width, int barWidth, @ColorInt int filledColor, float filledFactor) {
|
|
||||||
int height = width / 2;
|
|
||||||
int barMargin = (int) Math.ceil(barWidth / 2f);
|
|
||||||
|
|
||||||
Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
|
protected List<GBDevice> getSupportedDevices(final DashboardFragment.DashboardData dashboardData) {
|
||||||
Canvas canvas = new Canvas(bitmap);
|
return GBApplication.app().getDeviceManager().getDevices()
|
||||||
Paint paint = new Paint();
|
.stream()
|
||||||
paint.setAntiAlias(true);
|
.filter(dev -> dashboardData.showAllDevices || dashboardData.showDeviceList.contains(dev.getAddress()))
|
||||||
paint.setStyle(Paint.Style.STROKE);
|
.filter(this::isSupportedBy)
|
||||||
paint.setStrokeCap(Paint.Cap.ROUND);
|
.collect(Collectors.toList());
|
||||||
paint.setStrokeWidth(barWidth * 0.75f);
|
}
|
||||||
paint.setColor(color_unknown);
|
|
||||||
canvas.drawArc(barMargin, barMargin, width - barMargin, width - barMargin, 180 + 180 * filledFactor, 180 - 180 * filledFactor, false, paint);
|
|
||||||
paint.setStrokeWidth(barWidth);
|
|
||||||
paint.setColor(filledColor);
|
|
||||||
canvas.drawArc(barMargin, barMargin, width - barMargin, width - barMargin, 180, 180 * filledFactor, false, paint);
|
|
||||||
|
|
||||||
return bitmap;
|
protected void onClickOpenChart(final View view, final String chart, final int label) {
|
||||||
|
view.setOnClickListener(v -> {
|
||||||
|
chooseDevice(dashboardData, device -> {
|
||||||
|
final Intent startIntent;
|
||||||
|
startIntent = new Intent(requireContext(), ActivityChartsActivity.class);
|
||||||
|
startIntent.putExtra(GBDevice.EXTRA_DEVICE, device);
|
||||||
|
startIntent.putExtra(ActivityChartsActivity.EXTRA_SINGLE_FRAGMENT_NAME, chart);
|
||||||
|
startIntent.putExtra(ActivityChartsActivity.EXTRA_ACTIONBAR_TITLE, label);
|
||||||
|
startIntent.putExtra(ActivityChartsActivity.EXTRA_TIMESTAMP, dashboardData.timeTo);
|
||||||
|
requireContext().startActivity(startIntent);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void chooseDevice(final DashboardFragment.DashboardData dashboardData,
|
||||||
|
final Consumer<GBDevice> consumer) {
|
||||||
|
final List<GBDevice> devices = getSupportedDevices(dashboardData);
|
||||||
|
|
||||||
|
if (devices.size() == 1) {
|
||||||
|
consumer.accept(devices.get(0));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (devices.isEmpty()) {
|
||||||
|
GB.toast(GBApplication.getContext(), R.string.no_supported_devices_found, Toast.LENGTH_LONG, GB.WARN);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final String[] deviceNames = devices.stream()
|
||||||
|
.map(GBDevice::getAliasOrName)
|
||||||
|
.toArray(String[]::new);
|
||||||
|
|
||||||
|
final Context activity = getActivity();
|
||||||
|
if (activity == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
new MaterialAlertDialogBuilder(activity)
|
||||||
|
.setCancelable(true)
|
||||||
|
.setTitle(R.string.choose_device)
|
||||||
|
.setItems(deviceNames, (dialog, which) -> consumer.accept(devices.get(which)))
|
||||||
|
.setNegativeButton(android.R.string.cancel, (dialog, which) -> {
|
||||||
|
})
|
||||||
|
.show();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,318 @@
|
|||||||
|
/* Copyright (C) 2023-2024 Arjan Schrijver, 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.dashboard;
|
||||||
|
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.graphics.Paint;
|
||||||
|
import android.graphics.PorterDuff;
|
||||||
|
import android.graphics.PorterDuffXfermode;
|
||||||
|
import android.os.AsyncTask;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.util.TypedValue;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import androidx.annotation.ColorInt;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.annotation.StringRes;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.activities.DashboardFragment;
|
||||||
|
|
||||||
|
public abstract class AbstractGaugeWidget extends AbstractDashboardWidget {
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(AbstractGaugeWidget.class);
|
||||||
|
|
||||||
|
private TextView gaugeValue;
|
||||||
|
private ImageView gaugeBar;
|
||||||
|
|
||||||
|
private final int label;
|
||||||
|
private final String targetActivityTab;
|
||||||
|
|
||||||
|
public AbstractGaugeWidget(@StringRes final int label, @Nullable final String targetActivityTab) {
|
||||||
|
this.label = label;
|
||||||
|
this.targetActivityTab = targetActivityTab;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public View onCreateView(final LayoutInflater inflater, final ViewGroup container, final Bundle savedInstanceState) {
|
||||||
|
final View fragmentView = inflater.inflate(R.layout.dashboard_widget_generic_gauge, container, false);
|
||||||
|
|
||||||
|
if (targetActivityTab != null) {
|
||||||
|
onClickOpenChart(fragmentView, targetActivityTab, label);
|
||||||
|
}
|
||||||
|
|
||||||
|
gaugeValue = fragmentView.findViewById(R.id.gauge_value);
|
||||||
|
gaugeBar = fragmentView.findViewById(R.id.gauge_bar);
|
||||||
|
final TextView gaugeLabel = fragmentView.findViewById(R.id.gauge_label);
|
||||||
|
gaugeLabel.setText(label);
|
||||||
|
|
||||||
|
fillData();
|
||||||
|
|
||||||
|
return fragmentView;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onResume() {
|
||||||
|
super.onResume();
|
||||||
|
if (gaugeValue != null && gaugeBar != null) fillData();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void fillData() {
|
||||||
|
if (gaugeBar == null) return;
|
||||||
|
gaugeBar.post(() -> {
|
||||||
|
final FillDataAsyncTask myAsyncTask = new FillDataAsyncTask();
|
||||||
|
myAsyncTask.execute();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is called from the async task, outside of the UI thread. It's expected that
|
||||||
|
* {@link nodomain.freeyourgadget.gadgetbridge.activities.DashboardFragment.DashboardData} be
|
||||||
|
* populated with the necessary data for display.
|
||||||
|
*
|
||||||
|
* @param dashboardData the DashboardData to populate
|
||||||
|
*/
|
||||||
|
protected abstract void populateData(DashboardFragment.DashboardData dashboardData);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is called from the UI thread.
|
||||||
|
*
|
||||||
|
* @param dashboardData populated DashboardData
|
||||||
|
*/
|
||||||
|
protected abstract void draw(DashboardFragment.DashboardData dashboardData);
|
||||||
|
|
||||||
|
private class FillDataAsyncTask extends AsyncTask<Void, Void, Void> {
|
||||||
|
@Override
|
||||||
|
protected Void doInBackground(final Void... params) {
|
||||||
|
final long nanoStart = System.nanoTime();
|
||||||
|
try {
|
||||||
|
populateData(dashboardData);
|
||||||
|
} catch (final Exception e) {
|
||||||
|
LOG.error("fillData for {} failed", AbstractGaugeWidget.this.getClass().getSimpleName(), e);
|
||||||
|
}
|
||||||
|
final long nanoEnd = System.nanoTime();
|
||||||
|
final long executionTime = (nanoEnd - nanoStart) / 1000000;
|
||||||
|
LOG.debug("fillData for {} took {}ms", AbstractGaugeWidget.this.getClass().getSimpleName(), executionTime);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPostExecute(final Void unused) {
|
||||||
|
super.onPostExecute(unused);
|
||||||
|
try {
|
||||||
|
draw(dashboardData);
|
||||||
|
} catch (final Exception e) {
|
||||||
|
LOG.error("draw for {} failed", AbstractGaugeWidget.this.getClass().getSimpleName(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void setText(final CharSequence text) {
|
||||||
|
gaugeValue.setText(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw a simple gauge.
|
||||||
|
*
|
||||||
|
* @param color the gauge color
|
||||||
|
* @param value the gauge value. Range: [0, 1]
|
||||||
|
*/
|
||||||
|
protected void drawSimpleGauge(final int color,
|
||||||
|
final float value) {
|
||||||
|
|
||||||
|
final int width = (int) TypedValue.applyDimension(
|
||||||
|
TypedValue.COMPLEX_UNIT_DIP,
|
||||||
|
150,
|
||||||
|
GBApplication.getContext().getResources().getDisplayMetrics()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Draw gauge
|
||||||
|
gaugeBar.setImageBitmap(drawSimpleGaugeInternal(
|
||||||
|
width,
|
||||||
|
Math.round(width * 0.075f),
|
||||||
|
color,
|
||||||
|
value
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param width Bitmap width in pixels
|
||||||
|
* @param barWidth Gauge bar width in pixels
|
||||||
|
* @param filledColor Color of the filled part of the gauge
|
||||||
|
* @param filledFactor Factor between 0 and 1 that determines the amount of the gauge that should be filled
|
||||||
|
* @return Bitmap containing the gauge
|
||||||
|
*/
|
||||||
|
private Bitmap drawSimpleGaugeInternal(final int width, final int barWidth, @ColorInt final int filledColor, final float filledFactor) {
|
||||||
|
final int height = width / 2;
|
||||||
|
final int barMargin = (int) Math.ceil(barWidth / 2f);
|
||||||
|
|
||||||
|
final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
|
||||||
|
final Canvas canvas = new Canvas(bitmap);
|
||||||
|
final Paint paint = new Paint();
|
||||||
|
paint.setAntiAlias(true);
|
||||||
|
paint.setStyle(Paint.Style.STROKE);
|
||||||
|
paint.setStrokeCap(Paint.Cap.ROUND);
|
||||||
|
paint.setStrokeWidth(barWidth * 0.75f);
|
||||||
|
paint.setColor(color_unknown);
|
||||||
|
canvas.drawArc(barMargin, barMargin, width - barMargin, width - barMargin, 180 + 180 * filledFactor, 180 - 180 * filledFactor, false, paint);
|
||||||
|
|
||||||
|
if (filledFactor >= 0) {
|
||||||
|
paint.setStrokeWidth(barWidth);
|
||||||
|
paint.setColor(filledColor);
|
||||||
|
canvas.drawArc(barMargin, barMargin, width - barMargin, width - barMargin, 180, 180 * filledFactor, false, paint);
|
||||||
|
}
|
||||||
|
|
||||||
|
return bitmap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws a segmented gauge.
|
||||||
|
*
|
||||||
|
* @param colors the colors of each segment
|
||||||
|
* @param segments the size of each segment. The sum of all segments should be 1
|
||||||
|
* @param value the gauge value, in range [0, 1], or -1 for no value and only segments
|
||||||
|
* @param fadeOutsideDot whether to fade out colors outside the dot value
|
||||||
|
* @param gapBetweenSegments whether to introduce a small gap between the segments
|
||||||
|
*/
|
||||||
|
protected void drawSegmentedGauge(final int[] colors,
|
||||||
|
final float[] segments,
|
||||||
|
final float value,
|
||||||
|
final boolean fadeOutsideDot,
|
||||||
|
final boolean gapBetweenSegments) {
|
||||||
|
if (colors.length != segments.length) {
|
||||||
|
LOG.error("Colors length {} differs from segments length {}", colors.length, segments.length);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final int width = (int) TypedValue.applyDimension(
|
||||||
|
TypedValue.COMPLEX_UNIT_DIP,
|
||||||
|
150,
|
||||||
|
GBApplication.getContext().getResources().getDisplayMetrics()
|
||||||
|
);
|
||||||
|
|
||||||
|
final int barWidth = Math.round(width * 0.075f);
|
||||||
|
|
||||||
|
final int height = width / 2;
|
||||||
|
final int barMargin = (int) Math.ceil(barWidth / 2f);
|
||||||
|
|
||||||
|
final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
|
||||||
|
final Canvas canvas = new Canvas(bitmap);
|
||||||
|
final Paint paint = new Paint();
|
||||||
|
paint.setAntiAlias(true);
|
||||||
|
paint.setStyle(Paint.Style.STROKE);
|
||||||
|
paint.setStrokeCap(Paint.Cap.BUTT);
|
||||||
|
paint.setStrokeWidth(barWidth);
|
||||||
|
|
||||||
|
final double cornersGapRadians = Math.asin((width * 0.055f) / (double) height);
|
||||||
|
final double cornersGapFactor = cornersGapRadians / Math.PI;
|
||||||
|
|
||||||
|
int dotColor = 0;
|
||||||
|
float angleSum = 0;
|
||||||
|
for (int i = 0; i < segments.length; i++) {
|
||||||
|
if (segments[i] == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
paint.setColor(colors[i]);
|
||||||
|
paint.setStrokeWidth(barWidth);
|
||||||
|
|
||||||
|
if (value < 0 || (value >= angleSum && value <= angleSum + segments[i])) {
|
||||||
|
dotColor = colors[i];
|
||||||
|
} else {
|
||||||
|
if (fadeOutsideDot) {
|
||||||
|
paint.setColor(colors[i] - 0xB0000000);
|
||||||
|
} else {
|
||||||
|
paint.setStrokeWidth(barWidth * 0.75f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
float startAngleDegrees = 180 + angleSum * 180;
|
||||||
|
float sweepAngleDegrees = segments[i] * 180;
|
||||||
|
|
||||||
|
if (value >= 0) {
|
||||||
|
// Do not draw to the end if it will be overlapped by the dot
|
||||||
|
if (i == 0 && value <= cornersGapFactor) {
|
||||||
|
startAngleDegrees += (float) Math.toDegrees(cornersGapRadians);
|
||||||
|
sweepAngleDegrees -= (float) Math.toDegrees(cornersGapRadians);
|
||||||
|
} else if (i == segments.length - 1 && value >= 1 - cornersGapFactor) {
|
||||||
|
sweepAngleDegrees -= (float) Math.toDegrees(cornersGapRadians);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gapBetweenSegments) {
|
||||||
|
if (i + 1 < segments.length) {
|
||||||
|
sweepAngleDegrees -= 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas.drawArc(
|
||||||
|
barMargin,
|
||||||
|
barMargin,
|
||||||
|
width - barMargin,
|
||||||
|
width - barMargin,
|
||||||
|
startAngleDegrees,
|
||||||
|
sweepAngleDegrees,
|
||||||
|
false,
|
||||||
|
paint
|
||||||
|
);
|
||||||
|
angleSum += segments[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value >= 0) {
|
||||||
|
// Prevent the dot from going outside the widget in the extremities
|
||||||
|
final float angleRadians = (float) normalize(value, 0, 1, cornersGapRadians, Math.toRadians(180) - cornersGapRadians);
|
||||||
|
|
||||||
|
paint.setColor(Color.TRANSPARENT);
|
||||||
|
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
|
||||||
|
|
||||||
|
// In the corners the circle is slightly offset, so adjust it slightly
|
||||||
|
final float widthAdjustment = width * 0.04f * (float) normalize(Math.abs(value - 0.5d), 0, 0.5d);
|
||||||
|
|
||||||
|
final float x = ((width - (barWidth / 2f) - widthAdjustment) / 2f) * (float) Math.cos(angleRadians);
|
||||||
|
final float y = (height - (barWidth / 2f)) * (float) Math.sin(angleRadians);
|
||||||
|
|
||||||
|
// Draw hole
|
||||||
|
paint.setStyle(Paint.Style.FILL);
|
||||||
|
canvas.drawCircle((width / 2f) - x, height - y, barMargin * 1.6f, paint);
|
||||||
|
|
||||||
|
// Draw dot
|
||||||
|
paint.setColor(dotColor);
|
||||||
|
paint.setXfermode(null);
|
||||||
|
canvas.drawCircle((width / 2f) - x, height - y, barMargin, paint);
|
||||||
|
}
|
||||||
|
|
||||||
|
gaugeBar.setImageBitmap(bitmap);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static double normalize(final double value, final double min, final double max) {
|
||||||
|
return normalize(value, min, max, 0, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static double normalize(final double value, final double minSource, final double maxSource, final double minTarget, final double maxTarget) {
|
||||||
|
return ((value - minSource) * (maxTarget - minTarget)) / (maxSource - minSource) + minTarget;
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
/* Copyright (C) 2023-2024 Arjan Schrijver
|
/* Copyright (C) 2023-2024 Arjan Schrijver, José Rebelo
|
||||||
|
|
||||||
This file is part of Gadgetbridge.
|
This file is part of Gadgetbridge.
|
||||||
|
|
||||||
@ -16,19 +16,10 @@
|
|||||||
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.activities.dashboard;
|
package nodomain.freeyourgadget.gadgetbridge.activities.dashboard;
|
||||||
|
|
||||||
import android.os.AsyncTask;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.util.TypedValue;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.ImageView;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
import java.util.Locale;
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
|
||||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.activities.DashboardFragment;
|
import nodomain.freeyourgadget.gadgetbridge.activities.DashboardFragment;
|
||||||
|
|
||||||
@ -37,13 +28,9 @@ import nodomain.freeyourgadget.gadgetbridge.activities.DashboardFragment;
|
|||||||
* Use the {@link DashboardActiveTimeWidget#newInstance} factory method to
|
* Use the {@link DashboardActiveTimeWidget#newInstance} factory method to
|
||||||
* create an instance of this fragment.
|
* create an instance of this fragment.
|
||||||
*/
|
*/
|
||||||
public class DashboardActiveTimeWidget extends AbstractDashboardWidget {
|
public class DashboardActiveTimeWidget extends AbstractGaugeWidget {
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(DashboardActiveTimeWidget.class);
|
|
||||||
private TextView activeTime;
|
|
||||||
private ImageView activeTimeGauge;
|
|
||||||
|
|
||||||
public DashboardActiveTimeWidget() {
|
public DashboardActiveTimeWidget() {
|
||||||
// Required empty public constructor
|
super(R.string.activity_list_summary_active_time, "activity");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -53,69 +40,35 @@ public class DashboardActiveTimeWidget extends AbstractDashboardWidget {
|
|||||||
* @param dashboardData An instance of DashboardFragment.DashboardData.
|
* @param dashboardData An instance of DashboardFragment.DashboardData.
|
||||||
* @return A new instance of fragment DashboardActiveTimeWidget.
|
* @return A new instance of fragment DashboardActiveTimeWidget.
|
||||||
*/
|
*/
|
||||||
public static DashboardActiveTimeWidget newInstance(DashboardFragment.DashboardData dashboardData) {
|
public static DashboardActiveTimeWidget newInstance(final DashboardFragment.DashboardData dashboardData) {
|
||||||
DashboardActiveTimeWidget fragment = new DashboardActiveTimeWidget();
|
final DashboardActiveTimeWidget fragment = new DashboardActiveTimeWidget();
|
||||||
Bundle args = new Bundle();
|
final Bundle args = new Bundle();
|
||||||
args.putSerializable(ARG_DASHBOARD_DATA, dashboardData);
|
args.putSerializable(ARG_DASHBOARD_DATA, dashboardData);
|
||||||
fragment.setArguments(args);
|
fragment.setArguments(args);
|
||||||
return fragment;
|
return fragment;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
protected void populateData(final DashboardFragment.DashboardData dashboardData) {
|
||||||
View fragmentView = inflater.inflate(R.layout.dashboard_widget_active_time, container, false);
|
|
||||||
activeTime = fragmentView.findViewById(R.id.activetime_text);
|
|
||||||
activeTimeGauge = fragmentView.findViewById(R.id.activetime_gauge);
|
|
||||||
|
|
||||||
fillData();
|
|
||||||
|
|
||||||
return fragmentView;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onResume() {
|
|
||||||
super.onResume();
|
|
||||||
if (activeTime != null && activeTimeGauge != null) fillData();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void fillData() {
|
|
||||||
if (activeTimeGauge == null) return;
|
|
||||||
activeTimeGauge.post(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
FillDataAsyncTask myAsyncTask = new FillDataAsyncTask();
|
|
||||||
myAsyncTask.execute();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private class FillDataAsyncTask extends AsyncTask<Void, Void, Void> {
|
|
||||||
@Override
|
|
||||||
protected Void doInBackground(Void... params) {
|
|
||||||
dashboardData.getActiveMinutesTotal();
|
dashboardData.getActiveMinutesTotal();
|
||||||
dashboardData.getActiveMinutesGoalFactor();
|
dashboardData.getActiveMinutesGoalFactor();
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onPostExecute(Void unused) {
|
protected void draw(final DashboardFragment.DashboardData dashboardData) {
|
||||||
super.onPostExecute(unused);
|
final long totalActiveMinutes = dashboardData.getActiveMinutesTotal();
|
||||||
|
final String valueText = String.format(
|
||||||
// Update text representation
|
Locale.ROOT,
|
||||||
long totalActiveMinutes = dashboardData.getActiveMinutesTotal();
|
"%d:%02d",
|
||||||
String activeHours = String.format("%d", (int) Math.floor(totalActiveMinutes / 60f));
|
(int) Math.floor(totalActiveMinutes / 60f),
|
||||||
String activeMinutes = String.format("%02d", (int) (totalActiveMinutes % 60f));
|
(int) (totalActiveMinutes % 60f)
|
||||||
activeTime.setText(activeHours + ":" + activeMinutes);
|
|
||||||
|
|
||||||
final int width = (int) TypedValue.applyDimension(
|
|
||||||
TypedValue.COMPLEX_UNIT_DIP,
|
|
||||||
150,
|
|
||||||
GBApplication.getContext().getResources().getDisplayMetrics()
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Draw gauge
|
setText(valueText);
|
||||||
activeTimeGauge.setImageBitmap(drawGauge(width, Math.round(width * 0.075f), color_active_time, dashboardData.getActiveMinutesGoalFactor()));
|
|
||||||
}
|
drawSimpleGauge(
|
||||||
|
color_active_time,
|
||||||
|
dashboardData.getActiveMinutesGoalFactor()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -0,0 +1,191 @@
|
|||||||
|
/* Copyright (C) 2024 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.dashboard;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.text.Spannable;
|
||||||
|
import android.text.SpannableString;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
import android.text.format.DateUtils;
|
||||||
|
import android.text.style.RelativeSizeSpan;
|
||||||
|
|
||||||
|
import androidx.core.content.ContextCompat;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.activities.DashboardFragment;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.model.BodyEnergySample;
|
||||||
|
|
||||||
|
public class DashboardBodyEnergyWidget extends AbstractGaugeWidget {
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(DashboardBodyEnergyWidget.class);
|
||||||
|
|
||||||
|
public DashboardBodyEnergyWidget() {
|
||||||
|
super(R.string.body_energy, "bodyenergy");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static DashboardBodyEnergyWidget newInstance(final DashboardFragment.DashboardData dashboardData) {
|
||||||
|
final DashboardBodyEnergyWidget fragment = new DashboardBodyEnergyWidget();
|
||||||
|
final Bundle args = new Bundle();
|
||||||
|
args.putSerializable(ARG_DASHBOARD_DATA, dashboardData);
|
||||||
|
fragment.setArguments(args);
|
||||||
|
return fragment;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean isSupportedBy(final GBDevice device) {
|
||||||
|
return device.getDeviceCoordinator().supportsBodyEnergy();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void populateData(final DashboardFragment.DashboardData dashboardData) {
|
||||||
|
final List<GBDevice> devices = getSupportedDevices(dashboardData);
|
||||||
|
|
||||||
|
final boolean isToday = DateUtils.isToday(dashboardData.timeTo * 1000L);
|
||||||
|
|
||||||
|
final BodyEnergyData data = new BodyEnergyData();
|
||||||
|
data.isToday = isToday;
|
||||||
|
|
||||||
|
if (isToday) {
|
||||||
|
// Latest stress sample for today
|
||||||
|
BodyEnergySample sample = null;
|
||||||
|
|
||||||
|
try (DBHandler dbHandler = GBApplication.acquireDB()) {
|
||||||
|
for (GBDevice dev : devices) {
|
||||||
|
final BodyEnergySample latestSample = dev.getDeviceCoordinator().getBodyEnergySampleProvider(dev, dbHandler.getDaoSession())
|
||||||
|
.getLatestSample();
|
||||||
|
|
||||||
|
if (latestSample != null && (sample == null || latestSample.getTimestamp() > sample.getTimestamp())) {
|
||||||
|
sample = latestSample;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sample != null) {
|
||||||
|
data.value = sample.getEnergy();
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (final Exception e) {
|
||||||
|
LOG.error("Could not get body energy for today", e);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Gain / loss for the period
|
||||||
|
try (DBHandler dbHandler = GBApplication.acquireDB()) {
|
||||||
|
for (GBDevice dev : devices) {
|
||||||
|
if ((dashboardData.showAllDevices || dashboardData.showDeviceList.contains(dev.getAddress())) && dev.getDeviceCoordinator().supportsBodyEnergy()) {
|
||||||
|
final List<? extends BodyEnergySample> samples = dev.getDeviceCoordinator()
|
||||||
|
.getBodyEnergySampleProvider(dev, dbHandler.getDaoSession())
|
||||||
|
.getAllSamples(dashboardData.timeFrom * 1000L, dashboardData.timeTo * 1000L);
|
||||||
|
|
||||||
|
if (samples.size() > 1) {
|
||||||
|
int gained = 0;
|
||||||
|
int lost = 0;
|
||||||
|
for (int i = 1; i < samples.size(); i++) {
|
||||||
|
final BodyEnergySample s1 = samples.get(i - 1);
|
||||||
|
final BodyEnergySample s2 = samples.get(i);
|
||||||
|
if (s2.getEnergy() > s1.getEnergy()) {
|
||||||
|
gained += s2.getEnergy() - s1.getEnergy();
|
||||||
|
} else {
|
||||||
|
lost += s1.getEnergy() - s2.getEnergy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data.gained = gained;
|
||||||
|
data.lost = lost;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (final Exception e) {
|
||||||
|
LOG.error("Could not calculate average stress", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dashboardData.put("bodyenergy", data);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void draw(final DashboardFragment.DashboardData dashboardData) {
|
||||||
|
final BodyEnergyData bodyEnergyData = (BodyEnergyData) dashboardData.get("bodyenergy");
|
||||||
|
if (bodyEnergyData == null) {
|
||||||
|
drawSimpleGauge(0, -1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final int colorEnergy = ContextCompat.getColor(GBApplication.getContext(), R.color.body_energy_level_color);
|
||||||
|
|
||||||
|
if (bodyEnergyData.isToday) {
|
||||||
|
if (bodyEnergyData.value < 0) {
|
||||||
|
drawSimpleGauge(0, -1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setText(String.valueOf(bodyEnergyData.value));
|
||||||
|
drawSimpleGauge(
|
||||||
|
colorEnergy,
|
||||||
|
bodyEnergyData.value / 100f
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
if (bodyEnergyData.gained < 0 || bodyEnergyData.lost < 0) {
|
||||||
|
drawSimpleGauge(0, -1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final int diff = bodyEnergyData.gained - bodyEnergyData.lost;
|
||||||
|
|
||||||
|
final SpannableString spanGain = new SpannableString("↑" + bodyEnergyData.gained);
|
||||||
|
final SpannableString spanLost = new SpannableString("↓" + bodyEnergyData.lost);
|
||||||
|
spanGain.setSpan(new RelativeSizeSpan(0.65f), 0, 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
|
spanLost.setSpan(new RelativeSizeSpan(0.65f), 0, 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
|
|
||||||
|
setText(TextUtils.concat(spanGain, " ", spanLost));
|
||||||
|
drawSimpleGauge(
|
||||||
|
colorEnergy,
|
||||||
|
Math.abs(diff) / 100f
|
||||||
|
);
|
||||||
|
|
||||||
|
final int[] colors = {
|
||||||
|
colorEnergy,
|
||||||
|
ContextCompat.getColor(GBApplication.getContext(), R.color.body_energy_lost_color)
|
||||||
|
};
|
||||||
|
final float[] segments = {
|
||||||
|
bodyEnergyData.gained / (float) (bodyEnergyData.gained + bodyEnergyData.lost),
|
||||||
|
bodyEnergyData.lost / (float) (bodyEnergyData.gained + bodyEnergyData.lost),
|
||||||
|
};
|
||||||
|
|
||||||
|
drawSegmentedGauge(
|
||||||
|
colors,
|
||||||
|
segments,
|
||||||
|
-1,
|
||||||
|
false,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class BodyEnergyData implements Serializable {
|
||||||
|
private int value = -1;
|
||||||
|
private int gained = -1;
|
||||||
|
private int lost = -1;
|
||||||
|
private boolean isToday;
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
/* Copyright (C) 2023-2024 Arjan Schrijver
|
/* Copyright (C) 2023-2024 Arjan Schrijver, José Rebelo
|
||||||
|
|
||||||
This file is part of Gadgetbridge.
|
This file is part of Gadgetbridge.
|
||||||
|
|
||||||
@ -16,21 +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.activities.dashboard;
|
package nodomain.freeyourgadget.gadgetbridge.activities.dashboard;
|
||||||
|
|
||||||
import android.content.res.Resources;
|
|
||||||
import android.os.AsyncTask;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.util.DisplayMetrics;
|
|
||||||
import android.util.TypedValue;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.ImageView;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
|
||||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.activities.DashboardFragment;
|
import nodomain.freeyourgadget.gadgetbridge.activities.DashboardFragment;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.util.FormatUtils;
|
import nodomain.freeyourgadget.gadgetbridge.util.FormatUtils;
|
||||||
@ -40,13 +27,9 @@ import nodomain.freeyourgadget.gadgetbridge.util.FormatUtils;
|
|||||||
* Use the {@link DashboardDistanceWidget#newInstance} factory method to
|
* Use the {@link DashboardDistanceWidget#newInstance} factory method to
|
||||||
* create an instance of this fragment.
|
* create an instance of this fragment.
|
||||||
*/
|
*/
|
||||||
public class DashboardDistanceWidget extends AbstractDashboardWidget {
|
public class DashboardDistanceWidget extends AbstractGaugeWidget {
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(DashboardDistanceWidget.class);
|
|
||||||
private TextView distanceText;
|
|
||||||
private ImageView distanceGauge;
|
|
||||||
|
|
||||||
public DashboardDistanceWidget() {
|
public DashboardDistanceWidget() {
|
||||||
// Required empty public constructor
|
super(R.string.distance, "stepsweek");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -56,67 +39,26 @@ public class DashboardDistanceWidget extends AbstractDashboardWidget {
|
|||||||
* @param dashboardData An instance of DashboardFragment.DashboardData.
|
* @param dashboardData An instance of DashboardFragment.DashboardData.
|
||||||
* @return A new instance of fragment DashboardDistanceWidget.
|
* @return A new instance of fragment DashboardDistanceWidget.
|
||||||
*/
|
*/
|
||||||
public static DashboardDistanceWidget newInstance(DashboardFragment.DashboardData dashboardData) {
|
public static DashboardDistanceWidget newInstance(final DashboardFragment.DashboardData dashboardData) {
|
||||||
DashboardDistanceWidget fragment = new DashboardDistanceWidget();
|
final DashboardDistanceWidget fragment = new DashboardDistanceWidget();
|
||||||
Bundle args = new Bundle();
|
final Bundle args = new Bundle();
|
||||||
args.putSerializable(ARG_DASHBOARD_DATA, dashboardData);
|
args.putSerializable(ARG_DASHBOARD_DATA, dashboardData);
|
||||||
fragment.setArguments(args);
|
fragment.setArguments(args);
|
||||||
return fragment;
|
return fragment;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
protected void populateData(final DashboardFragment.DashboardData dashboardData) {
|
||||||
View fragmentView = inflater.inflate(R.layout.dashboard_widget_distance, container, false);
|
|
||||||
distanceText = fragmentView.findViewById(R.id.distance_text);
|
|
||||||
distanceGauge = fragmentView.findViewById(R.id.distance_gauge);
|
|
||||||
|
|
||||||
fillData();
|
|
||||||
|
|
||||||
return fragmentView;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onResume() {
|
|
||||||
super.onResume();
|
|
||||||
if (distanceText != null && distanceGauge != null) fillData();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void fillData() {
|
|
||||||
if (distanceGauge == null) return;
|
|
||||||
distanceGauge.post(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
FillDataAsyncTask myAsyncTask = new FillDataAsyncTask();
|
|
||||||
myAsyncTask.execute();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private class FillDataAsyncTask extends AsyncTask<Void, Void, Void> {
|
|
||||||
@Override
|
|
||||||
protected Void doInBackground(Void... params) {
|
|
||||||
dashboardData.getDistanceTotal();
|
dashboardData.getDistanceTotal();
|
||||||
dashboardData.getDistanceGoalFactor();
|
dashboardData.getDistanceGoalFactor();
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onPostExecute(Void unused) {
|
protected void draw(final DashboardFragment.DashboardData dashboardData) {
|
||||||
super.onPostExecute(unused);
|
setText(FormatUtils.getFormattedDistanceLabel(dashboardData.getDistanceTotal()));
|
||||||
|
drawSimpleGauge(
|
||||||
// Update text representation
|
color_distance,
|
||||||
String distanceFormatted = FormatUtils.getFormattedDistanceLabel(dashboardData.getDistanceTotal());
|
dashboardData.getDistanceGoalFactor()
|
||||||
distanceText.setText(distanceFormatted);
|
|
||||||
|
|
||||||
final int width = (int) TypedValue.applyDimension(
|
|
||||||
TypedValue.COMPLEX_UNIT_DIP,
|
|
||||||
150,
|
|
||||||
GBApplication.getContext().getResources().getDisplayMetrics()
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Draw gauge
|
|
||||||
distanceGauge.setImageBitmap(drawGauge(width, Math.round(width * 0.075f), color_distance, dashboardData.getDistanceGoalFactor()));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,4 +1,4 @@
|
|||||||
/* Copyright (C) 2023-2024 Arjan Schrijver
|
/* Copyright (C) 2023-2024 Arjan Schrijver, José Rebelo
|
||||||
|
|
||||||
This file is part of Gadgetbridge.
|
This file is part of Gadgetbridge.
|
||||||
|
|
||||||
@ -90,8 +90,6 @@ public class DashboardGoalsWidget extends AbstractDashboardWidget {
|
|||||||
Prefs prefs = GBApplication.getPrefs();
|
Prefs prefs = GBApplication.getPrefs();
|
||||||
legend.setVisibility(prefs.getBoolean("dashboard_widget_goals_legend", true) ? View.VISIBLE : View.GONE);
|
legend.setVisibility(prefs.getBoolean("dashboard_widget_goals_legend", true) ? View.VISIBLE : View.GONE);
|
||||||
|
|
||||||
fillData();
|
|
||||||
|
|
||||||
return goalsView;
|
return goalsView;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -118,6 +116,8 @@ public class DashboardGoalsWidget extends AbstractDashboardWidget {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Void doInBackground(Void... params) {
|
protected Void doInBackground(Void... params) {
|
||||||
|
final long nanoStart = System.nanoTime();
|
||||||
|
|
||||||
int width = Resources.getSystem().getDisplayMetrics().widthPixels;
|
int width = Resources.getSystem().getDisplayMetrics().widthPixels;
|
||||||
int height = width;
|
int height = width;
|
||||||
int barWidth = Math.round(height * 0.04f);
|
int barWidth = Math.round(height * 0.04f);
|
||||||
@ -160,6 +160,11 @@ public class DashboardGoalsWidget extends AbstractDashboardWidget {
|
|||||||
paint.setStrokeWidth(barWidth);
|
paint.setStrokeWidth(barWidth);
|
||||||
paint.setColor(color_light_sleep);
|
paint.setColor(color_light_sleep);
|
||||||
canvas.drawArc(barMargin, barMargin, width - barMargin, height - barMargin, 270, 360 * dashboardData.getSleepMinutesGoalFactor(), false, paint);
|
canvas.drawArc(barMargin, barMargin, width - barMargin, height - barMargin, 270, 360 * dashboardData.getSleepMinutesGoalFactor(), false, paint);
|
||||||
|
|
||||||
|
final long nanoEnd = System.nanoTime();
|
||||||
|
final long executionTime = (nanoEnd - nanoStart) / 1000000;
|
||||||
|
LOG.debug("fillData for {} took {}ms", DashboardGoalsWidget.this.getClass().getSimpleName(), executionTime);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,150 @@
|
|||||||
|
/* Copyright (C) 2024 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.dashboard;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
|
||||||
|
import androidx.core.content.ContextCompat;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.activities.DashboardFragment;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.model.HrvSummarySample;
|
||||||
|
|
||||||
|
public class DashboardHrvWidget extends AbstractGaugeWidget {
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(DashboardHrvWidget.class);
|
||||||
|
|
||||||
|
public DashboardHrvWidget() {
|
||||||
|
super(R.string.hrv, "hrvstatus");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static DashboardHrvWidget newInstance(final DashboardFragment.DashboardData dashboardData) {
|
||||||
|
final DashboardHrvWidget fragment = new DashboardHrvWidget();
|
||||||
|
final Bundle args = new Bundle();
|
||||||
|
args.putSerializable(ARG_DASHBOARD_DATA, dashboardData);
|
||||||
|
fragment.setArguments(args);
|
||||||
|
return fragment;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean isSupportedBy(final GBDevice device) {
|
||||||
|
return device.getDeviceCoordinator().supportsHrvMeasurement();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void populateData(final DashboardFragment.DashboardData dashboardData) {
|
||||||
|
final List<GBDevice> devices = getSupportedDevices(dashboardData);
|
||||||
|
|
||||||
|
HrvSummarySample latestSummary = null;
|
||||||
|
|
||||||
|
try (DBHandler dbHandler = GBApplication.acquireDB()) {
|
||||||
|
for (GBDevice dev : devices) {
|
||||||
|
final List<? extends HrvSummarySample> deviceLatestSummaries = dev.getDeviceCoordinator().getHrvSummarySampleProvider(dev, dbHandler.getDaoSession())
|
||||||
|
.getAllSamples(dashboardData.timeFrom * 1000L, dashboardData.timeTo * 1000L);
|
||||||
|
|
||||||
|
if (!deviceLatestSummaries.isEmpty() && (latestSummary == null || latestSummary.getTimestamp() < deviceLatestSummaries.get(deviceLatestSummaries.size() - 1).getTimestamp())) {
|
||||||
|
latestSummary = deviceLatestSummaries.get(deviceLatestSummaries.size() - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final HrvData hrvData = new HrvData();
|
||||||
|
|
||||||
|
if (latestSummary != null) {
|
||||||
|
hrvData.weeklyAverage = latestSummary.getWeeklyAverage() != null ? latestSummary.getWeeklyAverage() : 0;
|
||||||
|
hrvData.lastNightAverage = latestSummary.getLastNightAverage() != null ? latestSummary.getLastNightAverage() : 0;
|
||||||
|
hrvData.lastNight5MinHigh = latestSummary.getLastNight5MinHigh() != null ? latestSummary.getLastNight5MinHigh() : 0;
|
||||||
|
hrvData.baselineLowUpper = latestSummary.getBaselineLowUpper() != null ? latestSummary.getBaselineLowUpper() : 0;
|
||||||
|
hrvData.baselineBalancedLower = latestSummary.getBaselineBalancedLower() != null ? latestSummary.getBaselineBalancedLower() : 0;
|
||||||
|
hrvData.baselineBalancedUpper = latestSummary.getBaselineBalancedUpper() != null ? latestSummary.getBaselineBalancedUpper() : 0;
|
||||||
|
|
||||||
|
dashboardData.put("hrv", hrvData);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (final Exception e) {
|
||||||
|
LOG.error("Could not get hrv sample", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void draw(final DashboardFragment.DashboardData dashboardData) {
|
||||||
|
final int[] colors = new int[]{
|
||||||
|
ContextCompat.getColor(GBApplication.getContext(), R.color.hrv_status_low),
|
||||||
|
ContextCompat.getColor(GBApplication.getContext(), R.color.hrv_status_unbalanced),
|
||||||
|
ContextCompat.getColor(GBApplication.getContext(), R.color.hrv_status_balanced),
|
||||||
|
ContextCompat.getColor(GBApplication.getContext(), R.color.hrv_status_unbalanced),
|
||||||
|
};
|
||||||
|
|
||||||
|
final float[] segments = new float[]{
|
||||||
|
0.125f, // low
|
||||||
|
0.125f, // unbalanced
|
||||||
|
0.5f, // normal
|
||||||
|
0.25f, // unbalanced
|
||||||
|
};
|
||||||
|
|
||||||
|
final HrvData hrvData = (HrvData) dashboardData.get("hrv");
|
||||||
|
|
||||||
|
final float value;
|
||||||
|
final String valueText;
|
||||||
|
if (hrvData != null && hrvData.weeklyAverage != 0 && hrvData.hasBaselines()) {
|
||||||
|
valueText = getString(R.string.hrv_status_unit, hrvData.weeklyAverage);
|
||||||
|
|
||||||
|
if (hrvData.weeklyAverage < hrvData.baselineLowUpper) {
|
||||||
|
value = 0.125f * (float) normalize(hrvData.weeklyAverage, 0f, hrvData.baselineLowUpper);
|
||||||
|
} else if (hrvData.weeklyAverage < hrvData.baselineBalancedLower) {
|
||||||
|
value = 0.125f + 0.125f * (float) normalize((float) hrvData.weeklyAverage, hrvData.baselineLowUpper, hrvData.baselineBalancedLower);
|
||||||
|
} else if (hrvData.weeklyAverage < hrvData.baselineBalancedUpper) {
|
||||||
|
value = 0.125f + 0.125f + 0.5f * (float) normalize((float) hrvData.weeklyAverage, hrvData.baselineBalancedLower, hrvData.baselineBalancedUpper);
|
||||||
|
} else {
|
||||||
|
value = 0.125f + 0.125f + 0.5f + 0.125f * (float) normalize((float) hrvData.weeklyAverage, hrvData.baselineBalancedUpper, 2 * hrvData.baselineBalancedUpper);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
value = -1;
|
||||||
|
valueText = getString(R.string.stats_empty_value);
|
||||||
|
}
|
||||||
|
|
||||||
|
setText(valueText);
|
||||||
|
drawSegmentedGauge(
|
||||||
|
colors,
|
||||||
|
segments,
|
||||||
|
value,
|
||||||
|
false,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class HrvData implements Serializable {
|
||||||
|
private int weeklyAverage;
|
||||||
|
private int lastNightAverage;
|
||||||
|
private int lastNight5MinHigh;
|
||||||
|
private int baselineLowUpper;
|
||||||
|
private int baselineBalancedLower;
|
||||||
|
private int baselineBalancedUpper;
|
||||||
|
private int statusNum;
|
||||||
|
|
||||||
|
public boolean hasBaselines() {
|
||||||
|
return baselineLowUpper != 0 && baselineBalancedLower != 0 && baselineBalancedUpper != 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
/* Copyright (C) 2023-2024 Arjan Schrijver
|
/* Copyright (C) 2023-2024 Arjan Schrijver, José Rebelo
|
||||||
|
|
||||||
This file is part of Gadgetbridge.
|
This file is part of Gadgetbridge.
|
||||||
|
|
||||||
@ -16,34 +16,22 @@
|
|||||||
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.activities.dashboard;
|
package nodomain.freeyourgadget.gadgetbridge.activities.dashboard;
|
||||||
|
|
||||||
import android.os.AsyncTask;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.util.TypedValue;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.ImageView;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
import java.util.Locale;
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
|
||||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.activities.DashboardFragment;
|
import nodomain.freeyourgadget.gadgetbridge.activities.DashboardFragment;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A simple {@link AbstractDashboardWidget} subclass.
|
* A simple {@link AbstractDashboardWidget} subclass.
|
||||||
* Use the {@link DashboardSleepWidget#newInstance} factory method to
|
* Use the {@link DashboardSleepWidget#newInstance} factory method to
|
||||||
* create an instance of this fragment.
|
* create an instance of this fragment.
|
||||||
*/
|
*/
|
||||||
public class DashboardSleepWidget extends AbstractDashboardWidget {
|
public class DashboardSleepWidget extends AbstractGaugeWidget {
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(DashboardSleepWidget.class);
|
|
||||||
private TextView sleepAmount;
|
|
||||||
private ImageView sleepGauge;
|
|
||||||
|
|
||||||
public DashboardSleepWidget() {
|
public DashboardSleepWidget() {
|
||||||
// Required empty public constructor
|
super(R.string.menuitem_sleep, "sleep");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -53,69 +41,39 @@ public class DashboardSleepWidget extends AbstractDashboardWidget {
|
|||||||
* @param dashboardData An instance of DashboardFragment.DashboardData.
|
* @param dashboardData An instance of DashboardFragment.DashboardData.
|
||||||
* @return A new instance of fragment DashboardSleepWidget.
|
* @return A new instance of fragment DashboardSleepWidget.
|
||||||
*/
|
*/
|
||||||
public static DashboardSleepWidget newInstance(DashboardFragment.DashboardData dashboardData) {
|
public static DashboardSleepWidget newInstance(final DashboardFragment.DashboardData dashboardData) {
|
||||||
DashboardSleepWidget fragment = new DashboardSleepWidget();
|
final DashboardSleepWidget fragment = new DashboardSleepWidget();
|
||||||
Bundle args = new Bundle();
|
final Bundle args = new Bundle();
|
||||||
args.putSerializable(ARG_DASHBOARD_DATA, dashboardData);
|
args.putSerializable(ARG_DASHBOARD_DATA, dashboardData);
|
||||||
fragment.setArguments(args);
|
fragment.setArguments(args);
|
||||||
return fragment;
|
return fragment;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
protected boolean isSupportedBy(final GBDevice device) {
|
||||||
View fragmentView = inflater.inflate(R.layout.dashboard_widget_sleep, container, false);
|
return device.getDeviceCoordinator().supportsSleepMeasurement();
|
||||||
sleepAmount = fragmentView.findViewById(R.id.sleep_text);
|
|
||||||
sleepGauge = fragmentView.findViewById(R.id.sleep_gauge);
|
|
||||||
|
|
||||||
fillData();
|
|
||||||
|
|
||||||
return fragmentView;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onResume() {
|
protected void populateData(final DashboardFragment.DashboardData dashboardData) {
|
||||||
super.onResume();
|
|
||||||
if (sleepAmount != null && sleepGauge != null) fillData();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void fillData() {
|
|
||||||
if (sleepGauge == null) return;
|
|
||||||
sleepGauge.post(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
FillDataAsyncTask myAsyncTask = new FillDataAsyncTask();
|
|
||||||
myAsyncTask.execute();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private class FillDataAsyncTask extends AsyncTask<Void, Void, Void> {
|
|
||||||
@Override
|
|
||||||
protected Void doInBackground(Void... params) {
|
|
||||||
dashboardData.getSleepMinutesTotal();
|
dashboardData.getSleepMinutesTotal();
|
||||||
dashboardData.getSleepMinutesGoalFactor();
|
dashboardData.getSleepMinutesGoalFactor();
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onPostExecute(Void unused) {
|
protected void draw(final DashboardFragment.DashboardData dashboardData) {
|
||||||
super.onPostExecute(unused);
|
final long totalSleepMinutes = dashboardData.getSleepMinutesTotal();
|
||||||
|
final String valueText = String.format(
|
||||||
// Update text representation
|
Locale.ROOT,
|
||||||
long totalSleepMinutes = dashboardData.getSleepMinutesTotal();
|
"%d:%02d",
|
||||||
String sleepHours = String.format("%d", (int) Math.floor(totalSleepMinutes / 60f));
|
(int) Math.floor(totalSleepMinutes / 60f),
|
||||||
String sleepMinutes = String.format("%02d", (int) (totalSleepMinutes % 60f));
|
(int) (totalSleepMinutes % 60f)
|
||||||
sleepAmount.setText(sleepHours + ":" + sleepMinutes);
|
|
||||||
|
|
||||||
final int width = (int) TypedValue.applyDimension(
|
|
||||||
TypedValue.COMPLEX_UNIT_DIP,
|
|
||||||
150,
|
|
||||||
GBApplication.getContext().getResources().getDisplayMetrics()
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Draw gauge
|
setText(valueText);
|
||||||
sleepGauge.setImageBitmap(drawGauge(width, Math.round(width * 0.075f), color_light_sleep, dashboardData.getSleepMinutesGoalFactor()));
|
drawSimpleGauge(
|
||||||
}
|
color_light_sleep,
|
||||||
|
dashboardData.getSleepMinutesGoalFactor()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,4 +1,4 @@
|
|||||||
/* Copyright (C) 2023-2024 Arjan Schrijver
|
/* Copyright (C) 2023-2024 Arjan Schrijver, José Rebelo
|
||||||
|
|
||||||
This file is part of Gadgetbridge.
|
This file is part of Gadgetbridge.
|
||||||
|
|
||||||
@ -16,19 +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.activities.dashboard;
|
package nodomain.freeyourgadget.gadgetbridge.activities.dashboard;
|
||||||
|
|
||||||
import android.os.AsyncTask;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.util.TypedValue;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.ImageView;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
|
||||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.activities.DashboardFragment;
|
import nodomain.freeyourgadget.gadgetbridge.activities.DashboardFragment;
|
||||||
|
|
||||||
@ -37,13 +26,9 @@ import nodomain.freeyourgadget.gadgetbridge.activities.DashboardFragment;
|
|||||||
* Use the {@link DashboardStepsWidget#newInstance} factory method to
|
* Use the {@link DashboardStepsWidget#newInstance} factory method to
|
||||||
* create an instance of this fragment.
|
* create an instance of this fragment.
|
||||||
*/
|
*/
|
||||||
public class DashboardStepsWidget extends AbstractDashboardWidget {
|
public class DashboardStepsWidget extends AbstractGaugeWidget {
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(DashboardStepsWidget.class);
|
|
||||||
private TextView stepsCount;
|
|
||||||
private ImageView stepsGauge;
|
|
||||||
|
|
||||||
public DashboardStepsWidget() {
|
public DashboardStepsWidget() {
|
||||||
// Required empty public constructor
|
super(R.string.steps, "stepsweek");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -53,64 +38,26 @@ public class DashboardStepsWidget extends AbstractDashboardWidget {
|
|||||||
* @param dashboardData An instance of DashboardFragment.DashboardData.
|
* @param dashboardData An instance of DashboardFragment.DashboardData.
|
||||||
* @return A new instance of fragment DashboardStepsWidget.
|
* @return A new instance of fragment DashboardStepsWidget.
|
||||||
*/
|
*/
|
||||||
public static DashboardStepsWidget newInstance(DashboardFragment.DashboardData dashboardData) {
|
public static DashboardStepsWidget newInstance(final DashboardFragment.DashboardData dashboardData) {
|
||||||
DashboardStepsWidget fragment = new DashboardStepsWidget();
|
final DashboardStepsWidget fragment = new DashboardStepsWidget();
|
||||||
Bundle args = new Bundle();
|
final Bundle args = new Bundle();
|
||||||
args.putSerializable(ARG_DASHBOARD_DATA, dashboardData);
|
args.putSerializable(ARG_DASHBOARD_DATA, dashboardData);
|
||||||
fragment.setArguments(args);
|
fragment.setArguments(args);
|
||||||
return fragment;
|
return fragment;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
protected void populateData(final DashboardFragment.DashboardData dashboardData) {
|
||||||
View fragmentView = inflater.inflate(R.layout.dashboard_widget_steps, container, false);
|
|
||||||
stepsCount = fragmentView.findViewById(R.id.steps_count);
|
|
||||||
stepsGauge = fragmentView.findViewById(R.id.steps_gauge);
|
|
||||||
fillData();
|
|
||||||
return fragmentView;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onResume() {
|
|
||||||
super.onResume();
|
|
||||||
if (stepsCount != null && stepsGauge != null) fillData();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void fillData() {
|
|
||||||
if (stepsGauge == null) return;
|
|
||||||
stepsGauge.post(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
FillDataAsyncTask myAsyncTask = new FillDataAsyncTask();
|
|
||||||
myAsyncTask.execute();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private class FillDataAsyncTask extends AsyncTask<Void, Void, Void> {
|
|
||||||
@Override
|
|
||||||
protected Void doInBackground(Void... params) {
|
|
||||||
dashboardData.getStepsTotal();
|
dashboardData.getStepsTotal();
|
||||||
dashboardData.getStepsGoalFactor();
|
dashboardData.getStepsGoalFactor();
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onPostExecute(Void unused) {
|
protected void draw(final DashboardFragment.DashboardData dashboardData) {
|
||||||
super.onPostExecute(unused);
|
setText(String.valueOf(dashboardData.getStepsTotal()));
|
||||||
|
drawSimpleGauge(
|
||||||
// Update text representation
|
color_activity,
|
||||||
stepsCount.setText(String.valueOf(dashboardData.getStepsTotal()));
|
dashboardData.getStepsGoalFactor()
|
||||||
|
|
||||||
final int width = (int) TypedValue.applyDimension(
|
|
||||||
TypedValue.COMPLEX_UNIT_DIP,
|
|
||||||
150,
|
|
||||||
GBApplication.getContext().getResources().getDisplayMetrics()
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Draw gauge
|
|
||||||
stepsGauge.setImageBitmap(drawGauge(width, Math.round(width * 0.075f), color_activity, dashboardData.getStepsGoalFactor()));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -0,0 +1,89 @@
|
|||||||
|
/* Copyright (C) 2024 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.dashboard;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
|
||||||
|
import androidx.core.content.ContextCompat;
|
||||||
|
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.activities.DashboardFragment;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.activities.dashboard.data.DashboardStressData;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||||
|
|
||||||
|
public class DashboardStressBreakdownWidget extends AbstractGaugeWidget {
|
||||||
|
public DashboardStressBreakdownWidget() {
|
||||||
|
super(R.string.menuitem_stress, "stress");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static DashboardStressBreakdownWidget newInstance(final DashboardFragment.DashboardData dashboardData) {
|
||||||
|
final DashboardStressBreakdownWidget fragment = new DashboardStressBreakdownWidget();
|
||||||
|
final Bundle args = new Bundle();
|
||||||
|
args.putSerializable(ARG_DASHBOARD_DATA, dashboardData);
|
||||||
|
fragment.setArguments(args);
|
||||||
|
return fragment;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean isSupportedBy(final GBDevice device) {
|
||||||
|
return device.getDeviceCoordinator().supportsStressMeasurement();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void populateData(final DashboardFragment.DashboardData dashboardData) {
|
||||||
|
dashboardData.computeIfAbsent("stress", () -> DashboardStressData.compute(dashboardData));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void draw(final DashboardFragment.DashboardData dashboardData) {
|
||||||
|
final DashboardStressData stressData = (DashboardStressData) dashboardData.get("stress");
|
||||||
|
if (stressData == null) {
|
||||||
|
drawSimpleGauge(0, -1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final int[] colors = new int[]{
|
||||||
|
ContextCompat.getColor(GBApplication.getContext(), R.color.chart_stress_relaxed),
|
||||||
|
ContextCompat.getColor(GBApplication.getContext(), R.color.chart_stress_mild),
|
||||||
|
ContextCompat.getColor(GBApplication.getContext(), R.color.chart_stress_moderate),
|
||||||
|
ContextCompat.getColor(GBApplication.getContext(), R.color.chart_stress_high),
|
||||||
|
};
|
||||||
|
|
||||||
|
final float[] segments = new float[4];
|
||||||
|
|
||||||
|
int sum = 0;
|
||||||
|
for (final int stressTime : stressData.totalTime) {
|
||||||
|
sum += stressTime;
|
||||||
|
}
|
||||||
|
if (sum != 0) {
|
||||||
|
for (int i = 0; i < 4; i++) {
|
||||||
|
segments[i] = stressData.totalTime[i] / (float) sum;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setText(String.valueOf(stressData.value));
|
||||||
|
|
||||||
|
drawSegmentedGauge(
|
||||||
|
colors,
|
||||||
|
segments,
|
||||||
|
-1,
|
||||||
|
false,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,96 @@
|
|||||||
|
/* Copyright (C) 2024 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.dashboard;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
|
||||||
|
import androidx.core.content.ContextCompat;
|
||||||
|
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.activities.DashboardFragment;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.activities.dashboard.data.DashboardStressData;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||||
|
|
||||||
|
public class DashboardStressSegmentedWidget extends AbstractGaugeWidget {
|
||||||
|
public DashboardStressSegmentedWidget() {
|
||||||
|
super(R.string.menuitem_stress, "stress");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static DashboardStressSegmentedWidget newInstance(final DashboardFragment.DashboardData dashboardData) {
|
||||||
|
final DashboardStressSegmentedWidget fragment = new DashboardStressSegmentedWidget();
|
||||||
|
final Bundle args = new Bundle();
|
||||||
|
args.putSerializable(ARG_DASHBOARD_DATA, dashboardData);
|
||||||
|
fragment.setArguments(args);
|
||||||
|
return fragment;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean isSupportedBy(final GBDevice device) {
|
||||||
|
return device.getDeviceCoordinator().supportsStressMeasurement();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void populateData(final DashboardFragment.DashboardData dashboardData) {
|
||||||
|
dashboardData.computeIfAbsent("stress", () -> DashboardStressData.compute(dashboardData));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void draw(final DashboardFragment.DashboardData dashboardData) {
|
||||||
|
final int[] colors = new int[]{
|
||||||
|
ContextCompat.getColor(GBApplication.getContext(), R.color.chart_stress_relaxed),
|
||||||
|
ContextCompat.getColor(GBApplication.getContext(), R.color.chart_stress_mild),
|
||||||
|
ContextCompat.getColor(GBApplication.getContext(), R.color.chart_stress_moderate),
|
||||||
|
ContextCompat.getColor(GBApplication.getContext(), R.color.chart_stress_high),
|
||||||
|
};
|
||||||
|
|
||||||
|
final float[] segments;
|
||||||
|
final float value;
|
||||||
|
final String valueText;
|
||||||
|
|
||||||
|
final DashboardStressData stressData = (DashboardStressData) dashboardData.get("stress");
|
||||||
|
|
||||||
|
if (stressData != null) {
|
||||||
|
segments = new float[]{
|
||||||
|
(stressData.ranges[1] - stressData.ranges[0]) / 100f,
|
||||||
|
(stressData.ranges[2] - stressData.ranges[1]) / 100f,
|
||||||
|
(stressData.ranges[3] - stressData.ranges[2]) / 100f,
|
||||||
|
1 - stressData.ranges[2] / 100f,
|
||||||
|
};
|
||||||
|
value = stressData.value / 100f;
|
||||||
|
valueText = String.valueOf(stressData.value);
|
||||||
|
} else {
|
||||||
|
segments = new float[]{
|
||||||
|
40 / 100f,
|
||||||
|
20 / 100f,
|
||||||
|
20 / 100f,
|
||||||
|
20 / 100f,
|
||||||
|
};
|
||||||
|
value = -1;
|
||||||
|
valueText = GBApplication.getContext().getString(R.string.stats_empty_value);
|
||||||
|
}
|
||||||
|
|
||||||
|
setText(valueText);
|
||||||
|
drawSegmentedGauge(
|
||||||
|
colors,
|
||||||
|
segments,
|
||||||
|
value,
|
||||||
|
false,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,70 @@
|
|||||||
|
/* Copyright (C) 2024 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.dashboard;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.activities.DashboardFragment;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.activities.charts.StressChartFragment;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.activities.dashboard.data.DashboardStressData;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||||
|
|
||||||
|
public class DashboardStressSimpleWidget extends AbstractGaugeWidget {
|
||||||
|
public DashboardStressSimpleWidget() {
|
||||||
|
super(R.string.menuitem_stress, "stress");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static DashboardStressSimpleWidget newInstance(final DashboardFragment.DashboardData dashboardData) {
|
||||||
|
final DashboardStressSimpleWidget fragment = new DashboardStressSimpleWidget();
|
||||||
|
final Bundle args = new Bundle();
|
||||||
|
args.putSerializable(ARG_DASHBOARD_DATA, dashboardData);
|
||||||
|
fragment.setArguments(args);
|
||||||
|
return fragment;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean isSupportedBy(final GBDevice device) {
|
||||||
|
return device.getDeviceCoordinator().supportsStressMeasurement();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void populateData(final DashboardFragment.DashboardData dashboardData) {
|
||||||
|
dashboardData.computeIfAbsent("stress", () -> DashboardStressData.compute(dashboardData));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void draw(final DashboardFragment.DashboardData dashboardData) {
|
||||||
|
final DashboardStressData stressData = (DashboardStressData) dashboardData.get("stress");
|
||||||
|
if (stressData == null) {
|
||||||
|
drawSimpleGauge(0, -1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final int color = StressChartFragment.StressType.fromStress(
|
||||||
|
stressData.value,
|
||||||
|
stressData.ranges
|
||||||
|
).getColor(GBApplication.getContext());
|
||||||
|
|
||||||
|
final float value = stressData.value / 100f;
|
||||||
|
final String valueText = String.valueOf(stressData.value);
|
||||||
|
|
||||||
|
setText(valueText);
|
||||||
|
drawSimpleGauge(color, value);
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
/* Copyright (C) 2023-2024 Arjan Schrijver
|
/* Copyright (C) 2023-2024 Arjan Schrijver, José Rebelo
|
||||||
|
|
||||||
This file is part of Gadgetbridge.
|
This file is part of Gadgetbridge.
|
||||||
|
|
||||||
@ -122,9 +122,7 @@ public class DashboardTodayWidget extends AbstractDashboardWidget {
|
|||||||
|
|
||||||
legend.setVisibility(prefs.getBoolean("dashboard_widget_today_legend", true) ? View.VISIBLE : View.GONE);
|
legend.setVisibility(prefs.getBoolean("dashboard_widget_today_legend", true) ? View.VISIBLE : View.GONE);
|
||||||
|
|
||||||
if (dashboardData.generalizedActivities.isEmpty()) {
|
if (!dashboardData.generalizedActivities.isEmpty()) {
|
||||||
fillData();
|
|
||||||
} else {
|
|
||||||
draw();
|
draw();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -147,7 +145,11 @@ public class DashboardTodayWidget extends AbstractDashboardWidget {
|
|||||||
int height = width;
|
int height = width;
|
||||||
int barWidth = Math.round(width * 0.08f);
|
int barWidth = Math.round(width * 0.08f);
|
||||||
int hourTextSp = Math.round(width * 0.024f);
|
int hourTextSp = Math.round(width * 0.024f);
|
||||||
float hourTextPixels = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, hourTextSp, requireContext().getResources().getDisplayMetrics());
|
float hourTextPixels = TypedValue.applyDimension(
|
||||||
|
TypedValue.COMPLEX_UNIT_SP,
|
||||||
|
hourTextSp,
|
||||||
|
GBApplication.getContext().getResources().getDisplayMetrics()
|
||||||
|
);
|
||||||
float outerCircleMargin = mode_24h ? barWidth / 2f : barWidth / 2f + hourTextPixels * 1.3f;
|
float outerCircleMargin = mode_24h ? barWidth / 2f : barWidth / 2f + hourTextPixels * 1.3f;
|
||||||
float innerCircleMargin = outerCircleMargin + barWidth * 1.3f;
|
float innerCircleMargin = outerCircleMargin + barWidth * 1.3f;
|
||||||
float degreeFactor = mode_24h ? 240 : 120;
|
float degreeFactor = mode_24h ? 240 : 120;
|
||||||
@ -168,7 +170,7 @@ public class DashboardTodayWidget extends AbstractDashboardWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Draw hours
|
// Draw hours
|
||||||
boolean normalClock = DateFormat.is24HourFormat(getContext());
|
boolean normalClock = DateFormat.is24HourFormat(GBApplication.getContext());
|
||||||
Map<Integer, String> hours = new HashMap<Integer, String>() {
|
Map<Integer, String> hours = new HashMap<Integer, String>() {
|
||||||
{
|
{
|
||||||
put(0, normalClock ? (mode_24h ? "0" : "12") : "12pm");
|
put(0, normalClock ? (mode_24h ? "0" : "12") : "12pm");
|
||||||
@ -435,6 +437,8 @@ public class DashboardTodayWidget extends AbstractDashboardWidget {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Void doInBackground(Void... params) {
|
protected Void doInBackground(Void... params) {
|
||||||
|
final long nanoStart = System.nanoTime();
|
||||||
|
|
||||||
// Retrieve activity data
|
// Retrieve activity data
|
||||||
dashboardData.generalizedActivities.clear();
|
dashboardData.generalizedActivities.clear();
|
||||||
List<GBDevice> devices = GBApplication.app().getDeviceManager().getDevices();
|
List<GBDevice> devices = GBApplication.app().getDeviceManager().getDevices();
|
||||||
@ -476,16 +480,21 @@ public class DashboardTodayWidget extends AbstractDashboardWidget {
|
|||||||
addActivity(session.getStartTime().getTime() / 1000, session.getEndTime().getTime() / 1000, ActivityKind.ACTIVITY);
|
addActivity(session.getStartTime().getTime() / 1000, session.getEndTime().getTime() / 1000, ActivityKind.ACTIVITY);
|
||||||
}
|
}
|
||||||
createGeneralizedActivities();
|
createGeneralizedActivities();
|
||||||
|
|
||||||
|
final long nanoEnd = System.nanoTime();
|
||||||
|
final long executionTime = (nanoEnd - nanoStart) / 1000000;
|
||||||
|
LOG.debug("fillData for {} took {}ms", DashboardTodayWidget.this.getClass().getSimpleName(), executionTime);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onPostExecute(Void unused) {
|
protected void onPostExecute(final Void unused) {
|
||||||
super.onPostExecute(unused);
|
super.onPostExecute(unused);
|
||||||
try {
|
try {
|
||||||
draw();
|
draw();
|
||||||
} catch (IllegalStateException e) {
|
} catch (final Exception e) {
|
||||||
LOG.warn("calling draw() failed: " + e.getMessage());
|
LOG.error("calling draw() failed", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,85 @@
|
|||||||
|
/* Copyright (C) 2024 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.dashboard.data;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.activities.DashboardFragment;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.activities.charts.StressChartFragment;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.model.StressSample;
|
||||||
|
|
||||||
|
public class DashboardStressData implements Serializable {
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(DashboardStressData.class);
|
||||||
|
|
||||||
|
public int value;
|
||||||
|
public int[] ranges;
|
||||||
|
public int[] totalTime;
|
||||||
|
|
||||||
|
public static DashboardStressData compute(final DashboardFragment.DashboardData dashboardData) {
|
||||||
|
final List<GBDevice> devices = GBApplication.app().getDeviceManager().getDevices();
|
||||||
|
|
||||||
|
GBDevice stressDevice = null;
|
||||||
|
double averageStress = -1;
|
||||||
|
|
||||||
|
final int[] totalTime = new int[StressChartFragment.StressType.values().length];
|
||||||
|
|
||||||
|
try (DBHandler dbHandler = GBApplication.acquireDB()) {
|
||||||
|
for (GBDevice dev : devices) {
|
||||||
|
if ((dashboardData.showAllDevices || dashboardData.showDeviceList.contains(dev.getAddress())) && dev.getDeviceCoordinator().supportsStressMeasurement()) {
|
||||||
|
final List<? extends StressSample> samples = dev.getDeviceCoordinator()
|
||||||
|
.getStressSampleProvider(dev, dbHandler.getDaoSession())
|
||||||
|
.getAllSamples(dashboardData.timeFrom * 1000L, dashboardData.timeTo * 1000L);
|
||||||
|
|
||||||
|
if (!samples.isEmpty()) {
|
||||||
|
stressDevice = dev;
|
||||||
|
final int[] stressRanges = dev.getDeviceCoordinator().getStressRanges();
|
||||||
|
averageStress = samples.stream()
|
||||||
|
.mapToInt(StressSample::getStress)
|
||||||
|
.peek(stress -> {
|
||||||
|
final StressChartFragment.StressType stressType = StressChartFragment.StressType.fromStress(stress, stressRanges);
|
||||||
|
if (stressType != StressChartFragment.StressType.UNKNOWN) {
|
||||||
|
totalTime[stressType.ordinal() - 1] += 60;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.average()
|
||||||
|
.orElse(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (final Exception e) {
|
||||||
|
LOG.error("Could not compute stress", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stressDevice != null) {
|
||||||
|
final DashboardStressData stressData = new DashboardStressData();
|
||||||
|
stressData.value = (int) Math.round(averageStress);
|
||||||
|
stressData.ranges = stressDevice.getDeviceCoordinator().getStressRanges();
|
||||||
|
stressData.totalTime = totalTime;
|
||||||
|
|
||||||
|
return stressData;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
@ -1,3 +1,19 @@
|
|||||||
|
/* Copyright (C) 2024 a0z, 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.adapter;
|
package nodomain.freeyourgadget.gadgetbridge.adapter;
|
||||||
|
|
||||||
import androidx.fragment.app.FragmentManager;
|
import androidx.fragment.app.FragmentManager;
|
||||||
@ -7,7 +23,7 @@ import java.util.List;
|
|||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBFragment;
|
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBFragment;
|
||||||
|
|
||||||
abstract class NestedFragmentAdapter extends FragmentStateAdapter {
|
public abstract class NestedFragmentAdapter extends FragmentStateAdapter {
|
||||||
protected FragmentManager fragmentManager;
|
protected FragmentManager fragmentManager;
|
||||||
|
|
||||||
public NestedFragmentAdapter(AbstractGBFragment fragment, FragmentManager childFragmentManager) {
|
public NestedFragmentAdapter(AbstractGBFragment fragment, FragmentManager childFragmentManager) {
|
||||||
|
@ -1,42 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<LinearLayout
|
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:gravity="center_vertical"
|
|
||||||
tools:context=".activities.dashboard.DashboardActiveTimeWidget">
|
|
||||||
|
|
||||||
<RelativeLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_margin="8dp"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:id="@+id/card_layout">
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:layout_width="150dp"
|
|
||||||
android:layout_height="75dp"
|
|
||||||
android:layout_centerHorizontal="true"
|
|
||||||
android:scaleType="fitStart"
|
|
||||||
android:id="@+id/activetime_gauge" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/activetime_text"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="28dp"
|
|
||||||
android:layout_centerHorizontal="true"
|
|
||||||
android:text="0:00"
|
|
||||||
android:textSize="30dp" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_below="@+id/activetime_text"
|
|
||||||
android:layout_centerHorizontal="true"
|
|
||||||
android:text="@string/activity_list_summary_active_time" />
|
|
||||||
|
|
||||||
</RelativeLayout>
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
@ -1,41 +1,42 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<LinearLayout
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:gravity="center_vertical"
|
android:gravity="center_vertical"
|
||||||
tools:context=".activities.dashboard.DashboardDistanceWidget">
|
tools:context=".activities.dashboard.AbstractDashboardWidget">
|
||||||
|
|
||||||
<RelativeLayout
|
<RelativeLayout
|
||||||
|
android:id="@+id/card_layout"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_margin="8dp"
|
android:layout_margin="8dp"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:id="@+id/card_layout">
|
tools:ignore="UselessParent">
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
|
android:id="@+id/gauge_bar"
|
||||||
android:layout_width="150dp"
|
android:layout_width="150dp"
|
||||||
android:layout_height="75dp"
|
android:layout_height="75dp"
|
||||||
android:layout_centerHorizontal="true"
|
android:layout_centerHorizontal="true"
|
||||||
android:scaleType="fitStart"
|
android:scaleType="fitStart" />
|
||||||
android:id="@+id/distance_gauge" />
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/distance_text"
|
android:id="@+id/gauge_value"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_centerHorizontal="true"
|
||||||
android:layout_marginTop="28dp"
|
android:layout_marginTop="28dp"
|
||||||
android:layout_centerHorizontal="true"
|
android:text="@string/stats_empty_value"
|
||||||
android:text="0.0km"
|
android:textSize="30sp" />
|
||||||
android:textSize="30dp" />
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
|
android:id="@+id/gauge_label"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_below="@+id/distance_text"
|
android:layout_below="@+id/gauge_value"
|
||||||
android:layout_centerHorizontal="true"
|
android:layout_centerHorizontal="true"
|
||||||
android:text="@string/distance" />
|
android:text="@string/no_data" />
|
||||||
|
|
||||||
</RelativeLayout>
|
</RelativeLayout>
|
||||||
|
|
@ -1,42 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<LinearLayout
|
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:gravity="center_vertical"
|
|
||||||
tools:context=".activities.dashboard.DashboardSleepWidget">
|
|
||||||
|
|
||||||
<RelativeLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_margin="8dp"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:id="@+id/card_layout">
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:layout_width="150dp"
|
|
||||||
android:layout_height="75dp"
|
|
||||||
android:layout_centerHorizontal="true"
|
|
||||||
android:scaleType="fitStart"
|
|
||||||
android:id="@+id/sleep_gauge" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/sleep_text"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="28dp"
|
|
||||||
android:layout_centerHorizontal="true"
|
|
||||||
android:text="0:00"
|
|
||||||
android:textSize="30dp" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_below="@+id/sleep_text"
|
|
||||||
android:layout_centerHorizontal="true"
|
|
||||||
android:text="@string/menuitem_sleep" />
|
|
||||||
|
|
||||||
</RelativeLayout>
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
@ -1,42 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<LinearLayout
|
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:gravity="center_vertical"
|
|
||||||
tools:context=".activities.dashboard.DashboardStepsWidget">
|
|
||||||
|
|
||||||
<RelativeLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_margin="8dp"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:id="@+id/card_layout">
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:layout_width="150dp"
|
|
||||||
android:layout_height="75dp"
|
|
||||||
android:layout_centerHorizontal="true"
|
|
||||||
android:scaleType="fitStart"
|
|
||||||
android:id="@+id/steps_gauge" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/steps_count"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="28dp"
|
|
||||||
android:layout_centerHorizontal="true"
|
|
||||||
android:text="0"
|
|
||||||
android:textSize="30dp" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_below="@+id/steps_count"
|
|
||||||
android:layout_centerHorizontal="true"
|
|
||||||
android:text="@string/steps" />
|
|
||||||
|
|
||||||
</RelativeLayout>
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
@ -9,4 +9,10 @@
|
|||||||
android:title="@string/menuitem_calendar"
|
android:title="@string/menuitem_calendar"
|
||||||
app:iconTint="?attr/actionmenu_icon_color"
|
app:iconTint="?attr/actionmenu_icon_color"
|
||||||
app:showAsAction="ifRoom" />
|
app:showAsAction="ifRoom" />
|
||||||
|
<item
|
||||||
|
android:id="@+id/dashboard_settings"
|
||||||
|
android:icon="@drawable/ic_settings"
|
||||||
|
android:title="@string/dashboard_settings"
|
||||||
|
app:iconTint="?attr/actionmenu_icon_color"
|
||||||
|
app:showAsAction="never" />
|
||||||
</menu>
|
</menu>
|
||||||
|
@ -4167,6 +4167,11 @@
|
|||||||
<item>@string/distance</item>
|
<item>@string/distance</item>
|
||||||
<item>@string/active_time</item>
|
<item>@string/active_time</item>
|
||||||
<item>@string/menuitem_sleep</item>
|
<item>@string/menuitem_sleep</item>
|
||||||
|
<item>@string/body_energy</item>
|
||||||
|
<item>@string/menuitem_stress_simple</item>
|
||||||
|
<item>@string/menuitem_stress_segmented</item>
|
||||||
|
<item>@string/menuitem_stress_breakdown</item>
|
||||||
|
<item>@string/hrv</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
|
||||||
<string-array name="pref_dashboard_widgets_order_values">
|
<string-array name="pref_dashboard_widgets_order_values">
|
||||||
@ -4176,5 +4181,22 @@
|
|||||||
<item>distance</item>
|
<item>distance</item>
|
||||||
<item>activetime</item>
|
<item>activetime</item>
|
||||||
<item>sleep</item>
|
<item>sleep</item>
|
||||||
|
<item>bodyenergy</item>
|
||||||
|
<item>stress_simple</item>
|
||||||
|
<item>stress_segmented</item>
|
||||||
|
<item>stress_breakdown</item>
|
||||||
|
<item>hrv</item>
|
||||||
|
</string-array>
|
||||||
|
|
||||||
|
<string-array name="pref_dashboard_widgets_order_default">
|
||||||
|
<item>today</item>
|
||||||
|
<item>goals</item>
|
||||||
|
<item>steps</item>
|
||||||
|
<item>distance</item>
|
||||||
|
<item>activetime</item>
|
||||||
|
<item>sleep</item>
|
||||||
|
<item>bodyenergy</item>
|
||||||
|
<item>stress_segmented</item>
|
||||||
|
<item>hrv</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -52,10 +52,11 @@
|
|||||||
<color name="hrv_status_poor" type="color">#be03fc</color>
|
<color name="hrv_status_poor" type="color">#be03fc</color>
|
||||||
<color name="hrv_status_char_line_color" type="color">#d12a2a</color>
|
<color name="hrv_status_char_line_color" type="color">#d12a2a</color>
|
||||||
<color name="body_energy_level_color" type="color">#5ac234</color>
|
<color name="body_energy_level_color" type="color">#5ac234</color>
|
||||||
|
<color name="body_energy_lost_color" type="color">#ff6c43</color>
|
||||||
<color name="steps_color" type="color">#00c9bf</color>
|
<color name="steps_color" type="color">#00c9bf</color>
|
||||||
|
|
||||||
<color name="value_line_color" type="color">#858585</color>
|
<color name="value_line_color" type="color">#858585</color>
|
||||||
<color name="gauge_line_color" type="color">#383838</color>
|
<color name="gauge_line_color" type="color">#19808080</color>
|
||||||
|
|
||||||
<color name="alternate_row_background_light">#FFEDEDED</color>
|
<color name="alternate_row_background_light">#FFEDEDED</color>
|
||||||
<color name="alternate_row_background_dark">#545254</color>
|
<color name="alternate_row_background_dark">#545254</color>
|
||||||
|
@ -628,6 +628,8 @@
|
|||||||
<string name="android_pairing_hint">Use the Android Bluetooth pairing dialog to pair the device.</string>
|
<string name="android_pairing_hint">Use the Android Bluetooth pairing dialog to pair the device.</string>
|
||||||
<string name="title_activity_mi_band_pairing">Pair your Mi Band</string>
|
<string name="title_activity_mi_band_pairing">Pair your Mi Band</string>
|
||||||
<string name="pairing">Pairing with %s…</string>
|
<string name="pairing">Pairing with %s…</string>
|
||||||
|
<string name="choose_device">Choose a device</string>
|
||||||
|
<string name="no_supported_devices_found">No supported devices found</string>
|
||||||
<string name="pairing_creating_bond_with">"Creating bond with %1$s (%2$s)"</string>
|
<string name="pairing_creating_bond_with">"Creating bond with %1$s (%2$s)"</string>
|
||||||
<string name="pairing_unable_to_pair_with">"Unable to pair with %1$s (%2$s)"</string>
|
<string name="pairing_unable_to_pair_with">"Unable to pair with %1$s (%2$s)"</string>
|
||||||
<string name="pairing_in_progress">Bonding in progress: %1$s (%2$s)</string>
|
<string name="pairing_in_progress">Bonding in progress: %1$s (%2$s)</string>
|
||||||
@ -1871,6 +1873,9 @@
|
|||||||
<string name="menuitem_more">More</string>
|
<string name="menuitem_more">More</string>
|
||||||
<string name="menuitem_nfc">NFC</string>
|
<string name="menuitem_nfc">NFC</string>
|
||||||
<string name="menuitem_stress">Stress</string>
|
<string name="menuitem_stress">Stress</string>
|
||||||
|
<string name="menuitem_stress_simple">Stress (simple)</string>
|
||||||
|
<string name="menuitem_stress_segmented">Stress (segmented)</string>
|
||||||
|
<string name="menuitem_stress_breakdown">Stress (breakdown)</string>
|
||||||
<string name="menuitem_pai">PAI</string>
|
<string name="menuitem_pai">PAI</string>
|
||||||
<string name="menuitem_hr">Heart Rate</string>
|
<string name="menuitem_hr">Heart Rate</string>
|
||||||
<string name="menuitem_spo2">SpO2</string>
|
<string name="menuitem_spo2">SpO2</string>
|
||||||
|
@ -20,7 +20,7 @@
|
|||||||
android:summary="@string/pref_dashboard_cards_summary"
|
android:summary="@string/pref_dashboard_cards_summary"
|
||||||
app:iconSpaceReserved="false" />
|
app:iconSpaceReserved="false" />
|
||||||
<com.mobeta.android.dslv.DragSortListPreference
|
<com.mobeta.android.dslv.DragSortListPreference
|
||||||
android:defaultValue="@array/pref_dashboard_widgets_order_values"
|
android:defaultValue="@array/pref_dashboard_widgets_order_default"
|
||||||
android:dialogTitle="@string/menuitem_widgets"
|
android:dialogTitle="@string/menuitem_widgets"
|
||||||
android:entries="@array/pref_dashboard_widgets_order"
|
android:entries="@array/pref_dashboard_widgets_order"
|
||||||
android:entryValues="@array/pref_dashboard_widgets_order_values"
|
android:entryValues="@array/pref_dashboard_widgets_order_values"
|
||||||
|
Loading…
Reference in New Issue
Block a user