1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-12-25 10:05:49 +01: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:
José Rebelo 2024-08-28 09:36:25 +01:00
parent d4df00ccbf
commit f76180c4bd
33 changed files with 1652 additions and 761 deletions

View File

@ -123,7 +123,7 @@ public class GBApplication extends Application {
private static SharedPreferences sharedPrefs;
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
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 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.apply();

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2023-2024 Arjan Schrijver
/* Copyright (C) 2023-2024 Arjan Schrijver, José Rebelo
This file is part of Gadgetbridge.
@ -16,6 +16,7 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.activities;
import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
@ -30,8 +31,12 @@ import android.view.View;
import android.view.ViewGroup;
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.Nullable;
import androidx.core.view.MenuProvider;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentContainerView;
import androidx.gridlayout.widget.GridLayout;
@ -44,23 +49,31 @@ import org.slf4j.LoggerFactory;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collections;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Supplier;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.dashboard.AbstractDashboardWidget;
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.DashboardDistanceWidget;
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.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.impl.GBDevice;
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.Prefs;
public class DashboardFragment extends Fragment {
public class DashboardFragment extends Fragment implements MenuProvider {
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 arrowLeft;
private TextView arrowRight;
private GridLayout gridLayout;
private DashboardTodayWidget todayWidget;
private DashboardGoalsWidget goalsWidget;
private DashboardStepsWidget stepsWidget;
private DashboardDistanceWidget distanceWidget;
private DashboardActiveTimeWidget activeTimeWidget;
private DashboardSleepWidget sleepWidget;
private final Map<String, AbstractDashboardWidget> widgetMap = new HashMap<>();
private DashboardData dashboardData = new DashboardData();
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";
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@ -95,7 +113,7 @@ public class DashboardFragment extends Fragment {
switch (action) {
case GBApplication.ACTION_NEW_DATA:
final GBDevice dev = intent.getParcelableExtra(GBDevice.EXTRA_DEVICE);
if (dev != null && !dev.isBusy()) {
if (dev != null) {
if (dashboardData.showAllDevices || dashboardData.showDeviceList.contains(dev.getAddress())) {
refresh();
}
@ -109,20 +127,25 @@ public class DashboardFragment extends Fragment {
};
@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);
View dashboardView = inflater.inflate(R.layout.fragment_dashboard, container, false);
setHasOptionsMenu(true);
requireActivity().addMenuProvider(this);
textViewDate = dashboardView.findViewById(R.id.dashboard_date);
gridLayout = dashboardView.findViewById(R.id.dashboard_gridlayout);
calendarLauncher = registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(),
calendarCallback
);
// Increase column count on landscape, tablets and open foldables
DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
if (displayMetrics.widthPixels / displayMetrics.density >= 600) {
gridLayout.setColumnCount(4);
}
arrowLeft = dashboardView.findViewById(R.id.arrow_left);
final TextView arrowLeft = dashboardView.findViewById(R.id.arrow_left);
arrowLeft.setOnClickListener(v -> {
day.add(Calendar.DAY_OF_MONTH, -1);
refresh();
@ -155,7 +178,7 @@ public class DashboardFragment extends Fragment {
if (isConfigChanged) {
isConfigChanged = false;
fullRefresh();
} else if (dashboardData.isEmpty() || todayWidget == null) {
} else if (dashboardData.isEmpty() || !widgetMap.containsKey("today")) {
refresh();
}
}
@ -173,43 +196,29 @@ public class DashboardFragment extends Fragment {
}
@Override
public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
public void onCreateMenu(@NonNull final Menu menu, @NonNull final MenuInflater inflater) {
inflater.inflate(R.menu.dashboard_menu, menu);
}
@Override
public boolean onOptionsItemSelected(@NonNull final MenuItem item) {
public boolean onMenuItemSelected(@NonNull final MenuItem item) {
final int itemId = item.getItemId();
if (itemId == R.id.dashboard_show_calendar) {
final Intent intent = new Intent(requireActivity(), DashboardCalendarActivity.class);
intent.putExtra(DashboardCalendarActivity.EXTRA_TIMESTAMP, day.getTimeInMillis());
startActivityForResult(intent, 0);
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();
}
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;
}
private void fullRefresh() {
gridLayout.removeAllViews();
todayWidget = null;
goalsWidget = null;
stepsWidget = null;
distanceWidget = null;
activeTimeWidget = null;
sleepWidget = null;
widgetMap.clear();
refresh();
}
@ -229,13 +238,13 @@ public class DashboardFragment extends Fragment {
private void draw() {
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);
List<String> widgetsOrder = Arrays.asList(widgetsOrderPref.split(","));
String[] widgetsOrder = widgetsOrderPref.split(",");
Calendar today = GregorianCalendar.getInstance();
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);
} else {
textViewDate.setText(DateTimeUtils.formatDate(day.getTime()));
@ -245,55 +254,55 @@ public class DashboardFragment extends Fragment {
boolean cardsEnabled = prefs.getBoolean("dashboard_cards_enabled", true);
for (String widgetName : widgetsOrder) {
switch (widgetName) {
case "today":
if (todayWidget == null) {
todayWidget = DashboardTodayWidget.newInstance(dashboardData);
createWidget(todayWidget, cardsEnabled, prefs.getBoolean("dashboard_widget_today_2columns", true) ? 2 : 1);
} else {
todayWidget.update();
}
break;
case "goals":
if (goalsWidget == null) {
goalsWidget = DashboardGoalsWidget.newInstance(dashboardData);
createWidget(goalsWidget, cardsEnabled, prefs.getBoolean("dashboard_widget_goals_2columns", true) ? 2 : 1);
} else {
goalsWidget.update();
}
break;
case "steps":
if (stepsWidget == null) {
stepsWidget = DashboardStepsWidget.newInstance(dashboardData);
createWidget(stepsWidget, cardsEnabled, 1);
} else {
stepsWidget.update();
}
break;
case "distance":
if (distanceWidget == null) {
distanceWidget = DashboardDistanceWidget.newInstance(dashboardData);
createWidget(distanceWidget, cardsEnabled, 1);
} else {
distanceWidget.update();
}
break;
case "activetime":
if (activeTimeWidget == null) {
activeTimeWidget = DashboardActiveTimeWidget.newInstance(dashboardData);
createWidget(activeTimeWidget, cardsEnabled, 1);
} else {
activeTimeWidget.update();
}
break;
case "sleep":
if (sleepWidget == null) {
sleepWidget = DashboardSleepWidget.newInstance(dashboardData);
createWidget(sleepWidget, cardsEnabled, 1);
} else {
sleepWidget.update();
}
break;
AbstractDashboardWidget widget = widgetMap.get(widgetName);
if (widget == null) {
int columnSpan = 1;
switch (widgetName) {
case "today":
widget = DashboardTodayWidget.newInstance(dashboardData);
columnSpan = prefs.getBoolean("dashboard_widget_today_2columns", true) ? 2 : 1;
break;
case "goals":
widget = DashboardGoalsWidget.newInstance(dashboardData);
columnSpan = prefs.getBoolean("dashboard_widget_goals_2columns", true) ? 2 : 1;
break;
case "steps":
widget = DashboardStepsWidget.newInstance(dashboardData);
break;
case "distance":
widget = DashboardDistanceWidget.newInstance(dashboardData);
break;
case "activetime":
widget = DashboardActiveTimeWidget.newInstance(dashboardData);
break;
case "sleep":
widget = DashboardSleepWidget.newInstance(dashboardData);
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();
GridLayout.LayoutParams layoutParams = new GridLayout.LayoutParams(
GridLayout.spec(GridLayout.UNDEFINED, GridLayout.FILL,1f),
GridLayout.spec(GridLayout.UNDEFINED, columnSpan, GridLayout.FILL,1f)
GridLayout.spec(GridLayout.UNDEFINED, GridLayout.FILL, 1f),
GridLayout.spec(GridLayout.UNDEFINED, columnSpan, GridLayout.FILL, 1f)
);
layoutParams.width = 0;
int pixels_8dp = (int) (8 * scale + 0.5f);
@ -352,6 +361,7 @@ public class DashboardFragment extends Fragment {
private float distanceGoalFactor;
private long activeMinutesTotal;
private float activeMinutesGoalFactor;
private final Map<String, Serializable> genericData = new ConcurrentHashMap<>();
public void clear() {
stepsTotal = 0;
@ -363,6 +373,7 @@ public class DashboardFragment extends Fragment {
activeMinutesTotal = 0;
activeMinutesGoalFactor = 0;
generalizedActivities.clear();
genericData.clear();
}
public boolean isEmpty() {
@ -374,6 +385,7 @@ public class DashboardFragment extends Fragment {
distanceGoalFactor == 0 &&
activeMinutesTotal == 0 &&
activeMinutesGoalFactor == 0 &&
genericData.isEmpty() &&
generalizedActivities.isEmpty());
}
@ -425,6 +437,21 @@ public class DashboardFragment extends Fragment {
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 ActivityKind activityKind;
public long timeFrom;

View File

@ -16,6 +16,7 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.activities.charts;
import android.app.Activity;
import android.app.DatePickerDialog;
import android.content.BroadcastReceiver;
import android.content.Context;
@ -29,18 +30,27 @@ import android.widget.Button;
import android.widget.TextView;
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.appcompat.app.ActionBar;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import androidx.viewpager.widget.ViewPager;
import com.google.android.material.tabs.TabLayout;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
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_END_DATE = "stateEndDate";
public static final String EXTRA_FRAGMENT_ID = "fragment";
public static final int REQUEST_CODE_PREFERENCES = 1;
public static final String EXTRA_FRAGMENT_ID = "fragmentId";
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;
@ -70,13 +82,19 @@ public abstract class AbstractChartsActivity extends AbstractGBFragmentActivity
private GBDevice mGBDevice;
private ViewGroup dateBar;
private ActivityResultLauncher<Intent> chartsPreferencesLauncher;
private final ActivityResultCallback<ActivityResult> chartsPreferencesCallback = result -> {
recreate();
};
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
public void onReceive(final Context context, final Intent intent) {
final String action = intent.getAction();
//noinspection SwitchStatementWithTooFewBranches
switch (Objects.requireNonNull(action)) {
case GBDevice.ACTION_DEVICE_CHANGED:
GBDevice dev = intent.getParcelableExtra(GBDevice.EXTRA_DEVICE);
final GBDevice dev = intent.getParcelableExtra(GBDevice.EXTRA_DEVICE);
if (dev != null) {
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()) {
swipeLayout.setRefreshing(true);
} else {
boolean wasBusy = swipeLayout.isRefreshing();
final boolean wasBusy = swipeLayout.isRefreshing();
swipeLayout.setRefreshing(false);
if (wasBusy) {
LocalBroadcastManager.getInstance(this).sendBroadcast(new Intent(REFRESH));
@ -99,31 +117,51 @@ public abstract class AbstractChartsActivity extends AbstractGBFragmentActivity
}
@Override
protected void onCreate(Bundle savedInstanceState) {
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
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) {
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 {
setEndDate(new Date());
setStartDate(DateTimeUtils.shiftByDays(getEndDate(), -1));
}
setStartDate(DateTimeUtils.shiftByDays(getEndDate(), -1));
final IntentFilter filterLocal = new IntentFilter();
filterLocal.addAction(GBDevice.ACTION_DEVICE_CHANGED);
LocalBroadcastManager.getInstance(this).registerReceiver(mReceiver, filterLocal);
final Bundle extras = getIntent().getExtras();
if (extras != null) {
mGBDevice = extras.getParcelable(GBDevice.EXTRA_DEVICE);
tabFragmentToOpen = extras.getInt(EXTRA_FRAGMENT_ID);
} else {
throw new IllegalArgumentException("Must provide a device when invoking this activity");
// Open the specified fragment, if any, and setup single page view if specified
final int tabFragmentIdToOpen = extras.getInt(EXTRA_FRAGMENT_ID, -1);
final String singleFragmentName = extras.getString(EXTRA_SINGLE_FRAGMENT_NAME, null);
final int actionbarTitle = extras.getInt(EXTRA_ACTIONBAR_TITLE, 0);
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.setOnRefreshListener(this::fetchRecordedData);
@ -132,8 +170,23 @@ public abstract class AbstractChartsActivity extends AbstractGBFragmentActivity
// Set up the ViewPager with the sections adapter.
final NonSwipeableViewPager viewPager = findViewById(R.id.charts_pager);
viewPager.setAdapter(getPagerAdapter());
if (tabFragmentToOpen > -1) {
viewPager.setCurrentItem(tabFragmentToOpen); // open the tab as specified in the intent
if (tabFragmentIdToOpen > -1) {
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() {
@ -158,19 +211,19 @@ public abstract class AbstractChartsActivity extends AbstractGBFragmentActivity
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));
Button mNextButton = findViewById(R.id.charts_next_day);
final Button mNextButton = findViewById(R.id.charts_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));
Button mNextWeekButton = findViewById(R.id.charts_next_week);
final Button mNextWeekButton = findViewById(R.id.charts_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));
Button mNextMonthButton = findViewById(R.id.charts_next_month);
final Button mNextMonthButton = findViewById(R.id.charts_next_month);
mNextMonthButton.setOnClickListener(v -> handleButtonClicked(DATE_NEXT_MONTH));
}
@ -193,7 +246,7 @@ public abstract class AbstractChartsActivity extends AbstractGBFragmentActivity
protected abstract List<String> fillChartsTabsList();
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 dateStringTo = dateFormat.format(getEndDate());
@ -262,15 +315,7 @@ public abstract class AbstractChartsActivity extends AbstractGBFragmentActivity
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_CODE_PREFERENCES) {
this.recreate();
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
public boolean onOptionsItemSelected(final MenuItem item) {
final int itemId = item.getItemId();
if (itemId == R.id.charts_fetch_activity_data) {
fetchRecordedData();
@ -285,8 +330,8 @@ public abstract class AbstractChartsActivity extends AbstractGBFragmentActivity
LocalBroadcastManager.getInstance(this).sendBroadcast(new Intent(REFRESH));
}, currentDate.get(Calendar.YEAR), currentDate.get(Calendar.MONTH), currentDate.get(Calendar.DATE)).show();
} else if (itemId == R.id.prefs_charts_menu) {
Intent settingsIntent = new Intent(this, ChartsPreferencesActivity.class);
startActivityForResult(settingsIntent, REQUEST_CODE_PREFERENCES);
final Intent settingsIntent = new Intent(this, ChartsPreferencesActivity.class);
chartsPreferencesLauncher.launch(settingsIntent);
return true;
}
@ -294,7 +339,7 @@ public abstract class AbstractChartsActivity extends AbstractGBFragmentActivity
}
@Override
public void enableSwipeRefresh(boolean enable) {
public void enableSwipeRefresh(final boolean enable) {
swipeLayout.setEnabled(enable && allowRefresh());
}

View File

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

View File

@ -145,7 +145,7 @@ public class ActivityChartsActivity extends AbstractChartsActivity {
case "activitylist":
return new ActivityListingChartFragment();
case "sleep":
return new SleepCollectionFragment();
return SleepCollectionFragment.newInstance(enabledTabsList.size() == 1);
case "hrvstatus":
return new HRVStatusFragment();
case "bodyenergy":
@ -155,7 +155,7 @@ public class ActivityChartsActivity extends AbstractChartsActivity {
case "pai":
return new PaiChartFragment();
case "stepsweek":
return new StepsCollectionFragment();
return StepsCollectionFragment.newInstance(enabledTabsList.size() == 1);
case "speedzones":
return new SpeedZonesFragment();
case "livestats":
@ -177,14 +177,6 @@ public class ActivityChartsActivity extends AbstractChartsActivity {
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
public CharSequence getPageTitle(int position) {
switch (enabledTabsList.get(position)) {

View File

@ -25,14 +25,19 @@ import androidx.viewpager.widget.ViewPager;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
public class NonSwipeableViewPager extends ViewPager {
private boolean allowSwipe = true;
public NonSwipeableViewPager(final Context context, final AttributeSet attrs) {
super(context, attrs);
}
public void setAllowSwipe(final boolean allowSwipe) {
this.allowSwipe = allowSwipe;
}
@Override
public boolean onInterceptTouchEvent(final MotionEvent ev) {
if (GBApplication.getPrefs().getBoolean("charts_allow_swipe", true)) {
if (allowSwipe) {
return super.onInterceptTouchEvent(ev);
}
return false;
@ -40,7 +45,7 @@ public class NonSwipeableViewPager extends ViewPager {
@Override
public boolean onTouchEvent(final MotionEvent ev) {
if (GBApplication.getPrefs().getBoolean("charts_allow_swipe", true)) {
if (allowSwipe) {
return super.onTouchEvent(ev);
}
return false;

View File

@ -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;
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.viewpager2.widget.ViewPager2;
import androidx.fragment.app.FragmentManager;
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;
import nodomain.freeyourgadget.gadgetbridge.adapter.SleepFragmentAdapter;
public class SleepCollectionFragment extends AbstractGBFragment {
protected SleepFragmentAdapter nestedFragmentsAdapter;
protected ViewPager2 viewPager;
private int last_position = 0;
public class SleepCollectionFragment extends AbstractCollectionFragment {
public SleepCollectionFragment() {
@Override
protected void onMadeVisibleInActivity() {
super.onMadeVisibleInActivity();
nestedFragmentsAdapter.updateFragments(last_position);
}
public static SleepCollectionFragment newInstance(final boolean allowSwipe) {
final SleepCollectionFragment fragment = new SleepCollectionFragment();
final Bundle args = new Bundle();
args.putBoolean(ARG_ALLOW_SWIPE, allowSwipe);
fragment.setArguments(args);
return fragment;
}
@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 = 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;
public NestedFragmentAdapter getNestedFragmentAdapter(AbstractGBFragment fragment, FragmentManager childFragmentManager) {
return new SleepFragmentAdapter(this, getChildFragmentManager());
}
}

View File

@ -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;
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.viewpager2.widget.ViewPager2;
import androidx.fragment.app.FragmentManager;
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;
import nodomain.freeyourgadget.gadgetbridge.adapter.StepsFragmentAdapter;
public class StepsCollectionFragment extends AbstractGBFragment {
protected StepsFragmentAdapter nestedFragmentsAdapter;
protected ViewPager2 viewPager;
private int last_position = 0;
public class StepsCollectionFragment extends AbstractCollectionFragment {
public StepsCollectionFragment() {
@Override
protected void onMadeVisibleInActivity() {
super.onMadeVisibleInActivity();
nestedFragmentsAdapter.updateFragments(last_position);
}
public static StepsCollectionFragment newInstance(final boolean allowSwipe) {
final StepsCollectionFragment fragment = new StepsCollectionFragment();
final Bundle args = new Bundle();
args.putBoolean(ARG_ALLOW_SWIPE, allowSwipe);
fragment.setArguments(args);
return fragment;
}
@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 = 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;
public NestedFragmentAdapter getNestedFragmentAdapter(AbstractGBFragment fragment, FragmentManager childFragmentManager) {
return new StepsFragmentAdapter(this, getChildFragmentManager());
}
}

View File

@ -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),
RELAXED(R.string.stress_relaxed, R.color.chart_stress_relaxed),
MILD(R.string.stress_mild, R.color.chart_stress_mild),

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2023-2024 Arjan Schrijver
/* Copyright (C) 2023-2024 Arjan Schrijver, José Rebelo
This file is part of Gadgetbridge.
@ -16,19 +16,31 @@
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.content.Context;
import android.content.Intent;
import android.graphics.Color;
import android.graphics.Paint;
import android.os.Bundle;
import android.view.View;
import android.widget.Toast;
import androidx.annotation.ColorInt;
import androidx.fragment.app.Fragment;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import org.slf4j.Logger;
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.charts.ActivityChartsActivity;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
public abstract class AbstractDashboardWidget extends Fragment {
private static final Logger LOG = LoggerFactory.getLogger(AbstractDashboardWidget.class);
@ -57,37 +69,67 @@ public abstract class AbstractDashboardWidget extends Fragment {
}
}
public void update() {
fillData();
}
protected abstract void fillData();
/**
* @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
*/
Bitmap drawGauge(int width, int barWidth, @ColorInt int filledColor, float filledFactor) {
int height = width / 2;
int barMargin = (int) Math.ceil(barWidth / 2f);
protected boolean isSupportedBy(final GBDevice device) {
return device.getDeviceCoordinator().supportsActivityTracking();
}
Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
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);
paint.setStrokeWidth(barWidth);
paint.setColor(filledColor);
canvas.drawArc(barMargin, barMargin, width - barMargin, width - barMargin, 180, 180 * filledFactor, false, paint);
protected List<GBDevice> getSupportedDevices(final DashboardFragment.DashboardData dashboardData) {
return GBApplication.app().getDeviceManager().getDevices()
.stream()
.filter(dev -> dashboardData.showAllDevices || dashboardData.showDeviceList.contains(dev.getAddress()))
.filter(this::isSupportedBy)
.collect(Collectors.toList());
}
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();
}
}

View File

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

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2023-2024 Arjan Schrijver
/* Copyright (C) 2023-2024 Arjan Schrijver, José Rebelo
This file is part of Gadgetbridge.
@ -16,19 +16,10 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.activities.dashboard;
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 org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Locale;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.DashboardFragment;
@ -37,13 +28,9 @@ import nodomain.freeyourgadget.gadgetbridge.activities.DashboardFragment;
* Use the {@link DashboardActiveTimeWidget#newInstance} factory method to
* create an instance of this fragment.
*/
public class DashboardActiveTimeWidget extends AbstractDashboardWidget {
private static final Logger LOG = LoggerFactory.getLogger(DashboardActiveTimeWidget.class);
private TextView activeTime;
private ImageView activeTimeGauge;
public class DashboardActiveTimeWidget extends AbstractGaugeWidget {
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.
* @return A new instance of fragment DashboardActiveTimeWidget.
*/
public static DashboardActiveTimeWidget newInstance(DashboardFragment.DashboardData dashboardData) {
DashboardActiveTimeWidget fragment = new DashboardActiveTimeWidget();
Bundle args = new Bundle();
public static DashboardActiveTimeWidget newInstance(final DashboardFragment.DashboardData dashboardData) {
final DashboardActiveTimeWidget fragment = new DashboardActiveTimeWidget();
final Bundle args = new Bundle();
args.putSerializable(ARG_DASHBOARD_DATA, dashboardData);
fragment.setArguments(args);
return fragment;
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
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;
protected void populateData(final DashboardFragment.DashboardData dashboardData) {
dashboardData.getActiveMinutesTotal();
dashboardData.getActiveMinutesGoalFactor();
}
@Override
public void onResume() {
super.onResume();
if (activeTime != null && activeTimeGauge != null) fillData();
}
protected void draw(final DashboardFragment.DashboardData dashboardData) {
final long totalActiveMinutes = dashboardData.getActiveMinutesTotal();
final String valueText = String.format(
Locale.ROOT,
"%d:%02d",
(int) Math.floor(totalActiveMinutes / 60f),
(int) (totalActiveMinutes % 60f)
);
@Override
protected void fillData() {
if (activeTimeGauge == null) return;
activeTimeGauge.post(new Runnable() {
@Override
public void run() {
FillDataAsyncTask myAsyncTask = new FillDataAsyncTask();
myAsyncTask.execute();
}
});
}
setText(valueText);
private class FillDataAsyncTask extends AsyncTask<Void, Void, Void> {
@Override
protected Void doInBackground(Void... params) {
dashboardData.getActiveMinutesTotal();
dashboardData.getActiveMinutesGoalFactor();
return null;
}
@Override
protected void onPostExecute(Void unused) {
super.onPostExecute(unused);
// Update text representation
long totalActiveMinutes = dashboardData.getActiveMinutesTotal();
String activeHours = String.format("%d", (int) Math.floor(totalActiveMinutes / 60f));
String activeMinutes = String.format("%02d", (int) (totalActiveMinutes % 60f));
activeTime.setText(activeHours + ":" + activeMinutes);
final int width = (int) TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
150,
GBApplication.getContext().getResources().getDisplayMetrics()
);
// Draw gauge
activeTimeGauge.setImageBitmap(drawGauge(width, Math.round(width * 0.075f), color_active_time, dashboardData.getActiveMinutesGoalFactor()));
}
drawSimpleGauge(
color_active_time,
dashboardData.getActiveMinutesGoalFactor()
);
}
}

View File

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

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2023-2024 Arjan Schrijver
/* Copyright (C) 2023-2024 Arjan Schrijver, José Rebelo
This file is part of Gadgetbridge.
@ -16,21 +16,8 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.activities.dashboard;
import android.content.res.Resources;
import android.os.AsyncTask;
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.activities.DashboardFragment;
import nodomain.freeyourgadget.gadgetbridge.util.FormatUtils;
@ -40,13 +27,9 @@ import nodomain.freeyourgadget.gadgetbridge.util.FormatUtils;
* Use the {@link DashboardDistanceWidget#newInstance} factory method to
* create an instance of this fragment.
*/
public class DashboardDistanceWidget extends AbstractDashboardWidget {
private static final Logger LOG = LoggerFactory.getLogger(DashboardDistanceWidget.class);
private TextView distanceText;
private ImageView distanceGauge;
public class DashboardDistanceWidget extends AbstractGaugeWidget {
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.
* @return A new instance of fragment DashboardDistanceWidget.
*/
public static DashboardDistanceWidget newInstance(DashboardFragment.DashboardData dashboardData) {
DashboardDistanceWidget fragment = new DashboardDistanceWidget();
Bundle args = new Bundle();
public static DashboardDistanceWidget newInstance(final DashboardFragment.DashboardData dashboardData) {
final DashboardDistanceWidget fragment = new DashboardDistanceWidget();
final Bundle args = new Bundle();
args.putSerializable(ARG_DASHBOARD_DATA, dashboardData);
fragment.setArguments(args);
return fragment;
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
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;
protected void populateData(final DashboardFragment.DashboardData dashboardData) {
dashboardData.getDistanceTotal();
dashboardData.getDistanceGoalFactor();
}
@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.getDistanceGoalFactor();
return null;
}
@Override
protected void onPostExecute(Void unused) {
super.onPostExecute(unused);
// Update text representation
String distanceFormatted = FormatUtils.getFormattedDistanceLabel(dashboardData.getDistanceTotal());
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()));
}
protected void draw(final DashboardFragment.DashboardData dashboardData) {
setText(FormatUtils.getFormattedDistanceLabel(dashboardData.getDistanceTotal()));
drawSimpleGauge(
color_distance,
dashboardData.getDistanceGoalFactor()
);
}
}

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2023-2024 Arjan Schrijver
/* Copyright (C) 2023-2024 Arjan Schrijver, José Rebelo
This file is part of Gadgetbridge.
@ -90,8 +90,6 @@ public class DashboardGoalsWidget extends AbstractDashboardWidget {
Prefs prefs = GBApplication.getPrefs();
legend.setVisibility(prefs.getBoolean("dashboard_widget_goals_legend", true) ? View.VISIBLE : View.GONE);
fillData();
return goalsView;
}
@ -118,6 +116,8 @@ public class DashboardGoalsWidget extends AbstractDashboardWidget {
@Override
protected Void doInBackground(Void... params) {
final long nanoStart = System.nanoTime();
int width = Resources.getSystem().getDisplayMetrics().widthPixels;
int height = width;
int barWidth = Math.round(height * 0.04f);
@ -160,6 +160,11 @@ public class DashboardGoalsWidget extends AbstractDashboardWidget {
paint.setStrokeWidth(barWidth);
paint.setColor(color_light_sleep);
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;
}

View File

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

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2023-2024 Arjan Schrijver
/* Copyright (C) 2023-2024 Arjan Schrijver, José Rebelo
This file is part of Gadgetbridge.
@ -16,34 +16,22 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.activities.dashboard;
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 org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Locale;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.DashboardFragment;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
/**
* A simple {@link AbstractDashboardWidget} subclass.
* Use the {@link DashboardSleepWidget#newInstance} factory method to
* create an instance of this fragment.
*/
public class DashboardSleepWidget extends AbstractDashboardWidget {
private static final Logger LOG = LoggerFactory.getLogger(DashboardSleepWidget.class);
private TextView sleepAmount;
private ImageView sleepGauge;
public class DashboardSleepWidget extends AbstractGaugeWidget {
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.
* @return A new instance of fragment DashboardSleepWidget.
*/
public static DashboardSleepWidget newInstance(DashboardFragment.DashboardData dashboardData) {
DashboardSleepWidget fragment = new DashboardSleepWidget();
Bundle args = new Bundle();
public static DashboardSleepWidget newInstance(final DashboardFragment.DashboardData dashboardData) {
final DashboardSleepWidget fragment = new DashboardSleepWidget();
final Bundle args = new Bundle();
args.putSerializable(ARG_DASHBOARD_DATA, dashboardData);
fragment.setArguments(args);
return fragment;
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View fragmentView = inflater.inflate(R.layout.dashboard_widget_sleep, container, false);
sleepAmount = fragmentView.findViewById(R.id.sleep_text);
sleepGauge = fragmentView.findViewById(R.id.sleep_gauge);
fillData();
return fragmentView;
protected boolean isSupportedBy(final GBDevice device) {
return device.getDeviceCoordinator().supportsSleepMeasurement();
}
@Override
public void onResume() {
super.onResume();
if (sleepAmount != null && sleepGauge != null) fillData();
protected void populateData(final DashboardFragment.DashboardData dashboardData) {
dashboardData.getSleepMinutesTotal();
dashboardData.getSleepMinutesGoalFactor();
}
@Override
protected void fillData() {
if (sleepGauge == null) return;
sleepGauge.post(new Runnable() {
@Override
public void run() {
FillDataAsyncTask myAsyncTask = new FillDataAsyncTask();
myAsyncTask.execute();
}
});
}
protected void draw(final DashboardFragment.DashboardData dashboardData) {
final long totalSleepMinutes = dashboardData.getSleepMinutesTotal();
final String valueText = String.format(
Locale.ROOT,
"%d:%02d",
(int) Math.floor(totalSleepMinutes / 60f),
(int) (totalSleepMinutes % 60f)
);
private class FillDataAsyncTask extends AsyncTask<Void, Void, Void> {
@Override
protected Void doInBackground(Void... params) {
dashboardData.getSleepMinutesTotal();
dashboardData.getSleepMinutesGoalFactor();
return null;
}
@Override
protected void onPostExecute(Void unused) {
super.onPostExecute(unused);
// Update text representation
long totalSleepMinutes = dashboardData.getSleepMinutesTotal();
String sleepHours = String.format("%d", (int) Math.floor(totalSleepMinutes / 60f));
String sleepMinutes = String.format("%02d", (int) (totalSleepMinutes % 60f));
sleepAmount.setText(sleepHours + ":" + sleepMinutes);
final int width = (int) TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
150,
GBApplication.getContext().getResources().getDisplayMetrics()
);
// Draw gauge
sleepGauge.setImageBitmap(drawGauge(width, Math.round(width * 0.075f), color_light_sleep, dashboardData.getSleepMinutesGoalFactor()));
}
setText(valueText);
drawSimpleGauge(
color_light_sleep,
dashboardData.getSleepMinutesGoalFactor()
);
}
}

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2023-2024 Arjan Schrijver
/* Copyright (C) 2023-2024 Arjan Schrijver, José Rebelo
This file is part of Gadgetbridge.
@ -16,19 +16,8 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.activities.dashboard;
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 org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.DashboardFragment;
@ -37,13 +26,9 @@ import nodomain.freeyourgadget.gadgetbridge.activities.DashboardFragment;
* Use the {@link DashboardStepsWidget#newInstance} factory method to
* create an instance of this fragment.
*/
public class DashboardStepsWidget extends AbstractDashboardWidget {
private static final Logger LOG = LoggerFactory.getLogger(DashboardStepsWidget.class);
private TextView stepsCount;
private ImageView stepsGauge;
public class DashboardStepsWidget extends AbstractGaugeWidget {
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.
* @return A new instance of fragment DashboardStepsWidget.
*/
public static DashboardStepsWidget newInstance(DashboardFragment.DashboardData dashboardData) {
DashboardStepsWidget fragment = new DashboardStepsWidget();
Bundle args = new Bundle();
public static DashboardStepsWidget newInstance(final DashboardFragment.DashboardData dashboardData) {
final DashboardStepsWidget fragment = new DashboardStepsWidget();
final Bundle args = new Bundle();
args.putSerializable(ARG_DASHBOARD_DATA, dashboardData);
fragment.setArguments(args);
return fragment;
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
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;
protected void populateData(final DashboardFragment.DashboardData dashboardData) {
dashboardData.getStepsTotal();
dashboardData.getStepsGoalFactor();
}
@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.getStepsGoalFactor();
return null;
}
@Override
protected void onPostExecute(Void unused) {
super.onPostExecute(unused);
// Update text representation
stepsCount.setText(String.valueOf(dashboardData.getStepsTotal()));
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()));
}
protected void draw(final DashboardFragment.DashboardData dashboardData) {
setText(String.valueOf(dashboardData.getStepsTotal()));
drawSimpleGauge(
color_activity,
dashboardData.getStepsGoalFactor()
);
}
}

View File

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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2023-2024 Arjan Schrijver
/* Copyright (C) 2023-2024 Arjan Schrijver, José Rebelo
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);
if (dashboardData.generalizedActivities.isEmpty()) {
fillData();
} else {
if (!dashboardData.generalizedActivities.isEmpty()) {
draw();
}
@ -147,7 +145,11 @@ public class DashboardTodayWidget extends AbstractDashboardWidget {
int height = width;
int barWidth = Math.round(width * 0.08f);
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 innerCircleMargin = outerCircleMargin + barWidth * 1.3f;
float degreeFactor = mode_24h ? 240 : 120;
@ -168,7 +170,7 @@ public class DashboardTodayWidget extends AbstractDashboardWidget {
}
// Draw hours
boolean normalClock = DateFormat.is24HourFormat(getContext());
boolean normalClock = DateFormat.is24HourFormat(GBApplication.getContext());
Map<Integer, String> hours = new HashMap<Integer, String>() {
{
put(0, normalClock ? (mode_24h ? "0" : "12") : "12pm");
@ -435,6 +437,8 @@ public class DashboardTodayWidget extends AbstractDashboardWidget {
@Override
protected Void doInBackground(Void... params) {
final long nanoStart = System.nanoTime();
// Retrieve activity data
dashboardData.generalizedActivities.clear();
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);
}
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;
}
@Override
protected void onPostExecute(Void unused) {
protected void onPostExecute(final Void unused) {
super.onPostExecute(unused);
try {
draw();
} catch (IllegalStateException e) {
LOG.warn("calling draw() failed: " + e.getMessage());
} catch (final Exception e) {
LOG.error("calling draw() failed", e);
}
}
}

View File

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

View File

@ -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;
import androidx.fragment.app.FragmentManager;
@ -7,7 +23,7 @@ import java.util.List;
import java.util.stream.Collectors;
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBFragment;
abstract class NestedFragmentAdapter extends FragmentStateAdapter {
public abstract class NestedFragmentAdapter extends FragmentStateAdapter {
protected FragmentManager fragmentManager;
public NestedFragmentAdapter(AbstractGBFragment fragment, FragmentManager childFragmentManager) {

View File

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

View File

@ -1,41 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
<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.DashboardDistanceWidget">
tools:context=".activities.dashboard.AbstractDashboardWidget">
<RelativeLayout
android:id="@+id/card_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:orientation="vertical"
android:id="@+id/card_layout">
tools:ignore="UselessParent">
<ImageView
android:id="@+id/gauge_bar"
android:layout_width="150dp"
android:layout_height="75dp"
android:layout_centerHorizontal="true"
android:scaleType="fitStart"
android:id="@+id/distance_gauge" />
android:scaleType="fitStart" />
<TextView
android:id="@+id/distance_text"
android:id="@+id/gauge_value"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_marginTop="28dp"
android:layout_centerHorizontal="true"
android:text="0.0km"
android:textSize="30dp" />
android:text="@string/stats_empty_value"
android:textSize="30sp" />
<TextView
android:id="@+id/gauge_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/distance_text"
android:layout_below="@+id/gauge_value"
android:layout_centerHorizontal="true"
android:text="@string/distance" />
android:text="@string/no_data" />
</RelativeLayout>

View File

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

View File

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

View File

@ -9,4 +9,10 @@
android:title="@string/menuitem_calendar"
app:iconTint="?attr/actionmenu_icon_color"
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>

View File

@ -4167,6 +4167,11 @@
<item>@string/distance</item>
<item>@string/active_time</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 name="pref_dashboard_widgets_order_values">
@ -4176,5 +4181,22 @@
<item>distance</item>
<item>activetime</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>
</resources>

View File

@ -52,10 +52,11 @@
<color name="hrv_status_poor" type="color">#be03fc</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_lost_color" type="color">#ff6c43</color>
<color name="steps_color" type="color">#00c9bf</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_dark">#545254</color>

View File

@ -628,6 +628,8 @@
<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="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_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>
@ -1871,6 +1873,9 @@
<string name="menuitem_more">More</string>
<string name="menuitem_nfc">NFC</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_hr">Heart Rate</string>
<string name="menuitem_spo2">SpO2</string>

View File

@ -20,7 +20,7 @@
android:summary="@string/pref_dashboard_cards_summary"
app:iconSpaceReserved="false" />
<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:entries="@array/pref_dashboard_widgets_order"
android:entryValues="@array/pref_dashboard_widgets_order_values"