/****************************************************************************************
* Copyright (c) 2014 Michael Goldbach <michael@m-goldbach.net> *
* *
* 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 com.ichi2.anki;
import android.content.Intent;
import android.graphics.Color;
import android.os.AsyncTask;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter;
import android.support.v4.view.ViewPager;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AppCompatActivity;
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.webkit.WebView;
import android.widget.AdapterView;
import android.widget.ProgressBar;
import android.widget.Spinner;
import android.widget.TextView;
import com.ichi2.anim.ActivityTransitionAnimation;
import com.ichi2.anki.stats.AnkiStatsTaskHandler;
import com.ichi2.anki.stats.ChartView;
import com.ichi2.anki.widgets.DeckDropDownAdapter;
import com.ichi2.libanki.Collection;
import com.ichi2.libanki.Stats;
import com.ichi2.ui.SlidingTabLayout;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import timber.log.Timber;
public class Statistics extends NavigationDrawerActivity implements DeckDropDownAdapter.SubtitleListener {
public static final int TODAYS_STATS_TAB_POSITION = 0;
public static final int FORECAST_TAB_POSITION = 1;
public static final int REVIEW_COUNT_TAB_POSITION = 2;
public static final int REVIEW_TIME_TAB_POSITION = 3;
public static final int INTERVALS_TAB_POSITION = 4;
public static final int HOURLY_BREAKDOWN_TAB_POSITION = 5;
public static final int WEEKLY_BREAKDOWN_TAB_POSITION = 6;
public static final int ANSWER_BUTTONS_TAB_POSITION = 7;
public static final int CARDS_TYPES_TAB_POSITION = 8;
private SectionsPagerAdapter mSectionsPagerAdapter;
private ViewPager mViewPager;
private AnkiStatsTaskHandler mTaskHandler = null;
private ArrayList<JSONObject> mDropDownDecks;
private Spinner mActionBarSpinner;
private boolean mIsWholeCollection = false;
private static boolean sIsSubtitle;
@Override
protected void onCreate(Bundle savedInstanceState) {
Timber.d("onCreate()");
sIsSubtitle = true;
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_anki_stats);
initNavigationDrawer(findViewById(android.R.id.content));
startLoadingCollection();
}
@Override
protected void onCollectionLoaded(Collection col) {
Timber.d("onCollectionLoaded()");
SlidingTabLayout slidingTabLayout;
// Add drop-down menu to select deck to action bar.
mDropDownDecks = getCol().getDecks().allSorted();
ActionBar actionBar = getSupportActionBar();
actionBar.setDisplayShowTitleEnabled(false);
mActionBarSpinner = (Spinner) findViewById(R.id.toolbar_spinner);
mActionBarSpinner.setAdapter(new DeckDropDownAdapter(this, mDropDownDecks));
mActionBarSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
selectDropDownItem(position);
}
@Override
public void onNothingSelected(AdapterView<?> parent) {
// do nothing
}
});
mActionBarSpinner.setVisibility(View.VISIBLE);
// Setup Task Handler
mTaskHandler = new AnkiStatsTaskHandler(col);
// Create the adapter that will return a fragment for each of the three
// primary sections of the activity.
mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());
// Set up the ViewPager with the sections adapter.
mViewPager = (ViewPager) findViewById(R.id.pager);
mViewPager.setAdapter(mSectionsPagerAdapter);
mViewPager.setOffscreenPageLimit(8);
slidingTabLayout = (SlidingTabLayout) findViewById(R.id.sliding_tabs);
slidingTabLayout.setViewPager(mViewPager);
// Dirty way to get text size from a TextView with current style, change if possible
float size = new TextView(this).getTextSize();
mTaskHandler.setmStandardTextSize(size);
// Prepare options menu only after loading everything
supportInvalidateOptionsMenu();
mSectionsPagerAdapter.notifyDataSetChanged();
// set the currently selected deck
String currentDeckName;
try {
currentDeckName = getCol().getDecks().current().getString("name");
} catch (JSONException e) {
throw new RuntimeException(e);
}
if (mIsWholeCollection) {
selectDropDownItem(0);
} else {
for (int dropDownDeckIdx = 0; dropDownDeckIdx < mDropDownDecks.size(); dropDownDeckIdx++) {
JSONObject deck = mDropDownDecks.get(dropDownDeckIdx);
String deckName;
try {
deckName = deck.getString("name");
} catch (JSONException e) {
throw new RuntimeException();
}
if (deckName.equals(currentDeckName)) {
selectDropDownItem(dropDownDeckIdx + 1);
break;
}
}
}
}
@Override
protected void onResume() {
Timber.d("onResume()");
selectNavigationItem(R.id.nav_stats);
super.onResume();
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu);
//System.err.println("in onCreateOptionsMenu");
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.anki_stats, menu);
return true;
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
// exit if mTaskHandler not initialized yet
if (mTaskHandler == null) {
return true;
}
switch (mTaskHandler.getStatType()) {
case TYPE_MONTH:
MenuItem monthItem = menu.findItem(R.id.item_time_month);
monthItem.setChecked(true);
break;
case TYPE_YEAR:
MenuItem yearItem = menu.findItem(R.id.item_time_year);
yearItem.setChecked(true);
break;
case TYPE_LIFE:
MenuItem lifeItem = menu.findItem(R.id.item_time_all);
lifeItem.setChecked(true);
break;
}
return super.onPrepareOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (getDrawerToggle().onOptionsItemSelected(item)) {
return true;
}
int itemId = item.getItemId();
switch (itemId) {
case R.id.item_time_month:
if (item.isChecked()) item.setChecked(false);
else item.setChecked(true);
if (mTaskHandler.getStatType() != Stats.AxisType.TYPE_MONTH) {
mTaskHandler.setStatType(Stats.AxisType.TYPE_MONTH);
mSectionsPagerAdapter.notifyDataSetChanged();
}
return true;
case R.id.item_time_year:
if (item.isChecked()) item.setChecked(false);
else item.setChecked(true);
if (mTaskHandler.getStatType() != Stats.AxisType.TYPE_YEAR) {
mTaskHandler.setStatType(Stats.AxisType.TYPE_YEAR);
mSectionsPagerAdapter.notifyDataSetChanged();
}
return true;
case R.id.item_time_all:
if (item.isChecked()) item.setChecked(false);
else item.setChecked(true);
if (mTaskHandler.getStatType() != Stats.AxisType.TYPE_LIFE) {
mTaskHandler.setStatType(Stats.AxisType.TYPE_LIFE);
mSectionsPagerAdapter.notifyDataSetChanged();
}
return true;
case R.id.action_time_chooser:
//showTimeDialog();
return true;
}
return super.onOptionsItemSelected(item);
}
public void selectDropDownItem(int position) {
mActionBarSpinner.setSelection(position);
if (position == 0) {
mIsWholeCollection = true;
} else {
mIsWholeCollection = false;
JSONObject deck = mDropDownDecks.get(position - 1);
try {
getCol().getDecks().select(deck.getLong("id"));
} catch (JSONException e) {
Timber.e(e, "Could not get ID from deck");
}
}
mTaskHandler.setIsWholeCollection(mIsWholeCollection);
mSectionsPagerAdapter.notifyDataSetChanged();
}
/**
* @return text to be used in the subtitle of the drop-down deck selector
*/
public String getSubtitleText() {
return getResources().getString(R.string.statistics);
}
public AnkiStatsTaskHandler getTaskHandler(){
return mTaskHandler;
}
public ViewPager getViewPager(){
return mViewPager;
}
public SectionsPagerAdapter getSectionsPagerAdapter() {
return mSectionsPagerAdapter;
}
/**
* A {@link FragmentPagerAdapter} that returns a fragment corresponding to
* one of the sections/tabs/pages.
*/
public class SectionsPagerAdapter extends FragmentPagerAdapter {
public SectionsPagerAdapter(FragmentManager fm) {
super(fm);
}
//this is called when mSectionsPagerAdapter.notifyDataSetChanged() is called, so checkAndUpdate() here
//works best for updating all tabs
@Override
public int getItemPosition(Object object) {
if (object instanceof StatisticFragment) {
((StatisticFragment) object).checkAndUpdate();
}
//don't return POSITION_NONE, avoid fragment recreation.
return super.getItemPosition(object);
}
@Override
public Fragment getItem(int position) {
Fragment item = StatisticFragment.newInstance(position);
((StatisticFragment) item).checkAndUpdate();
return item;
}
@Override
public int getCount() {
return 9;
}
@Override
public CharSequence getPageTitle(int position) {
Locale l = Locale.getDefault();
switch (position) {
case TODAYS_STATS_TAB_POSITION:
return getString(R.string.stats_overview).toUpperCase(l);
case FORECAST_TAB_POSITION:
return getString(R.string.stats_forecast).toUpperCase(l);
case REVIEW_COUNT_TAB_POSITION:
return getString(R.string.stats_review_count).toUpperCase(l);
case REVIEW_TIME_TAB_POSITION:
return getString(R.string.stats_review_time).toUpperCase(l);
case INTERVALS_TAB_POSITION:
return getString(R.string.stats_review_intervals).toUpperCase(l);
case HOURLY_BREAKDOWN_TAB_POSITION:
return getString(R.string.stats_breakdown).toUpperCase(l);
case WEEKLY_BREAKDOWN_TAB_POSITION:
return getString(R.string.stats_weekly_breakdown).toUpperCase(l);
case ANSWER_BUTTONS_TAB_POSITION:
return getString(R.string.stats_answer_buttons).toUpperCase(l);
case CARDS_TYPES_TAB_POSITION:
return getString(R.string.stats_cards_types).toUpperCase(l);
}
return null;
}
}
public static abstract class StatisticFragment extends Fragment {
//track current settings for each individual fragment
protected long mDeckId;
protected ViewPager mActivityPager;
protected SectionsPagerAdapter mActivitySectionPagerAdapter;
/**
* The fragment argument representing the section number for this
* fragment.
*/
protected static final String ARG_SECTION_NUMBER = "section_number";
/**
* Returns a new instance of this fragment for the given section
* number.
*/
public static StatisticFragment newInstance(int sectionNumber) {
Fragment fragment;
Bundle args;
switch (sectionNumber) {
case FORECAST_TAB_POSITION:
case REVIEW_COUNT_TAB_POSITION:
case REVIEW_TIME_TAB_POSITION:
case INTERVALS_TAB_POSITION:
case HOURLY_BREAKDOWN_TAB_POSITION:
case WEEKLY_BREAKDOWN_TAB_POSITION:
case ANSWER_BUTTONS_TAB_POSITION:
case CARDS_TYPES_TAB_POSITION:
fragment = new ChartFragment();
args = new Bundle();
args.putInt(ARG_SECTION_NUMBER, sectionNumber);
fragment.setArguments(args);
return (ChartFragment) fragment;
case TODAYS_STATS_TAB_POSITION:
fragment = new OverviewStatisticsFragment();
args = new Bundle();
args.putInt(ARG_SECTION_NUMBER, sectionNumber);
fragment.setArguments(args);
return (OverviewStatisticsFragment) fragment;
default:
return null;
}
}
@Override
public void onResume() {
super.onResume();
checkAndUpdate();
}
public abstract void invalidateView();
public abstract void checkAndUpdate();
}
/**
* A chart fragment containing a ChartView.
*/
public static class ChartFragment extends StatisticFragment {
private ChartView mChart;
private ProgressBar mProgressBar;
private int mHeight = 0;
private int mWidth = 0;
private int mSectionNumber;
private Stats.AxisType mType = Stats.AxisType.TYPE_MONTH;
private boolean mIsCreated = false;
private AsyncTask mCreateChartTask;
public ChartFragment() {
super();
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
setHasOptionsMenu(true);
Bundle bundle = getArguments();
mSectionNumber = bundle.getInt(ARG_SECTION_NUMBER);
//int sectionNumber = 0;
//System.err.println("sectionNumber: " + mSectionNumber);
View rootView = inflater.inflate(R.layout.fragment_anki_stats, container, false);
mChart = (ChartView) rootView.findViewById(R.id.image_view_chart);
if (mChart == null) {
Timber.d("mChart null!");
} else {
Timber.d("mChart is not null!");
}
//mChart.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
mProgressBar = (ProgressBar) rootView.findViewById(R.id.progress_bar_stats);
mProgressBar.setVisibility(View.VISIBLE);
//mChart.setVisibility(View.GONE);
// TODO: Implementing loader for Collection in Fragment itself would be a better solution.
if ((((Statistics) getActivity()).getTaskHandler()) == null) {
// Close statistics if the TaskHandler hasn't been loaded yet
Timber.e("Statistics.ChartFragment.onCreateView() TaskHandler not found");
getActivity().finish();
return rootView;
}
createChart();
mHeight = mChart.getMeasuredHeight();
mWidth = mChart.getMeasuredWidth();
mChart.addFragment(this);
mType = (((Statistics) getActivity()).getTaskHandler()).getStatType();
mIsCreated = true;
mActivityPager = ((Statistics) getActivity()).getViewPager();
mActivitySectionPagerAdapter = ((Statistics) getActivity()).getSectionsPagerAdapter();
mDeckId = CollectionHelper.getInstance().getCol(getActivity()).getDecks().selected();
if (!isWholeCollection()) {
try {
Collection col = CollectionHelper.getInstance().getCol(getActivity());
List<String> parts = Arrays.asList(col.getDecks().current().getString("name").split("::", -1));
if (sIsSubtitle) {
((AppCompatActivity) getActivity()).getSupportActionBar().setSubtitle(parts.get(parts.size() - 1));
} else {
getActivity().setTitle(parts.get(parts.size() - 1));
}
} catch (JSONException e) {
throw new RuntimeException(e);
}
} else {
if (sIsSubtitle) {
((AppCompatActivity) getActivity()).getSupportActionBar().setSubtitle(R.string.stats_deck_collection);
} else {
getActivity().setTitle(getResources().getString(R.string.stats_deck_collection));
}
}
return rootView;
}
private void createChart() {
switch (mSectionNumber) {
case FORECAST_TAB_POSITION:
mCreateChartTask = (((Statistics) getActivity()).getTaskHandler()).createChart(
Stats.ChartType.FORECAST, mChart, mProgressBar);
break;
case REVIEW_COUNT_TAB_POSITION:
mCreateChartTask = (((Statistics) getActivity()).getTaskHandler()).createChart(
Stats.ChartType.REVIEW_COUNT, mChart, mProgressBar);
break;
case REVIEW_TIME_TAB_POSITION:
mCreateChartTask = (((Statistics) getActivity()).getTaskHandler()).createChart(
Stats.ChartType.REVIEW_TIME, mChart, mProgressBar);
break;
case INTERVALS_TAB_POSITION:
mCreateChartTask = (((Statistics) getActivity()).getTaskHandler()).createChart(
Stats.ChartType.INTERVALS, mChart, mProgressBar);
break;
case HOURLY_BREAKDOWN_TAB_POSITION:
mCreateChartTask = (((Statistics) getActivity()).getTaskHandler()).createChart(
Stats.ChartType.HOURLY_BREAKDOWN, mChart, mProgressBar);
break;
case WEEKLY_BREAKDOWN_TAB_POSITION:
mCreateChartTask = (((Statistics) getActivity()).getTaskHandler()).createChart(
Stats.ChartType.WEEKLY_BREAKDOWN, mChart, mProgressBar);
break;
case ANSWER_BUTTONS_TAB_POSITION:
mCreateChartTask = (((Statistics) getActivity()).getTaskHandler()).createChart(
Stats.ChartType.ANSWER_BUTTONS, mChart, mProgressBar);
break;
case CARDS_TYPES_TAB_POSITION:
mCreateChartTask = (((Statistics) getActivity()).getTaskHandler()).createChart(
Stats.ChartType.CARDS_TYPES, mChart, mProgressBar);
break;
}
}
@Override
public void checkAndUpdate() {
//System.err.println("<<<<<<<checkAndUpdate" + mSectionNumber);
if (!mIsCreated) {
return;
}
int height = mChart.getMeasuredHeight();
int width = mChart.getMeasuredWidth();
//are height and width checks still necessary without bitmaps?
if (height != 0 && width != 0) {
Collection col = CollectionHelper.getInstance().getCol(getActivity());
if (mHeight != height || mWidth != width ||
mType != (((Statistics) getActivity()).getTaskHandler()).getStatType() ||
mDeckId != col.getDecks().selected() || isWholeCollection()) {
mHeight = height;
mWidth = width;
mType = (((Statistics) getActivity()).getTaskHandler()).getStatType();
mProgressBar.setVisibility(View.VISIBLE);
mChart.setVisibility(View.GONE);
if (!isWholeCollection())
mDeckId = col.getDecks().selected();
else
mDeckId = -1;
if (mCreateChartTask != null && !mCreateChartTask.isCancelled()) {
mCreateChartTask.cancel(true);
}
createChart();
}
}
}
private boolean isWholeCollection() {
return ((Statistics) getActivity()).mIsWholeCollection;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
@Override
public void invalidateView() {
if (mChart != null) {
mChart.invalidate();
}
}
@Override
public void onDestroy() {
super.onDestroy();
if (mCreateChartTask != null && !mCreateChartTask.isCancelled()) {
mCreateChartTask.cancel(true);
}
}
}
public static class OverviewStatisticsFragment extends StatisticFragment {
private WebView mWebView;
private ProgressBar mProgressBar;
private Stats.AxisType mType = Stats.AxisType.TYPE_MONTH;
private boolean mIsCreated = false;
private AsyncTask mCreateStatisticsOverviewTask;
public OverviewStatisticsFragment() {
super();
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
setHasOptionsMenu(true);
View rootView = inflater.inflate(R.layout.fragment_anki_stats_overview, container, false);
AnkiStatsTaskHandler handler = (((Statistics) getActivity()).getTaskHandler());
// Workaround for issue 2406 -- crash when resuming after app is purged from RAM
// TODO: Implementing loader for Collection in Fragment itself would be a better solution.
if (handler == null) {
Timber.e("Statistics.OverviewStatisticsFragment.onCreateView() TaskHandler not found");
getActivity().finish();
return rootView;
}
mWebView = (WebView) rootView.findViewById(R.id.web_view_stats);
if (mWebView == null) {
Timber.d("mChart null!");
} else {
Timber.d("mChart is not null!");
// Set transparent color to prevent flashing white when night mode enabled
mWebView.setBackgroundColor(Color.argb(1, 0, 0, 0));
}
//mChart.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
mProgressBar = (ProgressBar) rootView.findViewById(R.id.progress_bar_stats_overview);
mProgressBar.setVisibility(View.VISIBLE);
//mChart.setVisibility(View.GONE);
createStatisticOverview();
mType = handler.getStatType();
mIsCreated = true;
mActivityPager = ((Statistics) getActivity()).getViewPager();
mActivitySectionPagerAdapter = ((Statistics) getActivity()).getSectionsPagerAdapter();
Collection col = CollectionHelper.getInstance().getCol(getActivity());
if (!isWholeCollection()) {
mDeckId = col.getDecks().selected();
try {
List<String> parts = Arrays.asList(col.getDecks().current().getString("name").split("::"));
if (sIsSubtitle) {
((AppCompatActivity) getActivity()).getSupportActionBar().setSubtitle(parts.get(parts.size() - 1));
} else {
getActivity().setTitle(parts.get(parts.size() - 1));
}
} catch (JSONException e) {
throw new RuntimeException(e);
}
} else {
if (sIsSubtitle) {
((AppCompatActivity) getActivity()).getSupportActionBar().setSubtitle(R.string.stats_deck_collection);
} else {
getActivity().setTitle(R.string.stats_deck_collection);
}
}
return rootView;
}
private boolean isWholeCollection() {
return ((Statistics) getActivity()).mIsWholeCollection;
}
private void createStatisticOverview(){
AnkiStatsTaskHandler handler = (((Statistics)getActivity()).getTaskHandler());
mCreateStatisticsOverviewTask = handler.createStatisticsOverview(mWebView, mProgressBar);
}
@Override
public void invalidateView() {
if (mWebView != null) {
mWebView.invalidate();
}
}
@Override
public void checkAndUpdate() {
if (!mIsCreated) {
return;
}
Collection col = CollectionHelper.getInstance().getCol(getActivity());
if (mType != (((Statistics) getActivity()).getTaskHandler()).getStatType() ||
mDeckId != col.getDecks().selected() || isWholeCollection()) {
mType = (((Statistics) getActivity()).getTaskHandler()).getStatType();
mProgressBar.setVisibility(View.VISIBLE);
mWebView.setVisibility(View.GONE);
if (!isWholeCollection())
mDeckId = col.getDecks().selected();
else
mDeckId = -1;
if (mCreateStatisticsOverviewTask != null && !mCreateStatisticsOverviewTask.isCancelled()) {
mCreateStatisticsOverviewTask.cancel(true);
}
createStatisticOverview();
}
}
@Override
public void onDestroy() {
super.onDestroy();
if (mCreateStatisticsOverviewTask != null && !mCreateStatisticsOverviewTask.isCancelled()) {
mCreateStatisticsOverviewTask.cancel(true);
}
}
}
@Override
public void onBackPressed() {
if (isDrawerOpen()) {
super.onBackPressed();
} else {
Timber.i("Back key pressed");
Intent data = new Intent();
if (getIntent().hasExtra("selectedDeck")) {
data.putExtra("originalDeck", getIntent().getLongExtra("selectedDeck", 0L));
}
setResult(RESULT_CANCELED, data);
finishWithAnimation(ActivityTransitionAnimation.RIGHT);
}
}
}