Java tutorial
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); } }