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