diff --git a/app/src/main/assets/olive_laurel.svg b/app/src/main/assets/olive_laurel.svg new file mode 100644 index 000000000..1e66711ec --- /dev/null +++ b/app/src/main/assets/olive_laurel.svg @@ -0,0 +1,340 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/AbstractWeekChartFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/AbstractWeekChartFragment.java index 0f7ec738d..c14b34a3d 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/AbstractWeekChartFragment.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/AbstractWeekChartFragment.java @@ -20,11 +20,15 @@ package nodomain.freeyourgadget.gadgetbridge.activities.charts; import android.app.Activity; import android.graphics.Color; import android.os.Bundle; +import android.text.format.DateUtils; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.ImageView; import android.widget.TextView; +import androidx.fragment.app.FragmentManager; + import com.github.mikephil.charting.charts.BarChart; import com.github.mikephil.charting.charts.PieChart; import com.github.mikephil.charting.components.LimitLine; @@ -69,6 +73,7 @@ public abstract class AbstractWeekChartFragment extends AbstractChartFragment { private TextView mBalanceView; private int mOffsetHours = getOffsetHours(); + ImageView stepsStreaksButton; @Override protected ChartsData refreshInBackground(ChartsHost chartsHost, DBHandler db, GBDevice device) { @@ -98,6 +103,20 @@ public abstract class AbstractWeekChartFragment extends AbstractChartFragment { mWeekChart.getXAxis().setValueFormatter(mcd.getWeekBeforeData().getXValueFormatter()); mBalanceView.setText(mcd.getWeekBeforeData().getBalanceMessage()); + + //disable the streak FAB once we move away from today + Calendar day = Calendar.getInstance(); + day.setTime(getChartsHost().getEndDate()); + if (DateUtils.isToday(day.getTimeInMillis()) && enableStepStreaksButton()){ + stepsStreaksButton.setVisibility(View.VISIBLE); + }else + { + stepsStreaksButton.setVisibility(View.GONE); + } + } + + private boolean enableStepStreaksButton(){ + return this.getClass().getSimpleName().equals("WeekStepsChartFragment"); } @Override @@ -225,7 +244,7 @@ public abstract class AbstractWeekChartFragment extends AbstractChartFragment { View rootView = inflater.inflate(R.layout.fragment_weeksteps_chart, container, false); - int goal = getGoal(); + final int goal = getGoal(); if (goal >= 0) { mTargetValue = goal; } @@ -237,12 +256,28 @@ public abstract class AbstractWeekChartFragment extends AbstractChartFragment { setupWeekChart(); setupTodayPieChart(); + stepsStreaksButton = rootView.findViewById(R.id.steps_streaks_button); + if (enableStepStreaksButton()) { + stepsStreaksButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + FragmentManager fm = getActivity().getSupportFragmentManager(); + StepStreaksDashboard stepStreaksDashboard = StepStreaksDashboard.newInstance(getGoal(), getChartsHost().getDevice()); + stepStreaksDashboard.show(fm, "steps_streaks_dashboard"); + } + }); + } + // refresh immediately instead of use refreshIfVisible(), for perceived performance refresh(); return rootView; } + + + + private void setupTodayPieChart() { mTodayPieChart.setBackgroundColor(BACKGROUND_COLOR); mTodayPieChart.getDescription().setTextColor(DESCRIPTION_COLOR); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/StepStreaksDashboard.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/StepStreaksDashboard.java new file mode 100644 index 000000000..6ad5cf924 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/StepStreaksDashboard.java @@ -0,0 +1,319 @@ +package nodomain.freeyourgadget.gadgetbridge.activities.charts; + +import android.content.Context; +import android.os.Build; +import android.os.Bundle; +import android.text.format.DateUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; +import android.widget.ProgressBar; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.Nullable; +import androidx.fragment.app.DialogFragment; +import androidx.fragment.app.FragmentActivity; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Calendar; +import java.util.Date; + +import nodomain.freeyourgadget.gadgetbridge.R; +import nodomain.freeyourgadget.gadgetbridge.database.DBAccess; +import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample; +import nodomain.freeyourgadget.gadgetbridge.model.DailyTotals; +import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils; +import nodomain.freeyourgadget.gadgetbridge.util.GB; + +public class StepStreaksDashboard extends DialogFragment { + protected static final Logger LOG = LoggerFactory.getLogger(StepStreaksDashboard.class); + GBDevice gbDevice; + int stepsGoal; + boolean cancelTasks = false; + boolean backgroundTaskFinished = false; + private View fragmentView; + private final StepsStreaks stepsStreaks = new StepsStreaks(); + private static final String GOAL = "goal"; + private static final String PERIOD_CURRENT = "current"; + private static final String PERIOD_TOTALS = "totals"; + private static final int MAX_YEAR = 2015; + + public StepStreaksDashboard() { + + } + + //Calculates some stats for longest streak (daily steps goal being reached for subsequent days + //without interruption (day with steps less then goal) + //Possible improvements/nice to haves: + //- cache values until new activity fetch is performed + //- create a parcel to allow screen rotation without recalculation + //- read the goals from the USER_ATTRIBUTES table. But, this would also require to be able + //to edit/add values there... + + public static StepStreaksDashboard newInstance(int goal, GBDevice device) { + + StepStreaksDashboard fragment = new StepStreaksDashboard(); + Bundle args = new Bundle(); + args.putInt(GOAL, goal); + args.putParcelable(GBDevice.EXTRA_DEVICE, device); + fragment.setArguments(args); + return fragment; + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + return inflater.inflate(R.layout.steps_streaks_dashboard, container); + } + + @Override + public void onStop() { + super.onStop(); + cancelTasks = true; + } + + @Override + public void onDestroy() { + super.onDestroy(); + cancelTasks = true; + } + + + @Override + public void onViewCreated(final View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + stepsGoal = getArguments().getInt(GOAL, 0); + gbDevice = getArguments().getParcelable(GBDevice.EXTRA_DEVICE); + fragmentView = view; + if (gbDevice == null) { + throw new IllegalArgumentException("Must provide a device when invoking this activity"); + } + createTaskCalculateLatestStepsStreak("Visualizing data current", getActivity(), PERIOD_CURRENT).execute(); + createTaskCalculateLatestStepsStreak("Visualizing data maximum", getActivity(), PERIOD_TOTALS).execute(); + } + + + void indicate_progress(boolean inProgress) { + ProgressBar step_streak_dashboard_loading_circle = fragmentView.findViewById(R.id.step_streak_dashboard_loading_circle); + if (inProgress) { + step_streak_dashboard_loading_circle.setAlpha(0.4f); //make it a bit softer + } else { + step_streak_dashboard_loading_circle.setAlpha(0); + } + } + + void populateData() { + + LinearLayout current = getView().findViewById(R.id.step_streak_current_layout); + TextView days_current = current.findViewById(R.id.step_streak_days_value); + TextView average_current = current.findViewById(R.id.step_streak_average_value); + TextView total_current = current.findViewById(R.id.step_streak_total_value); + TextView date_current_value = current.findViewById(R.id.step_streak_current_date_value); + + LinearLayout maximum = getView().findViewById(R.id.step_streak_maximum_layout); + TextView days_maximum = maximum.findViewById(R.id.step_streak_days_value); + TextView average_maximum = maximum.findViewById(R.id.step_streak_average_value); + TextView total_maximum = maximum.findViewById(R.id.step_streak_total_value); + TextView date_maximum_value = maximum.findViewById(R.id.step_streak_maximum_date_value); + + LinearLayout total = getView().findViewById(R.id.step_streak_total_layout); + TextView days_total = total.findViewById(R.id.step_streak_days_value); + TextView days_total_label = total.findViewById(R.id.step_streak_days_label); + TextView total_total = total.findViewById(R.id.step_streak_total_value); + TextView date_total_value = total.findViewById(R.id.step_streak_total_date_value); + TextView date_total_label = total.findViewById(R.id.step_streak_total_label); + + if (stepsStreaks.current.days > 0) { + current.setVisibility(View.VISIBLE); + days_current.setText(Integer.toString(stepsStreaks.current.days)); + average_current.setText(Integer.toString(stepsStreaks.current.steps / stepsStreaks.current.days)); + total_current.setText(Integer.toString(stepsStreaks.current.steps)); + + Date startDate = new Date(stepsStreaks.current.timestamp * 1000L); + Date endDate = DateTimeUtils.shiftByDays(startDate, stepsStreaks.current.days - 1); //first day is 1 not 0 + date_current_value.setText(DateTimeUtils.formatDateRange(startDate, endDate)); + } + + if (stepsStreaks.maximum.days > 0) { + maximum.setVisibility(View.VISIBLE); + days_maximum.setText(Integer.toString(stepsStreaks.maximum.days)); + average_maximum.setText(Integer.toString(stepsStreaks.maximum.steps / stepsStreaks.maximum.days)); + total_maximum.setText(Integer.toString(stepsStreaks.maximum.steps)); + + Date startDate = new Date(stepsStreaks.maximum.timestamp * 1000L); + Date endDate = DateTimeUtils.shiftByDays(startDate, stepsStreaks.maximum.days - 1); //first day is 1 not 0 + date_maximum_value.setText(DateTimeUtils.formatDateRange(startDate, endDate)); + } + if (stepsStreaks.total.steps > 0 || backgroundTaskFinished) { + total.setVisibility(View.VISIBLE); + days_total_label.setText(R.string.steps_streaks_achievement_rate); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + //labels here have diferent meaning, so we must also add proper hint + days_total_label.setTooltipText(getString(R.string.steps_streaks_total_days_hint_totals)); + days_total.setTooltipText(getString(R.string.steps_streaks_total_days_hint_totals)); + date_total_label.setTooltipText(getString(R.string.steps_streaks_total_steps_hint_totals)); + } + + days_total.setText(String.format("%.1f%%", 0.0)); + if (stepsStreaks.total.total_days > 0) { + days_total.setText(String.format("%.1f%%", (float) stepsStreaks.total.days / stepsStreaks.total.total_days * 100)); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + total_total.setTooltipText(String.format(getString(R.string.steps_streaks_total_steps_average_hint), stepsStreaks.total.steps / stepsStreaks.total.total_days)); + } + + } + if (stepsStreaks.total.timestamp > 0) { + date_total_value.setVisibility(View.VISIBLE); + date_total_value.setText(String.format(getString(R.string.steps_streaks_since_date), DateTimeUtils.formatDate(new Date(stepsStreaks.total.timestamp * 1000L)))); + } else { + date_total_value.setVisibility(View.GONE); + } + total_total.setText(Integer.toString(stepsStreaks.total.steps)); + } + } + + protected TaskCalculateLatestStepsStreak createTaskCalculateLatestStepsStreak(String taskName, Context context, String period) { + return new TaskCalculateLatestStepsStreak(taskName, context, period); + } + + public class TaskCalculateLatestStepsStreak extends DBAccess { + String period; + + public TaskCalculateLatestStepsStreak(String taskName, Context context, String period) { + super(taskName, context); + this.period = period; + } + + @Override + protected void doInBackground(DBHandler db) { + switch (period) { + case PERIOD_CURRENT: + calculateStreakData(db, PERIOD_CURRENT, gbDevice, stepsGoal); + + break; + case PERIOD_TOTALS: + calculateStreakData(db, PERIOD_TOTALS, gbDevice, stepsGoal); + break; + } + } + + @Override + protected void onPreExecute() { + indicate_progress(true); + } + + @Override + protected void onPostExecute(Object o) { + super.onPostExecute(o); + FragmentActivity activity = getActivity(); + if (activity != null && !activity.isFinishing() && !activity.isDestroyed()) { + if (period.equals(PERIOD_TOTALS)) { + backgroundTaskFinished = true; + indicate_progress(false); + } + populateData(); + } else { + LOG.info("Not filling data because activity is not available anymore"); + } + } + } + + private void calculateStreakData(DBHandler db, String period, GBDevice device, int goal) { + Calendar day = Calendar.getInstance(); + int streak_steps = 0; + int streak_days = 0; + int timestamp = 0; + + int all_step_days = 0; + int all_streak_days = 0; + int all_steps = 0; + int firstDataTimestamp = 0; + + DailyTotals dailyTotals = new DailyTotals(); + ActivitySample firstSample = dailyTotals.getFirstSample(db, device); + if (firstSample == null) { //no data at all + return; + } + Calendar firstDate = Calendar.getInstance(); + firstDate.setTime(DateTimeUtils.shiftByDays(new Date(firstSample.getTimestamp() * 1000L), -1)); + //go one day back, to ensure we are before the first day, to calculate first day data as well + + while (true) { + if (cancelTasks) { + GB.toast("Cancelling background jobs", Toast.LENGTH_SHORT, GB.INFO); + break; + } + + long[] daily_data = dailyTotals.getDailyTotalsForDevice(device, day, db); + int steps_this_day = (int) daily_data[0]; + + if (steps_this_day > 0) { + all_step_days++; + all_steps += steps_this_day; + firstDataTimestamp = (int) (day.getTimeInMillis() / 1000); + } + + if (steps_this_day >= goal) { + streak_steps += steps_this_day; + streak_days++; + all_streak_days++; + timestamp = (int) (day.getTimeInMillis() / 1000); + Date newDate = DateTimeUtils.shiftByDays(new Date(day.getTimeInMillis()), -1); + day.setTime(newDate); + } else if (DateUtils.isToday(day.getTimeInMillis())) { + //if goal is not reached today, we might still get our steps later + // so do not count this day but do not interrupt + Date newDate = DateTimeUtils.shiftByDays(new Date(day.getTimeInMillis()), -1); + day.setTime(newDate); + } else { + if (period.equals(PERIOD_CURRENT)) { + stepsStreaks.current.days = streak_days; + stepsStreaks.current.steps = streak_steps; + stepsStreaks.current.timestamp = timestamp; + return; + } else if (period.equals(PERIOD_TOTALS)) { + //reset max + if (streak_days > stepsStreaks.maximum.days) { + stepsStreaks.maximum.steps = streak_steps; + stepsStreaks.maximum.days = streak_days; + stepsStreaks.maximum.timestamp = timestamp; + } + stepsStreaks.total.steps = all_steps; + stepsStreaks.total.days = all_streak_days; + stepsStreaks.total.total_days = all_step_days; + stepsStreaks.total.timestamp = firstDataTimestamp; + + streak_days = 0; + streak_steps = 0; + Date newDate = DateTimeUtils.shiftByDays(new Date(day.getTimeInMillis()), -1); + day.setTime(newDate); + if (day.before(firstDate) || day.get(Calendar.YEAR) < MAX_YEAR) { + //avoid rolling back too far, if the data has a timestamp too far into future + //we could make this date configurable if needed for people who imported old data + return; + } + } + } + } + } + + private static class StepsStreak { + private int days = 0; + private int steps = 0; + private int timestamp; + private int total_days = 0; + } + + private class StepsStreaks { + private StepsStreak current = new StepsStreak(); + private StepsStreak maximum = new StepsStreak(); + private StepsStreak total = new StepsStreak(); + } +} + diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractSampleProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractSampleProvider.java index 039e9f264..7d83cadc7 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractSampleProvider.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractSampleProvider.java @@ -110,6 +110,26 @@ public abstract class AbstractSampleProvider i return sample; } + @Nullable + @Override + public T getFirstActivitySample() { + QueryBuilder qb = getSampleDao().queryBuilder(); + Device dbDevice = DBHelper.findDevice(getDevice(), getSession()); + if (dbDevice == null) { + // no device, no sample + return null; + } + Property deviceProperty = getDeviceIdentifierSampleProperty(); + qb.where(deviceProperty.eq(dbDevice.getId())).orderAsc(getTimestampSampleProperty()).limit(1); + List samples = qb.build().list(); + if (samples.isEmpty()) { + return null; + } + T sample = samples.get(0); + sample.setProvider(this); + return sample; + } + protected List getGBActivitySamples(int timestamp_from, int timestamp_to, int activityType) { if (getRawKindSampleProperty() == null && activityType != ActivityKind.TYPE_ALL) { // if we do not have a raw kind property we cannot query anything else then TYPE_ALL diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/SampleProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/SampleProvider.java index 4f993ebce..7a6ec8dc1 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/SampleProvider.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/SampleProvider.java @@ -99,4 +99,12 @@ public interface SampleProvider { */ @Nullable T getLatestActivitySample(); + + /** + * Returns the activity sample with the oldest timestamp or null if none + * @return the oldest sample or null + */ + @Nullable + T getFirstActivitySample(); + } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/UnknownDeviceCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/UnknownDeviceCoordinator.java index cd01d4179..197c81d48 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/UnknownDeviceCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/UnknownDeviceCoordinator.java @@ -87,6 +87,13 @@ public class UnknownDeviceCoordinator extends AbstractDeviceCoordinator { public AbstractActivitySample getLatestActivitySample() { return null; } + + @Nullable + @Override + public AbstractActivitySample getFirstActivitySample() { + return null; + } + } public UnknownDeviceCoordinator() { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DailyTotals.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DailyTotals.java index b110382b6..918bd6e77 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DailyTotals.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DailyTotals.java @@ -143,9 +143,14 @@ public class DailyTotals { return coordinator.getSampleProvider(device, db.getDaoSession()); } - protected List getAllSamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) { SampleProvider provider = getProvider(db, device); return provider.getAllActivitySamples(tsFrom, tsTo); } + + public ActivitySample getFirstSample(DBHandler db, GBDevice device) { + SampleProvider provider = getProvider(db, device); + return provider.getFirstActivitySample(); + } + } diff --git a/app/src/main/res/drawable/ic_events.xml b/app/src/main/res/drawable/ic_events.xml new file mode 100644 index 000000000..1e085de19 --- /dev/null +++ b/app/src/main/res/drawable/ic_events.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_events_gold.xml b/app/src/main/res/drawable/ic_events_gold.xml new file mode 100644 index 000000000..7db7a59ee --- /dev/null +++ b/app/src/main/res/drawable/ic_events_gold.xml @@ -0,0 +1,21 @@ + + + + + diff --git a/app/src/main/res/layout-land/fragment_weeksteps_chart.xml b/app/src/main/res/layout-land/fragment_weeksteps_chart.xml index 246a55b69..867d6f99f 100644 --- a/app/src/main/res/layout-land/fragment_weeksteps_chart.xml +++ b/app/src/main/res/layout-land/fragment_weeksteps_chart.xml @@ -1,37 +1,58 @@ - + - + android:orientation="horizontal" + tools:context="nodomain.freeyourgadget.gadgetbridge.activities.charts.ChartsActivity$PlaceholderFragment"> - + + + + + + + + + android:layout_weight="20" /> - - - - - + - + + + + + + diff --git a/app/src/main/res/layout/fragment_weeksteps_chart.xml b/app/src/main/res/layout/fragment_weeksteps_chart.xml index b51b1428a..8b419c9c8 100644 --- a/app/src/main/res/layout/fragment_weeksteps_chart.xml +++ b/app/src/main/res/layout/fragment_weeksteps_chart.xml @@ -1,25 +1,44 @@ - + android:layout_height="match_parent"> - + - + - + + + + + + + + + - diff --git a/app/src/main/res/layout/steps_streak_average.xml b/app/src/main/res/layout/steps_streak_average.xml new file mode 100644 index 000000000..727087390 --- /dev/null +++ b/app/src/main/res/layout/steps_streak_average.xml @@ -0,0 +1,38 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/steps_streak_current_line_layout.xml b/app/src/main/res/layout/steps_streak_current_line_layout.xml new file mode 100644 index 000000000..71aebe48a --- /dev/null +++ b/app/src/main/res/layout/steps_streak_current_line_layout.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/steps_streak_days.xml b/app/src/main/res/layout/steps_streak_days.xml new file mode 100644 index 000000000..bb32a5fd3 --- /dev/null +++ b/app/src/main/res/layout/steps_streak_days.xml @@ -0,0 +1,40 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/steps_streak_maximum_line_layout.xml b/app/src/main/res/layout/steps_streak_maximum_line_layout.xml new file mode 100644 index 000000000..f59a2dbb1 --- /dev/null +++ b/app/src/main/res/layout/steps_streak_maximum_line_layout.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/steps_streak_total.xml b/app/src/main/res/layout/steps_streak_total.xml new file mode 100644 index 000000000..278979a0e --- /dev/null +++ b/app/src/main/res/layout/steps_streak_total.xml @@ -0,0 +1,40 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/steps_streak_total_line_layout.xml b/app/src/main/res/layout/steps_streak_total_line_layout.xml new file mode 100644 index 000000000..59c1d68c4 --- /dev/null +++ b/app/src/main/res/layout/steps_streak_total_line_layout.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/steps_streaks_dashboard.xml b/app/src/main/res/layout/steps_streaks_dashboard.xml new file mode 100644 index 000000000..3a5348e56 --- /dev/null +++ b/app/src/main/res/layout/steps_streaks_dashboard.xml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4802b752d..a61f38541 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1730,4 +1730,22 @@ Connection over Bluetooth classic Connect on connection from device Establish a connection when connection is initiated by device, like headphones + + Steps streaks + Series of consecutive days without interruption with steps goal being reached + Ongoing + Longest + Total + Total \n steps + Streak \n Days + Average \n steps + Achievement \n rate + Since %s + Average steps per day of the streak + Total number of steps ever recorded + Percentage of days with achieved goal in relation to all days with steps + Number of consecutive days with steps goal being reached + Total number of steps in the whole streak + Total average %d steps per day +