/* Privacy Friendly Pedometer is licensed under the GPLv3. Copyright (C) 2017 Tobias Neidig This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program 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 General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.secuso.privacyfriendlyactivitytracker.adapters; import android.content.Context; import android.graphics.Color; import android.support.v4.content.ContextCompat; import android.support.v7.widget.PopupMenu; import android.support.v7.widget.RecyclerView; import android.util.Log; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.ImageButton; import android.widget.RelativeLayout; import android.widget.TextView; import com.github.mikephil.charting.charts.CombinedChart; import com.github.mikephil.charting.charts.LineChart; import com.github.mikephil.charting.components.AxisBase; import com.github.mikephil.charting.components.Legend; import com.github.mikephil.charting.components.XAxis; import com.github.mikephil.charting.components.YAxis; import com.github.mikephil.charting.data.BarData; import com.github.mikephil.charting.data.BarDataSet; import com.github.mikephil.charting.data.BarEntry; import com.github.mikephil.charting.data.CombinedData; import com.github.mikephil.charting.data.Entry; import com.github.mikephil.charting.data.LineData; import com.github.mikephil.charting.data.LineDataSet; import com.github.mikephil.charting.formatter.AxisValueFormatter; import com.github.mikephil.charting.formatter.ValueFormatter; import com.github.mikephil.charting.interfaces.datasets.ILineDataSet; import com.github.mikephil.charting.utils.ViewPortHandler; import org.secuso.privacyfriendlyactivitytracker.R; import org.secuso.privacyfriendlyactivitytracker.models.ActivityChart; import org.secuso.privacyfriendlyactivitytracker.models.ActivityChartDataSet; import org.secuso.privacyfriendlyactivitytracker.models.ActivityDayChart; import org.secuso.privacyfriendlyactivitytracker.models.ActivitySummary; import org.secuso.privacyfriendlyactivitytracker.utils.UnitHelper; import java.text.DecimalFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; /** * This adapter is used for ReportView. * * @author Tobias Neidig * @version 20160803 */ public class ReportAdapter extends RecyclerView.Adapter<ReportAdapter.ViewHolder> { private static final int TYPE_SUMMARY = 0; private static final int TYPE_DAY_CHART = 1; private static final int TYPE_CHART = 2; private List<Object> mItems; private OnItemClickListener mItemClickListener; /** * Creates a new Adapter for RecyclerView * * @param items The data displayed */ public ReportAdapter(List<Object> items) { mItems = items; } // Create new views (invoked by the layout manager) @Override public ReportAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { View v; ViewHolder vh; switch (viewType) { case TYPE_CHART: v = LayoutInflater.from(parent.getContext()) .inflate(R.layout.card_activity_bar_chart, parent, false); vh = new CombinedChartViewHolder(v); break; case TYPE_DAY_CHART: v = LayoutInflater.from(parent.getContext()) .inflate(R.layout.card_activity_chart, parent, false); vh = new ChartViewHolder(v); break; case TYPE_SUMMARY: default: v = LayoutInflater.from(parent.getContext()) .inflate(R.layout.card_activity_summary, parent, false); vh = new SummaryViewHolder(v); break; } return vh; } // Replace the contents of a view (invoked by the layout manager) @Override public void onBindViewHolder(ViewHolder holder, int position) { switch (getItemViewType(position)) { case TYPE_CHART: ActivityChart barChartData = (ActivityChart) mItems.get(position); CombinedChartViewHolder barChartViewHolder = (CombinedChartViewHolder) holder; barChartViewHolder.mTitleTextView.setText(barChartData.getTitle()); int barChartI = 0; ArrayList<String> barChartXValues = new ArrayList<>(); Map<String, Double> barChartDataMap; String barChartLabel; if (barChartData.getDisplayedDataType() == null) { barChartDataMap = barChartData.getSteps(); barChartLabel = barChartViewHolder.context.getString(R.string.steps); } else { switch (barChartData.getDisplayedDataType()) { case DISTANCE: barChartDataMap = barChartData.getDistance(); barChartLabel = barChartViewHolder.context.getString(R.string.action_distance); break; case CALORIES: barChartDataMap = barChartData.getCalories(); barChartLabel = barChartViewHolder.context.getString(R.string.calories); break; case STEPS: default: barChartDataMap = barChartData.getSteps(); barChartLabel = barChartViewHolder.context.getString(R.string.steps); break; } } List<BarEntry> dataEntries = new ArrayList<>(); List<BarEntry> dataEntriesReachedDailyGoal = new ArrayList<>(); for (Map.Entry<String, Double> dataEntry : barChartDataMap.entrySet()) { barChartXValues.add(barChartI, dataEntry.getKey()); if (dataEntry.getValue() != null) { float val = dataEntry.getValue().floatValue(); if (barChartData.getDisplayedDataType() == ActivityDayChart.DataType.DISTANCE) { val = Double.valueOf(UnitHelper.kilometerToUsersLengthUnit(UnitHelper.metersToKilometers(val), barChartViewHolder.context)).floatValue(); } if (dataEntry.getValue() >= barChartData.getGoal() && barChartData.getDisplayedDataType() == ActivityDayChart.DataType.STEPS) { dataEntriesReachedDailyGoal.add(new BarEntry(barChartI, val)); } else { dataEntries.add(new BarEntry(barChartI, val)); } } barChartI++; } BarDataSet barDataSet = new BarDataSet(dataEntries, barChartLabel); BarDataSet barDataSetReachedDailyGoal = new BarDataSet(dataEntriesReachedDailyGoal, barChartLabel); if (barChartData.getDisplayedDataType() == ActivityDayChart.DataType.DISTANCE) { String formatPattern = "###,###,##0.0"; barDataSet.setValueFormatter(new DoubleValueFormatter(formatPattern)); barDataSetReachedDailyGoal.setValueFormatter(new DoubleValueFormatter(formatPattern)); } if (barChartData.getDisplayedDataType() == ActivityDayChart.DataType.STEPS && barChartXValues.size() > 15) { barDataSet.setDrawValues(false); barDataSetReachedDailyGoal.setDrawValues(false); } ArrayList<ILineDataSet> lineDataSets = new ArrayList<>(); if (barChartData.getDisplayedDataType() != ActivityDayChart.DataType.STEPS) { // make sure, that the first and last entry are fully displayed Entry start = new Entry(0, 0); Entry end = new Entry(barChartI - 1, 0); LineDataSet chartLineDataSet = new LineDataSet(Arrays.asList(start, end), ""); chartLineDataSet.setAxisDependency(YAxis.AxisDependency.LEFT); chartLineDataSet.setDrawCircles(false); chartLineDataSet.setColor(ContextCompat.getColor(barChartViewHolder.context, R.color.transparent), 0); chartLineDataSet.setDrawValues(false); lineDataSets.add(chartLineDataSet); } // add daily step goal if (barChartXValues.size() > 0 && barChartData.getDisplayedDataType() == ActivityDayChart.DataType.STEPS) { Entry start = new Entry(0, barChartData.getGoal()); Entry end = new Entry(barChartXValues.size() - 1, barChartData.getGoal()); LineDataSet chartLineDataSet = new LineDataSet(Arrays.asList(start, end), barChartViewHolder.context.getString(R.string.activity_summary_chart_legend_stepgoal)); chartLineDataSet.setAxisDependency(YAxis.AxisDependency.LEFT); chartLineDataSet.setLineWidth(1); chartLineDataSet.setDrawCircles(false); chartLineDataSet.setColor(ContextCompat.getColor(barChartViewHolder.context, R.color.colorAccent), 200); chartLineDataSet.setDrawValues(false); lineDataSets.add(chartLineDataSet); } CombinedData combinedData = new CombinedData(); BarData barData = new BarData(barDataSet, barDataSetReachedDailyGoal); barData.setBarWidth(0.5f); combinedData.setData(barData); combinedData.setData(new LineData(lineDataSets)); barDataSet.setColor(ContextCompat.getColor(barChartViewHolder.context, R.color.colorPrimary)); barDataSetReachedDailyGoal.setColor(ContextCompat.getColor(barChartViewHolder.context, R.color.green)); barChartViewHolder.mChart.setData(combinedData); barChartViewHolder.mChart.getXAxis().setValueFormatter(new ArrayListAxisValueFormatter(barChartXValues)); barChartViewHolder.mChart.invalidate(); break; case TYPE_DAY_CHART: ActivityDayChart chartData = (ActivityDayChart) mItems.get(position); ChartViewHolder chartViewHolder = (ChartViewHolder) holder; chartViewHolder.mTitleTextView.setText(chartData.getTitle()); final ArrayList<String> chartXValues = new ArrayList<>(); int i = 0; Map<String, ActivityChartDataSet> dataMap; Map<Integer, String> legendValues = new LinkedHashMap<>(); String label; if (chartData.getDisplayedDataType() == null) { dataMap = chartData.getSteps(); label = chartViewHolder.context.getString(R.string.steps); } else { switch (chartData.getDisplayedDataType()) { case DISTANCE: dataMap = chartData.getDistance(); label = chartViewHolder.context.getString(R.string.action_distance); break; case CALORIES: dataMap = chartData.getCalories(); label = chartViewHolder.context.getString(R.string.calories); break; case STEPS: default: dataMap = chartData.getSteps(); label = chartViewHolder.context.getString(R.string.steps); legendValues.put(ContextCompat.getColor(chartViewHolder.itemView.getContext(), R.color.colorAccent), chartViewHolder.context.getString(R.string.activity_summary_chart_legend_stepgoal)); break; } } legendValues.put(ContextCompat.getColor(chartViewHolder.itemView.getContext(), R.color.colorPrimary), label); ArrayList<ILineDataSet> dataSets = new ArrayList<>(); float lastValue = 0; float maxValue = 0; // Generate data for line data sets for (Map.Entry<String, ActivityChartDataSet> dataEntry : dataMap.entrySet()) { // add data entry only if not null if (dataEntry.getValue() != null) { LineDataSet chartLineDataSet = getNewChartLineDataSet(chartViewHolder.context, label); // set color/legend by walking mode if (dataEntry.getValue().getStepCount() != null && dataEntry.getValue().getStepCount().getWalkingMode() != null) { int color = dataEntry.getValue().getStepCount().getWalkingMode().getColor(); int alpha = 85; int colorWithAlpha = Color.argb(alpha, Color.red(color), Color.green(color), Color.blue(color)); chartLineDataSet.setFillColor(color); chartLineDataSet.setFillAlpha(alpha); chartLineDataSet.setDrawFilled(true); legendValues.put(colorWithAlpha, dataEntry.getValue().getStepCount().getWalkingMode().getName()); } dataSets.add(chartLineDataSet); // add entries to data set. float val = Double.valueOf(dataEntry.getValue().getValue()).floatValue(); if (chartData.getDisplayedDataType() == ActivityDayChart.DataType.DISTANCE) { val = Double.valueOf(UnitHelper.kilometerToUsersLengthUnit(UnitHelper.metersToKilometers(val), chartViewHolder.context)).floatValue(); } if(lastValue > val){ Log.i("REPORT_ADAPTER", "lastvalue > val, using lastvalue"); val = lastValue; } Entry prevChartEntry, chartEntry; if (i == 0) { prevChartEntry = new Entry(0, 0); }else{ prevChartEntry = new Entry(i - 1, lastValue); } chartEntry = new Entry(i++, val); ((LineDataSet) dataSets.get(dataSets.size() - 1)).getValues().add(prevChartEntry); ((LineDataSet) dataSets.get(dataSets.size() - 1)).getValues().add(chartEntry); // remember variables lastValue = val; maxValue = Math.max(maxValue, val); } // add x val chartXValues.add(dataEntry.getKey()); } // add daily step goal if (chartXValues.size() > 0 && chartData.getDisplayedDataType() == ActivityDayChart.DataType.STEPS) { Entry start = new Entry(0, chartData.getGoal()); Entry end = new Entry(chartXValues.size() - 1, chartData.getGoal()); LineDataSet chartLineDataSet = new LineDataSet(Arrays.asList(start, end), ""); chartLineDataSet.setAxisDependency(YAxis.AxisDependency.LEFT); chartLineDataSet.setLineWidth(1); chartLineDataSet.setDrawCircles(false); chartLineDataSet.setColor(ContextCompat.getColor(chartViewHolder.context, R.color.colorAccent), 200); chartLineDataSet.setDrawValues(false); maxValue = Math.max(maxValue, chartData.getGoal()); dataSets.add(chartLineDataSet); } // Workaround since scaling does not work correctly in MPAndroidChart v3.0.0.0-beta1 { Entry start = new Entry(0, 0); Entry end = new Entry(chartXValues.size() - 1, maxValue * 1.03f); LineDataSet chartLineDataSet = new LineDataSet(Arrays.asList(start, end), ""); chartLineDataSet.setAxisDependency(YAxis.AxisDependency.LEFT); chartLineDataSet.setDrawCircles(false); chartLineDataSet.setColor(ContextCompat.getColor(chartViewHolder.context, R.color.transparent), 0); chartLineDataSet.setDrawValues(false); dataSets.add(chartLineDataSet); } LineData data = new LineData(dataSets); chartViewHolder.mChart.setData(data); chartViewHolder.mChart.getXAxis().setValueFormatter(new ArrayListAxisValueFormatter(chartXValues)); // add legend Legend legend = chartViewHolder.mChart.getLegend(); legend.setComputedColors(new ArrayList<Integer>()); legend.setComputedLabels(new ArrayList<String>()); legend.setCustom(new ArrayList<>(legendValues.keySet()), new ArrayList<>(legendValues.values())); // invalidate chartViewHolder.mChart.getData().notifyDataChanged(); chartViewHolder.mChart.notifyDataSetChanged(); chartViewHolder.mChart.invalidate(); break; case TYPE_SUMMARY: ActivitySummary summaryData = (ActivitySummary) mItems.get(position); SummaryViewHolder summaryViewHolder = (SummaryViewHolder) holder; UnitHelper.FormattedUnitPair distance = UnitHelper.formatKilometers(UnitHelper.metersToKilometers(summaryData.getDistance()), summaryViewHolder.itemView.getContext()); UnitHelper.FormattedUnitPair calories = UnitHelper.formatCalories(summaryData.getCalories(), summaryViewHolder.itemView.getContext()); summaryViewHolder.mTitleTextView.setText(summaryData.getTitle()); summaryViewHolder.mStepsTextView.setText(String.valueOf(summaryData.getSteps())); summaryViewHolder.mDistanceTextView.setText(distance.getValue()); summaryViewHolder.mDistanceTitleTextView.setText(distance.getUnit()); summaryViewHolder.mCaloriesTextView.setText(calories.getValue()); summaryViewHolder.mCaloriesTitleTextView.setText(calories.getUnit()); summaryViewHolder.mNextButton.setVisibility(summaryData.isHasSuccessor() ? View.VISIBLE : View.INVISIBLE); summaryViewHolder.mPrevButton.setVisibility(summaryData.isHasPredecessor() ? View.VISIBLE : View.INVISIBLE); if(summaryData.getCurrentSpeed() != null){ summaryViewHolder.mVelocityContainer.setVisibility(View.VISIBLE); summaryViewHolder.mVelocityTextView.setText(String.valueOf(UnitHelper.formatKilometersPerHour(UnitHelper.metersPerSecondToKilometersPerHour(summaryData.getCurrentSpeed()), summaryViewHolder.context))); }else{ summaryViewHolder.mVelocityContainer.setVisibility(View.GONE); } break; } } private LineDataSet getNewChartLineDataSet(Context context, String label){ LineDataSet chartLineDataSet = new LineDataSet(new ArrayList<Entry>(), label); chartLineDataSet.setAxisDependency(YAxis.AxisDependency.LEFT); chartLineDataSet.setLineWidth(3); chartLineDataSet.setCircleRadius(3.5f); chartLineDataSet.setDrawCircleHole(false); chartLineDataSet.setColor(ContextCompat.getColor(context, R.color.colorPrimary), 200); chartLineDataSet.setCircleColor(ContextCompat.getColor(context, R.color.colorPrimary)); chartLineDataSet.setDrawValues(false); return chartLineDataSet; } // Return the size of your dataset (invoked by the layout manager) @Override public int getItemCount() { return (mItems != null) ? mItems.size() : 0; } // Witht the following method we check what type of view is being passed @Override public int getItemViewType(int position) { Object item = mItems.get(position); if (item instanceof ActivityDayChart) { return TYPE_DAY_CHART; } else if (item instanceof ActivitySummary) { return TYPE_SUMMARY; } else if (item instanceof ActivityChart) { return TYPE_CHART; } else { return -1; } } public void setOnItemClickListener(final OnItemClickListener mItemClickListener) { this.mItemClickListener = mItemClickListener; } public interface OnItemClickListener { void onActivityChartDataTypeClicked(ActivityDayChart.DataType newDataType); void setActivityChartDataTypeChecked(Menu popup); void onPrevClicked(); void onNextClicked(); void onTitleClicked(); void inflateWalkingModeMenu(Menu menu); void onWalkingModeClicked(int id); } // Provide a reference to the views for each data item // Complex data items may need more than one view per item, and // you provide access to all the views for a data item in a view holder public class ViewHolder extends RecyclerView.ViewHolder { public Context context; public ViewHolder(View itemView) { super(itemView); context = itemView.getContext(); } } public class SummaryViewHolder extends ViewHolder implements PopupMenu.OnMenuItemClickListener { public TextView mTitleTextView; public TextView mStepsTextView; public TextView mDistanceTextView; public TextView mCaloriesTextView; public TextView mVelocityTextView; public TextView mDistanceTitleTextView; public TextView mCaloriesTitleTextView; public RelativeLayout mVelocityContainer; public ImageButton mPrevButton; public ImageButton mNextButton; public ImageButton mMenuButton; public SummaryViewHolder(View itemView) { super(itemView); mTitleTextView = (TextView) itemView.findViewById(R.id.period); mStepsTextView = (TextView) itemView.findViewById(R.id.stepCount); mDistanceTextView = (TextView) itemView.findViewById(R.id.distanceCount); mCaloriesTextView = (TextView) itemView.findViewById(R.id.calorieCount); mVelocityTextView = (TextView) itemView.findViewById(R.id.speed); mVelocityContainer = (RelativeLayout) itemView.findViewById(R.id.speedContainer); mDistanceTitleTextView = (TextView) itemView.findViewById(R.id.distanceTitle); mCaloriesTitleTextView = (TextView) itemView.findViewById(R.id.calorieTitle); mPrevButton = (ImageButton) itemView.findViewById(R.id.prev_btn); mNextButton = (ImageButton) itemView.findViewById(R.id.next_btn); mMenuButton = (ImageButton) itemView.findViewById(R.id.periodMoreButton); mMenuButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { showPopup(mMenuButton, context); } }); mPrevButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (mItemClickListener != null) { mItemClickListener.onPrevClicked(); } } }); mNextButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (mItemClickListener != null) { mItemClickListener.onNextClicked(); } } }); mTitleTextView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (mItemClickListener != null) { mItemClickListener.onTitleClicked(); } } }); } public void showPopup(View v, Context c) { PopupMenu popup = new PopupMenu(c, v); if (mItemClickListener != null) { mItemClickListener.inflateWalkingModeMenu(popup.getMenu()); } popup.setOnMenuItemClickListener(this); popup.show(); } @Override public boolean onMenuItemClick(MenuItem item) { item.setChecked(!item.isChecked()); if (mItemClickListener != null) { mItemClickListener.onWalkingModeClicked(item.getItemId()); return true; } else { return false; } } } public abstract class AbstractChartViewHolder extends ViewHolder implements PopupMenu.OnMenuItemClickListener { public TextView mTitleTextView; public ImageButton mMenuButton; public AbstractChartViewHolder(View itemView) { super(itemView); mTitleTextView = (TextView) itemView.findViewById(R.id.period); mMenuButton = (ImageButton) itemView.findViewById(R.id.periodMoreButton); mMenuButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { showPopup(mMenuButton, context); } }); } public void showPopup(View v, Context c) { PopupMenu popup = new PopupMenu(c, v); MenuInflater inflater = popup.getMenuInflater(); inflater.inflate(R.menu.menu_card_activity_summary, popup.getMenu()); popup.setOnMenuItemClickListener(this); if (mItemClickListener != null) { mItemClickListener.setActivityChartDataTypeChecked(popup.getMenu()); } popup.show(); } @Override public boolean onMenuItemClick(MenuItem item) { ActivityDayChart.DataType dataType; item.setChecked(!item.isChecked()); switch (item.getItemId()) { case R.id.menu_steps: dataType = ActivityDayChart.DataType.STEPS; break; case R.id.menu_distance: dataType = ActivityDayChart.DataType.DISTANCE; break; case R.id.menu_calories: dataType = ActivityDayChart.DataType.CALORIES; break; default: return false; } if (mItemClickListener != null) { mItemClickListener.onActivityChartDataTypeClicked(dataType); return true; } else { return false; } } } public class ChartViewHolder extends AbstractChartViewHolder { public LineChart mChart; public ChartViewHolder(View itemView) { super(itemView); mChart = (LineChart) itemView.findViewById(R.id.chart); mChart.getXAxis().setPosition(XAxis.XAxisPosition.BOTTOM); mChart.getAxisRight().setEnabled(false); mChart.setTouchEnabled(false); mChart.setDoubleTapToZoomEnabled(false); mChart.setPinchZoom(false); mChart.setDescription(""); } } public class CombinedChartViewHolder extends AbstractChartViewHolder { public CombinedChart mChart; public CombinedChartViewHolder(View itemView) { super(itemView); mChart = (CombinedChart) itemView.findViewById(R.id.chart); mChart.getXAxis().setPosition(XAxis.XAxisPosition.BOTTOM); mChart.getAxisRight().setEnabled(false); mChart.setTouchEnabled(false); mChart.setDoubleTapToZoomEnabled(false); mChart.setPinchZoom(false); mChart.setDescription(""); mChart.setDrawOrder(new CombinedChart.DrawOrder[]{ CombinedChart.DrawOrder.BAR, CombinedChart.DrawOrder.BUBBLE, CombinedChart.DrawOrder.CANDLE, CombinedChart.DrawOrder.LINE, CombinedChart.DrawOrder.SCATTER }); } } public class ArrayListAxisValueFormatter implements AxisValueFormatter { private List<String> values; public ArrayListAxisValueFormatter(List<String> values) { this.values = values; } @Override public String getFormattedValue(float value, AxisBase axis) { if (this.values.size() <= (int) value || (int) value < 0) { return "--"; } return this.values.get((int) value); } @Override public int getDecimalDigits() { return -1; } } public class DoubleValueFormatter implements ValueFormatter { private DecimalFormat mFormat; public DoubleValueFormatter(String formatPattern) { mFormat = new DecimalFormat(formatPattern); } @Override public String getFormattedValue(float value, Entry entry, int dataSetIndex, ViewPortHandler viewPortHandler) { if (value == 0) { return "0"; } else { return mFormat.format(value); } } } }