1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-12-26 02:25:50 +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 SharedPreferences sharedPrefs;
private static final String PREFS_VERSION = "shared_preferences_version"; private static final String PREFS_VERSION = "shared_preferences_version";
//if preferences have to be migrated, increment the following and add the migration logic in migratePrefs below; see http://stackoverflow.com/questions/16397848/how-can-i-migrate-android-preferences-with-a-new-version //if preferences have to be migrated, increment the following and add the migration logic in migratePrefs below; see http://stackoverflow.com/questions/16397848/how-can-i-migrate-android-preferences-with-a-new-version
private static final int CURRENT_PREFS_VERSION = 36; private static final int CURRENT_PREFS_VERSION = 37;
private static final LimitedQueue<Integer, String> mIDSenderLookup = new LimitedQueue<>(16); private static final LimitedQueue<Integer, String> mIDSenderLookup = new LimitedQueue<>(16);
private static GBPrefs prefs; private static GBPrefs prefs;
@ -1718,6 +1718,13 @@ public class GBApplication extends Application {
} }
} }
if (oldVersion < 37) {
// Add new dashboard widgets
final String dashboardWidgetsOrder = sharedPrefs.getString("pref_dashboard_widgets_order", null);
if (!StringUtils.isBlank(dashboardWidgetsOrder) && !dashboardWidgetsOrder.contains("bodyenergy")) {
editor.putString("pref_dashboard_widgets_order", dashboardWidgetsOrder + ",bodyenergy,stress_segmented,hrv");
}
}
editor.putString(PREFS_VERSION, Integer.toString(CURRENT_PREFS_VERSION)); editor.putString(PREFS_VERSION, Integer.toString(CURRENT_PREFS_VERSION));
editor.apply(); editor.apply();

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. This file is part of Gadgetbridge.
@ -16,6 +16,7 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. */ along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.activities; package nodomain.freeyourgadget.gadgetbridge.activities;
import android.app.Activity;
import android.content.BroadcastReceiver; import android.content.BroadcastReceiver;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
@ -30,8 +31,12 @@ import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.TextView; import android.widget.TextView;
import androidx.activity.result.ActivityResult;
import androidx.activity.result.ActivityResultCallback;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.core.view.MenuProvider;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentContainerView; import androidx.fragment.app.FragmentContainerView;
import androidx.gridlayout.widget.GridLayout; import androidx.gridlayout.widget.GridLayout;
@ -44,23 +49,31 @@ import org.slf4j.LoggerFactory;
import java.io.Serializable; import java.io.Serializable;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar; import java.util.Calendar;
import java.util.Collections; import java.util.Collections;
import java.util.GregorianCalendar; import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Supplier;
import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.dashboard.AbstractDashboardWidget; import nodomain.freeyourgadget.gadgetbridge.activities.dashboard.AbstractDashboardWidget;
import nodomain.freeyourgadget.gadgetbridge.activities.dashboard.DashboardActiveTimeWidget; import nodomain.freeyourgadget.gadgetbridge.activities.dashboard.DashboardActiveTimeWidget;
import nodomain.freeyourgadget.gadgetbridge.activities.dashboard.DashboardBodyEnergyWidget;
import nodomain.freeyourgadget.gadgetbridge.activities.dashboard.DashboardCalendarActivity; import nodomain.freeyourgadget.gadgetbridge.activities.dashboard.DashboardCalendarActivity;
import nodomain.freeyourgadget.gadgetbridge.activities.dashboard.DashboardDistanceWidget; import nodomain.freeyourgadget.gadgetbridge.activities.dashboard.DashboardDistanceWidget;
import nodomain.freeyourgadget.gadgetbridge.activities.dashboard.DashboardGoalsWidget; import nodomain.freeyourgadget.gadgetbridge.activities.dashboard.DashboardGoalsWidget;
import nodomain.freeyourgadget.gadgetbridge.activities.dashboard.DashboardHrvWidget;
import nodomain.freeyourgadget.gadgetbridge.activities.dashboard.DashboardSleepWidget; import nodomain.freeyourgadget.gadgetbridge.activities.dashboard.DashboardSleepWidget;
import nodomain.freeyourgadget.gadgetbridge.activities.dashboard.DashboardStepsWidget; import nodomain.freeyourgadget.gadgetbridge.activities.dashboard.DashboardStepsWidget;
import nodomain.freeyourgadget.gadgetbridge.activities.dashboard.DashboardStressBreakdownWidget;
import nodomain.freeyourgadget.gadgetbridge.activities.dashboard.DashboardStressSegmentedWidget;
import nodomain.freeyourgadget.gadgetbridge.activities.dashboard.DashboardStressSimpleWidget;
import nodomain.freeyourgadget.gadgetbridge.activities.dashboard.DashboardTodayWidget; import nodomain.freeyourgadget.gadgetbridge.activities.dashboard.DashboardTodayWidget;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind; import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
@ -68,23 +81,28 @@ import nodomain.freeyourgadget.gadgetbridge.util.DashboardUtils;
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils; import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs; import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
public class DashboardFragment extends Fragment { public class DashboardFragment extends Fragment implements MenuProvider {
private static final Logger LOG = LoggerFactory.getLogger(DashboardFragment.class); private static final Logger LOG = LoggerFactory.getLogger(DashboardFragment.class);
private Calendar day = GregorianCalendar.getInstance(); private final Calendar day = GregorianCalendar.getInstance();
private TextView textViewDate; private TextView textViewDate;
private TextView arrowLeft;
private TextView arrowRight; private TextView arrowRight;
private GridLayout gridLayout; private GridLayout gridLayout;
private DashboardTodayWidget todayWidget; private final Map<String, AbstractDashboardWidget> widgetMap = new HashMap<>();
private DashboardGoalsWidget goalsWidget;
private DashboardStepsWidget stepsWidget;
private DashboardDistanceWidget distanceWidget;
private DashboardActiveTimeWidget activeTimeWidget;
private DashboardSleepWidget sleepWidget;
private DashboardData dashboardData = new DashboardData(); private DashboardData dashboardData = new DashboardData();
private boolean isConfigChanged = false; private boolean isConfigChanged = false;
private ActivityResultLauncher<Intent> calendarLauncher;
private final ActivityResultCallback<ActivityResult> calendarCallback = result -> {
if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) {
long timeMillis = result.getData().getLongExtra(DashboardCalendarActivity.EXTRA_TIMESTAMP, 0);
if (timeMillis != 0) {
day.setTimeInMillis(timeMillis);
fullRefresh();
}
}
};
public static final String ACTION_CONFIG_CHANGE = "nodomain.freeyourgadget.gadgetbridge.activities.dashboardfragment.action.config_change"; public static final String ACTION_CONFIG_CHANGE = "nodomain.freeyourgadget.gadgetbridge.activities.dashboardfragment.action.config_change";
private final BroadcastReceiver mReceiver = new BroadcastReceiver() { private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@ -95,7 +113,7 @@ public class DashboardFragment extends Fragment {
switch (action) { switch (action) {
case GBApplication.ACTION_NEW_DATA: case GBApplication.ACTION_NEW_DATA:
final GBDevice dev = intent.getParcelableExtra(GBDevice.EXTRA_DEVICE); final GBDevice dev = intent.getParcelableExtra(GBDevice.EXTRA_DEVICE);
if (dev != null && !dev.isBusy()) { if (dev != null) {
if (dashboardData.showAllDevices || dashboardData.showDeviceList.contains(dev.getAddress())) { if (dashboardData.showAllDevices || dashboardData.showDeviceList.contains(dev.getAddress())) {
refresh(); refresh();
} }
@ -109,20 +127,25 @@ public class DashboardFragment extends Fragment {
}; };
@Override @Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
super.onCreateView(inflater, container, savedInstanceState); super.onCreateView(inflater, container, savedInstanceState);
View dashboardView = inflater.inflate(R.layout.fragment_dashboard, container, false); View dashboardView = inflater.inflate(R.layout.fragment_dashboard, container, false);
setHasOptionsMenu(true); requireActivity().addMenuProvider(this);
textViewDate = dashboardView.findViewById(R.id.dashboard_date); textViewDate = dashboardView.findViewById(R.id.dashboard_date);
gridLayout = dashboardView.findViewById(R.id.dashboard_gridlayout); gridLayout = dashboardView.findViewById(R.id.dashboard_gridlayout);
calendarLauncher = registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(),
calendarCallback
);
// Increase column count on landscape, tablets and open foldables // Increase column count on landscape, tablets and open foldables
DisplayMetrics displayMetrics = getResources().getDisplayMetrics(); DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
if (displayMetrics.widthPixels / displayMetrics.density >= 600) { if (displayMetrics.widthPixels / displayMetrics.density >= 600) {
gridLayout.setColumnCount(4); gridLayout.setColumnCount(4);
} }
arrowLeft = dashboardView.findViewById(R.id.arrow_left); final TextView arrowLeft = dashboardView.findViewById(R.id.arrow_left);
arrowLeft.setOnClickListener(v -> { arrowLeft.setOnClickListener(v -> {
day.add(Calendar.DAY_OF_MONTH, -1); day.add(Calendar.DAY_OF_MONTH, -1);
refresh(); refresh();
@ -155,7 +178,7 @@ public class DashboardFragment extends Fragment {
if (isConfigChanged) { if (isConfigChanged) {
isConfigChanged = false; isConfigChanged = false;
fullRefresh(); fullRefresh();
} else if (dashboardData.isEmpty() || todayWidget == null) { } else if (dashboardData.isEmpty() || !widgetMap.containsKey("today")) {
refresh(); refresh();
} }
} }
@ -173,43 +196,29 @@ public class DashboardFragment extends Fragment {
} }
@Override @Override
public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) { public void onCreateMenu(@NonNull final Menu menu, @NonNull final MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
inflater.inflate(R.menu.dashboard_menu, menu); inflater.inflate(R.menu.dashboard_menu, menu);
} }
@Override @Override
public boolean onOptionsItemSelected(@NonNull final MenuItem item) { public boolean onMenuItemSelected(@NonNull final MenuItem item) {
final int itemId = item.getItemId(); final int itemId = item.getItemId();
if (itemId == R.id.dashboard_show_calendar) { if (itemId == R.id.dashboard_show_calendar) {
final Intent intent = new Intent(requireActivity(), DashboardCalendarActivity.class); final Intent intent = new Intent(requireActivity(), DashboardCalendarActivity.class);
intent.putExtra(DashboardCalendarActivity.EXTRA_TIMESTAMP, day.getTimeInMillis()); intent.putExtra(DashboardCalendarActivity.EXTRA_TIMESTAMP, day.getTimeInMillis());
startActivityForResult(intent, 0); calendarLauncher.launch(intent);
return false; return true;
} } else if (itemId == R.id.dashboard_settings) {
return super.onOptionsItemSelected(item); final Intent intent = new Intent(requireActivity(), DashboardPreferencesActivity.class);
} startActivity(intent);
return true;
@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();
}
} }
return false;
} }
private void fullRefresh() { private void fullRefresh() {
gridLayout.removeAllViews(); gridLayout.removeAllViews();
todayWidget = null; widgetMap.clear();
goalsWidget = null;
stepsWidget = null;
distanceWidget = null;
activeTimeWidget = null;
sleepWidget = null;
refresh(); refresh();
} }
@ -229,13 +238,13 @@ public class DashboardFragment extends Fragment {
private void draw() { private void draw() {
Prefs prefs = GBApplication.getPrefs(); Prefs prefs = GBApplication.getPrefs();
String defaultWidgetsOrder = String.join(",", getResources().getStringArray(R.array.pref_dashboard_widgets_order_values)); String defaultWidgetsOrder = String.join(",", getResources().getStringArray(R.array.pref_dashboard_widgets_order_default));
String widgetsOrderPref = prefs.getString("pref_dashboard_widgets_order", defaultWidgetsOrder); String widgetsOrderPref = prefs.getString("pref_dashboard_widgets_order", defaultWidgetsOrder);
List<String> widgetsOrder = Arrays.asList(widgetsOrderPref.split(",")); String[] widgetsOrder = widgetsOrderPref.split(",");
Calendar today = GregorianCalendar.getInstance(); Calendar today = GregorianCalendar.getInstance();
if (DateTimeUtils.isSameDay(today, day)) { if (DateTimeUtils.isSameDay(today, day)) {
textViewDate.setText(getContext().getString(R.string.activity_summary_today)); textViewDate.setText(requireContext().getString(R.string.activity_summary_today));
arrowRight.setAlpha(0.5f); arrowRight.setAlpha(0.5f);
} else { } else {
textViewDate.setText(DateTimeUtils.formatDate(day.getTime())); textViewDate.setText(DateTimeUtils.formatDate(day.getTime()));
@ -245,55 +254,55 @@ public class DashboardFragment extends Fragment {
boolean cardsEnabled = prefs.getBoolean("dashboard_cards_enabled", true); boolean cardsEnabled = prefs.getBoolean("dashboard_cards_enabled", true);
for (String widgetName : widgetsOrder) { for (String widgetName : widgetsOrder) {
switch (widgetName) { AbstractDashboardWidget widget = widgetMap.get(widgetName);
case "today": if (widget == null) {
if (todayWidget == null) { int columnSpan = 1;
todayWidget = DashboardTodayWidget.newInstance(dashboardData); switch (widgetName) {
createWidget(todayWidget, cardsEnabled, prefs.getBoolean("dashboard_widget_today_2columns", true) ? 2 : 1); case "today":
} else { widget = DashboardTodayWidget.newInstance(dashboardData);
todayWidget.update(); columnSpan = prefs.getBoolean("dashboard_widget_today_2columns", true) ? 2 : 1;
} break;
break; case "goals":
case "goals": widget = DashboardGoalsWidget.newInstance(dashboardData);
if (goalsWidget == null) { columnSpan = prefs.getBoolean("dashboard_widget_goals_2columns", true) ? 2 : 1;
goalsWidget = DashboardGoalsWidget.newInstance(dashboardData); break;
createWidget(goalsWidget, cardsEnabled, prefs.getBoolean("dashboard_widget_goals_2columns", true) ? 2 : 1); case "steps":
} else { widget = DashboardStepsWidget.newInstance(dashboardData);
goalsWidget.update(); break;
} case "distance":
break; widget = DashboardDistanceWidget.newInstance(dashboardData);
case "steps": break;
if (stepsWidget == null) { case "activetime":
stepsWidget = DashboardStepsWidget.newInstance(dashboardData); widget = DashboardActiveTimeWidget.newInstance(dashboardData);
createWidget(stepsWidget, cardsEnabled, 1); break;
} else { case "sleep":
stepsWidget.update(); widget = DashboardSleepWidget.newInstance(dashboardData);
} break;
break; case "stress_simple":
case "distance": widget = DashboardStressSimpleWidget.newInstance(dashboardData);
if (distanceWidget == null) { break;
distanceWidget = DashboardDistanceWidget.newInstance(dashboardData); case "stress_segmented":
createWidget(distanceWidget, cardsEnabled, 1); widget = DashboardStressSegmentedWidget.newInstance(dashboardData);
} else { break;
distanceWidget.update(); case "stress_breakdown":
} widget = DashboardStressBreakdownWidget.newInstance(dashboardData);
break; break;
case "activetime": case "bodyenergy":
if (activeTimeWidget == null) { widget = DashboardBodyEnergyWidget.newInstance(dashboardData);
activeTimeWidget = DashboardActiveTimeWidget.newInstance(dashboardData); break;
createWidget(activeTimeWidget, cardsEnabled, 1); case "hrv":
} else { widget = DashboardHrvWidget.newInstance(dashboardData);
activeTimeWidget.update(); break;
} default:
break; LOG.error("Unknown dashboard widget {}", widgetName);
case "sleep": continue;
if (sleepWidget == null) { }
sleepWidget = DashboardSleepWidget.newInstance(dashboardData);
createWidget(sleepWidget, cardsEnabled, 1); createWidget(widget, cardsEnabled, columnSpan);
} else {
sleepWidget.update(); widgetMap.put(widgetName, widget);
} } else {
break; widget.update();
} }
} }
} }
@ -309,8 +318,8 @@ public class DashboardFragment extends Fragment {
.commit(); .commit();
GridLayout.LayoutParams layoutParams = new GridLayout.LayoutParams( GridLayout.LayoutParams layoutParams = new GridLayout.LayoutParams(
GridLayout.spec(GridLayout.UNDEFINED, GridLayout.FILL,1f), GridLayout.spec(GridLayout.UNDEFINED, GridLayout.FILL, 1f),
GridLayout.spec(GridLayout.UNDEFINED, columnSpan, GridLayout.FILL,1f) GridLayout.spec(GridLayout.UNDEFINED, columnSpan, GridLayout.FILL, 1f)
); );
layoutParams.width = 0; layoutParams.width = 0;
int pixels_8dp = (int) (8 * scale + 0.5f); int pixels_8dp = (int) (8 * scale + 0.5f);
@ -352,6 +361,7 @@ public class DashboardFragment extends Fragment {
private float distanceGoalFactor; private float distanceGoalFactor;
private long activeMinutesTotal; private long activeMinutesTotal;
private float activeMinutesGoalFactor; private float activeMinutesGoalFactor;
private final Map<String, Serializable> genericData = new ConcurrentHashMap<>();
public void clear() { public void clear() {
stepsTotal = 0; stepsTotal = 0;
@ -363,6 +373,7 @@ public class DashboardFragment extends Fragment {
activeMinutesTotal = 0; activeMinutesTotal = 0;
activeMinutesGoalFactor = 0; activeMinutesGoalFactor = 0;
generalizedActivities.clear(); generalizedActivities.clear();
genericData.clear();
} }
public boolean isEmpty() { public boolean isEmpty() {
@ -374,6 +385,7 @@ public class DashboardFragment extends Fragment {
distanceGoalFactor == 0 && distanceGoalFactor == 0 &&
activeMinutesTotal == 0 && activeMinutesTotal == 0 &&
activeMinutesGoalFactor == 0 && activeMinutesGoalFactor == 0 &&
genericData.isEmpty() &&
generalizedActivities.isEmpty()); generalizedActivities.isEmpty());
} }
@ -425,6 +437,21 @@ public class DashboardFragment extends Fragment {
return sleepGoalFactor; return sleepGoalFactor;
} }
public void put(final String key, final Serializable value) {
genericData.put(key, value);
}
public Serializable get(final String key) {
return genericData.get(key);
}
/**
* @noinspection UnusedReturnValue
*/
public Serializable computeIfAbsent(final String key, final Supplier<Serializable> supplier) {
return genericData.computeIfAbsent(key, absent -> supplier.get());
}
public static class GeneralizedActivity implements Serializable { public static class GeneralizedActivity implements Serializable {
public ActivityKind activityKind; public ActivityKind activityKind;
public long timeFrom; public long timeFrom;

View File

@ -16,6 +16,7 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. */ along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.activities.charts; package nodomain.freeyourgadget.gadgetbridge.activities.charts;
import android.app.Activity;
import android.app.DatePickerDialog; import android.app.DatePickerDialog;
import android.content.BroadcastReceiver; import android.content.BroadcastReceiver;
import android.content.Context; import android.content.Context;
@ -29,18 +30,27 @@ import android.widget.Button;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import androidx.activity.result.ActivityResult;
import androidx.activity.result.ActivityResultCallback;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.appcompat.app.ActionBar;
import androidx.localbroadcastmanager.content.LocalBroadcastManager; import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import androidx.viewpager.widget.ViewPager; import androidx.viewpager.widget.ViewPager;
import com.google.android.material.tabs.TabLayout;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.Calendar; import java.util.Calendar;
import java.util.Collections;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import java.util.Locale;
import java.util.Objects; import java.util.Objects;
import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.GBApplication;
@ -56,8 +66,10 @@ public abstract class AbstractChartsActivity extends AbstractGBFragmentActivity
public static final String STATE_START_DATE = "stateStartDate"; public static final String STATE_START_DATE = "stateStartDate";
public static final String STATE_END_DATE = "stateEndDate"; public static final String STATE_END_DATE = "stateEndDate";
public static final String EXTRA_FRAGMENT_ID = "fragment"; public static final String EXTRA_FRAGMENT_ID = "fragmentId";
public static final int REQUEST_CODE_PREFERENCES = 1; public static final String EXTRA_SINGLE_FRAGMENT_NAME = "singleFragmentName";
public static final String EXTRA_ACTIONBAR_TITLE = "actionbarTitle";
public static final String EXTRA_TIMESTAMP = "timestamp";
private TextView mDateControl; private TextView mDateControl;
@ -70,13 +82,19 @@ public abstract class AbstractChartsActivity extends AbstractGBFragmentActivity
private GBDevice mGBDevice; private GBDevice mGBDevice;
private ViewGroup dateBar; private ViewGroup dateBar;
private ActivityResultLauncher<Intent> chartsPreferencesLauncher;
private final ActivityResultCallback<ActivityResult> chartsPreferencesCallback = result -> {
recreate();
};
private final BroadcastReceiver mReceiver = new BroadcastReceiver() { private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override @Override
public void onReceive(Context context, Intent intent) { public void onReceive(final Context context, final Intent intent) {
String action = intent.getAction(); final String action = intent.getAction();
//noinspection SwitchStatementWithTooFewBranches
switch (Objects.requireNonNull(action)) { switch (Objects.requireNonNull(action)) {
case GBDevice.ACTION_DEVICE_CHANGED: case GBDevice.ACTION_DEVICE_CHANGED:
GBDevice dev = intent.getParcelableExtra(GBDevice.EXTRA_DEVICE); final GBDevice dev = intent.getParcelableExtra(GBDevice.EXTRA_DEVICE);
if (dev != null) { if (dev != null) {
refreshBusyState(dev); refreshBusyState(dev);
} }
@ -85,11 +103,11 @@ public abstract class AbstractChartsActivity extends AbstractGBFragmentActivity
} }
}; };
private void refreshBusyState(GBDevice dev) { private void refreshBusyState(final GBDevice dev) {
if (dev.isBusy()) { if (dev.isBusy()) {
swipeLayout.setRefreshing(true); swipeLayout.setRefreshing(true);
} else { } else {
boolean wasBusy = swipeLayout.isRefreshing(); final boolean wasBusy = swipeLayout.isRefreshing();
swipeLayout.setRefreshing(false); swipeLayout.setRefreshing(false);
if (wasBusy) { if (wasBusy) {
LocalBroadcastManager.getInstance(this).sendBroadcast(new Intent(REFRESH)); LocalBroadcastManager.getInstance(this).sendBroadcast(new Intent(REFRESH));
@ -99,31 +117,51 @@ public abstract class AbstractChartsActivity extends AbstractGBFragmentActivity
} }
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
setContentView(R.layout.activity_charts); setContentView(R.layout.activity_charts);
int tabFragmentToOpen = -1;
final Bundle extras = getIntent().getExtras();
if (extras == null) {
throw new IllegalArgumentException("Must provide a device when invoking this activity");
}
mGBDevice = extras.getParcelable(GBDevice.EXTRA_DEVICE);
chartsPreferencesLauncher = registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(),
chartsPreferencesCallback
);
// Set start and end date
if (savedInstanceState != null) { if (savedInstanceState != null) {
setEndDate(new Date(savedInstanceState.getLong(STATE_END_DATE, System.currentTimeMillis()))); setEndDate(new Date(savedInstanceState.getLong(STATE_END_DATE, System.currentTimeMillis())));
setStartDate(new Date(savedInstanceState.getLong(STATE_START_DATE, DateTimeUtils.shiftByDays(getEndDate(), -1).getTime()))); } else if (extras.containsKey(EXTRA_TIMESTAMP)) {
final int endTimestamp = extras.getInt(EXTRA_TIMESTAMP, 0);
setEndDate(new Date(endTimestamp * 1000L));
} else { } else {
setEndDate(new Date()); setEndDate(new Date());
setStartDate(DateTimeUtils.shiftByDays(getEndDate(), -1));
} }
setStartDate(DateTimeUtils.shiftByDays(getEndDate(), -1));
final IntentFilter filterLocal = new IntentFilter(); final IntentFilter filterLocal = new IntentFilter();
filterLocal.addAction(GBDevice.ACTION_DEVICE_CHANGED); filterLocal.addAction(GBDevice.ACTION_DEVICE_CHANGED);
LocalBroadcastManager.getInstance(this).registerReceiver(mReceiver, filterLocal); LocalBroadcastManager.getInstance(this).registerReceiver(mReceiver, filterLocal);
final Bundle extras = getIntent().getExtras(); // Open the specified fragment, if any, and setup single page view if specified
if (extras != null) { final int tabFragmentIdToOpen = extras.getInt(EXTRA_FRAGMENT_ID, -1);
mGBDevice = extras.getParcelable(GBDevice.EXTRA_DEVICE); final String singleFragmentName = extras.getString(EXTRA_SINGLE_FRAGMENT_NAME, null);
tabFragmentToOpen = extras.getInt(EXTRA_FRAGMENT_ID); final int actionbarTitle = extras.getInt(EXTRA_ACTIONBAR_TITLE, 0);
} else {
throw new IllegalArgumentException("Must provide a device when invoking this activity"); if (tabFragmentIdToOpen >= 0 && singleFragmentName != null) {
throw new IllegalArgumentException("Must specify either fragment ID or single fragment name, not both");
}
if (singleFragmentName != null) {
enabledTabsList = Collections.singletonList(singleFragmentName);
} else {
enabledTabsList = fillChartsTabsList();
} }
enabledTabsList = fillChartsTabsList();
swipeLayout = findViewById(R.id.activity_swipe_layout); swipeLayout = findViewById(R.id.activity_swipe_layout);
swipeLayout.setOnRefreshListener(this::fetchRecordedData); swipeLayout.setOnRefreshListener(this::fetchRecordedData);
@ -132,8 +170,23 @@ public abstract class AbstractChartsActivity extends AbstractGBFragmentActivity
// Set up the ViewPager with the sections adapter. // Set up the ViewPager with the sections adapter.
final NonSwipeableViewPager viewPager = findViewById(R.id.charts_pager); final NonSwipeableViewPager viewPager = findViewById(R.id.charts_pager);
viewPager.setAdapter(getPagerAdapter()); viewPager.setAdapter(getPagerAdapter());
if (tabFragmentToOpen > -1) { if (tabFragmentIdToOpen > -1) {
viewPager.setCurrentItem(tabFragmentToOpen); // open the tab as specified in the intent viewPager.setCurrentItem(tabFragmentIdToOpen); // open the tab as specified in the intent
}
viewPager.setAllowSwipe(singleFragmentName == null && GBApplication.getPrefs().getBoolean("charts_allow_swipe", true));
if (singleFragmentName != null) {
final TabLayout tabLayout = findViewById(R.id.charts_pagerTabStrip);
tabLayout.setVisibility(TextView.GONE);
}
if (actionbarTitle != 0) {
final ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
actionBar.setTitle(actionbarTitle);
}
} }
viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() { viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
@ -158,19 +211,19 @@ public abstract class AbstractChartsActivity extends AbstractGBFragmentActivity
new ShowDurationDialog(detailedDuration, AbstractChartsActivity.this).show(); new ShowDurationDialog(detailedDuration, AbstractChartsActivity.this).show();
}); });
Button mPrevButton = findViewById(R.id.charts_previous_day); final Button mPrevButton = findViewById(R.id.charts_previous_day);
mPrevButton.setOnClickListener(v -> handleButtonClicked(DATE_PREV_DAY)); mPrevButton.setOnClickListener(v -> handleButtonClicked(DATE_PREV_DAY));
Button mNextButton = findViewById(R.id.charts_next_day); final Button mNextButton = findViewById(R.id.charts_next_day);
mNextButton.setOnClickListener(v -> handleButtonClicked(DATE_NEXT_DAY)); mNextButton.setOnClickListener(v -> handleButtonClicked(DATE_NEXT_DAY));
Button mPrevWeekButton = findViewById(R.id.charts_previous_week); final Button mPrevWeekButton = findViewById(R.id.charts_previous_week);
mPrevWeekButton.setOnClickListener(v -> handleButtonClicked(DATE_PREV_WEEK)); mPrevWeekButton.setOnClickListener(v -> handleButtonClicked(DATE_PREV_WEEK));
Button mNextWeekButton = findViewById(R.id.charts_next_week); final Button mNextWeekButton = findViewById(R.id.charts_next_week);
mNextWeekButton.setOnClickListener(v -> handleButtonClicked(DATE_NEXT_WEEK)); mNextWeekButton.setOnClickListener(v -> handleButtonClicked(DATE_NEXT_WEEK));
Button mPrevMonthButton = findViewById(R.id.charts_previous_month); final Button mPrevMonthButton = findViewById(R.id.charts_previous_month);
mPrevMonthButton.setOnClickListener(v -> handleButtonClicked(DATE_PREV_MONTH)); mPrevMonthButton.setOnClickListener(v -> handleButtonClicked(DATE_PREV_MONTH));
Button mNextMonthButton = findViewById(R.id.charts_next_month); final Button mNextMonthButton = findViewById(R.id.charts_next_month);
mNextMonthButton.setOnClickListener(v -> handleButtonClicked(DATE_NEXT_MONTH)); mNextMonthButton.setOnClickListener(v -> handleButtonClicked(DATE_NEXT_MONTH));
} }
@ -193,7 +246,7 @@ public abstract class AbstractChartsActivity extends AbstractGBFragmentActivity
protected abstract List<String> fillChartsTabsList(); protected abstract List<String> fillChartsTabsList();
private String formatDetailedDuration() { private String formatDetailedDuration() {
final SimpleDateFormat dateFormat = new SimpleDateFormat("dd.MM.yyyy HH:mm"); final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.getDefault());
final String dateStringFrom = dateFormat.format(getStartDate()); final String dateStringFrom = dateFormat.format(getStartDate());
final String dateStringTo = dateFormat.format(getEndDate()); final String dateStringTo = dateFormat.format(getEndDate());
@ -262,15 +315,7 @@ public abstract class AbstractChartsActivity extends AbstractGBFragmentActivity
} }
@Override @Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) { public boolean onOptionsItemSelected(final MenuItem item) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_CODE_PREFERENCES) {
this.recreate();
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
final int itemId = item.getItemId(); final int itemId = item.getItemId();
if (itemId == R.id.charts_fetch_activity_data) { if (itemId == R.id.charts_fetch_activity_data) {
fetchRecordedData(); fetchRecordedData();
@ -285,8 +330,8 @@ public abstract class AbstractChartsActivity extends AbstractGBFragmentActivity
LocalBroadcastManager.getInstance(this).sendBroadcast(new Intent(REFRESH)); LocalBroadcastManager.getInstance(this).sendBroadcast(new Intent(REFRESH));
}, currentDate.get(Calendar.YEAR), currentDate.get(Calendar.MONTH), currentDate.get(Calendar.DATE)).show(); }, currentDate.get(Calendar.YEAR), currentDate.get(Calendar.MONTH), currentDate.get(Calendar.DATE)).show();
} else if (itemId == R.id.prefs_charts_menu) { } else if (itemId == R.id.prefs_charts_menu) {
Intent settingsIntent = new Intent(this, ChartsPreferencesActivity.class); final Intent settingsIntent = new Intent(this, ChartsPreferencesActivity.class);
startActivityForResult(settingsIntent, REQUEST_CODE_PREFERENCES); chartsPreferencesLauncher.launch(settingsIntent);
return true; return true;
} }
@ -294,7 +339,7 @@ public abstract class AbstractChartsActivity extends AbstractGBFragmentActivity
} }
@Override @Override
public void enableSwipeRefresh(boolean enable) { public void enableSwipeRefresh(final boolean enable) {
swipeLayout.setEnabled(enable && allowRefresh()); swipeLayout.setEnabled(enable && allowRefresh());
} }

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": case "activitylist":
return new ActivityListingChartFragment(); return new ActivityListingChartFragment();
case "sleep": case "sleep":
return new SleepCollectionFragment(); return SleepCollectionFragment.newInstance(enabledTabsList.size() == 1);
case "hrvstatus": case "hrvstatus":
return new HRVStatusFragment(); return new HRVStatusFragment();
case "bodyenergy": case "bodyenergy":
@ -155,7 +155,7 @@ public class ActivityChartsActivity extends AbstractChartsActivity {
case "pai": case "pai":
return new PaiChartFragment(); return new PaiChartFragment();
case "stepsweek": case "stepsweek":
return new StepsCollectionFragment(); return StepsCollectionFragment.newInstance(enabledTabsList.size() == 1);
case "speedzones": case "speedzones":
return new SpeedZonesFragment(); return new SpeedZonesFragment();
case "livestats": case "livestats":
@ -177,14 +177,6 @@ public class ActivityChartsActivity extends AbstractChartsActivity {
return enabledTabsList.toArray().length; return enabledTabsList.toArray().length;
} }
private String getSleepTitle() {
if (GBApplication.getPrefs().getBoolean("charts_range", true)) {
return getString(R.string.weeksleepchart_sleep_a_month);
} else {
return getString(R.string.weeksleepchart_sleep_a_week);
}
}
@Override @Override
public CharSequence getPageTitle(int position) { public CharSequence getPageTitle(int position) {
switch (enabledTabsList.get(position)) { switch (enabledTabsList.get(position)) {

View File

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

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; package nodomain.freeyourgadget.gadgetbridge.activities.charts;
import android.os.Bundle; import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull; import androidx.fragment.app.FragmentManager;
import androidx.annotation.Nullable;
import androidx.viewpager2.widget.ViewPager2;
import com.google.android.material.tabs.TabLayout;
import com.google.android.material.tabs.TabLayoutMediator;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBFragment; import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBFragment;
import nodomain.freeyourgadget.gadgetbridge.adapter.NestedFragmentAdapter;
import nodomain.freeyourgadget.gadgetbridge.adapter.SleepFragmentAdapter; import nodomain.freeyourgadget.gadgetbridge.adapter.SleepFragmentAdapter;
public class SleepCollectionFragment extends AbstractGBFragment { public class SleepCollectionFragment extends AbstractCollectionFragment {
protected SleepFragmentAdapter nestedFragmentsAdapter; public SleepCollectionFragment() {
protected ViewPager2 viewPager;
private int last_position = 0;
@Override }
protected void onMadeVisibleInActivity() {
super.onMadeVisibleInActivity(); public static SleepCollectionFragment newInstance(final boolean allowSwipe) {
nestedFragmentsAdapter.updateFragments(last_position); final SleepCollectionFragment fragment = new SleepCollectionFragment();
final Bundle args = new Bundle();
args.putBoolean(ARG_ALLOW_SWIPE, allowSwipe);
fragment.setArguments(args);
return fragment;
} }
@Override @Override
public void onMadeInvisibleInActivity() { public NestedFragmentAdapter getNestedFragmentAdapter(AbstractGBFragment fragment, FragmentManager childFragmentManager) {
if (nestedFragmentsAdapter != null) { return new SleepFragmentAdapter(this, getChildFragmentManager());
nestedFragmentsAdapter.updateFragments(-1);
}
}
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.fragment_nested_tabs, container, false);
nestedFragmentsAdapter = new SleepFragmentAdapter(this, getChildFragmentManager());
viewPager = rootView.findViewById(R.id.pager);
viewPager.setAdapter(nestedFragmentsAdapter);
viewPager.setOrientation(ViewPager2.ORIENTATION_VERTICAL);
viewPager.setUserInputEnabled(false);
viewPager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {
@Override
public void onPageSelected(int position) {
super.onPageSelected(position);
last_position = position;
viewPager.post(new Runnable() {
@Override
public void run() {
if (isVisibleInActivity()) {
nestedFragmentsAdapter.updateFragments(position);
}
}
});
}
});
return rootView;
}
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
TabLayout tabLayout = view.findViewById(R.id.tab_layout);
new TabLayoutMediator(tabLayout, viewPager, (tab, position) -> {
switch (position) {
case 0:
tab.setText(getString(R.string.calendar_day));
break;
case 1:
tab.setText(getString(R.string.calendar_week));
break;
case 2:
tab.setText(getString(R.string.calendar_month));
break;
}
}).attach();
}
@Nullable
@Override
protected CharSequence getTitle() {
return null;
} }
} }

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; package nodomain.freeyourgadget.gadgetbridge.activities.charts;
import android.os.Bundle; import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull; import androidx.fragment.app.FragmentManager;
import androidx.annotation.Nullable;
import androidx.viewpager2.widget.ViewPager2;
import com.google.android.material.tabs.TabLayout;
import com.google.android.material.tabs.TabLayoutMediator;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBFragment; import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBFragment;
import nodomain.freeyourgadget.gadgetbridge.adapter.NestedFragmentAdapter;
import nodomain.freeyourgadget.gadgetbridge.adapter.StepsFragmentAdapter; import nodomain.freeyourgadget.gadgetbridge.adapter.StepsFragmentAdapter;
public class StepsCollectionFragment extends AbstractGBFragment { public class StepsCollectionFragment extends AbstractCollectionFragment {
protected StepsFragmentAdapter nestedFragmentsAdapter; public StepsCollectionFragment() {
protected ViewPager2 viewPager;
private int last_position = 0;
@Override }
protected void onMadeVisibleInActivity() {
super.onMadeVisibleInActivity(); public static StepsCollectionFragment newInstance(final boolean allowSwipe) {
nestedFragmentsAdapter.updateFragments(last_position); final StepsCollectionFragment fragment = new StepsCollectionFragment();
final Bundle args = new Bundle();
args.putBoolean(ARG_ALLOW_SWIPE, allowSwipe);
fragment.setArguments(args);
return fragment;
} }
@Override @Override
public void onMadeInvisibleInActivity() { public NestedFragmentAdapter getNestedFragmentAdapter(AbstractGBFragment fragment, FragmentManager childFragmentManager) {
if (nestedFragmentsAdapter != null) { return new StepsFragmentAdapter(this, getChildFragmentManager());
nestedFragmentsAdapter.updateFragments(-1);
}
}
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.fragment_nested_tabs, container, false);
nestedFragmentsAdapter = new StepsFragmentAdapter(this, getChildFragmentManager());
viewPager = rootView.findViewById(R.id.pager);
viewPager.setAdapter(nestedFragmentsAdapter);
viewPager.setOrientation(ViewPager2.ORIENTATION_VERTICAL);
viewPager.setUserInputEnabled(false);
viewPager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {
@Override
public void onPageSelected(int position) {
super.onPageSelected(position);
last_position = position;
viewPager.post(new Runnable() {
@Override
public void run() {
if (isVisibleInActivity()) {
nestedFragmentsAdapter.updateFragments(position);
}
}
});
}
});
return rootView;
}
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
TabLayout tabLayout = view.findViewById(R.id.tab_layout);
new TabLayoutMediator(tabLayout, viewPager, (tab, position) -> {
switch (position) {
case 0:
tab.setText(getString(R.string.calendar_day));
break;
case 1:
tab.setText(getString(R.string.calendar_week));
break;
case 2:
tab.setText(getString(R.string.calendar_month));
break;
}
}).attach();
}
@Nullable
@Override
protected CharSequence getTitle() {
return null;
} }
} }

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

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. This file is part of Gadgetbridge.
@ -16,19 +16,31 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. */ along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.activities.dashboard; package nodomain.freeyourgadget.gadgetbridge.activities.dashboard;
import android.graphics.Bitmap; import android.content.Context;
import android.graphics.Canvas; import android.content.Intent;
import android.graphics.Color; import android.graphics.Color;
import android.graphics.Paint;
import android.os.Bundle; import android.os.Bundle;
import android.view.View;
import android.widget.Toast;
import androidx.annotation.ColorInt; import androidx.annotation.ColorInt;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.util.List;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.DashboardFragment; import nodomain.freeyourgadget.gadgetbridge.activities.DashboardFragment;
import nodomain.freeyourgadget.gadgetbridge.activities.charts.ActivityChartsActivity;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
public abstract class AbstractDashboardWidget extends Fragment { public abstract class AbstractDashboardWidget extends Fragment {
private static final Logger LOG = LoggerFactory.getLogger(AbstractDashboardWidget.class); private static final Logger LOG = LoggerFactory.getLogger(AbstractDashboardWidget.class);
@ -57,37 +69,67 @@ public abstract class AbstractDashboardWidget extends Fragment {
} }
} }
public void update() { public void update() {
fillData(); fillData();
} }
protected abstract void fillData(); protected abstract void fillData();
/** protected boolean isSupportedBy(final GBDevice device) {
* @param width Bitmap width in pixels return device.getDeviceCoordinator().supportsActivityTracking();
* @param barWidth Gauge bar width in pixels }
* @param filledColor Color of the filled part of the gauge
* @param filledFactor Factor between 0 and 1 that determines the amount of the gauge that should be filled
* @return Bitmap containing the gauge
*/
Bitmap drawGauge(int width, int barWidth, @ColorInt int filledColor, float filledFactor) {
int height = width / 2;
int barMargin = (int) Math.ceil(barWidth / 2f);
Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); protected List<GBDevice> getSupportedDevices(final DashboardFragment.DashboardData dashboardData) {
Canvas canvas = new Canvas(bitmap); return GBApplication.app().getDeviceManager().getDevices()
Paint paint = new Paint(); .stream()
paint.setAntiAlias(true); .filter(dev -> dashboardData.showAllDevices || dashboardData.showDeviceList.contains(dev.getAddress()))
paint.setStyle(Paint.Style.STROKE); .filter(this::isSupportedBy)
paint.setStrokeCap(Paint.Cap.ROUND); .collect(Collectors.toList());
paint.setStrokeWidth(barWidth * 0.75f); }
paint.setColor(color_unknown);
canvas.drawArc(barMargin, barMargin, width - barMargin, width - barMargin, 180 + 180 * filledFactor, 180 - 180 * filledFactor, false, paint);
paint.setStrokeWidth(barWidth);
paint.setColor(filledColor);
canvas.drawArc(barMargin, barMargin, width - barMargin, width - barMargin, 180, 180 * filledFactor, false, paint);
return bitmap; protected void onClickOpenChart(final View view, final String chart, final int label) {
view.setOnClickListener(v -> {
chooseDevice(dashboardData, device -> {
final Intent startIntent;
startIntent = new Intent(requireContext(), ActivityChartsActivity.class);
startIntent.putExtra(GBDevice.EXTRA_DEVICE, device);
startIntent.putExtra(ActivityChartsActivity.EXTRA_SINGLE_FRAGMENT_NAME, chart);
startIntent.putExtra(ActivityChartsActivity.EXTRA_ACTIONBAR_TITLE, label);
startIntent.putExtra(ActivityChartsActivity.EXTRA_TIMESTAMP, dashboardData.timeTo);
requireContext().startActivity(startIntent);
});
});
}
protected void chooseDevice(final DashboardFragment.DashboardData dashboardData,
final Consumer<GBDevice> consumer) {
final List<GBDevice> devices = getSupportedDevices(dashboardData);
if (devices.size() == 1) {
consumer.accept(devices.get(0));
return;
}
if (devices.isEmpty()) {
GB.toast(GBApplication.getContext(), R.string.no_supported_devices_found, Toast.LENGTH_LONG, GB.WARN);
return;
}
final String[] deviceNames = devices.stream()
.map(GBDevice::getAliasOrName)
.toArray(String[]::new);
final Context activity = getActivity();
if (activity == null) {
return;
}
new MaterialAlertDialogBuilder(activity)
.setCancelable(true)
.setTitle(R.string.choose_device)
.setItems(deviceNames, (dialog, which) -> consumer.accept(devices.get(which)))
.setNegativeButton(android.R.string.cancel, (dialog, which) -> {
})
.show();
} }
} }

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. This file is part of Gadgetbridge.
@ -16,19 +16,10 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. */ along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.activities.dashboard; package nodomain.freeyourgadget.gadgetbridge.activities.dashboard;
import android.os.AsyncTask;
import android.os.Bundle; import android.os.Bundle;
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import org.slf4j.Logger; import java.util.Locale;
import org.slf4j.LoggerFactory;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.DashboardFragment; import nodomain.freeyourgadget.gadgetbridge.activities.DashboardFragment;
@ -37,13 +28,9 @@ import nodomain.freeyourgadget.gadgetbridge.activities.DashboardFragment;
* Use the {@link DashboardActiveTimeWidget#newInstance} factory method to * Use the {@link DashboardActiveTimeWidget#newInstance} factory method to
* create an instance of this fragment. * create an instance of this fragment.
*/ */
public class DashboardActiveTimeWidget extends AbstractDashboardWidget { public class DashboardActiveTimeWidget extends AbstractGaugeWidget {
private static final Logger LOG = LoggerFactory.getLogger(DashboardActiveTimeWidget.class);
private TextView activeTime;
private ImageView activeTimeGauge;
public DashboardActiveTimeWidget() { public DashboardActiveTimeWidget() {
// Required empty public constructor super(R.string.activity_list_summary_active_time, "activity");
} }
/** /**
@ -53,69 +40,35 @@ public class DashboardActiveTimeWidget extends AbstractDashboardWidget {
* @param dashboardData An instance of DashboardFragment.DashboardData. * @param dashboardData An instance of DashboardFragment.DashboardData.
* @return A new instance of fragment DashboardActiveTimeWidget. * @return A new instance of fragment DashboardActiveTimeWidget.
*/ */
public static DashboardActiveTimeWidget newInstance(DashboardFragment.DashboardData dashboardData) { public static DashboardActiveTimeWidget newInstance(final DashboardFragment.DashboardData dashboardData) {
DashboardActiveTimeWidget fragment = new DashboardActiveTimeWidget(); final DashboardActiveTimeWidget fragment = new DashboardActiveTimeWidget();
Bundle args = new Bundle(); final Bundle args = new Bundle();
args.putSerializable(ARG_DASHBOARD_DATA, dashboardData); args.putSerializable(ARG_DASHBOARD_DATA, dashboardData);
fragment.setArguments(args); fragment.setArguments(args);
return fragment; return fragment;
} }
@Override @Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { protected void populateData(final DashboardFragment.DashboardData dashboardData) {
View fragmentView = inflater.inflate(R.layout.dashboard_widget_active_time, container, false); dashboardData.getActiveMinutesTotal();
activeTime = fragmentView.findViewById(R.id.activetime_text); dashboardData.getActiveMinutesGoalFactor();
activeTimeGauge = fragmentView.findViewById(R.id.activetime_gauge);
fillData();
return fragmentView;
} }
@Override @Override
public void onResume() { protected void draw(final DashboardFragment.DashboardData dashboardData) {
super.onResume(); final long totalActiveMinutes = dashboardData.getActiveMinutesTotal();
if (activeTime != null && activeTimeGauge != null) fillData(); final String valueText = String.format(
} Locale.ROOT,
"%d:%02d",
(int) Math.floor(totalActiveMinutes / 60f),
(int) (totalActiveMinutes % 60f)
);
@Override setText(valueText);
protected void fillData() {
if (activeTimeGauge == null) return;
activeTimeGauge.post(new Runnable() {
@Override
public void run() {
FillDataAsyncTask myAsyncTask = new FillDataAsyncTask();
myAsyncTask.execute();
}
});
}
private class FillDataAsyncTask extends AsyncTask<Void, Void, Void> { drawSimpleGauge(
@Override color_active_time,
protected Void doInBackground(Void... params) { dashboardData.getActiveMinutesGoalFactor()
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()));
}
} }
} }

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. This file is part of Gadgetbridge.
@ -16,21 +16,8 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. */ along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.activities.dashboard; package nodomain.freeyourgadget.gadgetbridge.activities.dashboard;
import android.content.res.Resources;
import android.os.AsyncTask;
import android.os.Bundle; import android.os.Bundle;
import android.util.DisplayMetrics;
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.DashboardFragment; import nodomain.freeyourgadget.gadgetbridge.activities.DashboardFragment;
import nodomain.freeyourgadget.gadgetbridge.util.FormatUtils; import nodomain.freeyourgadget.gadgetbridge.util.FormatUtils;
@ -40,13 +27,9 @@ import nodomain.freeyourgadget.gadgetbridge.util.FormatUtils;
* Use the {@link DashboardDistanceWidget#newInstance} factory method to * Use the {@link DashboardDistanceWidget#newInstance} factory method to
* create an instance of this fragment. * create an instance of this fragment.
*/ */
public class DashboardDistanceWidget extends AbstractDashboardWidget { public class DashboardDistanceWidget extends AbstractGaugeWidget {
private static final Logger LOG = LoggerFactory.getLogger(DashboardDistanceWidget.class);
private TextView distanceText;
private ImageView distanceGauge;
public DashboardDistanceWidget() { public DashboardDistanceWidget() {
// Required empty public constructor super(R.string.distance, "stepsweek");
} }
/** /**
@ -56,67 +39,26 @@ public class DashboardDistanceWidget extends AbstractDashboardWidget {
* @param dashboardData An instance of DashboardFragment.DashboardData. * @param dashboardData An instance of DashboardFragment.DashboardData.
* @return A new instance of fragment DashboardDistanceWidget. * @return A new instance of fragment DashboardDistanceWidget.
*/ */
public static DashboardDistanceWidget newInstance(DashboardFragment.DashboardData dashboardData) { public static DashboardDistanceWidget newInstance(final DashboardFragment.DashboardData dashboardData) {
DashboardDistanceWidget fragment = new DashboardDistanceWidget(); final DashboardDistanceWidget fragment = new DashboardDistanceWidget();
Bundle args = new Bundle(); final Bundle args = new Bundle();
args.putSerializable(ARG_DASHBOARD_DATA, dashboardData); args.putSerializable(ARG_DASHBOARD_DATA, dashboardData);
fragment.setArguments(args); fragment.setArguments(args);
return fragment; return fragment;
} }
@Override @Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { protected void populateData(final DashboardFragment.DashboardData dashboardData) {
View fragmentView = inflater.inflate(R.layout.dashboard_widget_distance, container, false); dashboardData.getDistanceTotal();
distanceText = fragmentView.findViewById(R.id.distance_text); dashboardData.getDistanceGoalFactor();
distanceGauge = fragmentView.findViewById(R.id.distance_gauge);
fillData();
return fragmentView;
} }
@Override @Override
public void onResume() { protected void draw(final DashboardFragment.DashboardData dashboardData) {
super.onResume(); setText(FormatUtils.getFormattedDistanceLabel(dashboardData.getDistanceTotal()));
if (distanceText != null && distanceGauge != null) fillData(); drawSimpleGauge(
} color_distance,
dashboardData.getDistanceGoalFactor()
@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()));
}
} }
} }

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. This file is part of Gadgetbridge.
@ -90,8 +90,6 @@ public class DashboardGoalsWidget extends AbstractDashboardWidget {
Prefs prefs = GBApplication.getPrefs(); Prefs prefs = GBApplication.getPrefs();
legend.setVisibility(prefs.getBoolean("dashboard_widget_goals_legend", true) ? View.VISIBLE : View.GONE); legend.setVisibility(prefs.getBoolean("dashboard_widget_goals_legend", true) ? View.VISIBLE : View.GONE);
fillData();
return goalsView; return goalsView;
} }
@ -118,6 +116,8 @@ public class DashboardGoalsWidget extends AbstractDashboardWidget {
@Override @Override
protected Void doInBackground(Void... params) { protected Void doInBackground(Void... params) {
final long nanoStart = System.nanoTime();
int width = Resources.getSystem().getDisplayMetrics().widthPixels; int width = Resources.getSystem().getDisplayMetrics().widthPixels;
int height = width; int height = width;
int barWidth = Math.round(height * 0.04f); int barWidth = Math.round(height * 0.04f);
@ -160,6 +160,11 @@ public class DashboardGoalsWidget extends AbstractDashboardWidget {
paint.setStrokeWidth(barWidth); paint.setStrokeWidth(barWidth);
paint.setColor(color_light_sleep); paint.setColor(color_light_sleep);
canvas.drawArc(barMargin, barMargin, width - barMargin, height - barMargin, 270, 360 * dashboardData.getSleepMinutesGoalFactor(), false, paint); canvas.drawArc(barMargin, barMargin, width - barMargin, height - barMargin, 270, 360 * dashboardData.getSleepMinutesGoalFactor(), false, paint);
final long nanoEnd = System.nanoTime();
final long executionTime = (nanoEnd - nanoStart) / 1000000;
LOG.debug("fillData for {} took {}ms", DashboardGoalsWidget.this.getClass().getSimpleName(), executionTime);
return null; return null;
} }

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. This file is part of Gadgetbridge.
@ -16,34 +16,22 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. */ along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.activities.dashboard; package nodomain.freeyourgadget.gadgetbridge.activities.dashboard;
import android.os.AsyncTask;
import android.os.Bundle; import android.os.Bundle;
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import org.slf4j.Logger; import java.util.Locale;
import org.slf4j.LoggerFactory;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.DashboardFragment; import nodomain.freeyourgadget.gadgetbridge.activities.DashboardFragment;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
/** /**
* A simple {@link AbstractDashboardWidget} subclass. * A simple {@link AbstractDashboardWidget} subclass.
* Use the {@link DashboardSleepWidget#newInstance} factory method to * Use the {@link DashboardSleepWidget#newInstance} factory method to
* create an instance of this fragment. * create an instance of this fragment.
*/ */
public class DashboardSleepWidget extends AbstractDashboardWidget { public class DashboardSleepWidget extends AbstractGaugeWidget {
private static final Logger LOG = LoggerFactory.getLogger(DashboardSleepWidget.class);
private TextView sleepAmount;
private ImageView sleepGauge;
public DashboardSleepWidget() { public DashboardSleepWidget() {
// Required empty public constructor super(R.string.menuitem_sleep, "sleep");
} }
/** /**
@ -53,69 +41,39 @@ public class DashboardSleepWidget extends AbstractDashboardWidget {
* @param dashboardData An instance of DashboardFragment.DashboardData. * @param dashboardData An instance of DashboardFragment.DashboardData.
* @return A new instance of fragment DashboardSleepWidget. * @return A new instance of fragment DashboardSleepWidget.
*/ */
public static DashboardSleepWidget newInstance(DashboardFragment.DashboardData dashboardData) { public static DashboardSleepWidget newInstance(final DashboardFragment.DashboardData dashboardData) {
DashboardSleepWidget fragment = new DashboardSleepWidget(); final DashboardSleepWidget fragment = new DashboardSleepWidget();
Bundle args = new Bundle(); final Bundle args = new Bundle();
args.putSerializable(ARG_DASHBOARD_DATA, dashboardData); args.putSerializable(ARG_DASHBOARD_DATA, dashboardData);
fragment.setArguments(args); fragment.setArguments(args);
return fragment; return fragment;
} }
@Override @Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { protected boolean isSupportedBy(final GBDevice device) {
View fragmentView = inflater.inflate(R.layout.dashboard_widget_sleep, container, false); return device.getDeviceCoordinator().supportsSleepMeasurement();
sleepAmount = fragmentView.findViewById(R.id.sleep_text);
sleepGauge = fragmentView.findViewById(R.id.sleep_gauge);
fillData();
return fragmentView;
} }
@Override @Override
public void onResume() { protected void populateData(final DashboardFragment.DashboardData dashboardData) {
super.onResume(); dashboardData.getSleepMinutesTotal();
if (sleepAmount != null && sleepGauge != null) fillData(); dashboardData.getSleepMinutesGoalFactor();
} }
@Override @Override
protected void fillData() { protected void draw(final DashboardFragment.DashboardData dashboardData) {
if (sleepGauge == null) return; final long totalSleepMinutes = dashboardData.getSleepMinutesTotal();
sleepGauge.post(new Runnable() { final String valueText = String.format(
@Override Locale.ROOT,
public void run() { "%d:%02d",
FillDataAsyncTask myAsyncTask = new FillDataAsyncTask(); (int) Math.floor(totalSleepMinutes / 60f),
myAsyncTask.execute(); (int) (totalSleepMinutes % 60f)
} );
});
}
private class FillDataAsyncTask extends AsyncTask<Void, Void, Void> { setText(valueText);
@Override drawSimpleGauge(
protected Void doInBackground(Void... params) { color_light_sleep,
dashboardData.getSleepMinutesTotal(); dashboardData.getSleepMinutesGoalFactor()
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()));
}
} }
} }

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. This file is part of Gadgetbridge.
@ -16,19 +16,8 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. */ along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.activities.dashboard; package nodomain.freeyourgadget.gadgetbridge.activities.dashboard;
import android.os.AsyncTask;
import android.os.Bundle; import android.os.Bundle;
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.DashboardFragment; import nodomain.freeyourgadget.gadgetbridge.activities.DashboardFragment;
@ -37,13 +26,9 @@ import nodomain.freeyourgadget.gadgetbridge.activities.DashboardFragment;
* Use the {@link DashboardStepsWidget#newInstance} factory method to * Use the {@link DashboardStepsWidget#newInstance} factory method to
* create an instance of this fragment. * create an instance of this fragment.
*/ */
public class DashboardStepsWidget extends AbstractDashboardWidget { public class DashboardStepsWidget extends AbstractGaugeWidget {
private static final Logger LOG = LoggerFactory.getLogger(DashboardStepsWidget.class);
private TextView stepsCount;
private ImageView stepsGauge;
public DashboardStepsWidget() { public DashboardStepsWidget() {
// Required empty public constructor super(R.string.steps, "stepsweek");
} }
/** /**
@ -53,64 +38,26 @@ public class DashboardStepsWidget extends AbstractDashboardWidget {
* @param dashboardData An instance of DashboardFragment.DashboardData. * @param dashboardData An instance of DashboardFragment.DashboardData.
* @return A new instance of fragment DashboardStepsWidget. * @return A new instance of fragment DashboardStepsWidget.
*/ */
public static DashboardStepsWidget newInstance(DashboardFragment.DashboardData dashboardData) { public static DashboardStepsWidget newInstance(final DashboardFragment.DashboardData dashboardData) {
DashboardStepsWidget fragment = new DashboardStepsWidget(); final DashboardStepsWidget fragment = new DashboardStepsWidget();
Bundle args = new Bundle(); final Bundle args = new Bundle();
args.putSerializable(ARG_DASHBOARD_DATA, dashboardData); args.putSerializable(ARG_DASHBOARD_DATA, dashboardData);
fragment.setArguments(args); fragment.setArguments(args);
return fragment; return fragment;
} }
@Override @Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { protected void populateData(final DashboardFragment.DashboardData dashboardData) {
View fragmentView = inflater.inflate(R.layout.dashboard_widget_steps, container, false); dashboardData.getStepsTotal();
stepsCount = fragmentView.findViewById(R.id.steps_count); dashboardData.getStepsGoalFactor();
stepsGauge = fragmentView.findViewById(R.id.steps_gauge);
fillData();
return fragmentView;
} }
@Override @Override
public void onResume() { protected void draw(final DashboardFragment.DashboardData dashboardData) {
super.onResume(); setText(String.valueOf(dashboardData.getStepsTotal()));
if (stepsCount != null && stepsGauge != null) fillData(); drawSimpleGauge(
} color_activity,
dashboardData.getStepsGoalFactor()
@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()));
}
} }
} }

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. This file is part of Gadgetbridge.
@ -122,9 +122,7 @@ public class DashboardTodayWidget extends AbstractDashboardWidget {
legend.setVisibility(prefs.getBoolean("dashboard_widget_today_legend", true) ? View.VISIBLE : View.GONE); legend.setVisibility(prefs.getBoolean("dashboard_widget_today_legend", true) ? View.VISIBLE : View.GONE);
if (dashboardData.generalizedActivities.isEmpty()) { if (!dashboardData.generalizedActivities.isEmpty()) {
fillData();
} else {
draw(); draw();
} }
@ -147,7 +145,11 @@ public class DashboardTodayWidget extends AbstractDashboardWidget {
int height = width; int height = width;
int barWidth = Math.round(width * 0.08f); int barWidth = Math.round(width * 0.08f);
int hourTextSp = Math.round(width * 0.024f); int hourTextSp = Math.round(width * 0.024f);
float hourTextPixels = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, hourTextSp, requireContext().getResources().getDisplayMetrics()); float hourTextPixels = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_SP,
hourTextSp,
GBApplication.getContext().getResources().getDisplayMetrics()
);
float outerCircleMargin = mode_24h ? barWidth / 2f : barWidth / 2f + hourTextPixels * 1.3f; float outerCircleMargin = mode_24h ? barWidth / 2f : barWidth / 2f + hourTextPixels * 1.3f;
float innerCircleMargin = outerCircleMargin + barWidth * 1.3f; float innerCircleMargin = outerCircleMargin + barWidth * 1.3f;
float degreeFactor = mode_24h ? 240 : 120; float degreeFactor = mode_24h ? 240 : 120;
@ -168,7 +170,7 @@ public class DashboardTodayWidget extends AbstractDashboardWidget {
} }
// Draw hours // Draw hours
boolean normalClock = DateFormat.is24HourFormat(getContext()); boolean normalClock = DateFormat.is24HourFormat(GBApplication.getContext());
Map<Integer, String> hours = new HashMap<Integer, String>() { Map<Integer, String> hours = new HashMap<Integer, String>() {
{ {
put(0, normalClock ? (mode_24h ? "0" : "12") : "12pm"); put(0, normalClock ? (mode_24h ? "0" : "12") : "12pm");
@ -435,6 +437,8 @@ public class DashboardTodayWidget extends AbstractDashboardWidget {
@Override @Override
protected Void doInBackground(Void... params) { protected Void doInBackground(Void... params) {
final long nanoStart = System.nanoTime();
// Retrieve activity data // Retrieve activity data
dashboardData.generalizedActivities.clear(); dashboardData.generalizedActivities.clear();
List<GBDevice> devices = GBApplication.app().getDeviceManager().getDevices(); List<GBDevice> devices = GBApplication.app().getDeviceManager().getDevices();
@ -476,16 +480,21 @@ public class DashboardTodayWidget extends AbstractDashboardWidget {
addActivity(session.getStartTime().getTime() / 1000, session.getEndTime().getTime() / 1000, ActivityKind.ACTIVITY); addActivity(session.getStartTime().getTime() / 1000, session.getEndTime().getTime() / 1000, ActivityKind.ACTIVITY);
} }
createGeneralizedActivities(); createGeneralizedActivities();
final long nanoEnd = System.nanoTime();
final long executionTime = (nanoEnd - nanoStart) / 1000000;
LOG.debug("fillData for {} took {}ms", DashboardTodayWidget.this.getClass().getSimpleName(), executionTime);
return null; return null;
} }
@Override @Override
protected void onPostExecute(Void unused) { protected void onPostExecute(final Void unused) {
super.onPostExecute(unused); super.onPostExecute(unused);
try { try {
draw(); draw();
} catch (IllegalStateException e) { } catch (final Exception e) {
LOG.warn("calling draw() failed: " + e.getMessage()); LOG.error("calling draw() failed", e);
} }
} }
} }

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

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

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" android:title="@string/menuitem_calendar"
app:iconTint="?attr/actionmenu_icon_color" app:iconTint="?attr/actionmenu_icon_color"
app:showAsAction="ifRoom" /> app:showAsAction="ifRoom" />
<item
android:id="@+id/dashboard_settings"
android:icon="@drawable/ic_settings"
android:title="@string/dashboard_settings"
app:iconTint="?attr/actionmenu_icon_color"
app:showAsAction="never" />
</menu> </menu>

View File

@ -4167,6 +4167,11 @@
<item>@string/distance</item> <item>@string/distance</item>
<item>@string/active_time</item> <item>@string/active_time</item>
<item>@string/menuitem_sleep</item> <item>@string/menuitem_sleep</item>
<item>@string/body_energy</item>
<item>@string/menuitem_stress_simple</item>
<item>@string/menuitem_stress_segmented</item>
<item>@string/menuitem_stress_breakdown</item>
<item>@string/hrv</item>
</string-array> </string-array>
<string-array name="pref_dashboard_widgets_order_values"> <string-array name="pref_dashboard_widgets_order_values">
@ -4176,5 +4181,22 @@
<item>distance</item> <item>distance</item>
<item>activetime</item> <item>activetime</item>
<item>sleep</item> <item>sleep</item>
<item>bodyenergy</item>
<item>stress_simple</item>
<item>stress_segmented</item>
<item>stress_breakdown</item>
<item>hrv</item>
</string-array>
<string-array name="pref_dashboard_widgets_order_default">
<item>today</item>
<item>goals</item>
<item>steps</item>
<item>distance</item>
<item>activetime</item>
<item>sleep</item>
<item>bodyenergy</item>
<item>stress_segmented</item>
<item>hrv</item>
</string-array> </string-array>
</resources> </resources>

View File

@ -52,10 +52,11 @@
<color name="hrv_status_poor" type="color">#be03fc</color> <color name="hrv_status_poor" type="color">#be03fc</color>
<color name="hrv_status_char_line_color" type="color">#d12a2a</color> <color name="hrv_status_char_line_color" type="color">#d12a2a</color>
<color name="body_energy_level_color" type="color">#5ac234</color> <color name="body_energy_level_color" type="color">#5ac234</color>
<color name="body_energy_lost_color" type="color">#ff6c43</color>
<color name="steps_color" type="color">#00c9bf</color> <color name="steps_color" type="color">#00c9bf</color>
<color name="value_line_color" type="color">#858585</color> <color name="value_line_color" type="color">#858585</color>
<color name="gauge_line_color" type="color">#383838</color> <color name="gauge_line_color" type="color">#19808080</color>
<color name="alternate_row_background_light">#FFEDEDED</color> <color name="alternate_row_background_light">#FFEDEDED</color>
<color name="alternate_row_background_dark">#545254</color> <color name="alternate_row_background_dark">#545254</color>

View File

@ -628,6 +628,8 @@
<string name="android_pairing_hint">Use the Android Bluetooth pairing dialog to pair the device.</string> <string name="android_pairing_hint">Use the Android Bluetooth pairing dialog to pair the device.</string>
<string name="title_activity_mi_band_pairing">Pair your Mi Band</string> <string name="title_activity_mi_band_pairing">Pair your Mi Band</string>
<string name="pairing">Pairing with %s…</string> <string name="pairing">Pairing with %s…</string>
<string name="choose_device">Choose a device</string>
<string name="no_supported_devices_found">No supported devices found</string>
<string name="pairing_creating_bond_with">"Creating bond with %1$s (%2$s)"</string> <string name="pairing_creating_bond_with">"Creating bond with %1$s (%2$s)"</string>
<string name="pairing_unable_to_pair_with">"Unable to pair with %1$s (%2$s)"</string> <string name="pairing_unable_to_pair_with">"Unable to pair with %1$s (%2$s)"</string>
<string name="pairing_in_progress">Bonding in progress: %1$s (%2$s)</string> <string name="pairing_in_progress">Bonding in progress: %1$s (%2$s)</string>
@ -1871,6 +1873,9 @@
<string name="menuitem_more">More</string> <string name="menuitem_more">More</string>
<string name="menuitem_nfc">NFC</string> <string name="menuitem_nfc">NFC</string>
<string name="menuitem_stress">Stress</string> <string name="menuitem_stress">Stress</string>
<string name="menuitem_stress_simple">Stress (simple)</string>
<string name="menuitem_stress_segmented">Stress (segmented)</string>
<string name="menuitem_stress_breakdown">Stress (breakdown)</string>
<string name="menuitem_pai">PAI</string> <string name="menuitem_pai">PAI</string>
<string name="menuitem_hr">Heart Rate</string> <string name="menuitem_hr">Heart Rate</string>
<string name="menuitem_spo2">SpO2</string> <string name="menuitem_spo2">SpO2</string>

View File

@ -20,7 +20,7 @@
android:summary="@string/pref_dashboard_cards_summary" android:summary="@string/pref_dashboard_cards_summary"
app:iconSpaceReserved="false" /> app:iconSpaceReserved="false" />
<com.mobeta.android.dslv.DragSortListPreference <com.mobeta.android.dslv.DragSortListPreference
android:defaultValue="@array/pref_dashboard_widgets_order_values" android:defaultValue="@array/pref_dashboard_widgets_order_default"
android:dialogTitle="@string/menuitem_widgets" android:dialogTitle="@string/menuitem_widgets"
android:entries="@array/pref_dashboard_widgets_order" android:entries="@array/pref_dashboard_widgets_order"
android:entryValues="@array/pref_dashboard_widgets_order_values" android:entryValues="@array/pref_dashboard_widgets_order_values"