Merge pull request #5735 from ByteHamster/statistics-line-graph
Add line graph to statistics screen
This commit is contained in:
commit
be9093911f
@ -0,0 +1,121 @@
|
|||||||
|
package de.danoeh.antennapod.adapter;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.TextView;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
import de.danoeh.antennapod.R;
|
||||||
|
import de.danoeh.antennapod.core.storage.DBReader;
|
||||||
|
import de.danoeh.antennapod.core.util.LongList;
|
||||||
|
import de.danoeh.antennapod.view.LineChartView;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adapter for the yearly playback statistics list.
|
||||||
|
*/
|
||||||
|
public class YearStatisticsListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
|
||||||
|
private static final int TYPE_HEADER = 0;
|
||||||
|
private static final int TYPE_FEED = 1;
|
||||||
|
final Context context;
|
||||||
|
private List<DBReader.MonthlyStatisticsItem> statisticsData = new ArrayList<>();
|
||||||
|
LineChartView.LineChartData lineChartData;
|
||||||
|
|
||||||
|
public YearStatisticsListAdapter(Context context) {
|
||||||
|
this.context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getItemCount() {
|
||||||
|
return statisticsData.size() + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getItemViewType(int position) {
|
||||||
|
return position == 0 ? TYPE_HEADER : TYPE_FEED;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||||
|
LayoutInflater inflater = LayoutInflater.from(context);
|
||||||
|
if (viewType == TYPE_HEADER) {
|
||||||
|
return new HeaderHolder(inflater.inflate(R.layout.statistics_listitem_linechart, parent, false));
|
||||||
|
}
|
||||||
|
return new StatisticsHolder(inflater.inflate(R.layout.statistics_year_listitem, parent, false));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder h, int position) {
|
||||||
|
if (getItemViewType(position) == TYPE_HEADER) {
|
||||||
|
HeaderHolder holder = (HeaderHolder) h;
|
||||||
|
holder.lineChart.setData(lineChartData);
|
||||||
|
} else {
|
||||||
|
StatisticsHolder holder = (StatisticsHolder) h;
|
||||||
|
DBReader.MonthlyStatisticsItem statsItem = statisticsData.get(position - 1);
|
||||||
|
holder.year.setText(String.format(Locale.getDefault(), "%d ", statsItem.year));
|
||||||
|
holder.hours.setText(String.format(Locale.getDefault(), "%.1f ", statsItem.timePlayed / 3600000.0f)
|
||||||
|
+ context.getString(R.string.time_hours));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void update(List<DBReader.MonthlyStatisticsItem> statistics) {
|
||||||
|
int lastYear = statistics.size() > 0 ? statistics.get(0).year : 0;
|
||||||
|
int lastDataPoint = statistics.size() > 0 ? (statistics.get(0).month - 1) + lastYear * 12 : 0;
|
||||||
|
long yearSum = 0;
|
||||||
|
statisticsData.clear();
|
||||||
|
LongList lineChartValues = new LongList();
|
||||||
|
LongList lineChartHorizontalLines = new LongList();
|
||||||
|
for (DBReader.MonthlyStatisticsItem statistic : statistics) {
|
||||||
|
if (statistic.year != lastYear) {
|
||||||
|
DBReader.MonthlyStatisticsItem yearAggregate = new DBReader.MonthlyStatisticsItem();
|
||||||
|
yearAggregate.year = lastYear;
|
||||||
|
yearAggregate.timePlayed = yearSum;
|
||||||
|
statisticsData.add(yearAggregate);
|
||||||
|
yearSum = 0;
|
||||||
|
lastYear = statistic.year;
|
||||||
|
lineChartHorizontalLines.add(lineChartValues.size());
|
||||||
|
}
|
||||||
|
yearSum += statistic.timePlayed;
|
||||||
|
while (lastDataPoint + 1 < (statistic.month - 1) + statistic.year * 12) {
|
||||||
|
lineChartValues.add(0); // Compensate for months without playback
|
||||||
|
lastDataPoint++;
|
||||||
|
}
|
||||||
|
lineChartValues.add(statistic.timePlayed);
|
||||||
|
lastDataPoint = (statistic.month - 1) + statistic.year * 12;
|
||||||
|
}
|
||||||
|
DBReader.MonthlyStatisticsItem yearAggregate = new DBReader.MonthlyStatisticsItem();
|
||||||
|
yearAggregate.year = lastYear;
|
||||||
|
yearAggregate.timePlayed = yearSum;
|
||||||
|
statisticsData.add(yearAggregate);
|
||||||
|
Collections.reverse(statisticsData);
|
||||||
|
lineChartData = new LineChartView.LineChartData(lineChartValues.toArray(), lineChartHorizontalLines.toArray());
|
||||||
|
notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
static class HeaderHolder extends RecyclerView.ViewHolder {
|
||||||
|
LineChartView lineChart;
|
||||||
|
|
||||||
|
HeaderHolder(View itemView) {
|
||||||
|
super(itemView);
|
||||||
|
lineChart = itemView.findViewById(R.id.lineChart);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static class StatisticsHolder extends RecyclerView.ViewHolder {
|
||||||
|
TextView year;
|
||||||
|
TextView hours;
|
||||||
|
|
||||||
|
StatisticsHolder(View itemView) {
|
||||||
|
super(itemView);
|
||||||
|
year = itemView.findViewById(R.id.yearLabel);
|
||||||
|
hours = itemView.findViewById(R.id.hoursLabel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -25,9 +25,10 @@ public class StatisticsFragment extends PagedToolbarFragment {
|
|||||||
|
|
||||||
public static final String TAG = "StatisticsFragment";
|
public static final String TAG = "StatisticsFragment";
|
||||||
|
|
||||||
private static final int POS_LISTENED_HOURS = 0;
|
private static final int POS_SUBSCRIPTIONS = 0;
|
||||||
private static final int POS_SPACE_TAKEN = 1;
|
private static final int POS_YEARS = 1;
|
||||||
private static final int TOTAL_COUNT = 2;
|
private static final int POS_SPACE_TAKEN = 2;
|
||||||
|
private static final int TOTAL_COUNT = 3;
|
||||||
|
|
||||||
private TabLayout tabLayout;
|
private TabLayout tabLayout;
|
||||||
private ViewPager2 viewPager;
|
private ViewPager2 viewPager;
|
||||||
@ -51,11 +52,14 @@ public class StatisticsFragment extends PagedToolbarFragment {
|
|||||||
super.setupPagedToolbar(toolbar, viewPager);
|
super.setupPagedToolbar(toolbar, viewPager);
|
||||||
new TabLayoutMediator(tabLayout, viewPager, (tab, position) -> {
|
new TabLayoutMediator(tabLayout, viewPager, (tab, position) -> {
|
||||||
switch (position) {
|
switch (position) {
|
||||||
case POS_LISTENED_HOURS:
|
case POS_SUBSCRIPTIONS:
|
||||||
tab.setText(R.string.playback_statistics_label);
|
tab.setText(R.string.subscriptions_label);
|
||||||
|
break;
|
||||||
|
case POS_YEARS:
|
||||||
|
tab.setText(R.string.years_statistics_label);
|
||||||
break;
|
break;
|
||||||
case POS_SPACE_TAKEN:
|
case POS_SPACE_TAKEN:
|
||||||
tab.setText(R.string.download_statistics_label);
|
tab.setText(R.string.downloads_label);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
@ -82,8 +86,10 @@ public class StatisticsFragment extends PagedToolbarFragment {
|
|||||||
@Override
|
@Override
|
||||||
public Fragment createFragment(int position) {
|
public Fragment createFragment(int position) {
|
||||||
switch (position) {
|
switch (position) {
|
||||||
case POS_LISTENED_HOURS:
|
case POS_SUBSCRIPTIONS:
|
||||||
return new PlaybackStatisticsFragment();
|
return new SubscriptionStatisticsFragment();
|
||||||
|
case POS_YEARS:
|
||||||
|
return new YearsStatisticsFragment();
|
||||||
default:
|
default:
|
||||||
case POS_SPACE_TAKEN:
|
case POS_SPACE_TAKEN:
|
||||||
return new DownloadStatisticsFragment();
|
return new DownloadStatisticsFragment();
|
||||||
|
@ -43,8 +43,8 @@ import java.util.Locale;
|
|||||||
/**
|
/**
|
||||||
* Displays the 'playback statistics' screen
|
* Displays the 'playback statistics' screen
|
||||||
*/
|
*/
|
||||||
public class PlaybackStatisticsFragment extends Fragment {
|
public class SubscriptionStatisticsFragment extends Fragment {
|
||||||
private static final String TAG = PlaybackStatisticsFragment.class.getSimpleName();
|
private static final String TAG = SubscriptionStatisticsFragment.class.getSimpleName();
|
||||||
private static final String PREF_NAME = "StatisticsActivityPrefs";
|
private static final String PREF_NAME = "StatisticsActivityPrefs";
|
||||||
private static final String PREF_INCLUDE_MARKED_PLAYED = "countAll";
|
private static final String PREF_INCLUDE_MARKED_PLAYED = "countAll";
|
||||||
private static final String PREF_FILTER_FROM = "filterFrom";
|
private static final String PREF_FILTER_FROM = "filterFrom";
|
@ -0,0 +1,87 @@
|
|||||||
|
package de.danoeh.antennapod.fragment.preferences;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.Menu;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.ProgressBar;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.fragment.app.Fragment;
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
import de.danoeh.antennapod.R;
|
||||||
|
import de.danoeh.antennapod.adapter.YearStatisticsListAdapter;
|
||||||
|
import de.danoeh.antennapod.core.storage.DBReader;
|
||||||
|
import io.reactivex.Observable;
|
||||||
|
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||||
|
import io.reactivex.disposables.Disposable;
|
||||||
|
import io.reactivex.schedulers.Schedulers;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays the yearly statistics screen
|
||||||
|
*/
|
||||||
|
public class YearsStatisticsFragment extends Fragment {
|
||||||
|
private static final String TAG = YearsStatisticsFragment.class.getSimpleName();
|
||||||
|
|
||||||
|
private Disposable disposable;
|
||||||
|
private RecyclerView yearStatisticsList;
|
||||||
|
private ProgressBar progressBar;
|
||||||
|
private YearStatisticsListAdapter listAdapter;
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
|
||||||
|
@Nullable Bundle savedInstanceState) {
|
||||||
|
View root = inflater.inflate(R.layout.statistics_activity, container, false);
|
||||||
|
yearStatisticsList = root.findViewById(R.id.statistics_list);
|
||||||
|
progressBar = root.findViewById(R.id.progressBar);
|
||||||
|
listAdapter = new YearStatisticsListAdapter(getContext());
|
||||||
|
yearStatisticsList.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||||
|
yearStatisticsList.setAdapter(listAdapter);
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onStart() {
|
||||||
|
super.onStart();
|
||||||
|
refreshStatistics();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroyView() {
|
||||||
|
super.onDestroyView();
|
||||||
|
if (disposable != null) {
|
||||||
|
disposable.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPrepareOptionsMenu(@NonNull Menu menu) {
|
||||||
|
super.onPrepareOptionsMenu(menu);
|
||||||
|
menu.findItem(R.id.statistics_reset).setVisible(false);
|
||||||
|
menu.findItem(R.id.statistics_filter).setVisible(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void refreshStatistics() {
|
||||||
|
progressBar.setVisibility(View.VISIBLE);
|
||||||
|
yearStatisticsList.setVisibility(View.GONE);
|
||||||
|
loadStatistics();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadStatistics() {
|
||||||
|
if (disposable != null) {
|
||||||
|
disposable.dispose();
|
||||||
|
}
|
||||||
|
disposable = Observable.fromCallable(DBReader::getMonthlyTimeStatistics)
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(result -> {
|
||||||
|
listAdapter.update(result);
|
||||||
|
progressBar.setVisibility(View.GONE);
|
||||||
|
yearStatisticsList.setVisibility(View.VISIBLE);
|
||||||
|
}, error -> Log.e(TAG, Log.getStackTraceString(error)));
|
||||||
|
}
|
||||||
|
}
|
138
app/src/main/java/de/danoeh/antennapod/view/LineChartView.java
Normal file
138
app/src/main/java/de/danoeh/antennapod/view/LineChartView.java
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
package de.danoeh.antennapod.view;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.graphics.ColorFilter;
|
||||||
|
import android.graphics.DashPathEffect;
|
||||||
|
import android.graphics.LinearGradient;
|
||||||
|
import android.graphics.Paint;
|
||||||
|
import android.graphics.Path;
|
||||||
|
import android.graphics.PixelFormat;
|
||||||
|
import android.graphics.Shader;
|
||||||
|
import android.graphics.drawable.Drawable;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.appcompat.widget.AppCompatImageView;
|
||||||
|
import androidx.appcompat.widget.ThemeUtils;
|
||||||
|
import de.danoeh.antennapod.R;
|
||||||
|
import io.reactivex.annotations.Nullable;
|
||||||
|
|
||||||
|
public class LineChartView extends AppCompatImageView {
|
||||||
|
private LineChartDrawable drawable;
|
||||||
|
|
||||||
|
public LineChartView(Context context) {
|
||||||
|
super(context);
|
||||||
|
setup();
|
||||||
|
}
|
||||||
|
|
||||||
|
public LineChartView(Context context, @Nullable AttributeSet attrs) {
|
||||||
|
super(context, attrs);
|
||||||
|
setup();
|
||||||
|
}
|
||||||
|
|
||||||
|
public LineChartView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
|
||||||
|
super(context, attrs, defStyleAttr);
|
||||||
|
setup();
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
|
private void setup() {
|
||||||
|
drawable = new LineChartDrawable();
|
||||||
|
setImageDrawable(drawable);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set of data values to display.
|
||||||
|
*/
|
||||||
|
public void setData(LineChartData data) {
|
||||||
|
drawable.data = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class LineChartData {
|
||||||
|
private final long valueMax;
|
||||||
|
private final long[] values;
|
||||||
|
private final long[] verticalLines;
|
||||||
|
|
||||||
|
public LineChartData(long[] values, long[] verticalLines) {
|
||||||
|
this.values = values;
|
||||||
|
long valueMax = 0;
|
||||||
|
for (long datum : values) {
|
||||||
|
valueMax = Math.max(datum, valueMax);
|
||||||
|
}
|
||||||
|
this.valueMax = valueMax;
|
||||||
|
this.verticalLines = verticalLines;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getHeight(int item) {
|
||||||
|
return (float) values[item] / valueMax;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class LineChartDrawable extends Drawable {
|
||||||
|
private LineChartData data;
|
||||||
|
private final Paint paintLine;
|
||||||
|
private final Paint paintBackground;
|
||||||
|
private final Paint paintVerticalLines;
|
||||||
|
|
||||||
|
private LineChartDrawable() {
|
||||||
|
paintLine = new Paint();
|
||||||
|
paintLine.setFlags(Paint.ANTI_ALIAS_FLAG);
|
||||||
|
paintLine.setStyle(Paint.Style.STROKE);
|
||||||
|
paintLine.setStrokeJoin(Paint.Join.ROUND);
|
||||||
|
paintLine.setStrokeCap(Paint.Cap.ROUND);
|
||||||
|
paintLine.setColor(ThemeUtils.getThemeAttrColor(getContext(), R.attr.colorAccent));
|
||||||
|
paintBackground = new Paint();
|
||||||
|
paintBackground.setStyle(Paint.Style.FILL);
|
||||||
|
paintVerticalLines = new Paint();
|
||||||
|
paintVerticalLines.setStyle(Paint.Style.STROKE);
|
||||||
|
paintVerticalLines.setPathEffect(new DashPathEffect(new float[] {10f, 10f}, 0f));
|
||||||
|
paintVerticalLines.setColor(0x66777777);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void draw(@NonNull Canvas canvas) {
|
||||||
|
float width = getBounds().width();
|
||||||
|
float height = getBounds().height();
|
||||||
|
float usableHeight = height * 0.9f;
|
||||||
|
float stepSize = width / (data.values.length + 1);
|
||||||
|
|
||||||
|
paintVerticalLines.setStrokeWidth(height * 0.005f);
|
||||||
|
for (long line : data.verticalLines) {
|
||||||
|
canvas.drawLine((line + 1) * stepSize, 0, (line + 1) * stepSize, height, paintVerticalLines);
|
||||||
|
}
|
||||||
|
|
||||||
|
paintLine.setStrokeWidth(height * 0.015f);
|
||||||
|
Path path = new Path();
|
||||||
|
for (int i = 0; i < data.values.length; i++) {
|
||||||
|
if (i == 0) {
|
||||||
|
path.moveTo((i + 1) * stepSize, (1 - data.getHeight(i)) * usableHeight + height * 0.05f);
|
||||||
|
} else {
|
||||||
|
path.lineTo((i + 1) * stepSize, (1 - data.getHeight(i)) * usableHeight + height * 0.05f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
canvas.drawPath(path, paintLine);
|
||||||
|
|
||||||
|
path.lineTo(data.values.length * stepSize, height);
|
||||||
|
path.lineTo(stepSize, height);
|
||||||
|
paintBackground.setShader(new LinearGradient(0, 0, 0, height,
|
||||||
|
(ThemeUtils.getThemeAttrColor(getContext(), R.attr.colorAccent) & 0xffffff) + 0x66000000,
|
||||||
|
Color.TRANSPARENT, Shader.TileMode.CLAMP));
|
||||||
|
canvas.drawPath(path, paintBackground);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getOpacity() {
|
||||||
|
return PixelFormat.TRANSLUCENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setAlpha(int alpha) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setColorFilter(ColorFilter cf) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
22
app/src/main/res/layout/statistics_listitem_linechart.xml
Normal file
22
app/src/main/res/layout/statistics_listitem_linechart.xml
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<de.danoeh.antennapod.view.LineChartView
|
||||||
|
android:id="@+id/lineChart"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="200dp"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_marginEnd="8dp" />
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="1dp"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:background="?android:attr/dividerVertical" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
32
app/src/main/res/layout/statistics_year_listitem.xml
Normal file
32
app/src/main/res/layout/statistics_year_listitem.xml
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
<?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="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:paddingLeft="16dp"
|
||||||
|
android:paddingRight="16dp"
|
||||||
|
android:paddingTop="16dp"
|
||||||
|
android:paddingBottom="8dp"
|
||||||
|
android:background="?android:attr/selectableItemBackground">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/yearLabel"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:lines="1"
|
||||||
|
android:textColor="?android:attr/textColorPrimary"
|
||||||
|
android:textSize="16sp"
|
||||||
|
tools:text="2020" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/hoursLabel"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:lines="1"
|
||||||
|
android:textColor="?android:attr/textColorTertiary"
|
||||||
|
android:textSize="14sp"
|
||||||
|
tools:text="23 hours" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
@ -771,6 +771,33 @@ public final class DBReader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static class MonthlyStatisticsItem {
|
||||||
|
public int year = 0;
|
||||||
|
public int month = 0;
|
||||||
|
public long timePlayed = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public static List<MonthlyStatisticsItem> getMonthlyTimeStatistics() {
|
||||||
|
List<MonthlyStatisticsItem> months = new ArrayList<>();
|
||||||
|
PodDBAdapter adapter = PodDBAdapter.getInstance();
|
||||||
|
adapter.open();
|
||||||
|
try (Cursor cursor = adapter.getMonthlyStatisticsCursor()) {
|
||||||
|
int indexMonth = cursor.getColumnIndexOrThrow("month");
|
||||||
|
int indexYear = cursor.getColumnIndexOrThrow("year");
|
||||||
|
int indexTotalDuration = cursor.getColumnIndexOrThrow("total_duration");
|
||||||
|
while (cursor.moveToNext()) {
|
||||||
|
MonthlyStatisticsItem item = new MonthlyStatisticsItem();
|
||||||
|
item.month = Integer.parseInt(cursor.getString(indexMonth));
|
||||||
|
item.year = Integer.parseInt(cursor.getString(indexYear));
|
||||||
|
item.timePlayed = cursor.getLong(indexTotalDuration);
|
||||||
|
months.add(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
adapter.close();
|
||||||
|
return months;
|
||||||
|
}
|
||||||
|
|
||||||
public static class StatisticsResult {
|
public static class StatisticsResult {
|
||||||
public List<StatisticsItem> feedTime = new ArrayList<>();
|
public List<StatisticsItem> feedTime = new ArrayList<>();
|
||||||
public long oldestDate = System.currentTimeMillis();
|
public long oldestDate = System.currentTimeMillis();
|
||||||
|
@ -1133,6 +1133,17 @@ public class PodDBAdapter {
|
|||||||
return db.rawQuery(query, null);
|
return db.rawQuery(query, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public final Cursor getMonthlyStatisticsCursor() {
|
||||||
|
final String query = "SELECT SUM(" + KEY_PLAYED_DURATION + ") AS total_duration"
|
||||||
|
+ ", strftime('%m', datetime(" + KEY_LAST_PLAYED_TIME + "/1000, 'unixepoch')) AS month"
|
||||||
|
+ ", strftime('%Y', datetime(" + KEY_LAST_PLAYED_TIME + "/1000, 'unixepoch')) AS year"
|
||||||
|
+ " FROM " + TABLE_NAME_FEED_MEDIA
|
||||||
|
+ " WHERE " + KEY_LAST_PLAYED_TIME + " > 0 AND " + KEY_PLAYED_DURATION + " > 0"
|
||||||
|
+ " GROUP BY year, month"
|
||||||
|
+ " ORDER BY year, month";
|
||||||
|
return db.rawQuery(query, null);
|
||||||
|
}
|
||||||
|
|
||||||
public int getQueueSize() {
|
public int getQueueSize() {
|
||||||
final String query = String.format("SELECT COUNT(%s) FROM %s", KEY_ID, TABLE_NAME_QUEUE);
|
final String query = String.format("SELECT COUNT(%s) FROM %s", KEY_ID, TABLE_NAME_QUEUE);
|
||||||
Cursor c = db.rawQuery(query, null);
|
Cursor c = db.rawQuery(query, null);
|
||||||
|
@ -28,8 +28,7 @@
|
|||||||
<string name="gpodnet_main_label">gpodder.net</string>
|
<string name="gpodnet_main_label">gpodder.net</string>
|
||||||
<string name="episode_cache_full_title">Episode cache full</string>
|
<string name="episode_cache_full_title">Episode cache full</string>
|
||||||
<string name="episode_cache_full_message">The episode cache limit has been reached. You can increase the cache size in the Settings.</string>
|
<string name="episode_cache_full_message">The episode cache limit has been reached. You can increase the cache size in the Settings.</string>
|
||||||
<string name="playback_statistics_label">Playback</string>
|
<string name="years_statistics_label">Years</string>
|
||||||
<string name="download_statistics_label">Downloads</string>
|
|
||||||
<string name="notification_pref_fragment">Notifications</string>
|
<string name="notification_pref_fragment">Notifications</string>
|
||||||
|
|
||||||
<!-- Google Assistant -->
|
<!-- Google Assistant -->
|
||||||
|
Loading…
x
Reference in New Issue
Block a user