com.wanikani.androidnotifier.StatsFragment.java Source code

Java tutorial

Introduction

Here is the source code for com.wanikani.androidnotifier.StatsFragment.java

Source

package com.wanikani.androidnotifier;

import java.io.IOException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.EnumSet;
import java.util.Hashtable;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Vector;

import android.app.Activity;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.content.res.Resources;
import android.os.AsyncTask;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

import com.wanikani.androidnotifier.db.HistoryDatabase;
import com.wanikani.androidnotifier.db.HistoryDatabaseCache;
import com.wanikani.androidnotifier.db.HistoryDatabaseCache.PageSegment;
import com.wanikani.androidnotifier.graph.HistogramChart;
import com.wanikani.androidnotifier.graph.HistogramPlot;
import com.wanikani.androidnotifier.graph.IconizableChart;
import com.wanikani.androidnotifier.graph.Pager;
import com.wanikani.androidnotifier.graph.Pager.Series;
import com.wanikani.androidnotifier.graph.PieChart;
import com.wanikani.androidnotifier.graph.PieChart.InfoSet;
import com.wanikani.androidnotifier.graph.PiePlot.DataSet;
import com.wanikani.androidnotifier.graph.TYChart;
import com.wanikani.androidnotifier.stats.ItemAgeChart;
import com.wanikani.androidnotifier.stats.ItemDistributionChart;
import com.wanikani.androidnotifier.stats.KanjiProgressChart;
import com.wanikani.androidnotifier.stats.NetworkEngine;
import com.wanikani.androidnotifier.stats.ReviewsTimelineChart;
import com.wanikani.wklib.Connection;
import com.wanikani.wklib.Item;
import com.wanikani.wklib.ItemLibrary;
import com.wanikani.wklib.SRSDistribution;
import com.wanikani.wklib.SRSLevel;

/* 
 *  Copyright (c) 2013 Alberto Cuda
 *
 *  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/>.
 */

/**
 * A fragment that displays some charts that show the user's progress.
 * Currently we show the overall SRS distribution and kanji/vocab
 * progress. In a future release, we may also include historical data
 * (we need to create and maintain a database, though).
 */
public class StatsFragment extends Fragment implements Tab {

    /**
     * This task gets called at application start time to acquire some overall
     * info from the database. The main object, here, is to fix the Y scale. 
     */
    private class GetCoreStatsTask extends AsyncTask<Void, Void, HistoryDatabase.CoreStats> {

        /// The context
        private Context ctxt;

        /// The connection
        private Connection conn;

        /**
         * Constructor
         * @param ctxt the context
         * @param conn the connection
         */
        public GetCoreStatsTask(Context ctxt, Connection conn) {
            this.ctxt = ctxt;
            this.conn = conn;
        }

        /**
         * The worker function. We simply call the DB that will perform the real job.
         * @return the stats
         */
        @Override
        protected HistoryDatabase.CoreStats doInBackground(Void... v) {
            Connection.Meter meter;

            meter = MeterSpec.T.DASHBOARD_REFRESH.get(ctxt);

            try {
                return HistoryDatabase.getCoreStats(ctxt, conn.getUserInformation(meter));
            } catch (IOException e) {
                return HistoryDatabase.getCoreStats(ctxt, null);
            }
        }

        /**
         * Called when DB has been accesses. Publish the info to the main class
         *    @param stats
         */
        @Override
        protected void onPostExecute(HistoryDatabase.CoreStats cs) {
            setCoreStats(cs);
        }

    }

    /**
     * Handler of reconstruction dialog events.
     */
    private class ReconstructListener implements ReconstructDialog.Interface {

        /// The reconstruct dialog
        ReconstructDialog rd;

        /**
         * Constructor. Builds the reconstruct dialog, and start reconstruction process.
         */
        public ReconstructListener() {
            rd = new ReconstructDialog(this, main, main.getConnection());
        }

        /**
         * Called by reconstruction dialog when it's over and everything went
         * smoothly. Notify the main class.
         * @param cs the new core stats
         */
        public void completed(HistoryDatabase.CoreStats cs) {
            reconstructCompleted(this, cs);
        }
    }

    /**
     * The base class for all the datasources in this main class. All the common tasks 
     * are implemented at this level
     */
    private abstract class DataSource extends HistoryDatabaseCache.DataSource {

        /// The series.
        protected List<Series> series;

        /// Y axis scale
        protected int maxY;

        /// A mapping between levels and their levelup days
        protected Map<Integer, Pager.Marker> markers;

        /// The levelup color
        protected int markerColor;

        /**
         * Constructor
         * @param hdbc the database cache
         */
        public DataSource(HistoryDatabaseCache hdbc) {
            super(hdbc);

            Resources res;

            res = getResources();

            markerColor = res.getColor(R.color.levelup);
            maxY = 100;
            markers = new Hashtable<Integer, Pager.Marker>();
        }

        /**
         * Returns the series, which nonetheless must have been created by the
         * subclass.
         *    @param return the series
         */
        @Override
        public List<Series> getSeries() {
            return series;
        }

        /**
         * Returns the Y scale, which nonetheless must have been set by
         * the subclass
         *    @param the Y scale
         */
        public float getMaxY() {
            return maxY;
        }

        /**
         * Called by the main class when core stats become available.
         * Subclasses can (should) override this method, by must still call
         * this implementation, that takes care of populating the levelup hashtable.
         * @param cs the new corestats
         */
        public void setCoreStats(HistoryDatabase.CoreStats cs) {
            if (cs.levelInfo != null)
                loadLevelInfo(cs.levelInfo);
        }

        /**
         * Creates the levelups hashtable. Called by 
         * {@link #setCoreStats(com.wanikani.androidnotifier.db.HistoryDatabase.CoreStats)},
         * so there is no need to access the DB.
         * @param levelInfo the level information
         */
        private void loadLevelInfo(Map<Integer, HistoryDatabase.LevelInfo> levelInfo) {
            markers.clear();

            for (Map.Entry<Integer, HistoryDatabase.LevelInfo> e : levelInfo.entrySet()) {
                markers.put(e.getValue().day, newMarker(e.getValue().day, e.getKey()));
            }
        }

        /**
         * Adds a levelup marker. This method may be overridden by subclasses in order
         * to change marker color or tag
         * @param day the day
         * @param level the level
         * @return a marker
         */
        protected Pager.Marker newMarker(int day, int level) {
            return new Pager.Marker(markerColor, Integer.toString(level));
        }

        /**
         * Returns the marker hashtable. This is populated at this level, so
         * subclasses need not override this method (though they can still to it
         * if they want to display something fancier).
         * @return the leveup markers 
         */
        @Override
        public Map<Integer, Pager.Marker> getMarkers() {
            return markers;
        }

        /**
         * Called when the user requested partial info to be reconstructed.
         * We open the reconstruct dialog and let it handle the business
         */
        public void fillPartial() {
            openReconstructDialog();
        }
    }

    /**
     * The SRS distribution datasource implementation.
     */
    private class SRSDataSource extends DataSource {

        /// The series of complete segments 
        private List<Series> completeSeries;

        /// The series of partial segments
        private List<Series> partialSeries;

        /**
         * Constructor.
         * @param hdbc the database cache
         */
        public SRSDataSource(HistoryDatabaseCache hdbc) {
            super(hdbc);

            Resources res;
            Series burned;

            res = getResources();

            burned = new Series(res.getColor(R.color.burned), res.getString(R.string.tag_burned));
            completeSeries = new Vector<Series>();
            completeSeries
                    .add(new Series(res.getColor(R.color.apprentice), res.getString(R.string.tag_apprentice)));
            completeSeries.add(new Series(res.getColor(R.color.guru), res.getString(R.string.tag_guru)));
            completeSeries.add(new Series(res.getColor(R.color.master), res.getString(R.string.tag_master)));
            completeSeries
                    .add(new Series(res.getColor(R.color.enlightened), res.getString(R.string.tag_enlightened)));

            partialSeries = new Vector<Series>();
            partialSeries.add(new Series(res.getColor(R.color.unlocked), res.getString(R.string.tag_unlocked)));

            series = new Vector<Series>();
            series.addAll(completeSeries);
            series.addAll(partialSeries);

            series.add(burned);
            partialSeries.add(burned);
            completeSeries.add(burned);
        }

        /**
         * Called when core stats become available. Setup the {@link DataSource#maxY}
         * field
         * @param cs the core stats
         */
        @Override
        public void setCoreStats(HistoryDatabase.CoreStats cs) {
            super.setCoreStats(cs);

            maxY = cs.maxRadicals + cs.maxKanji + cs.maxVocab;
            if (maxY == 0)
                maxY = 100;
        }

        /**
         * Called by some ancestor when we need to translate a raw page segment
         * into a plot segment <i>and</i> data is partial. 
         * @param segment raw input segment
         * @param pseg output segment
         */
        @Override
        protected void fillPartialSegment(Pager.Segment segment, PageSegment pseg) {
            int i;

            segment.series = partialSeries;
            segment.data = new float[2][pseg.srsl.size()];
            i = 0;
            for (SRSDistribution srs : pseg.srsl) {
                segment.data[0][i] = srs.apprentice.total;
                segment.data[1][i++] = srs.burned.total;
            }
        }

        /**
         * Called by some ancestor when we need to translate a raw page segment
         * into a plot segment <i>and</i> data is complete. 
         * @param segment raw input segment
         * @param pseg output segment
         */
        protected void fillSegment(Pager.Segment segment, PageSegment pseg) {
            int i;

            segment.series = completeSeries;
            segment.data = new float[5][pseg.srsl.size()];
            i = 0;
            for (SRSDistribution srs : pseg.srsl) {
                segment.data[0][i] = srs.apprentice.total;
                segment.data[1][i] = srs.guru.total;
                segment.data[2][i] = srs.master.total;
                segment.data[3][i] = srs.enlighten.total;
                segment.data[4][i] = srs.burned.total;
                i++;
            }
        }

    }

    /**
     * The kanji distribution datasource implementation.
     */
    private class KanjiDataSource extends DataSource {

        /// The series of complete segments 
        private List<Series> completeSeries;

        /// The series of partial segments 
        private List<Series> partialSeries;

        /**
         * Constructor.
         * @param hdbc the database cache
         */
        public KanjiDataSource(HistoryDatabaseCache hdbc) {
            super(hdbc);

            Resources res;
            Series burned;

            res = getResources();

            burned = new Series(res.getColor(R.color.burned), res.getString(R.string.tag_burned));
            completeSeries = new Vector<Series>();
            completeSeries
                    .add(new Series(res.getColor(R.color.apprentice), res.getString(R.string.tag_apprentice)));
            completeSeries.add(new Series(res.getColor(R.color.guru), res.getString(R.string.tag_guru)));
            completeSeries.add(new Series(res.getColor(R.color.master), res.getString(R.string.tag_master)));
            completeSeries
                    .add(new Series(res.getColor(R.color.enlightened), res.getString(R.string.tag_enlightened)));

            partialSeries = new Vector<Series>();
            partialSeries.add(new Series(res.getColor(R.color.unlocked), res.getString(R.string.tag_unlocked)));

            series = new Vector<Series>();
            series.addAll(completeSeries);
            series.addAll(partialSeries);

            series.add(burned);
            partialSeries.add(burned);
            completeSeries.add(burned);
        }

        /**
         * Called when core stats become available. Setup the {@link DataSource#maxY}
         * field
         * @param cs the core stats
         */
        public void setCoreStats(HistoryDatabase.CoreStats cs) {
            super.setCoreStats(cs);

            maxY = cs.maxKanji;
            if (maxY == 0)
                maxY = 100;
        }

        /**
         * Called by some ancestor when we need to translate a raw page segment
         * into a plot segment <i>and</i> data is partial. 
         * @param segment raw input segment
         * @param pseg output segment
         */
        protected void fillPartialSegment(Pager.Segment segment, PageSegment pseg) {
            int i;

            segment.series = partialSeries;
            segment.data = new float[2][pseg.srsl.size()];
            i = 0;
            for (SRSDistribution srs : pseg.srsl) {
                segment.data[0][i] = srs.apprentice.kanji;
                segment.data[1][i++] = srs.burned.kanji;
            }
        }

        /**
         * Called by some ancestor when we need to translate a raw page segment
         * into a plot segment <i>and</i> data is complete. 
         * @param segment raw input segment
         * @param pseg output segment
         */
        protected void fillSegment(Pager.Segment segment, PageSegment pseg) {
            int i;

            segment.series = completeSeries;
            segment.data = new float[5][pseg.srsl.size()];
            i = 0;
            for (SRSDistribution srs : pseg.srsl) {
                segment.data[0][i] = srs.apprentice.kanji;
                segment.data[1][i] = srs.guru.kanji;
                segment.data[2][i] = srs.master.kanji;
                segment.data[3][i] = srs.enlighten.kanji;
                segment.data[4][i] = srs.burned.kanji;
                i++;
            }
        }

    }

    /**
     * The vocab distribution datasource implementation.
     */
    private class VocabDataSource extends DataSource {

        /// The series of complete segments 
        private List<Series> completeSeries;

        /// The series of partial segments 
        private List<Series> partialSeries;

        /**
         * Constructor.
         * @param hdbc the database cache
         */
        public VocabDataSource(HistoryDatabaseCache hdbc) {
            super(hdbc);

            Resources res;
            Series burned;

            res = getResources();

            burned = new Series(res.getColor(R.color.burned), res.getString(R.string.tag_burned));
            completeSeries = new Vector<Series>();
            completeSeries
                    .add(new Series(res.getColor(R.color.apprentice), res.getString(R.string.tag_apprentice)));
            completeSeries.add(new Series(res.getColor(R.color.guru), res.getString(R.string.tag_guru)));
            completeSeries.add(new Series(res.getColor(R.color.master), res.getString(R.string.tag_master)));
            completeSeries
                    .add(new Series(res.getColor(R.color.enlightened), res.getString(R.string.tag_enlightened)));

            partialSeries = new Vector<Series>();
            partialSeries.add(new Series(res.getColor(R.color.unlocked), res.getString(R.string.tag_unlocked)));

            series = new Vector<Series>();
            series.addAll(completeSeries);
            series.addAll(partialSeries);

            series.add(burned);
            partialSeries.add(burned);
            completeSeries.add(burned);
        }

        /**
         * Called when core stats become available. Setup the {@link DataSource#maxY}
         * field
         * @param cs the core stats
         */
        public void setCoreStats(HistoryDatabase.CoreStats cs) {
            super.setCoreStats(cs);

            maxY = cs.maxVocab;
            if (maxY == 0)
                maxY = 100;
        }

        /**
         * Called by some ancestor when we need to translate a raw page segment
         * into a plot segment <i>and</i> data is partial. 
         * @param segment raw input segment
         * @param pseg output segment
         */
        protected void fillPartialSegment(Pager.Segment segment, PageSegment pseg) {
            int i;

            segment.series = partialSeries;
            segment.data = new float[2][pseg.srsl.size()];
            i = 0;
            for (SRSDistribution srs : pseg.srsl) {
                segment.data[0][i] = srs.apprentice.vocabulary;
                segment.data[1][i++] = srs.burned.vocabulary;
            }
        }

        /**
         * Called by some ancestor when we need to translate a raw page segment
         * into a plot segment <i>and</i> data is complete. 
         * @param segment raw input segment
         * @param pseg output segment
         */
        protected void fillSegment(Pager.Segment segment, PageSegment pseg) {
            int i;

            segment.series = completeSeries;
            segment.data = new float[5][pseg.srsl.size()];
            i = 0;
            for (SRSDistribution srs : pseg.srsl) {
                segment.data[0][i] = srs.apprentice.vocabulary;
                segment.data[1][i] = srs.guru.vocabulary;
                segment.data[2][i] = srs.master.vocabulary;
                segment.data[3][i] = srs.enlighten.vocabulary;
                segment.data[4][i] = srs.burned.vocabulary;
                i++;
            }
        }

    }

    private abstract class GenericChart {

        HistoryDatabase.CoreStats cs;

        DashboardData dd;

        public void setCoreStats(HistoryDatabase.CoreStats cs) {
            this.cs = cs;

            updateIfComplete();
        }

        public void setDashboardData(DashboardData dd) {
            this.dd = dd;

            updateIfComplete();
        }

        public boolean scrolling(boolean strict) {
            return false;
        }

        protected void updateIfComplete() {
            if (dd != null && cs != null)
                update();
        }

        protected abstract void update();

        protected int getVacation() {
            HistoryDatabase.LevelInfo li;
            int i, ans;

            if (dd == null || cs == null || cs.levelInfo == null)
                return 0;

            ans = 0;
            for (i = 1; i <= dd.level; i++) {
                li = cs.levelInfo.get(i);
                if (li != null)
                    ans += li.vacation;
            }

            return ans;
        }

        protected Map<Integer, Integer> getDays() {
            Map<Integer, Integer> ans;
            HistoryDatabase.LevelInfo li;
            Integer lday;
            int i;

            lday = 0;
            ans = new Hashtable<Integer, Integer>();
            for (i = 1; i <= dd.level; i++) {
                li = cs.levelInfo.get(i);
                if (li != null && lday != null && lday < li.day)
                    ans.put(i - 1, li.day - lday);
                lday = li != null ? (li.day + li.vacation) : null;
            }

            /* We don't want the last level to grow... */
            for (i = LAST_LEVEL; i <= dd.level; i++)
                ans.remove(i);

            return ans;
        }
    }

    /**
     * This class, given the core stats, calculates the expected completion time
     * of the fifty WK levels and of next level. This is done through an exponentially
     * weighted average. The exponent is different in the two cases.
     */
    private class LevelEstimates extends GenericChart {

        /// The exponent for l50 completion
        private static final float WEXP_L50 = 0.707f;

        /// The exponent for next level completion
        private static final float WEXP_NEXT = 0.42f;

        /// The Date formatter
        private DateFormat df;

        public LevelEstimates() {
            df = new SimpleDateFormat("dd MMM yyyy", Locale.US);
        }

        protected void update() {
            Map<Integer, Integer> days;
            int vity, vacation;
            boolean show;

            days = getDays();
            vacation = getVacation();
            show = false;

            show |= updateL50(days, vacation);
            show |= updateNextLevel(days);
            vity = show ? View.VISIBLE : View.GONE;

            parent.findViewById(R.id.ct_eta).setVisibility(vity);
            parent.findViewById(R.id.ctab_eta).setVisibility(vity);
        }

        private boolean updateL50(Map<Integer, Integer> days, int vacation) {
            HistoryDatabase.LevelInfo li;
            TextView tw;
            View dw;
            Float delay;
            Calendar cal;
            boolean show;

            delay = weight(days, WEXP_L50);
            tw = (TextView) parent.findViewById(R.id.tv_eta_l50);
            dw = parent.findViewById(R.id.div_eta_l50);
            li = cs != null && cs.levelInfo != null ? cs.levelInfo.get(Math.min(dd.level, ALL_THE_LEVELS)) : null;

            cal = Calendar.getInstance();
            cal.setTime(dd.creation);
            if (dd.level >= ALL_THE_LEVELS) {
                /* Give info only if we know when we levelled up */
                if (li != null) {
                    cal.add(Calendar.DATE, li.day);
                    show = true;
                } else
                    show = false;

            } else if (delay != null) {

                if (li != null) {
                    /* Algorithm 1: last_levelup_time + avg_time * (50 - current_level)  
                     * More accurate but may fail if for some reason we have no leveup info for the current level
                     */
                    cal.add(Calendar.DATE, li.day + li.vacation);
                    cal.add(Calendar.DATE, (int) (delay * (ALL_THE_LEVELS - dd.level)));
                } else {
                    /* Algorithm 2: subscription_date + total_vacation + 50 * avg_time */
                    cal.add(Calendar.DATE, (int) (delay * ALL_THE_LEVELS) + vacation);
                }

                show = true;
            } else
                show = false;

            tw.setText(df.format(cal.getTime()));
            dw.setVisibility(show ? View.VISIBLE : View.GONE);

            return show;
        }

        private boolean updateNextLevel(Map<Integer, Integer> days) {
            View nlw, avgw;
            TextView tw, nltag;
            HistoryDatabase.LevelInfo lastli;
            Float delay, avgl;
            Calendar cal;
            String s;

            avgl = delay = weight(days, WEXP_NEXT);
            nlw = parent.findViewById(R.id.div_eta_next);
            avgw = parent.findViewById(R.id.div_eta_avg);
            if (delay != null && dd.level < ALL_THE_LEVELS) {
                avgw.setVisibility(View.VISIBLE);

                tw = (TextView) parent.findViewById(R.id.tv_eta_avg);
                tw.setText(beautify(delay));

                tw = (TextView) parent.findViewById(R.id.tv_eta_next);
                lastli = cs.levelInfo.get(dd.level);
                if (lastli != null) {
                    cal = Calendar.getInstance();
                    cal.setTime(normalize(dd.creation));
                    cal.add(Calendar.DATE, lastli.day + lastli.vacation);
                    delay -= ((float) System.currentTimeMillis() - cal.getTimeInMillis()) / (24 * 3600 * 1000);
                    delay -= 0.5F; /* This compensates the fact that granularity is one day */

                    if (delay <= avgl && delay >= 0) {
                        nltag = (TextView) parent.findViewById(R.id.tag_eta_next);
                        nlw.setVisibility(View.VISIBLE);
                        nltag.setText(R.string.tag_eta_next_future);
                        s = main.getString(R.string.fmt_eta_next_future, beautify(delay));
                        tw.setText(s);
                    } else
                        nlw.setVisibility(View.GONE);
                } else
                    nlw.setVisibility(View.GONE);

                return true;

            } else {
                avgw.setVisibility(View.GONE);
                nlw.setVisibility(View.GONE);

                return false;
            }
        }

        private Date normalize(Date date) {
            Calendar cal;

            cal = Calendar.getInstance();
            cal.setTime(date);
            cal.set(Calendar.HOUR_OF_DAY, 0);
            cal.set(Calendar.MINUTE, 0);
            cal.set(Calendar.SECOND, 0);
            cal.set(Calendar.MILLISECOND, 0);

            return cal.getTime();
        }

        private List<Date> getExpectedDates(ItemLibrary<? extends Item> lib, int aint, Date uldate) {
            List<Date> dates;
            Calendar cal;
            int slevel;

            dates = new Vector<Date>();
            for (Item i : lib.list) {
                cal = Calendar.getInstance();
                if (i.stats == null || i.stats.srs == null) {
                    slevel = -1;
                    cal.setTime(uldate);
                } else if (i.stats.srs == SRSLevel.APPRENTICE) {
                    cal.setTime(i.stats.availableDate);
                    slevel = Math.min(i.stats.meaning.currentStreak, i.stats.reading.currentStreak);
                } else
                    slevel = 3;

                switch (slevel) {
                case -1:
                    cal.add(Calendar.HOUR, 4);
                case 0:
                    cal.add(Calendar.HOUR, 8);
                case 1:
                    cal.add(Calendar.DATE, 1);
                case 2:
                    cal.add(Calendar.DATE, aint);
                }
                dates.add(cal.getTime());
            }

            Collections.sort(dates);

            return dates;
        }

        public String beautify(float days) {
            long dfield, hfield;
            Resources res;
            String ds, hs;

            res = getResources();
            dfield = (long) days;
            hfield = (long) ((days - dfield) * 24);
            if (dfield > 1)
                ds = res.getString(R.string.fmts_bd, dfield);
            else if (dfield == 1)
                ds = res.getString(R.string.fmts_bd_one);
            else
                ds = null;

            if (hfield > 1)
                hs = res.getString(R.string.fmts_bh, hfield);
            else if (hfield == 1)
                hs = res.getString(R.string.fmts_bh_one);
            else
                hs = null;

            if (ds != null)
                if (hs != null)
                    return ds + ", " + hs;
                else
                    return ds;
            else if (hs != null)
                return hs;
            else
                return res.getString(R.string.fmts_bnow);

        }

        private Float weight(Map<Integer, Integer> days, float wexp) {
            float cw, num, den;
            Integer delta;
            int i;

            num = den = 0;
            cw = wexp;
            for (i = dd.level - 1; i > 0; i--) {
                delta = days.get(i);
                if (delta != null) {
                    num += cw * delta;
                    den += cw;
                }
                cw *= wexp;
            }

            return den != 0 ? num / den : null;
        }

        @Override
        protected Map<Integer, Integer> getDays() {
            Map<Integer, Integer> ans;
            int i, minl, maxl, minv, maxv;
            Integer days;

            ans = super.getDays();

            minl = maxl = minv = maxv = -1;
            for (i = 1; i < dd.level; i++) {
                days = ans.get(i);
                if (days != null) {
                    if (minv == -1 || days < minv) {
                        minl = i;
                        minv = days;
                    }
                    if (maxv == -1 || days > maxv) {
                        maxl = i;
                        maxv = days;
                    }
                }
            }

            if (minl > 0)
                ans.remove(minl);
            if (maxl > 0)
                ans.remove(maxl);

            return ans;
        }

    }

    private class LevelupSource extends GenericChart implements View.OnClickListener {

        List<HistogramPlot.Series> series;

        private static final long LEVELUP_CAP = 30;

        public LevelupSource() {
            Resources res;

            res = getResources();

            series = new Vector<HistogramPlot.Series>();
            series.add(new HistogramPlot.Series(res.getColor(R.color.apprentice)));
            series.add(new HistogramPlot.Series(res.getColor(R.color.guru)));
            series.add(new HistogramPlot.Series(res.getColor(R.color.master)));
            series.add(new HistogramPlot.Series(res.getColor(R.color.enlightened)));
        }

        protected void update() {
            List<HistogramPlot.Samples> bars;
            Map<Integer, Integer> days;
            HistogramPlot.Samples bar;
            HistogramPlot.Sample sample;
            HistogramChart chart;
            Integer day;
            int i, slups;

            bars = new Vector<HistogramPlot.Samples>();
            days = getDays();
            for (i = 1; i < dd.level; i++) {
                bar = new HistogramPlot.Samples(Integer.toString(i));
                sample = new HistogramPlot.Sample();
                bar.samples.add(sample);

                sample.series = series.get(i % series.size());
                day = days.get(i);
                sample.value = day != null ? day : 0;

                bars.add(bar);
            }

            chart = (HistogramChart) parent.findViewById(R.id.hi_levels);
            chart.setData(series, bars, LEVELUP_CAP);

            if (!bars.isEmpty()) {
                chart.setVisibility(View.VISIBLE);
                slups = DatabaseFixup.getSuspectLevels(main, dd.level, cs);
                if (slups > 0)
                    chart.alert(getResources().getString(R.string.alert_suspect_levels, slups), this);
            } else
                chart.setVisibility(View.GONE);
        }

        @Override
        public void onClick(View view) {
            if (isVisible())
                main.dbFixup();
        }

        public boolean scrolling(boolean strict) {
            return ((HistogramChart) parent.findViewById(R.id.hi_levels)).scrolling(strict);
        }
    }

    public class Callback implements LowPriorityScrollView.Callback {

        @Override
        public boolean canScroll(LowPriorityScrollView lpsw) {
            return !scrolling(true);
        }

    }

    /// The main activity
    MainActivity main;

    /// The root view of the fragment
    View parent;

    /// The network engine
    NetworkEngine netwe;

    /// All the TY charts. This is needed to lock and unlock scrolling
    List<TYChart> charts;

    /// All the generic (non TY) charts.
    List<GenericChart> gcharts;

    /// All the charts that must be flushed when requested
    List<IconizableChart> fcharts;

    /// The core stats, used to trim graph scales
    HistoryDatabase.CoreStats cs;

    /// The task that retrives core stats
    GetCoreStatsTask task;

    /// The SRS plot datasource
    SRSDataSource srsds;

    /// The Kanji progress plot datasource
    KanjiDataSource kanjids;

    /// The Vocab progress plot datasource
    VocabDataSource vocabds;

    /// The review timeline charts
    ReviewsTimelineChart timeline;

    int preserved[] = new int[] { R.id.pc_srs, R.id.ty_srs, R.id.pc_vocab, R.id.ty_vocab, R.id.pc_kanji,
            R.id.ty_kanji, R.id.hi_levels };

    int semiPreserved[] = new int[] { R.id.ct_age_distribution, R.id.os_jlpt, R.id.os_joyo, R.id.os_kanji_levels,
            R.id.os_levels, R.id.os_review_timeline_srs, R.id.os_review_timeline_item };

    private Map<Integer, Boolean> semiPreservedState;

    /// Overall number of kanji
    private static final int ALL_THE_KANJI = 2027;

    /// Overall number of vocab items
    private static final int ALL_THE_VOCAB = 6190;

    /// Overall number of levels
    private static final int ALL_THE_LEVELS = 50;

    /// Last level (should be set to ALL_THE_LEVELS when/if we fix the algorithm
    /// to take care of the fact that l50 should be ignored when doing estimates)
    private static final int LAST_LEVEL = 60;

    /// The database
    HistoryDatabaseCache hdbc;

    /// The object that listens for reconstruction events
    ReconstructListener rlist;

    /// Level source
    LevelupSource levels;

    public static final String PREFIX = StatsFragment.class.getName() + ".";

    public static final String KEY_OPEN = PREFIX + "POPEN.";

    private static final String KLIB_JLPT_1 = "???????????????????"
            + "?????????????"
            + "????????????????????"
            + "???????????????"
            + "?????????????????"
            + "????????????????"
            + "?????????????????"
            + "??????????"
            + "???????????????????"
            + "???????????????????"
            + "???????????????"
            + "???????????"
            + "?????";

    private static final String KLIB_JLPT_2 = "?????????????????"
            + "????????????????????"
            + "????????????????"
            + "?????????";

    private static final String KLIB_JLPT_3 = "?????????????????"
            + "?????????????????"
            + "???????????????"
            + "?????????????";

    private static final String KLIB_JLPT_4 = "??????????????????"
            + "?????";

    private static final String KLIB_JLPT_5 = "????????????????????";

    private static final String KLIB_JOYO_1 = "????????????????";

    private static final String KLIB_JOYO_2 = "????????"
            + "????????????";

    private static final String KLIB_JOYO_3 = "???????????????"
            + "???????????????????";

    private static final String KLIB_JOYO_4 = "????????????????????"
            + "???????????????????";

    private static final String KLIB_JOYO_5 = "????????????????"
            + "?????????????";

    private static final String KLIB_JOYO_6 = "??????????????????"
            + "???????????";

    private static final String KLIB_JOYO_S = "??????????????????????"
            + "???????????????"
            + "?????????????????????"
            + "??????????"
            + "???????????????"
            + "???????????????"
            + "?????????????????"
            + "?????????????????"
            + "?????????????????"
            + "???????????????"
            + "???????????"
            + "??";

    /**
     * Constructor
     */
    public StatsFragment() {
        charts = new Vector<TYChart>();
        gcharts = new Vector<GenericChart>();
        fcharts = new Vector<IconizableChart>();
        hdbc = new HistoryDatabaseCache();

        semiPreservedState = new Hashtable<Integer, Boolean>();
        netwe = new NetworkEngine();

        timeline = new ReviewsTimelineChart(netwe, R.id.os_review_timeline_item, R.id.os_review_timeline_srs,
                MeterSpec.T.OTHER_STATS);
        netwe.add(timeline);

        netwe.add(new ItemDistributionChart(netwe, R.id.os_kanji_levels, MeterSpec.T.OTHER_STATS,
                EnumSet.of(Item.Type.KANJI)));
        netwe.add(new ItemDistributionChart(netwe, R.id.os_levels, MeterSpec.T.MORE_STATS,
                EnumSet.of(Item.Type.VOCABULARY)));

        netwe.add(new ItemAgeChart(netwe, R.id.ct_age_distribution, MeterSpec.T.OTHER_STATS,
                EnumSet.allOf(Item.Type.class)));

        netwe.add(
                new KanjiProgressChart(netwe, R.id.os_jlpt, MeterSpec.T.OTHER_STATS, R.string.jlpt5, KLIB_JLPT_5));
        netwe.add(
                new KanjiProgressChart(netwe, R.id.os_jlpt, MeterSpec.T.OTHER_STATS, R.string.jlpt4, KLIB_JLPT_4));
        netwe.add(
                new KanjiProgressChart(netwe, R.id.os_jlpt, MeterSpec.T.OTHER_STATS, R.string.jlpt3, KLIB_JLPT_3));
        netwe.add(
                new KanjiProgressChart(netwe, R.id.os_jlpt, MeterSpec.T.OTHER_STATS, R.string.jlpt2, KLIB_JLPT_2));
        netwe.add(
                new KanjiProgressChart(netwe, R.id.os_jlpt, MeterSpec.T.OTHER_STATS, R.string.jlpt1, KLIB_JLPT_1));

        netwe.add(
                new KanjiProgressChart(netwe, R.id.os_joyo, MeterSpec.T.OTHER_STATS, R.string.joyo1, KLIB_JOYO_1));
        netwe.add(
                new KanjiProgressChart(netwe, R.id.os_joyo, MeterSpec.T.OTHER_STATS, R.string.joyo2, KLIB_JOYO_2));
        netwe.add(
                new KanjiProgressChart(netwe, R.id.os_joyo, MeterSpec.T.OTHER_STATS, R.string.joyo3, KLIB_JOYO_3));
        netwe.add(
                new KanjiProgressChart(netwe, R.id.os_joyo, MeterSpec.T.OTHER_STATS, R.string.joyo4, KLIB_JOYO_4));
        netwe.add(
                new KanjiProgressChart(netwe, R.id.os_joyo, MeterSpec.T.OTHER_STATS, R.string.joyo5, KLIB_JOYO_5));
        netwe.add(
                new KanjiProgressChart(netwe, R.id.os_joyo, MeterSpec.T.OTHER_STATS, R.string.joyo6, KLIB_JOYO_6));
        netwe.add(
                new KanjiProgressChart(netwe, R.id.os_joyo, MeterSpec.T.OTHER_STATS, R.string.joyoS, KLIB_JOYO_S));
    }

    @Override
    public void onAttach(Activity main) {
        super.onAttach(main);

        this.main = (MainActivity) main;
        this.main.register(this);

        hdbc.open(main);

        if (rlist != null)
            rlist.rd.attach(main);
    }

    @Override
    public void onDetach() {
        super.onDetach();

        hdbc.close();

        if (rlist != null)
            rlist.rd.detach();
    }

    /**
     * Called when core stats become available. Update each datasource and
     * request plots to be refreshed.
     * @param cs the core stats
     */
    private void setCoreStats(HistoryDatabase.CoreStats cs) {
        this.cs = cs;

        if (getActivity() != null) {
            srsds.setCoreStats(cs);
            kanjids.setCoreStats(cs);
            vocabds.setCoreStats(cs);
            for (TYChart tyc : charts)
                tyc.refresh();

            for (GenericChart gc : gcharts)
                gc.setCoreStats(cs);
        }
    }

    /**
     * Called at fragment creation. Since it keeps valuable information
     * we enable retain instance flag.
     */
    @Override
    public void onCreate(Bundle bundle) {
        super.onCreate(bundle);

        setRetainInstance(true);
    }

    /**
     * Builds the GUI.
     * @param inflater the inflater
     * @param container
     *  the parent view
     * @param savedInstance an (unused) bundle
     */
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        super.onCreateView(inflater, container, savedInstanceState);

        parent = inflater.inflate(R.layout.stats, container, false);
        charts = new Vector<TYChart>();
        gcharts = new Vector<GenericChart>();

        srsds = new SRSDataSource(hdbc);
        kanjids = new KanjiDataSource(hdbc);
        vocabds = new VocabDataSource(hdbc);

        hdbc.addDataSource(srsds);
        hdbc.addDataSource(kanjids);
        hdbc.addDataSource(vocabds);

        attach(R.id.ty_srs, srsds);
        attach(R.id.ty_kanji, kanjids);
        attach(R.id.ty_vocab, vocabds);

        gcharts.add(new LevelEstimates());
        gcharts.add(levels = new LevelupSource());

        if (cs != null)
            setCoreStats(cs);
        else if (task == null) {
            task = new GetCoreStatsTask(main, main.getConnection());
            task.execute();
        }

        ((LowPriorityScrollView) parent).setCallback(new Callback());

        return parent;
    }

    @Override
    public void onActivityCreated(Bundle bundle) {
        super.onActivityCreated(bundle);

        fcharts = new Vector<IconizableChart>();
        fcharts.add((IconizableChart) parent.findViewById(R.id.ct_age_distribution));
        fcharts.add((IconizableChart) parent.findViewById(R.id.os_kanji_levels));
        fcharts.add((IconizableChart) parent.findViewById(R.id.os_levels));
        fcharts.add((IconizableChart) parent.findViewById(R.id.os_jlpt));
        fcharts.add((IconizableChart) parent.findViewById(R.id.os_joyo));
        fcharts.add((IconizableChart) parent.findViewById(R.id.os_review_timeline_item));
        fcharts.add((IconizableChart) parent.findViewById(R.id.os_review_timeline_srs));

        netwe.bind(main, parent);
    }

    @Override
    public void onDestroyView() {
        super.onDestroyView();

        netwe.unbind();
    }

    @Override
    public void onStart() {
        super.onStart();

        SharedPreferences prefs;
        IconizableChart ic;
        Boolean value;
        int i;

        prefs = PreferenceManager.getDefaultSharedPreferences(main);
        for (i = 0; i < preserved.length; i++) {
            ic = (IconizableChart) parent.findViewById(preserved[i]);
            ic.setOpen(prefs.getBoolean(KEY_OPEN + preserved[i], false));
        }

        for (i = 0; i < semiPreserved.length; i++) {
            ic = (IconizableChart) parent.findViewById(semiPreserved[i]);
            value = semiPreservedState.get(semiPreserved[i]);
            ic.setOpen(value != null ? value : false);
        }
    }

    @Override
    public void onStop() {
        super.onStop();

        Editor prefs;
        IconizableChart ic;
        int i;

        prefs = PreferenceManager.getDefaultSharedPreferences(main).edit();
        for (i = 0; i < preserved.length; i++) {
            ic = (IconizableChart) parent.findViewById(preserved[i]);
            prefs.putBoolean(KEY_OPEN + preserved[i], ic.isOpen());
        }
        prefs.commit();

        for (i = 0; i < semiPreserved.length; i++) {
            ic = (IconizableChart) parent.findViewById(semiPreserved[i]);
            semiPreservedState.put(semiPreserved[i], ic.isOpen());
        }
    }

    /**
     * Binds each datasource to its chart 
     * @param chart the chart ID
     * @param ds the datasource
     */
    private void attach(int chart, DataSource ds) {
        TYChart tyc;

        tyc = (TYChart) parent.findViewById(chart);
        if (cs != null)
            ds.setCoreStats(cs);

        tyc.setDataSource(ds);
        charts.add(tyc);
    }

    @Override
    public void onResume() {
        super.onResume();

        refreshComplete(main.getDashboardData());
        setFixup(main.dbfixup);
    }

    public void setFixup(MainActivity.FixupState state) {
        HistogramChart hc;
        Resources res;

        res = getResources();
        hc = (HistogramChart) parent.findViewById(R.id.hi_levels);
        switch (state) {
        case NOT_RUNNING:
            break;

        case RUNNING:
            hc.alert(res.getString(R.string.db_fixup_text));
            break;

        case FAILED:
            if (levels != null)
                hc.alert(res.getString(R.string.db_fixup_fail), levels);
            break;

        case DONE:
            hc.hideAlert();
            new GetCoreStatsTask(main, main.getConnection()).execute();
        }
    }

    @Override
    public int getName() {
        return R.string.tag_stats;
    }

    @Override
    public void refreshComplete(DashboardData dd) {
        List<DataSet> ds;
        PieChart pc;

        if (dd == null || !isResumed())
            return;

        for (GenericChart gc : gcharts)
            gc.setDashboardData(dd);

        switch (dd.od.srsStatus) {
        case RETRIEVING:
            break;

        case RETRIEVED:
            pc = (PieChart) parent.findViewById(R.id.pc_srs);
            if (hasSRSData(dd.od.srs)) {
                pc.setVisibility(View.VISIBLE);
                ds = getSRSDataSets(dd.od.srs);
                pc.setData(ds, getInfoSet(ds), 5);
            } else
                pc.setVisibility(View.GONE);

            pc = (PieChart) parent.findViewById(R.id.pc_kanji);
            ds = getKanjiProgDataSets(dd.od.srs);
            pc.setData(ds, getInfoSet(ds), 5);

            pc = (PieChart) parent.findViewById(R.id.pc_vocab);
            ds = getVocabProgDataSets(dd.od.srs);
            pc.setData(ds, getInfoSet(ds), 5);

            for (TYChart chart : charts)
                chart.setOrigin(dd.creation);

            timeline.refresh(main.getConnection(), dd);

            break;

        case FAILED:
            if (dd.od.srs == null)
                showAlerts();
        }
    }

    /**
     * Tells whether the SRS pie chart will contain at least one slice or not.
     * @param srs the SRS data
     * @return <tt>true</tt> if at least one item exists
     */
    protected boolean hasSRSData(SRSDistribution srs) {
        return (srs.apprentice.total + srs.guru.total + srs.master.total + srs.enlighten.total) > 0;
    }

    /**
     * Creates the datasets needed by the SRS distribution pie chart
     * @param srs the SRS data 
     * @return a list of dataset (one for each SRS level)
     */
    protected List<DataSet> getSRSDataSets(SRSDistribution srs) {
        List<DataSet> ans;
        Resources res;
        DataSet ds;

        res = getResources();
        ans = new Vector<DataSet>();

        ds = new DataSet(res.getString(R.string.tag_apprentice), res.getColor(R.color.apprentice),
                srs.apprentice.total);
        ans.add(ds);

        ds = new DataSet(res.getString(R.string.tag_guru), res.getColor(R.color.guru), srs.guru.total);

        ans.add(ds);

        ds = new DataSet(res.getString(R.string.tag_master), res.getColor(R.color.master), srs.master.total);

        ans.add(ds);

        ds = new DataSet(res.getString(R.string.tag_enlightened), res.getColor(R.color.enlightened),
                srs.enlighten.total);

        ans.add(ds);

        ds = new DataSet(res.getString(R.string.tag_burned), res.getColor(R.color.burned), srs.burned.total);

        ans.add(ds);

        return ans;
    }

    /**
     * Creates the datasets needed by the kanji progression pie chart
     * @param srs the SRS data 
     * @return a list of dataset (one for each SRS level, plus burned and unlocked)
     */
    protected List<DataSet> getKanjiProgDataSets(SRSDistribution srs) {
        List<DataSet> ans;
        Resources res;
        DataSet ds;
        int locked;

        res = getResources();
        ans = new Vector<DataSet>();

        locked = ALL_THE_KANJI;

        locked -= srs.apprentice.kanji;
        ds = new DataSet(res.getString(R.string.tag_apprentice), res.getColor(R.color.apprentice),
                srs.apprentice.kanji);
        ans.add(ds);

        locked -= srs.guru.kanji;
        ds = new DataSet(res.getString(R.string.tag_guru), res.getColor(R.color.guru), srs.guru.kanji);

        ans.add(ds);

        locked -= srs.master.kanji;
        ds = new DataSet(res.getString(R.string.tag_master), res.getColor(R.color.master), srs.master.kanji);

        ans.add(ds);

        locked -= srs.enlighten.kanji;
        ds = new DataSet(res.getString(R.string.tag_enlightened), res.getColor(R.color.enlightened),
                srs.enlighten.kanji);
        ans.add(ds);

        locked -= srs.burned.kanji;
        ds = new DataSet(res.getString(R.string.tag_burned), res.getColor(R.color.burned), srs.burned.kanji);
        ans.add(ds);

        if (locked < 0)
            locked = 0;
        ds = new DataSet(res.getString(R.string.tag_locked), res.getColor(R.color.locked), locked);
        ans.add(ds);

        return ans;
    }

    /**
     * Creates the datasets needed by the kanji progression pie chart
     * @param srs the SRS data 
     * @return a list of dataset (one for each SRS level, plus burned and unlocked)
     */
    protected List<DataSet> getVocabProgDataSets(SRSDistribution srs) {
        List<DataSet> ans;
        Resources res;
        DataSet ds;
        int locked;

        res = getResources();
        ans = new Vector<DataSet>();

        locked = ALL_THE_VOCAB;

        locked -= srs.apprentice.vocabulary;
        ds = new DataSet(res.getString(R.string.tag_apprentice), res.getColor(R.color.apprentice),
                srs.apprentice.vocabulary);
        ans.add(ds);

        locked -= srs.guru.vocabulary;
        ds = new DataSet(res.getString(R.string.tag_guru), res.getColor(R.color.guru), srs.guru.vocabulary);

        ans.add(ds);

        locked -= srs.master.vocabulary;
        ds = new DataSet(res.getString(R.string.tag_master), res.getColor(R.color.master), srs.master.vocabulary);

        ans.add(ds);

        locked -= srs.enlighten.vocabulary;
        ds = new DataSet(res.getString(R.string.tag_enlightened), res.getColor(R.color.enlightened),
                srs.enlighten.vocabulary);
        ans.add(ds);

        locked -= srs.burned.vocabulary;
        ds = new DataSet(res.getString(R.string.tag_burned), res.getColor(R.color.burned), srs.burned.vocabulary);
        ans.add(ds);

        if (locked < 0)
            locked = 0;
        ds = new DataSet(res.getString(R.string.tag_locked), res.getColor(R.color.locked), locked);
        ans.add(ds);

        return ans;
    }

    /**
     * Creates the infoset needed by the any progression distribution plot
     * @param ds the input datasets 
     * @return a list of information dataset 
     */
    protected List<InfoSet> getInfoSet(List<DataSet> dses) {
        List<InfoSet> ans;
        Resources res;
        InfoSet ds;
        float count;

        res = getResources();
        ans = new Vector<InfoSet>();

        count = dses.get(1).value + /* guru */
                dses.get(2).value + /* master */
                dses.get(3).value; /* enlightened */
        if (dses.size() > 4)
            count += dses.get(4).value; /* burned */

        ds = new InfoSet(res.getString(R.string.tag_unlocked), count + dses.get(0).value);
        ans.add(ds);

        ds = new InfoSet(res.getString(R.string.tag_learned), count);
        ans.add(ds);

        return ans;
    }

    @Override
    public void spin(boolean enable) {
        View pb;

        if (parent != null) {
            pb = parent.findViewById(R.id.pb_status);
            pb.setVisibility(enable ? View.VISIBLE : View.GONE);
        }
    }

    @Override
    public void flush(Tab.RefreshType rtype, boolean fg) {
        switch (rtype) {
        case FULL_EXPLICIT:
            if (fg) {
                netwe.flush();
                for (IconizableChart chart : fcharts)
                    chart.flush();
            }
            /* Fall through */
        case FULL_IMPLICIT:
            /* Fall through */
        case MEDIUM:
            /* Fall through */
        case LIGHT:
            /* Fall through */
        }
    }

    /**
     * Shows an error message on each pie chart, when something goes wrong.
     */
    public void showAlerts() {
        PieChart pc;
        String msg;

        msg = getResources().getString(R.string.status_msg_error);
        if (parent != null) {
            pc = (PieChart) parent.findViewById(R.id.pc_srs);
            pc.alert(msg);

            pc = (PieChart) parent.findViewById(R.id.pc_kanji);
            pc.alert(msg);

            pc = (PieChart) parent.findViewById(R.id.pc_vocab);
            pc.alert(msg);
        }
    }

    @Override
    public boolean scrollLock() {
        return scrolling(false);
    }

    public boolean scrolling(boolean strict) {
        for (TYChart chart : charts)
            if (chart.scrolling(strict))
                return true;

        for (GenericChart chart : gcharts)
            if (chart.scrolling(strict))
                return true;

        if (netwe.scrolling(strict))
            return true;

        return false;
    }

    /**
     * Called when the user wants to start the reconstruct process.
     * We open the dialog and let it handle the process.
     */
    private void openReconstructDialog() {
        if (!isDetached()) {
            if (rlist != null)
                rlist.rd.cancel();
            rlist = new ReconstructListener();
        }
    }

    /**
     * Called when reconstruction is successfully completed. Refresh datasources 
     * and plots.
     * @param rlist the listener
     * @param cs the core stats
     */
    private void reconstructCompleted(ReconstructListener rlist, HistoryDatabase.CoreStats cs) {
        if (this.rlist == rlist) {

            flushDatabase();

            rlist = null;
        }
    }

    /**
     * The back button is not handled.
     *    @return false
     */
    @Override
    public boolean backButton() {
        return false;
    }

    @Override
    public boolean contains(Contents c) {
        return c == Contents.STATS;
    }

    @Override
    public void flushDatabase() {
        hdbc.flush();
        setCoreStats(cs);
    }

}