diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/GBApplication.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/GBApplication.java index f12b5ad69..b825e9ae4 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/GBApplication.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/GBApplication.java @@ -123,7 +123,7 @@ public class GBApplication extends Application { private static SharedPreferences sharedPrefs; private static final String PREFS_VERSION = "shared_preferences_version"; //if preferences have to be migrated, increment the following and add the migration logic in migratePrefs below; see http://stackoverflow.com/questions/16397848/how-can-i-migrate-android-preferences-with-a-new-version - private static final int CURRENT_PREFS_VERSION = 36; + private static final int CURRENT_PREFS_VERSION = 37; private static final LimitedQueue mIDSenderLookup = new LimitedQueue<>(16); private static GBPrefs prefs; @@ -1718,6 +1718,13 @@ public class GBApplication extends Application { } } + if (oldVersion < 37) { + // Add new dashboard widgets + final String dashboardWidgetsOrder = sharedPrefs.getString("pref_dashboard_widgets_order", null); + if (!StringUtils.isBlank(dashboardWidgetsOrder) && !dashboardWidgetsOrder.contains("bodyenergy")) { + editor.putString("pref_dashboard_widgets_order", dashboardWidgetsOrder + ",bodyenergy,stress_segmented,hrv"); + } + } editor.putString(PREFS_VERSION, Integer.toString(CURRENT_PREFS_VERSION)); editor.apply(); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/DashboardFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/DashboardFragment.java index a5252cf40..16ea40061 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/DashboardFragment.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/DashboardFragment.java @@ -1,4 +1,4 @@ -/* Copyright (C) 2023-2024 Arjan Schrijver +/* Copyright (C) 2023-2024 Arjan Schrijver, José Rebelo This file is part of Gadgetbridge. @@ -16,6 +16,7 @@ along with this program. If not, see . */ package nodomain.freeyourgadget.gadgetbridge.activities; +import android.app.Activity; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; @@ -30,8 +31,12 @@ import android.view.View; import android.view.ViewGroup; import android.widget.TextView; +import androidx.activity.result.ActivityResult; +import androidx.activity.result.ActivityResultCallback; +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; +import androidx.core.view.MenuProvider; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentContainerView; import androidx.gridlayout.widget.GridLayout; @@ -44,23 +49,31 @@ import org.slf4j.LoggerFactory; import java.io.Serializable; import java.util.ArrayList; -import java.util.Arrays; import java.util.Calendar; import java.util.Collections; import java.util.GregorianCalendar; +import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Supplier; import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.activities.dashboard.AbstractDashboardWidget; import nodomain.freeyourgadget.gadgetbridge.activities.dashboard.DashboardActiveTimeWidget; +import nodomain.freeyourgadget.gadgetbridge.activities.dashboard.DashboardBodyEnergyWidget; import nodomain.freeyourgadget.gadgetbridge.activities.dashboard.DashboardCalendarActivity; import nodomain.freeyourgadget.gadgetbridge.activities.dashboard.DashboardDistanceWidget; import nodomain.freeyourgadget.gadgetbridge.activities.dashboard.DashboardGoalsWidget; +import nodomain.freeyourgadget.gadgetbridge.activities.dashboard.DashboardHrvWidget; import nodomain.freeyourgadget.gadgetbridge.activities.dashboard.DashboardSleepWidget; import nodomain.freeyourgadget.gadgetbridge.activities.dashboard.DashboardStepsWidget; +import nodomain.freeyourgadget.gadgetbridge.activities.dashboard.DashboardStressBreakdownWidget; +import nodomain.freeyourgadget.gadgetbridge.activities.dashboard.DashboardStressSegmentedWidget; +import nodomain.freeyourgadget.gadgetbridge.activities.dashboard.DashboardStressSimpleWidget; import nodomain.freeyourgadget.gadgetbridge.activities.dashboard.DashboardTodayWidget; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind; @@ -68,23 +81,28 @@ import nodomain.freeyourgadget.gadgetbridge.util.DashboardUtils; import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils; import nodomain.freeyourgadget.gadgetbridge.util.Prefs; -public class DashboardFragment extends Fragment { +public class DashboardFragment extends Fragment implements MenuProvider { private static final Logger LOG = LoggerFactory.getLogger(DashboardFragment.class); - private Calendar day = GregorianCalendar.getInstance(); + private final Calendar day = GregorianCalendar.getInstance(); private TextView textViewDate; - private TextView arrowLeft; private TextView arrowRight; private GridLayout gridLayout; - private DashboardTodayWidget todayWidget; - private DashboardGoalsWidget goalsWidget; - private DashboardStepsWidget stepsWidget; - private DashboardDistanceWidget distanceWidget; - private DashboardActiveTimeWidget activeTimeWidget; - private DashboardSleepWidget sleepWidget; + private final Map widgetMap = new HashMap<>(); private DashboardData dashboardData = new DashboardData(); private boolean isConfigChanged = false; + private ActivityResultLauncher calendarLauncher; + private final ActivityResultCallback calendarCallback = result -> { + if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) { + long timeMillis = result.getData().getLongExtra(DashboardCalendarActivity.EXTRA_TIMESTAMP, 0); + if (timeMillis != 0) { + day.setTimeInMillis(timeMillis); + fullRefresh(); + } + } + }; + public static final String ACTION_CONFIG_CHANGE = "nodomain.freeyourgadget.gadgetbridge.activities.dashboardfragment.action.config_change"; private final BroadcastReceiver mReceiver = new BroadcastReceiver() { @@ -95,7 +113,7 @@ public class DashboardFragment extends Fragment { switch (action) { case GBApplication.ACTION_NEW_DATA: final GBDevice dev = intent.getParcelableExtra(GBDevice.EXTRA_DEVICE); - if (dev != null && !dev.isBusy()) { + if (dev != null) { if (dashboardData.showAllDevices || dashboardData.showDeviceList.contains(dev.getAddress())) { refresh(); } @@ -109,20 +127,25 @@ public class DashboardFragment extends Fragment { }; @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { super.onCreateView(inflater, container, savedInstanceState); View dashboardView = inflater.inflate(R.layout.fragment_dashboard, container, false); - setHasOptionsMenu(true); + requireActivity().addMenuProvider(this); textViewDate = dashboardView.findViewById(R.id.dashboard_date); gridLayout = dashboardView.findViewById(R.id.dashboard_gridlayout); + calendarLauncher = registerForActivityResult( + new ActivityResultContracts.StartActivityForResult(), + calendarCallback + ); + // Increase column count on landscape, tablets and open foldables DisplayMetrics displayMetrics = getResources().getDisplayMetrics(); if (displayMetrics.widthPixels / displayMetrics.density >= 600) { gridLayout.setColumnCount(4); } - arrowLeft = dashboardView.findViewById(R.id.arrow_left); + final TextView arrowLeft = dashboardView.findViewById(R.id.arrow_left); arrowLeft.setOnClickListener(v -> { day.add(Calendar.DAY_OF_MONTH, -1); refresh(); @@ -155,7 +178,7 @@ public class DashboardFragment extends Fragment { if (isConfigChanged) { isConfigChanged = false; fullRefresh(); - } else if (dashboardData.isEmpty() || todayWidget == null) { + } else if (dashboardData.isEmpty() || !widgetMap.containsKey("today")) { refresh(); } } @@ -173,43 +196,29 @@ public class DashboardFragment extends Fragment { } @Override - public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) { - super.onCreateOptionsMenu(menu, inflater); + public void onCreateMenu(@NonNull final Menu menu, @NonNull final MenuInflater inflater) { inflater.inflate(R.menu.dashboard_menu, menu); } @Override - public boolean onOptionsItemSelected(@NonNull final MenuItem item) { + public boolean onMenuItemSelected(@NonNull final MenuItem item) { final int itemId = item.getItemId(); if (itemId == R.id.dashboard_show_calendar) { final Intent intent = new Intent(requireActivity(), DashboardCalendarActivity.class); intent.putExtra(DashboardCalendarActivity.EXTRA_TIMESTAMP, day.getTimeInMillis()); - startActivityForResult(intent, 0); - return false; - } - return super.onOptionsItemSelected(item); - } - - @Override - public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { - super.onActivityResult(requestCode, resultCode, data); - if (requestCode == 0 && resultCode == DashboardCalendarActivity.RESULT_OK && data != null) { - long timeMillis = data.getLongExtra(DashboardCalendarActivity.EXTRA_TIMESTAMP, 0); - if (timeMillis != 0) { - day.setTimeInMillis(timeMillis); - fullRefresh(); - } + calendarLauncher.launch(intent); + return true; + } else if (itemId == R.id.dashboard_settings) { + final Intent intent = new Intent(requireActivity(), DashboardPreferencesActivity.class); + startActivity(intent); + return true; } + return false; } private void fullRefresh() { gridLayout.removeAllViews(); - todayWidget = null; - goalsWidget = null; - stepsWidget = null; - distanceWidget = null; - activeTimeWidget = null; - sleepWidget = null; + widgetMap.clear(); refresh(); } @@ -229,13 +238,13 @@ public class DashboardFragment extends Fragment { private void draw() { Prefs prefs = GBApplication.getPrefs(); - String defaultWidgetsOrder = String.join(",", getResources().getStringArray(R.array.pref_dashboard_widgets_order_values)); + String defaultWidgetsOrder = String.join(",", getResources().getStringArray(R.array.pref_dashboard_widgets_order_default)); String widgetsOrderPref = prefs.getString("pref_dashboard_widgets_order", defaultWidgetsOrder); - List widgetsOrder = Arrays.asList(widgetsOrderPref.split(",")); + String[] widgetsOrder = widgetsOrderPref.split(","); Calendar today = GregorianCalendar.getInstance(); if (DateTimeUtils.isSameDay(today, day)) { - textViewDate.setText(getContext().getString(R.string.activity_summary_today)); + textViewDate.setText(requireContext().getString(R.string.activity_summary_today)); arrowRight.setAlpha(0.5f); } else { textViewDate.setText(DateTimeUtils.formatDate(day.getTime())); @@ -245,55 +254,55 @@ public class DashboardFragment extends Fragment { boolean cardsEnabled = prefs.getBoolean("dashboard_cards_enabled", true); for (String widgetName : widgetsOrder) { - switch (widgetName) { - case "today": - if (todayWidget == null) { - todayWidget = DashboardTodayWidget.newInstance(dashboardData); - createWidget(todayWidget, cardsEnabled, prefs.getBoolean("dashboard_widget_today_2columns", true) ? 2 : 1); - } else { - todayWidget.update(); - } - break; - case "goals": - if (goalsWidget == null) { - goalsWidget = DashboardGoalsWidget.newInstance(dashboardData); - createWidget(goalsWidget, cardsEnabled, prefs.getBoolean("dashboard_widget_goals_2columns", true) ? 2 : 1); - } else { - goalsWidget.update(); - } - break; - case "steps": - if (stepsWidget == null) { - stepsWidget = DashboardStepsWidget.newInstance(dashboardData); - createWidget(stepsWidget, cardsEnabled, 1); - } else { - stepsWidget.update(); - } - break; - case "distance": - if (distanceWidget == null) { - distanceWidget = DashboardDistanceWidget.newInstance(dashboardData); - createWidget(distanceWidget, cardsEnabled, 1); - } else { - distanceWidget.update(); - } - break; - case "activetime": - if (activeTimeWidget == null) { - activeTimeWidget = DashboardActiveTimeWidget.newInstance(dashboardData); - createWidget(activeTimeWidget, cardsEnabled, 1); - } else { - activeTimeWidget.update(); - } - break; - case "sleep": - if (sleepWidget == null) { - sleepWidget = DashboardSleepWidget.newInstance(dashboardData); - createWidget(sleepWidget, cardsEnabled, 1); - } else { - sleepWidget.update(); - } - break; + AbstractDashboardWidget widget = widgetMap.get(widgetName); + if (widget == null) { + int columnSpan = 1; + switch (widgetName) { + case "today": + widget = DashboardTodayWidget.newInstance(dashboardData); + columnSpan = prefs.getBoolean("dashboard_widget_today_2columns", true) ? 2 : 1; + break; + case "goals": + widget = DashboardGoalsWidget.newInstance(dashboardData); + columnSpan = prefs.getBoolean("dashboard_widget_goals_2columns", true) ? 2 : 1; + break; + case "steps": + widget = DashboardStepsWidget.newInstance(dashboardData); + break; + case "distance": + widget = DashboardDistanceWidget.newInstance(dashboardData); + break; + case "activetime": + widget = DashboardActiveTimeWidget.newInstance(dashboardData); + break; + case "sleep": + widget = DashboardSleepWidget.newInstance(dashboardData); + break; + case "stress_simple": + widget = DashboardStressSimpleWidget.newInstance(dashboardData); + break; + case "stress_segmented": + widget = DashboardStressSegmentedWidget.newInstance(dashboardData); + break; + case "stress_breakdown": + widget = DashboardStressBreakdownWidget.newInstance(dashboardData); + break; + case "bodyenergy": + widget = DashboardBodyEnergyWidget.newInstance(dashboardData); + break; + case "hrv": + widget = DashboardHrvWidget.newInstance(dashboardData); + break; + default: + LOG.error("Unknown dashboard widget {}", widgetName); + continue; + } + + createWidget(widget, cardsEnabled, columnSpan); + + widgetMap.put(widgetName, widget); + } else { + widget.update(); } } } @@ -309,8 +318,8 @@ public class DashboardFragment extends Fragment { .commit(); GridLayout.LayoutParams layoutParams = new GridLayout.LayoutParams( - GridLayout.spec(GridLayout.UNDEFINED, GridLayout.FILL,1f), - GridLayout.spec(GridLayout.UNDEFINED, columnSpan, GridLayout.FILL,1f) + GridLayout.spec(GridLayout.UNDEFINED, GridLayout.FILL, 1f), + GridLayout.spec(GridLayout.UNDEFINED, columnSpan, GridLayout.FILL, 1f) ); layoutParams.width = 0; int pixels_8dp = (int) (8 * scale + 0.5f); @@ -352,6 +361,7 @@ public class DashboardFragment extends Fragment { private float distanceGoalFactor; private long activeMinutesTotal; private float activeMinutesGoalFactor; + private final Map genericData = new ConcurrentHashMap<>(); public void clear() { stepsTotal = 0; @@ -363,6 +373,7 @@ public class DashboardFragment extends Fragment { activeMinutesTotal = 0; activeMinutesGoalFactor = 0; generalizedActivities.clear(); + genericData.clear(); } public boolean isEmpty() { @@ -374,6 +385,7 @@ public class DashboardFragment extends Fragment { distanceGoalFactor == 0 && activeMinutesTotal == 0 && activeMinutesGoalFactor == 0 && + genericData.isEmpty() && generalizedActivities.isEmpty()); } @@ -425,6 +437,21 @@ public class DashboardFragment extends Fragment { return sleepGoalFactor; } + public void put(final String key, final Serializable value) { + genericData.put(key, value); + } + + public Serializable get(final String key) { + return genericData.get(key); + } + + /** + * @noinspection UnusedReturnValue + */ + public Serializable computeIfAbsent(final String key, final Supplier supplier) { + return genericData.computeIfAbsent(key, absent -> supplier.get()); + } + public static class GeneralizedActivity implements Serializable { public ActivityKind activityKind; public long timeFrom; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/AbstractChartsActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/AbstractChartsActivity.java index 8500a03fe..445e8fc1b 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/AbstractChartsActivity.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/AbstractChartsActivity.java @@ -16,6 +16,7 @@ along with this program. If not, see . */ package nodomain.freeyourgadget.gadgetbridge.activities.charts; +import android.app.Activity; import android.app.DatePickerDialog; import android.content.BroadcastReceiver; import android.content.Context; @@ -29,18 +30,27 @@ import android.widget.Button; import android.widget.TextView; import android.widget.Toast; +import androidx.activity.result.ActivityResult; +import androidx.activity.result.ActivityResultCallback; +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; +import androidx.appcompat.app.ActionBar; import androidx.localbroadcastmanager.content.LocalBroadcastManager; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; import androidx.viewpager.widget.ViewPager; +import com.google.android.material.tabs.TabLayout; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.text.SimpleDateFormat; import java.util.Calendar; +import java.util.Collections; import java.util.Date; import java.util.List; +import java.util.Locale; import java.util.Objects; import nodomain.freeyourgadget.gadgetbridge.GBApplication; @@ -56,8 +66,10 @@ public abstract class AbstractChartsActivity extends AbstractGBFragmentActivity public static final String STATE_START_DATE = "stateStartDate"; public static final String STATE_END_DATE = "stateEndDate"; - public static final String EXTRA_FRAGMENT_ID = "fragment"; - public static final int REQUEST_CODE_PREFERENCES = 1; + public static final String EXTRA_FRAGMENT_ID = "fragmentId"; + public static final String EXTRA_SINGLE_FRAGMENT_NAME = "singleFragmentName"; + public static final String EXTRA_ACTIONBAR_TITLE = "actionbarTitle"; + public static final String EXTRA_TIMESTAMP = "timestamp"; private TextView mDateControl; @@ -70,13 +82,19 @@ public abstract class AbstractChartsActivity extends AbstractGBFragmentActivity private GBDevice mGBDevice; private ViewGroup dateBar; + private ActivityResultLauncher chartsPreferencesLauncher; + private final ActivityResultCallback chartsPreferencesCallback = result -> { + recreate(); + }; + private final BroadcastReceiver mReceiver = new BroadcastReceiver() { @Override - public void onReceive(Context context, Intent intent) { - String action = intent.getAction(); + public void onReceive(final Context context, final Intent intent) { + final String action = intent.getAction(); + //noinspection SwitchStatementWithTooFewBranches switch (Objects.requireNonNull(action)) { case GBDevice.ACTION_DEVICE_CHANGED: - GBDevice dev = intent.getParcelableExtra(GBDevice.EXTRA_DEVICE); + final GBDevice dev = intent.getParcelableExtra(GBDevice.EXTRA_DEVICE); if (dev != null) { refreshBusyState(dev); } @@ -85,11 +103,11 @@ public abstract class AbstractChartsActivity extends AbstractGBFragmentActivity } }; - private void refreshBusyState(GBDevice dev) { + private void refreshBusyState(final GBDevice dev) { if (dev.isBusy()) { swipeLayout.setRefreshing(true); } else { - boolean wasBusy = swipeLayout.isRefreshing(); + final boolean wasBusy = swipeLayout.isRefreshing(); swipeLayout.setRefreshing(false); if (wasBusy) { LocalBroadcastManager.getInstance(this).sendBroadcast(new Intent(REFRESH)); @@ -99,31 +117,51 @@ public abstract class AbstractChartsActivity extends AbstractGBFragmentActivity } @Override - protected void onCreate(Bundle savedInstanceState) { + protected void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_charts); - int tabFragmentToOpen = -1; + final Bundle extras = getIntent().getExtras(); + if (extras == null) { + throw new IllegalArgumentException("Must provide a device when invoking this activity"); + } + + mGBDevice = extras.getParcelable(GBDevice.EXTRA_DEVICE); + + chartsPreferencesLauncher = registerForActivityResult( + new ActivityResultContracts.StartActivityForResult(), + chartsPreferencesCallback + ); + + // Set start and end date if (savedInstanceState != null) { setEndDate(new Date(savedInstanceState.getLong(STATE_END_DATE, System.currentTimeMillis()))); - setStartDate(new Date(savedInstanceState.getLong(STATE_START_DATE, DateTimeUtils.shiftByDays(getEndDate(), -1).getTime()))); + } else if (extras.containsKey(EXTRA_TIMESTAMP)) { + final int endTimestamp = extras.getInt(EXTRA_TIMESTAMP, 0); + setEndDate(new Date(endTimestamp * 1000L)); } else { setEndDate(new Date()); - setStartDate(DateTimeUtils.shiftByDays(getEndDate(), -1)); } + setStartDate(DateTimeUtils.shiftByDays(getEndDate(), -1)); final IntentFilter filterLocal = new IntentFilter(); filterLocal.addAction(GBDevice.ACTION_DEVICE_CHANGED); LocalBroadcastManager.getInstance(this).registerReceiver(mReceiver, filterLocal); - final Bundle extras = getIntent().getExtras(); - if (extras != null) { - mGBDevice = extras.getParcelable(GBDevice.EXTRA_DEVICE); - tabFragmentToOpen = extras.getInt(EXTRA_FRAGMENT_ID); - } else { - throw new IllegalArgumentException("Must provide a device when invoking this activity"); + // Open the specified fragment, if any, and setup single page view if specified + final int tabFragmentIdToOpen = extras.getInt(EXTRA_FRAGMENT_ID, -1); + final String singleFragmentName = extras.getString(EXTRA_SINGLE_FRAGMENT_NAME, null); + final int actionbarTitle = extras.getInt(EXTRA_ACTIONBAR_TITLE, 0); + + if (tabFragmentIdToOpen >= 0 && singleFragmentName != null) { + throw new IllegalArgumentException("Must specify either fragment ID or single fragment name, not both"); + } + + if (singleFragmentName != null) { + enabledTabsList = Collections.singletonList(singleFragmentName); + } else { + enabledTabsList = fillChartsTabsList(); } - enabledTabsList = fillChartsTabsList(); swipeLayout = findViewById(R.id.activity_swipe_layout); swipeLayout.setOnRefreshListener(this::fetchRecordedData); @@ -132,8 +170,23 @@ public abstract class AbstractChartsActivity extends AbstractGBFragmentActivity // Set up the ViewPager with the sections adapter. final NonSwipeableViewPager viewPager = findViewById(R.id.charts_pager); viewPager.setAdapter(getPagerAdapter()); - if (tabFragmentToOpen > -1) { - viewPager.setCurrentItem(tabFragmentToOpen); // open the tab as specified in the intent + if (tabFragmentIdToOpen > -1) { + viewPager.setCurrentItem(tabFragmentIdToOpen); // open the tab as specified in the intent + } + + viewPager.setAllowSwipe(singleFragmentName == null && GBApplication.getPrefs().getBoolean("charts_allow_swipe", true)); + + if (singleFragmentName != null) { + final TabLayout tabLayout = findViewById(R.id.charts_pagerTabStrip); + tabLayout.setVisibility(TextView.GONE); + } + + if (actionbarTitle != 0) { + final ActionBar actionBar = getSupportActionBar(); + + if (actionBar != null) { + actionBar.setTitle(actionbarTitle); + } } viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() { @@ -158,19 +211,19 @@ public abstract class AbstractChartsActivity extends AbstractGBFragmentActivity new ShowDurationDialog(detailedDuration, AbstractChartsActivity.this).show(); }); - Button mPrevButton = findViewById(R.id.charts_previous_day); + final Button mPrevButton = findViewById(R.id.charts_previous_day); mPrevButton.setOnClickListener(v -> handleButtonClicked(DATE_PREV_DAY)); - Button mNextButton = findViewById(R.id.charts_next_day); + final Button mNextButton = findViewById(R.id.charts_next_day); mNextButton.setOnClickListener(v -> handleButtonClicked(DATE_NEXT_DAY)); - Button mPrevWeekButton = findViewById(R.id.charts_previous_week); + final Button mPrevWeekButton = findViewById(R.id.charts_previous_week); mPrevWeekButton.setOnClickListener(v -> handleButtonClicked(DATE_PREV_WEEK)); - Button mNextWeekButton = findViewById(R.id.charts_next_week); + final Button mNextWeekButton = findViewById(R.id.charts_next_week); mNextWeekButton.setOnClickListener(v -> handleButtonClicked(DATE_NEXT_WEEK)); - Button mPrevMonthButton = findViewById(R.id.charts_previous_month); + final Button mPrevMonthButton = findViewById(R.id.charts_previous_month); mPrevMonthButton.setOnClickListener(v -> handleButtonClicked(DATE_PREV_MONTH)); - Button mNextMonthButton = findViewById(R.id.charts_next_month); + final Button mNextMonthButton = findViewById(R.id.charts_next_month); mNextMonthButton.setOnClickListener(v -> handleButtonClicked(DATE_NEXT_MONTH)); } @@ -193,7 +246,7 @@ public abstract class AbstractChartsActivity extends AbstractGBFragmentActivity protected abstract List fillChartsTabsList(); private String formatDetailedDuration() { - final SimpleDateFormat dateFormat = new SimpleDateFormat("dd.MM.yyyy HH:mm"); + final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.getDefault()); final String dateStringFrom = dateFormat.format(getStartDate()); final String dateStringTo = dateFormat.format(getEndDate()); @@ -262,15 +315,7 @@ public abstract class AbstractChartsActivity extends AbstractGBFragmentActivity } @Override - protected void onActivityResult(int requestCode, int resultCode, Intent data) { - super.onActivityResult(requestCode, resultCode, data); - if (requestCode == REQUEST_CODE_PREFERENCES) { - this.recreate(); - } - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { + public boolean onOptionsItemSelected(final MenuItem item) { final int itemId = item.getItemId(); if (itemId == R.id.charts_fetch_activity_data) { fetchRecordedData(); @@ -285,8 +330,8 @@ public abstract class AbstractChartsActivity extends AbstractGBFragmentActivity LocalBroadcastManager.getInstance(this).sendBroadcast(new Intent(REFRESH)); }, currentDate.get(Calendar.YEAR), currentDate.get(Calendar.MONTH), currentDate.get(Calendar.DATE)).show(); } else if (itemId == R.id.prefs_charts_menu) { - Intent settingsIntent = new Intent(this, ChartsPreferencesActivity.class); - startActivityForResult(settingsIntent, REQUEST_CODE_PREFERENCES); + final Intent settingsIntent = new Intent(this, ChartsPreferencesActivity.class); + chartsPreferencesLauncher.launch(settingsIntent); return true; } @@ -294,7 +339,7 @@ public abstract class AbstractChartsActivity extends AbstractGBFragmentActivity } @Override - public void enableSwipeRefresh(boolean enable) { + public void enableSwipeRefresh(final boolean enable) { swipeLayout.setEnabled(enable && allowRefresh()); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/AbstractCollectionFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/AbstractCollectionFragment.java new file mode 100644 index 000000000..aa61fe7a7 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/AbstractCollectionFragment.java @@ -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 . */ +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; + } +} + diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/ActivityChartsActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/ActivityChartsActivity.java index 3e3d0ceea..29c334a06 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/ActivityChartsActivity.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/ActivityChartsActivity.java @@ -145,7 +145,7 @@ public class ActivityChartsActivity extends AbstractChartsActivity { case "activitylist": return new ActivityListingChartFragment(); case "sleep": - return new SleepCollectionFragment(); + return SleepCollectionFragment.newInstance(enabledTabsList.size() == 1); case "hrvstatus": return new HRVStatusFragment(); case "bodyenergy": @@ -155,7 +155,7 @@ public class ActivityChartsActivity extends AbstractChartsActivity { case "pai": return new PaiChartFragment(); case "stepsweek": - return new StepsCollectionFragment(); + return StepsCollectionFragment.newInstance(enabledTabsList.size() == 1); case "speedzones": return new SpeedZonesFragment(); case "livestats": @@ -177,14 +177,6 @@ public class ActivityChartsActivity extends AbstractChartsActivity { return enabledTabsList.toArray().length; } - private String getSleepTitle() { - if (GBApplication.getPrefs().getBoolean("charts_range", true)) { - return getString(R.string.weeksleepchart_sleep_a_month); - } else { - return getString(R.string.weeksleepchart_sleep_a_week); - } - } - @Override public CharSequence getPageTitle(int position) { switch (enabledTabsList.get(position)) { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/NonSwipeableViewPager.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/NonSwipeableViewPager.java index a82b98303..6aebcbc37 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/NonSwipeableViewPager.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/NonSwipeableViewPager.java @@ -25,14 +25,19 @@ import androidx.viewpager.widget.ViewPager; import nodomain.freeyourgadget.gadgetbridge.GBApplication; public class NonSwipeableViewPager extends ViewPager { + private boolean allowSwipe = true; public NonSwipeableViewPager(final Context context, final AttributeSet attrs) { super(context, attrs); } + public void setAllowSwipe(final boolean allowSwipe) { + this.allowSwipe = allowSwipe; + } + @Override public boolean onInterceptTouchEvent(final MotionEvent ev) { - if (GBApplication.getPrefs().getBoolean("charts_allow_swipe", true)) { + if (allowSwipe) { return super.onInterceptTouchEvent(ev); } return false; @@ -40,7 +45,7 @@ public class NonSwipeableViewPager extends ViewPager { @Override public boolean onTouchEvent(final MotionEvent ev) { - if (GBApplication.getPrefs().getBoolean("charts_allow_swipe", true)) { + if (allowSwipe) { return super.onTouchEvent(ev); } return false; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/SleepCollectionFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/SleepCollectionFragment.java index 8bc966dda..5377f5da9 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/SleepCollectionFragment.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/SleepCollectionFragment.java @@ -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 . */ package nodomain.freeyourgadget.gadgetbridge.activities.charts; import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.viewpager2.widget.ViewPager2; +import androidx.fragment.app.FragmentManager; -import com.google.android.material.tabs.TabLayout; -import com.google.android.material.tabs.TabLayoutMediator; - -import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBFragment; +import nodomain.freeyourgadget.gadgetbridge.adapter.NestedFragmentAdapter; import nodomain.freeyourgadget.gadgetbridge.adapter.SleepFragmentAdapter; -public class SleepCollectionFragment extends AbstractGBFragment { - protected SleepFragmentAdapter nestedFragmentsAdapter; - protected ViewPager2 viewPager; - private int last_position = 0; +public class SleepCollectionFragment extends AbstractCollectionFragment { + public SleepCollectionFragment() { - @Override - protected void onMadeVisibleInActivity() { - super.onMadeVisibleInActivity(); - nestedFragmentsAdapter.updateFragments(last_position); + } + + public static SleepCollectionFragment newInstance(final boolean allowSwipe) { + final SleepCollectionFragment fragment = new SleepCollectionFragment(); + final Bundle args = new Bundle(); + args.putBoolean(ARG_ALLOW_SWIPE, allowSwipe); + fragment.setArguments(args); + return fragment; } @Override - public void onMadeInvisibleInActivity() { - if (nestedFragmentsAdapter != null) { - nestedFragmentsAdapter.updateFragments(-1); - } - } - - public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, - @Nullable Bundle savedInstanceState) { - View rootView = inflater.inflate(R.layout.fragment_nested_tabs, container, false); - nestedFragmentsAdapter = new SleepFragmentAdapter(this, getChildFragmentManager()); - viewPager = rootView.findViewById(R.id.pager); - viewPager.setAdapter(nestedFragmentsAdapter); - viewPager.setOrientation(ViewPager2.ORIENTATION_VERTICAL); - viewPager.setUserInputEnabled(false); - viewPager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() { - @Override - public void onPageSelected(int position) { - super.onPageSelected(position); - last_position = position; - viewPager.post(new Runnable() { - @Override - public void run() { - if (isVisibleInActivity()) { - nestedFragmentsAdapter.updateFragments(position); - } - } - }); - } - }); - - return rootView; - } - - public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { - TabLayout tabLayout = view.findViewById(R.id.tab_layout); - new TabLayoutMediator(tabLayout, viewPager, (tab, position) -> { - switch (position) { - case 0: - tab.setText(getString(R.string.calendar_day)); - break; - case 1: - tab.setText(getString(R.string.calendar_week)); - break; - case 2: - tab.setText(getString(R.string.calendar_month)); - break; - } - }).attach(); - } - - @Nullable - @Override - protected CharSequence getTitle() { - return null; + public NestedFragmentAdapter getNestedFragmentAdapter(AbstractGBFragment fragment, FragmentManager childFragmentManager) { + return new SleepFragmentAdapter(this, getChildFragmentManager()); } } - diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/StepsCollectionFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/StepsCollectionFragment.java index bb8b2cc77..b9004260f 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/StepsCollectionFragment.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/StepsCollectionFragment.java @@ -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 . */ package nodomain.freeyourgadget.gadgetbridge.activities.charts; import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.viewpager2.widget.ViewPager2; +import androidx.fragment.app.FragmentManager; -import com.google.android.material.tabs.TabLayout; -import com.google.android.material.tabs.TabLayoutMediator; - -import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBFragment; +import nodomain.freeyourgadget.gadgetbridge.adapter.NestedFragmentAdapter; import nodomain.freeyourgadget.gadgetbridge.adapter.StepsFragmentAdapter; -public class StepsCollectionFragment extends AbstractGBFragment { - protected StepsFragmentAdapter nestedFragmentsAdapter; - protected ViewPager2 viewPager; - private int last_position = 0; +public class StepsCollectionFragment extends AbstractCollectionFragment { + public StepsCollectionFragment() { - @Override - protected void onMadeVisibleInActivity() { - super.onMadeVisibleInActivity(); - nestedFragmentsAdapter.updateFragments(last_position); + } + + public static StepsCollectionFragment newInstance(final boolean allowSwipe) { + final StepsCollectionFragment fragment = new StepsCollectionFragment(); + final Bundle args = new Bundle(); + args.putBoolean(ARG_ALLOW_SWIPE, allowSwipe); + fragment.setArguments(args); + return fragment; } @Override - public void onMadeInvisibleInActivity() { - if (nestedFragmentsAdapter != null) { - nestedFragmentsAdapter.updateFragments(-1); - } - } - - public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, - @Nullable Bundle savedInstanceState) { - View rootView = inflater.inflate(R.layout.fragment_nested_tabs, container, false); - nestedFragmentsAdapter = new StepsFragmentAdapter(this, getChildFragmentManager()); - viewPager = rootView.findViewById(R.id.pager); - viewPager.setAdapter(nestedFragmentsAdapter); - viewPager.setOrientation(ViewPager2.ORIENTATION_VERTICAL); - viewPager.setUserInputEnabled(false); - viewPager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() { - @Override - public void onPageSelected(int position) { - super.onPageSelected(position); - last_position = position; - viewPager.post(new Runnable() { - @Override - public void run() { - if (isVisibleInActivity()) { - nestedFragmentsAdapter.updateFragments(position); - } - } - }); - } - }); - - return rootView; - } - - public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { - TabLayout tabLayout = view.findViewById(R.id.tab_layout); - new TabLayoutMediator(tabLayout, viewPager, (tab, position) -> { - switch (position) { - case 0: - tab.setText(getString(R.string.calendar_day)); - break; - case 1: - tab.setText(getString(R.string.calendar_week)); - break; - case 2: - tab.setText(getString(R.string.calendar_month)); - break; - } - }).attach(); - } - - @Nullable - @Override - protected CharSequence getTitle() { - return null; + public NestedFragmentAdapter getNestedFragmentAdapter(AbstractGBFragment fragment, FragmentManager childFragmentManager) { + return new StepsFragmentAdapter(this, getChildFragmentManager()); } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/StressChartFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/StressChartFragment.java index 84b50943f..adfd9b12c 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/StressChartFragment.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/StressChartFragment.java @@ -520,7 +520,7 @@ public class StressChartFragment extends AbstractChartFragment. */ package nodomain.freeyourgadget.gadgetbridge.activities.dashboard; -import android.graphics.Bitmap; -import android.graphics.Canvas; +import android.content.Context; +import android.content.Intent; import android.graphics.Color; -import android.graphics.Paint; import android.os.Bundle; +import android.view.View; +import android.widget.Toast; import androidx.annotation.ColorInt; import androidx.fragment.app.Fragment; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.List; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +import nodomain.freeyourgadget.gadgetbridge.GBApplication; +import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.activities.DashboardFragment; +import nodomain.freeyourgadget.gadgetbridge.activities.charts.ActivityChartsActivity; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.util.GB; public abstract class AbstractDashboardWidget extends Fragment { private static final Logger LOG = LoggerFactory.getLogger(AbstractDashboardWidget.class); @@ -57,37 +69,67 @@ public abstract class AbstractDashboardWidget extends Fragment { } } - public void update() { fillData(); } protected abstract void fillData(); - /** - * @param width Bitmap width in pixels - * @param barWidth Gauge bar width in pixels - * @param filledColor Color of the filled part of the gauge - * @param filledFactor Factor between 0 and 1 that determines the amount of the gauge that should be filled - * @return Bitmap containing the gauge - */ - Bitmap drawGauge(int width, int barWidth, @ColorInt int filledColor, float filledFactor) { - int height = width / 2; - int barMargin = (int) Math.ceil(barWidth / 2f); + protected boolean isSupportedBy(final GBDevice device) { + return device.getDeviceCoordinator().supportsActivityTracking(); + } - Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); - Canvas canvas = new Canvas(bitmap); - Paint paint = new Paint(); - paint.setAntiAlias(true); - paint.setStyle(Paint.Style.STROKE); - paint.setStrokeCap(Paint.Cap.ROUND); - paint.setStrokeWidth(barWidth * 0.75f); - paint.setColor(color_unknown); - canvas.drawArc(barMargin, barMargin, width - barMargin, width - barMargin, 180 + 180 * filledFactor, 180 - 180 * filledFactor, false, paint); - paint.setStrokeWidth(barWidth); - paint.setColor(filledColor); - canvas.drawArc(barMargin, barMargin, width - barMargin, width - barMargin, 180, 180 * filledFactor, false, paint); + protected List getSupportedDevices(final DashboardFragment.DashboardData dashboardData) { + return GBApplication.app().getDeviceManager().getDevices() + .stream() + .filter(dev -> dashboardData.showAllDevices || dashboardData.showDeviceList.contains(dev.getAddress())) + .filter(this::isSupportedBy) + .collect(Collectors.toList()); + } - return bitmap; + protected void onClickOpenChart(final View view, final String chart, final int label) { + view.setOnClickListener(v -> { + chooseDevice(dashboardData, device -> { + final Intent startIntent; + startIntent = new Intent(requireContext(), ActivityChartsActivity.class); + startIntent.putExtra(GBDevice.EXTRA_DEVICE, device); + startIntent.putExtra(ActivityChartsActivity.EXTRA_SINGLE_FRAGMENT_NAME, chart); + startIntent.putExtra(ActivityChartsActivity.EXTRA_ACTIONBAR_TITLE, label); + startIntent.putExtra(ActivityChartsActivity.EXTRA_TIMESTAMP, dashboardData.timeTo); + requireContext().startActivity(startIntent); + }); + }); + } + + protected void chooseDevice(final DashboardFragment.DashboardData dashboardData, + final Consumer consumer) { + final List 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(); } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/dashboard/AbstractGaugeWidget.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/dashboard/AbstractGaugeWidget.java new file mode 100644 index 000000000..e2a1e30b1 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/dashboard/AbstractGaugeWidget.java @@ -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 . */ +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 { + @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; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/dashboard/DashboardActiveTimeWidget.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/dashboard/DashboardActiveTimeWidget.java index 8bdfbb902..ebeaac5c3 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/dashboard/DashboardActiveTimeWidget.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/dashboard/DashboardActiveTimeWidget.java @@ -1,4 +1,4 @@ -/* Copyright (C) 2023-2024 Arjan Schrijver +/* Copyright (C) 2023-2024 Arjan Schrijver, José Rebelo This file is part of Gadgetbridge. @@ -16,19 +16,10 @@ along with this program. If not, see . */ package nodomain.freeyourgadget.gadgetbridge.activities.dashboard; -import android.os.AsyncTask; import android.os.Bundle; -import android.util.TypedValue; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.TextView; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import java.util.Locale; -import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.activities.DashboardFragment; @@ -37,13 +28,9 @@ import nodomain.freeyourgadget.gadgetbridge.activities.DashboardFragment; * Use the {@link DashboardActiveTimeWidget#newInstance} factory method to * create an instance of this fragment. */ -public class DashboardActiveTimeWidget extends AbstractDashboardWidget { - private static final Logger LOG = LoggerFactory.getLogger(DashboardActiveTimeWidget.class); - private TextView activeTime; - private ImageView activeTimeGauge; - +public class DashboardActiveTimeWidget extends AbstractGaugeWidget { public DashboardActiveTimeWidget() { - // Required empty public constructor + super(R.string.activity_list_summary_active_time, "activity"); } /** @@ -53,69 +40,35 @@ public class DashboardActiveTimeWidget extends AbstractDashboardWidget { * @param dashboardData An instance of DashboardFragment.DashboardData. * @return A new instance of fragment DashboardActiveTimeWidget. */ - public static DashboardActiveTimeWidget newInstance(DashboardFragment.DashboardData dashboardData) { - DashboardActiveTimeWidget fragment = new DashboardActiveTimeWidget(); - Bundle args = new Bundle(); + public static DashboardActiveTimeWidget newInstance(final DashboardFragment.DashboardData dashboardData) { + final DashboardActiveTimeWidget fragment = new DashboardActiveTimeWidget(); + final Bundle args = new Bundle(); args.putSerializable(ARG_DASHBOARD_DATA, dashboardData); fragment.setArguments(args); return fragment; } @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View fragmentView = inflater.inflate(R.layout.dashboard_widget_active_time, container, false); - activeTime = fragmentView.findViewById(R.id.activetime_text); - activeTimeGauge = fragmentView.findViewById(R.id.activetime_gauge); - - fillData(); - - return fragmentView; + protected void populateData(final DashboardFragment.DashboardData dashboardData) { + dashboardData.getActiveMinutesTotal(); + dashboardData.getActiveMinutesGoalFactor(); } @Override - public void onResume() { - super.onResume(); - if (activeTime != null && activeTimeGauge != null) fillData(); + protected void draw(final DashboardFragment.DashboardData dashboardData) { + final long totalActiveMinutes = dashboardData.getActiveMinutesTotal(); + final String valueText = String.format( + Locale.ROOT, + "%d:%02d", + (int) Math.floor(totalActiveMinutes / 60f), + (int) (totalActiveMinutes % 60f) + ); + + setText(valueText); + + drawSimpleGauge( + color_active_time, + dashboardData.getActiveMinutesGoalFactor() + ); } - - @Override - protected void fillData() { - if (activeTimeGauge == null) return; - activeTimeGauge.post(new Runnable() { - @Override - public void run() { - FillDataAsyncTask myAsyncTask = new FillDataAsyncTask(); - myAsyncTask.execute(); - } - }); - } - - private class FillDataAsyncTask extends AsyncTask { - @Override - protected Void doInBackground(Void... params) { - dashboardData.getActiveMinutesTotal(); - dashboardData.getActiveMinutesGoalFactor(); - return null; - } - - @Override - protected void onPostExecute(Void unused) { - super.onPostExecute(unused); - - // Update text representation - long totalActiveMinutes = dashboardData.getActiveMinutesTotal(); - String activeHours = String.format("%d", (int) Math.floor(totalActiveMinutes / 60f)); - String activeMinutes = String.format("%02d", (int) (totalActiveMinutes % 60f)); - activeTime.setText(activeHours + ":" + activeMinutes); - - final int width = (int) TypedValue.applyDimension( - TypedValue.COMPLEX_UNIT_DIP, - 150, - GBApplication.getContext().getResources().getDisplayMetrics() - ); - - // Draw gauge - activeTimeGauge.setImageBitmap(drawGauge(width, Math.round(width * 0.075f), color_active_time, dashboardData.getActiveMinutesGoalFactor())); - } - } -} \ No newline at end of file +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/dashboard/DashboardBodyEnergyWidget.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/dashboard/DashboardBodyEnergyWidget.java new file mode 100644 index 000000000..820486ac3 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/dashboard/DashboardBodyEnergyWidget.java @@ -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 . */ +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 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 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; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/dashboard/DashboardDistanceWidget.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/dashboard/DashboardDistanceWidget.java index a77052f4b..0a32cbb34 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/dashboard/DashboardDistanceWidget.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/dashboard/DashboardDistanceWidget.java @@ -1,4 +1,4 @@ -/* Copyright (C) 2023-2024 Arjan Schrijver +/* Copyright (C) 2023-2024 Arjan Schrijver, José Rebelo This file is part of Gadgetbridge. @@ -16,21 +16,8 @@ along with this program. If not, see . */ package nodomain.freeyourgadget.gadgetbridge.activities.dashboard; -import android.content.res.Resources; -import android.os.AsyncTask; import android.os.Bundle; -import android.util.DisplayMetrics; -import android.util.TypedValue; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.TextView; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.activities.DashboardFragment; import nodomain.freeyourgadget.gadgetbridge.util.FormatUtils; @@ -40,13 +27,9 @@ import nodomain.freeyourgadget.gadgetbridge.util.FormatUtils; * Use the {@link DashboardDistanceWidget#newInstance} factory method to * create an instance of this fragment. */ -public class DashboardDistanceWidget extends AbstractDashboardWidget { - private static final Logger LOG = LoggerFactory.getLogger(DashboardDistanceWidget.class); - private TextView distanceText; - private ImageView distanceGauge; - +public class DashboardDistanceWidget extends AbstractGaugeWidget { public DashboardDistanceWidget() { - // Required empty public constructor + super(R.string.distance, "stepsweek"); } /** @@ -56,67 +39,26 @@ public class DashboardDistanceWidget extends AbstractDashboardWidget { * @param dashboardData An instance of DashboardFragment.DashboardData. * @return A new instance of fragment DashboardDistanceWidget. */ - public static DashboardDistanceWidget newInstance(DashboardFragment.DashboardData dashboardData) { - DashboardDistanceWidget fragment = new DashboardDistanceWidget(); - Bundle args = new Bundle(); + public static DashboardDistanceWidget newInstance(final DashboardFragment.DashboardData dashboardData) { + final DashboardDistanceWidget fragment = new DashboardDistanceWidget(); + final Bundle args = new Bundle(); args.putSerializable(ARG_DASHBOARD_DATA, dashboardData); fragment.setArguments(args); return fragment; } @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View fragmentView = inflater.inflate(R.layout.dashboard_widget_distance, container, false); - distanceText = fragmentView.findViewById(R.id.distance_text); - distanceGauge = fragmentView.findViewById(R.id.distance_gauge); - - fillData(); - - return fragmentView; + protected void populateData(final DashboardFragment.DashboardData dashboardData) { + dashboardData.getDistanceTotal(); + dashboardData.getDistanceGoalFactor(); } @Override - public void onResume() { - super.onResume(); - if (distanceText != null && distanceGauge != null) fillData(); + protected void draw(final DashboardFragment.DashboardData dashboardData) { + setText(FormatUtils.getFormattedDistanceLabel(dashboardData.getDistanceTotal())); + 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 { - @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())); - } - } -} \ No newline at end of file +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/dashboard/DashboardGoalsWidget.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/dashboard/DashboardGoalsWidget.java index af51bbbc3..1b857e24c 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/dashboard/DashboardGoalsWidget.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/dashboard/DashboardGoalsWidget.java @@ -1,4 +1,4 @@ -/* Copyright (C) 2023-2024 Arjan Schrijver +/* Copyright (C) 2023-2024 Arjan Schrijver, José Rebelo This file is part of Gadgetbridge. @@ -90,8 +90,6 @@ public class DashboardGoalsWidget extends AbstractDashboardWidget { Prefs prefs = GBApplication.getPrefs(); legend.setVisibility(prefs.getBoolean("dashboard_widget_goals_legend", true) ? View.VISIBLE : View.GONE); - fillData(); - return goalsView; } @@ -118,6 +116,8 @@ public class DashboardGoalsWidget extends AbstractDashboardWidget { @Override protected Void doInBackground(Void... params) { + final long nanoStart = System.nanoTime(); + int width = Resources.getSystem().getDisplayMetrics().widthPixels; int height = width; int barWidth = Math.round(height * 0.04f); @@ -160,6 +160,11 @@ public class DashboardGoalsWidget extends AbstractDashboardWidget { paint.setStrokeWidth(barWidth); paint.setColor(color_light_sleep); canvas.drawArc(barMargin, barMargin, width - barMargin, height - barMargin, 270, 360 * dashboardData.getSleepMinutesGoalFactor(), false, paint); + + final long nanoEnd = System.nanoTime(); + final long executionTime = (nanoEnd - nanoStart) / 1000000; + LOG.debug("fillData for {} took {}ms", DashboardGoalsWidget.this.getClass().getSimpleName(), executionTime); + return null; } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/dashboard/DashboardHrvWidget.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/dashboard/DashboardHrvWidget.java new file mode 100644 index 000000000..b760e2964 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/dashboard/DashboardHrvWidget.java @@ -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 . */ +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 devices = getSupportedDevices(dashboardData); + + HrvSummarySample latestSummary = null; + + try (DBHandler dbHandler = GBApplication.acquireDB()) { + for (GBDevice dev : devices) { + final List 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; + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/dashboard/DashboardSleepWidget.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/dashboard/DashboardSleepWidget.java index 45ec0faab..41818bda2 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/dashboard/DashboardSleepWidget.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/dashboard/DashboardSleepWidget.java @@ -1,4 +1,4 @@ -/* Copyright (C) 2023-2024 Arjan Schrijver +/* Copyright (C) 2023-2024 Arjan Schrijver, José Rebelo This file is part of Gadgetbridge. @@ -16,34 +16,22 @@ along with this program. If not, see . */ package nodomain.freeyourgadget.gadgetbridge.activities.dashboard; -import android.os.AsyncTask; import android.os.Bundle; -import android.util.TypedValue; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.TextView; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import java.util.Locale; -import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.activities.DashboardFragment; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; /** * A simple {@link AbstractDashboardWidget} subclass. * Use the {@link DashboardSleepWidget#newInstance} factory method to * create an instance of this fragment. */ -public class DashboardSleepWidget extends AbstractDashboardWidget { - private static final Logger LOG = LoggerFactory.getLogger(DashboardSleepWidget.class); - private TextView sleepAmount; - private ImageView sleepGauge; - +public class DashboardSleepWidget extends AbstractGaugeWidget { public DashboardSleepWidget() { - // Required empty public constructor + super(R.string.menuitem_sleep, "sleep"); } /** @@ -53,69 +41,39 @@ public class DashboardSleepWidget extends AbstractDashboardWidget { * @param dashboardData An instance of DashboardFragment.DashboardData. * @return A new instance of fragment DashboardSleepWidget. */ - public static DashboardSleepWidget newInstance(DashboardFragment.DashboardData dashboardData) { - DashboardSleepWidget fragment = new DashboardSleepWidget(); - Bundle args = new Bundle(); + public static DashboardSleepWidget newInstance(final DashboardFragment.DashboardData dashboardData) { + final DashboardSleepWidget fragment = new DashboardSleepWidget(); + final Bundle args = new Bundle(); args.putSerializable(ARG_DASHBOARD_DATA, dashboardData); fragment.setArguments(args); return fragment; } @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View fragmentView = inflater.inflate(R.layout.dashboard_widget_sleep, container, false); - sleepAmount = fragmentView.findViewById(R.id.sleep_text); - sleepGauge = fragmentView.findViewById(R.id.sleep_gauge); - - fillData(); - - return fragmentView; + protected boolean isSupportedBy(final GBDevice device) { + return device.getDeviceCoordinator().supportsSleepMeasurement(); } @Override - public void onResume() { - super.onResume(); - if (sleepAmount != null && sleepGauge != null) fillData(); + protected void populateData(final DashboardFragment.DashboardData dashboardData) { + dashboardData.getSleepMinutesTotal(); + dashboardData.getSleepMinutesGoalFactor(); } @Override - protected void fillData() { - if (sleepGauge == null) return; - sleepGauge.post(new Runnable() { - @Override - public void run() { - FillDataAsyncTask myAsyncTask = new FillDataAsyncTask(); - myAsyncTask.execute(); - } - }); + protected void draw(final DashboardFragment.DashboardData dashboardData) { + final long totalSleepMinutes = dashboardData.getSleepMinutesTotal(); + final String valueText = String.format( + Locale.ROOT, + "%d:%02d", + (int) Math.floor(totalSleepMinutes / 60f), + (int) (totalSleepMinutes % 60f) + ); + + setText(valueText); + drawSimpleGauge( + color_light_sleep, + dashboardData.getSleepMinutesGoalFactor() + ); } - - private class FillDataAsyncTask extends AsyncTask { - @Override - protected Void doInBackground(Void... params) { - dashboardData.getSleepMinutesTotal(); - dashboardData.getSleepMinutesGoalFactor(); - return null; - } - - @Override - protected void onPostExecute(Void unused) { - super.onPostExecute(unused); - - // Update text representation - long totalSleepMinutes = dashboardData.getSleepMinutesTotal(); - String sleepHours = String.format("%d", (int) Math.floor(totalSleepMinutes / 60f)); - String sleepMinutes = String.format("%02d", (int) (totalSleepMinutes % 60f)); - sleepAmount.setText(sleepHours + ":" + sleepMinutes); - - final int width = (int) TypedValue.applyDimension( - TypedValue.COMPLEX_UNIT_DIP, - 150, - GBApplication.getContext().getResources().getDisplayMetrics() - ); - - // Draw gauge - sleepGauge.setImageBitmap(drawGauge(width, Math.round(width * 0.075f), color_light_sleep, dashboardData.getSleepMinutesGoalFactor())); - } - } -} \ No newline at end of file +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/dashboard/DashboardStepsWidget.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/dashboard/DashboardStepsWidget.java index b8ceef08a..d86e1e316 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/dashboard/DashboardStepsWidget.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/dashboard/DashboardStepsWidget.java @@ -1,4 +1,4 @@ -/* Copyright (C) 2023-2024 Arjan Schrijver +/* Copyright (C) 2023-2024 Arjan Schrijver, José Rebelo This file is part of Gadgetbridge. @@ -16,19 +16,8 @@ along with this program. If not, see . */ package nodomain.freeyourgadget.gadgetbridge.activities.dashboard; -import android.os.AsyncTask; import android.os.Bundle; -import android.util.TypedValue; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.TextView; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.activities.DashboardFragment; @@ -37,13 +26,9 @@ import nodomain.freeyourgadget.gadgetbridge.activities.DashboardFragment; * Use the {@link DashboardStepsWidget#newInstance} factory method to * create an instance of this fragment. */ -public class DashboardStepsWidget extends AbstractDashboardWidget { - private static final Logger LOG = LoggerFactory.getLogger(DashboardStepsWidget.class); - private TextView stepsCount; - private ImageView stepsGauge; - +public class DashboardStepsWidget extends AbstractGaugeWidget { public DashboardStepsWidget() { - // Required empty public constructor + super(R.string.steps, "stepsweek"); } /** @@ -53,64 +38,26 @@ public class DashboardStepsWidget extends AbstractDashboardWidget { * @param dashboardData An instance of DashboardFragment.DashboardData. * @return A new instance of fragment DashboardStepsWidget. */ - public static DashboardStepsWidget newInstance(DashboardFragment.DashboardData dashboardData) { - DashboardStepsWidget fragment = new DashboardStepsWidget(); - Bundle args = new Bundle(); + public static DashboardStepsWidget newInstance(final DashboardFragment.DashboardData dashboardData) { + final DashboardStepsWidget fragment = new DashboardStepsWidget(); + final Bundle args = new Bundle(); args.putSerializable(ARG_DASHBOARD_DATA, dashboardData); fragment.setArguments(args); return fragment; } @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View fragmentView = inflater.inflate(R.layout.dashboard_widget_steps, container, false); - stepsCount = fragmentView.findViewById(R.id.steps_count); - stepsGauge = fragmentView.findViewById(R.id.steps_gauge); - fillData(); - return fragmentView; + protected void populateData(final DashboardFragment.DashboardData dashboardData) { + dashboardData.getStepsTotal(); + dashboardData.getStepsGoalFactor(); } @Override - public void onResume() { - super.onResume(); - if (stepsCount != null && stepsGauge != null) fillData(); + protected void draw(final DashboardFragment.DashboardData dashboardData) { + setText(String.valueOf(dashboardData.getStepsTotal())); + 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 { - @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())); - } - } -} \ No newline at end of file +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/dashboard/DashboardStressBreakdownWidget.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/dashboard/DashboardStressBreakdownWidget.java new file mode 100644 index 000000000..af98222f0 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/dashboard/DashboardStressBreakdownWidget.java @@ -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 . */ +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 + ); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/dashboard/DashboardStressSegmentedWidget.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/dashboard/DashboardStressSegmentedWidget.java new file mode 100644 index 000000000..7f3969760 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/dashboard/DashboardStressSegmentedWidget.java @@ -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 . */ +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 + ); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/dashboard/DashboardStressSimpleWidget.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/dashboard/DashboardStressSimpleWidget.java new file mode 100644 index 000000000..d1f114923 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/dashboard/DashboardStressSimpleWidget.java @@ -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 . */ +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); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/dashboard/DashboardTodayWidget.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/dashboard/DashboardTodayWidget.java index 019c7e930..fbc227775 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/dashboard/DashboardTodayWidget.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/dashboard/DashboardTodayWidget.java @@ -1,4 +1,4 @@ -/* Copyright (C) 2023-2024 Arjan Schrijver +/* Copyright (C) 2023-2024 Arjan Schrijver, José Rebelo This file is part of Gadgetbridge. @@ -122,9 +122,7 @@ public class DashboardTodayWidget extends AbstractDashboardWidget { legend.setVisibility(prefs.getBoolean("dashboard_widget_today_legend", true) ? View.VISIBLE : View.GONE); - if (dashboardData.generalizedActivities.isEmpty()) { - fillData(); - } else { + if (!dashboardData.generalizedActivities.isEmpty()) { draw(); } @@ -147,7 +145,11 @@ public class DashboardTodayWidget extends AbstractDashboardWidget { int height = width; int barWidth = Math.round(width * 0.08f); int hourTextSp = Math.round(width * 0.024f); - float hourTextPixels = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, hourTextSp, requireContext().getResources().getDisplayMetrics()); + float hourTextPixels = TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_SP, + hourTextSp, + GBApplication.getContext().getResources().getDisplayMetrics() + ); float outerCircleMargin = mode_24h ? barWidth / 2f : barWidth / 2f + hourTextPixels * 1.3f; float innerCircleMargin = outerCircleMargin + barWidth * 1.3f; float degreeFactor = mode_24h ? 240 : 120; @@ -168,7 +170,7 @@ public class DashboardTodayWidget extends AbstractDashboardWidget { } // Draw hours - boolean normalClock = DateFormat.is24HourFormat(getContext()); + boolean normalClock = DateFormat.is24HourFormat(GBApplication.getContext()); Map hours = new HashMap() { { put(0, normalClock ? (mode_24h ? "0" : "12") : "12pm"); @@ -435,6 +437,8 @@ public class DashboardTodayWidget extends AbstractDashboardWidget { @Override protected Void doInBackground(Void... params) { + final long nanoStart = System.nanoTime(); + // Retrieve activity data dashboardData.generalizedActivities.clear(); List devices = GBApplication.app().getDeviceManager().getDevices(); @@ -476,16 +480,21 @@ public class DashboardTodayWidget extends AbstractDashboardWidget { addActivity(session.getStartTime().getTime() / 1000, session.getEndTime().getTime() / 1000, ActivityKind.ACTIVITY); } createGeneralizedActivities(); + + final long nanoEnd = System.nanoTime(); + final long executionTime = (nanoEnd - nanoStart) / 1000000; + LOG.debug("fillData for {} took {}ms", DashboardTodayWidget.this.getClass().getSimpleName(), executionTime); + return null; } @Override - protected void onPostExecute(Void unused) { + protected void onPostExecute(final Void unused) { super.onPostExecute(unused); try { draw(); - } catch (IllegalStateException e) { - LOG.warn("calling draw() failed: " + e.getMessage()); + } catch (final Exception e) { + LOG.error("calling draw() failed", e); } } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/dashboard/data/DashboardStressData.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/dashboard/data/DashboardStressData.java new file mode 100644 index 000000000..3106ae6b8 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/dashboard/data/DashboardStressData.java @@ -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 . */ +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 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 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; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/NestedFragmentAdapter.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/NestedFragmentAdapter.java index 628556f60..48f40e5f0 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/NestedFragmentAdapter.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/NestedFragmentAdapter.java @@ -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 . */ package nodomain.freeyourgadget.gadgetbridge.adapter; import androidx.fragment.app.FragmentManager; @@ -7,7 +23,7 @@ import java.util.List; import java.util.stream.Collectors; import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBFragment; -abstract class NestedFragmentAdapter extends FragmentStateAdapter { +public abstract class NestedFragmentAdapter extends FragmentStateAdapter { protected FragmentManager fragmentManager; public NestedFragmentAdapter(AbstractGBFragment fragment, FragmentManager childFragmentManager) { diff --git a/app/src/main/res/layout/dashboard_widget_active_time.xml b/app/src/main/res/layout/dashboard_widget_active_time.xml deleted file mode 100644 index 44879cc2d..000000000 --- a/app/src/main/res/layout/dashboard_widget_active_time.xml +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/dashboard_widget_distance.xml b/app/src/main/res/layout/dashboard_widget_generic_gauge.xml similarity index 61% rename from app/src/main/res/layout/dashboard_widget_distance.xml rename to app/src/main/res/layout/dashboard_widget_generic_gauge.xml index eb37b1654..d8039fa0a 100644 --- a/app/src/main/res/layout/dashboard_widget_distance.xml +++ b/app/src/main/res/layout/dashboard_widget_generic_gauge.xml @@ -1,42 +1,43 @@ - + tools:context=".activities.dashboard.AbstractDashboardWidget"> + tools:ignore="UselessParent"> + android:scaleType="fitStart" /> + android:text="@string/stats_empty_value" + android:textSize="30sp" /> + android:text="@string/no_data" /> - \ No newline at end of file + diff --git a/app/src/main/res/layout/dashboard_widget_sleep.xml b/app/src/main/res/layout/dashboard_widget_sleep.xml deleted file mode 100644 index 438845c30..000000000 --- a/app/src/main/res/layout/dashboard_widget_sleep.xml +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/dashboard_widget_steps.xml b/app/src/main/res/layout/dashboard_widget_steps.xml deleted file mode 100644 index e4e40e04e..000000000 --- a/app/src/main/res/layout/dashboard_widget_steps.xml +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/menu/dashboard_menu.xml b/app/src/main/res/menu/dashboard_menu.xml index 57d1974d8..2927899e2 100644 --- a/app/src/main/res/menu/dashboard_menu.xml +++ b/app/src/main/res/menu/dashboard_menu.xml @@ -9,4 +9,10 @@ android:title="@string/menuitem_calendar" app:iconTint="?attr/actionmenu_icon_color" app:showAsAction="ifRoom" /> + diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index 2d457e985..0fed3bfc0 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -4167,6 +4167,11 @@ @string/distance @string/active_time @string/menuitem_sleep + @string/body_energy + @string/menuitem_stress_simple + @string/menuitem_stress_segmented + @string/menuitem_stress_breakdown + @string/hrv @@ -4176,5 +4181,22 @@ distance activetime sleep + bodyenergy + stress_simple + stress_segmented + stress_breakdown + hrv + + + + today + goals + steps + distance + activetime + sleep + bodyenergy + stress_segmented + hrv diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 6b45d5ff3..2486b734e 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -52,10 +52,11 @@ #be03fc #d12a2a #5ac234 + #ff6c43 #00c9bf #858585 - #383838 + #19808080 #FFEDEDED #545254 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ec1e2d1e4..7434aedad 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -628,6 +628,8 @@ Use the Android Bluetooth pairing dialog to pair the device. Pair your Mi Band Pairing with %s… + Choose a device + No supported devices found "Creating bond with %1$s (%2$s)" "Unable to pair with %1$s (%2$s)" Bonding in progress: %1$s (%2$s) @@ -1871,6 +1873,9 @@ More NFC Stress + Stress (simple) + Stress (segmented) + Stress (breakdown) PAI Heart Rate SpO2 diff --git a/app/src/main/res/xml/dashboard_preferences.xml b/app/src/main/res/xml/dashboard_preferences.xml index 17adfc00d..dc8add63b 100644 --- a/app/src/main/res/xml/dashboard_preferences.xml +++ b/app/src/main/res/xml/dashboard_preferences.xml @@ -20,7 +20,7 @@ android:summary="@string/pref_dashboard_cards_summary" app:iconSpaceReserved="false" />