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 @@
+
+
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 extends ActivitySample> getAllSamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) {
SampleProvider extends ActivitySample> provider = getProvider(db, device);
return provider.getAllActivitySamples(tsFrom, tsTo);
}
+
+ public ActivitySample getFirstSample(DBHandler db, GBDevice device) {
+ SampleProvider extends ActivitySample> 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
+