Java tutorial
package com.wanikani.androidnotifier; import java.io.IOException; import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.EnumMap; import java.util.EnumSet; import java.util.Hashtable; import java.util.List; import java.util.Map; import java.util.Vector; import android.app.Activity; import android.content.Context; import android.content.SharedPreferences; import android.content.res.Resources; import android.graphics.Typeface; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.preference.PreferenceManager; import android.support.v4.app.Fragment; import android.text.SpannableStringBuilder; import android.text.style.ForegroundColorSpan; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.BaseAdapter; import android.widget.ImageButton; import android.widget.ImageView; import android.widget.ListView; import android.widget.RadioButton; import android.widget.RadioGroup; import android.widget.TextView; import com.wanikani.androidnotifier.db.FontDatabase; import com.wanikani.androidnotifier.db.FontDatabase.FontBox; import com.wanikani.wklib.Connection; import com.wanikani.wklib.Item; import com.wanikani.wklib.Kanji; import com.wanikani.wklib.Radical; import com.wanikani.wklib.SRSLevel; import com.wanikani.wklib.Vocabulary; /* * 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/>. */ /** * This fragment shows item lists. The user can choose how to filter * and how to sort them. Sorting is done through a simple comparator; * stock comparators provided by WKLib are enough for our purposes. * Filters is slightly more complex, and we use an implementation * of a specific interface (@link Filter) for each of them. */ public class ItemsFragment extends Fragment implements Tab, Filter.Callback { /** * This enum represents the kind of additional information to display on * each item's description. Since this app should run on small handests * too, the room is not much, so the kind of string returned depends * a lot on the kind of ordering the user chose. * Note also that the string may depend on the filter, since they * use different WK APIs, returning different subsets of the data schema. */ enum ItemInfo { /** * Display days elapsed since unlock date. If the item has not * been unlocked yet (or if unlock date is not available), * it returns an empty string. */ AGE { public String getInfo(Resources res, Item i) { Date unlock; long now, age; unlock = i.getUnlockedDate(); if (unlock == null) return ""; /* Not unlocked, or not available */ now = System.currentTimeMillis(); age = now - unlock.getTime(); /* Express age in minutes */ age /= 60 * 1000; if (age < 60) return res.getString(R.string.fmt_ii_less_than_one_hour); /* Express age in hours */ age = Math.round(((float) age) / 60); if (age == 1) return res.getString(R.string.fmt_ii_one_hour); if (age < 24) return res.getString(R.string.fmt_ii_hours, age); /* Express age in days */ age = Math.round(((float) age) / 24); if (age == 1) return res.getString(R.string.fmt_ii_one_day); return res.getString(R.string.fmt_ii_days, age); } }, /** * Display time before next review. If the item has not * been unlocked yet, it is burned or if available date is not available, * it returns an empty string. */ AVAILABLE { public String getInfo(Resources res, Item i) { Date date; long now, fw, sub; date = i.getAvailableDate(); if (date == null) return ""; if (i.stats != null && i.stats.srs == SRSLevel.BURNED) return ""; now = System.currentTimeMillis(); fw = date.getTime() - now; if (fw <= 0) return res.getString(R.string.fmt_ni_now); /* Express fw in seconds */ fw /= 1000; if (fw < 60) return res.getString(R.string.fmt_ni_less_than_one_minute); /* Express fw in minutes */ fw /= 60; if (fw == 1) return res.getString(R.string.fmt_ni_one_minute); if (fw < 60) return res.getString(R.string.fmt_ni_minutes, fw); /* Express fw in hours */ sub = fw % 60; fw /= 60; if (sub < 2) { if (fw == 1) return res.getString(R.string.fmt_ni_one_hour); if (fw < 24) return res.getString(R.string.fmt_ni_hours, fw); } else { if (fw == 1) return res.getString(R.string.fmt_ni_one_hour_mins, sub); if (fw < 24) return res.getString(R.string.fmt_ni_hours_mins, fw, sub); } /* Express fw in days */ sub = fw % 24; fw /= 24; if (sub == 0) { if (fw == 1) return res.getString(R.string.fmt_ni_one_day); if (fw < 30) return res.getString(R.string.fmt_ni_days, fw); } else if (sub == 1) { if (fw == 1) return res.getString(R.string.fmt_ni_one_day_one_hour); if (fw < 30) return res.getString(R.string.fmt_ni_days_one_hour, fw); } else { if (fw == 1) return res.getString(R.string.fmt_ni_one_day_hours, sub); if (fw < 30) return res.getString(R.string.fmt_ni_days_hours, fw, sub); } sub = fw % 30; fw /= 30; if (sub == 0) { if (fw == 1) return res.getString(R.string.fmt_ni_one_month); else return res.getString(R.string.fmt_ni_months, fw); } else if (sub == 1) { if (fw == 1) return res.getString(R.string.fmt_ni_one_month_one_day); else return res.getString(R.string.fmt_ni_months_one_day, fw); } else { if (fw == 1) return res.getString(R.string.fmt_ni_one_month_days, sub); else return res.getString(R.string.fmt_ni_months_days, fw, sub); } } }, /** * Displays the percentage of correct answers. * If intermediate (meaning & reading) stats are available, they * are displayed too. */ ERRORS { public String getInfo(Resources res, Item i) { int pmean, pread; String simple; if (i.percentage < 0) return ""; simple = res.getString(R.string.fmt_ii_percent, i.percentage); if (i.stats == null || i.stats.reading == null || i.stats.meaning == null) return simple; if ((i.stats.meaning.correct + i.stats.meaning.incorrect) == 0) return simple; if ((i.stats.reading.correct + i.stats.reading.incorrect) == 0) return simple; pmean = i.stats.meaning.correct * 100 / (i.stats.meaning.correct + i.stats.meaning.incorrect); pread = i.stats.reading.correct * 100 / (i.stats.reading.correct + i.stats.reading.incorrect); return res.getString(R.string.fmt_ii_percent_full, i.percentage, pmean, pread); } }; /** * Returns a string which describes the item. * @param res the resource container * @param i the item to be described * @return a description */ public abstract String getInfo(Resources res, Item i); } /** * A simple wrapper of a level item, to be associated to the ListView. * Currently only the level number is used, so this structure is * almost pointless... */ private static class Level { /// The level number public int level; /** * Constructor * @param level the level number */ public Level(int level) { this.level = level; } } /** * This listener is registered to the level's ListView. * When an item is clicked, all the items of that level are displayed. */ class LevelClickListener implements AdapterView.OnItemClickListener { public void onItemClick(AdapterView<?> adapter, View view, int position, long id) { Level l; l = (Level) adapter.getItemAtPosition(position); setLevelFilter(l.level); } } /** * The implementation of the levels' ViewList. Pretty straightforward. * No sorting or filtering. * Levels are instances of the {@link Level} class (though I guess * Integers could have been used as well. */ class LevelListAdapter extends BaseAdapter { /// The current level set List<Level> levels; /// A position to view dictionary Map<Integer, View> l2v; /** * Constructor. */ public LevelListAdapter() { levels = new Vector<Level>(); l2v = new Hashtable<Integer, View>(); } @Override public int getCount() { return levels.size(); } @Override public Level getItem(int position) { return levels.get(position); } /** * Deletes all the items in the list */ public void clear() { levels = new Vector<Level>(); notifyDataSetChanged(); } /** * Replaces the items in the list with a new set. * @param levels the new list * @return if the new levels are different from the old ones */ public boolean replace(List<Level> levels) { if (this.levels.size() == levels.size()) return false; clear(); this.levels.addAll(levels); notifyDataSetChanged(); return true; } @Override public long getItemId(int position) { return position; } @Override public View getView(int position, View row, ViewGroup parent) { LayoutInflater inflater; TextView view; Level l; l = getItem(position); inflater = main.getLayoutInflater(); row = inflater.inflate(R.layout.items_level, parent, false); view = (TextView) row.findViewById(R.id.tgr_level); view.setText(Integer.toString(l.level)); row.setTag(l); l2v.put(l.level, row); levelAdded(getItem(position).level, row); return row; } /** * Return the view associated to a given level * @param level the level * @return a view, or <tt>null</tt> if no view is (currently) associated */ public View getViewByLevel(int level) { View view; Level l; view = l2v.get(level); if (view != null) { l = (Level) view.getTag(); if (l.level != level) view = null; } return view; } } /** * The item list holder. Keeps all the references to relevant views and * takes care of updating the fields of each row. It is meant to be set * as a tag (@see {@link View#setTag(Object)}, according to the holder pattern, * though this is not done directly by this class. */ private abstract class ItemListHolder { /// The item type public Item.Type type; /// The row view, parent of each field public View row; /// Glyph, as a text public TextView glyphText; /// Reading public TextView reading; /// SRS image public ImageView srs; /// Additional (filter/order dependent) information public TextView info; /// Meanining public TextView meaning; /// Level public TextView level; /// The scroll view, needed fro scroll interaction public HiPriorityScrollView hpsw; /// The click listener associated to the row public ItemClickListener icl; // --- Now stuff that gets changed while scrolling the list -- // /// The current item Item item; /// The item information ItemInfo iinfo; /** * Constructor * @param ila the list adapter that will receive high priority scroll view * events * @param inflater the inflater * @param parent scroll view * @param type the item type represented by this holder */ public ItemListHolder(ItemListAdapter ila, LayoutInflater inflater, ViewGroup parent, Item.Type type) { this.type = type; row = inflater.inflate(getLayout(), parent, false); glyphText = (TextView) row.findViewById(R.id.it_glyph); reading = (TextView) row.findViewById(R.id.it_reading); srs = (ImageView) row.findViewById(R.id.img_srs); info = (TextView) row.findViewById(R.id.it_info); meaning = (TextView) row.findViewById(R.id.it_meaning); hpsw = (HiPriorityScrollView) row.findViewById(R.id.hsv_item); level = (TextView) row.findViewById(R.id.it_level); icl = new ItemClickListener(ila); hpsw.setCallback(icl); row.setOnClickListener(icl); if (jtf != null) glyphText.setTypeface(jtf); } /** * Returns the resource ID of the layout representing this object * @return the layout id */ protected abstract int getLayout(); /** * Updates the row fields. * @param res the resources * @param item the item * @param iinfo additional (filter/sort order dependent) information */ public void fill(Resources res, Item item, ItemInfo iinfo) { this.item = item; this.iinfo = iinfo; refresh(res); } /** * Updates the row fields, using cached data. * @param res the resources */ public void refresh(Resources res) { SpannableStringBuilder sb; String us[]; int i; if (filterType != FilterType.LEVEL) level.setText(Integer.toString(item.level)); if (item.stats != null) { srs.setImageDrawable(srsht.get(item.stats.srs)); srs.setVisibility(View.VISIBLE); } else srs.setVisibility(View.INVISIBLE); info.setText(iinfo.getInfo(res, item)); if (showAnswers) { sb = new SpannableStringBuilder(); if (item.stats != null && item.stats.userSynonyms != null) { us = item.stats.userSynonyms; for (i = 0; i < us.length; i++) sb.append(us[i]).append(", "); sb.setSpan(new ForegroundColorSpan(importantColor), 0, sb.length(), 0); } sb.append(item.meaning); meaning.setText(sb); } else meaning.setText(""); icl.setURL(item.getURL(tls)); if (jtf != null) glyphText.setTypeface(jtf); } } /** * Implementation of the holder for radical item rows. */ private class RadicalHolder extends ItemListHolder { /// The glyph, as an image ImageView glyphImage; /// Glyph view View glyphView; /** * Constructor. * @param ila the list adapter that will receive high priority scroll view * events * @param inflater the inflater * @param parent scroll view */ public RadicalHolder(ItemListAdapter ila, LayoutInflater inflater, ViewGroup parent) { super(ila, inflater, parent, Item.Type.RADICAL); glyphImage = (ImageView) row.findViewById(R.id.img_glyph); glyphView = row.findViewById(R.id.f_glyph); } @Override protected int getLayout() { return R.layout.items_radical; } @Override public void refresh(Resources res) { Radical radical; radical = (Radical) item; super.refresh(res); if (radical.character != null) { glyphText.setText(radical.character); glyphText.setVisibility(View.VISIBLE); glyphView.setVisibility(View.GONE); } else { try { glyphImage.setImageBitmap(rimg.getImage(getActivity(), radical)); glyphText.setVisibility(View.GONE); glyphView.setVisibility(View.VISIBLE); } catch (IOException e) { /* Should not happen */ } } } } /** * Implementation of the holder for kanji item rows. */ private class KanjiHolder extends ItemListHolder { public TextView onyomi; public TextView kunyomi; public TextView nanori; /** * Constructor. * @param ila the list adapter that will receive high priority scroll view * events * @param inflater the inflater * @param parent scroll view */ public KanjiHolder(ItemListAdapter ila, LayoutInflater inflater, ViewGroup parent) { super(ila, inflater, parent, Item.Type.KANJI); onyomi = (TextView) row.findViewById(R.id.it_onyomi); kunyomi = (TextView) row.findViewById(R.id.it_kunyomi); nanori = (TextView) row.findViewById(R.id.it_nanori); } @Override protected int getLayout() { return R.layout.items_kanji; } @Override public void refresh(Resources res) { Kanji kanji; kanji = (Kanji) item; super.refresh(res); onyomi.setText(showAnswers ? kanji.onyomi : ""); kunyomi.setText(showAnswers ? kanji.kunyomi : ""); if (kanji.nanori != null && !kanji.nanori.equals("")) nanori.setText(showAnswers ? "[" + kanji.nanori + "]" : ""); else nanori.setText(""); onyomi.setTextColor(normalColor); kunyomi.setTextColor(normalColor); nanori.setTextColor(normalColor); switch (kanji.importantReading) { case ONYOMI: onyomi.setTextColor(importantColor); break; case KUNYOMI: kunyomi.setTextColor(importantColor); break; case NANORI: nanori.setTextColor(importantColor); break; } glyphText.setText(kanji.character); } } /** * Implementation of the holder for vocab item rows. */ private class VocabHolder extends ItemListHolder { /** * Constructor. * @param ila the list adapter that will receive high priority scroll view * events * @param inflater the inflater * @param parent scroll view */ public VocabHolder(ItemListAdapter ila, LayoutInflater inflater, ViewGroup parent) { super(ila, inflater, parent, Item.Type.VOCABULARY); } @Override protected int getLayout() { return R.layout.items_vocab; } @Override public void refresh(Resources res) { Vocabulary vocab; vocab = (Vocabulary) item; super.refresh(res); reading.setText(showAnswers ? vocab.kana : ""); glyphText.setText(vocab.character); } } /** * The implementation of the items' ViewList. Items are instances * of the WKLib {@link Item} class. This class implements sorting * and filtering through {@link ItemSearchDialog}. */ class ItemListAdapter extends BaseAdapter implements ItemSearchDialog.Listener { /// The full (unfiltered) list of items. List<Item> allItems; /// The current list of items. It is always sorted. List<Item> filteredItems; /// The current comparator Comparator<Item> cmp; /// What to put into the "extra info" textview ItemInfo iinfo; /// If set, tabs should be locked, because the user is swiping /// a row larger than screen size boolean lock; /** * Constructor * @param cmp the comparator * @param iinfo what to put into the "extra info" textview */ public ItemListAdapter(Comparator<Item> cmp, ItemInfo iinfo) { this.cmp = cmp; this.iinfo = iinfo; allItems = new Vector<Item>(); filteredItems = new Vector<Item>(); } /** * Changes the comparator. Ordinarily the extra info is strictly * bound to the comparator, so we give a chance to update it as well. * @param cmp the comparator * @param iinfo what to put into the "extra info" textview */ public void setComparator(Comparator<Item> cmp, ItemInfo iinfo) { this.cmp = cmp; this.iinfo = iinfo; invalidate(); } @Override public void filterChanged() { invalidate(); } @Override public int getCount() { return filteredItems.size(); } @Override public Item getItem(int position) { return filteredItems.get(position); } @Override public long getItemId(int position) { return position; } /** * Empties the list */ public void clear() { allItems.clear(); filteredItems.clear(); notifyDataSetChanged(); } /** * Appends the items to the list. Afterward the list is sorted again. * @param newItems the additional items to show */ public void addAll(List<Item> newItems) { allItems.addAll(newItems); invalidate(); } /** * Sorts the collection again, also refreshing the list. */ private void invalidate() { filteredItems = isd != null ? isd.filter(allItems) : allItems; Collections.sort(filteredItems, cmp); notifyDataSetChanged(); } @Override public View getView(int position, View row, ViewGroup parent) { LayoutInflater inflater; ItemListHolder holder; Item item; item = getItem(position); holder = row != null ? (ItemListHolder) row.getTag() : null; if (holder == null || holder.type != item.type) { inflater = main.getLayoutInflater(); switch (item.type) { case RADICAL: holder = new RadicalHolder(this, inflater, parent); break; case KANJI: holder = new KanjiHolder(this, inflater, parent); break; case VOCABULARY: holder = new VocabHolder(this, inflater, parent); break; } holder.row.setTag(holder); } holder.fill(getResources(), item, iinfo); return holder.row; } } /** * The listener registered to the filter and sort buttons. * It shows or hide the menu, according to the well-known * menu pattern. */ class MenuPopupListener implements View.OnClickListener { public void onClick(View view) { boolean filterV, sortV; View filterW, sortW; filterW = parent.findViewById(R.id.menu_filter); filterV = filterW.getVisibility() == View.VISIBLE; sortW = parent.findViewById(R.id.menu_order); sortV = sortW.getVisibility() == View.VISIBLE; filterW.setVisibility(View.GONE); sortW.setVisibility(View.GONE); switch (view.getId()) { case R.id.btn_item_filter: if (!filterV) filterW.setVisibility(View.VISIBLE); break; case R.id.btn_item_sort: if (!sortV) sortW.setVisibility(View.VISIBLE); break; case R.id.btn_item_view: toggleShowAnswers(); break; case R.id.btn_item_search: if (isd != null) isd.toggleVisibility(); } } } /** * The task that periodically refreshes the contents of the list view. */ class RefreshTask implements Runnable { @Override public void run() { ItemListHolder holder; Resources res; View row; int i; res = getResources(); if (iview != null) { for (i = iview.getChildCount() - 1; i >= 0; i--) { row = iview.getChildAt(i); holder = (ItemListHolder) row.getTag(); if (holder != null) holder.refresh(res); } } scheduleRefresh(); } } public enum FilterType { NONE { @Override public int getId() { return R.id.btn_filter_none; } @Override public SortOrder getDefaultOrder() { return SortOrder.TYPE; } }, LEVEL { @Override public int getId() { return R.id.btn_filter_by_level; } @Override public SortOrder getDefaultOrder() { return SortOrder.TYPE; } }, TOXIC { @Override public int getId() { return R.id.btn_filter_toxic; } @Override public SortOrder getDefaultOrder() { return SortOrder.TOXICITY; } }, CRITICAL { @Override public int getId() { return R.id.btn_filter_critical; } @Override public SortOrder getDefaultOrder() { return SortOrder.ERRORS; } }, UNLOCKS { @Override public int getId() { return R.id.btn_filter_unlocks; } @Override public SortOrder getDefaultOrder() { return SortOrder.TIME; } }; public abstract int getId(); public abstract SortOrder getDefaultOrder(); } enum SortOrder { SRS { @Override public int getId() { return R.id.btn_sort_srs; } @Override public Comparator<Item> getComparator() { return Item.SortBySRS.INSTANCE; } @Override public ItemInfo getItemInfo() { return ItemInfo.AVAILABLE; } }, TIME { @Override public int getId() { return R.id.btn_sort_time; } @Override public Comparator<Item> getComparator() { return Item.SortByTime.INSTANCE; } @Override public ItemInfo getItemInfo() { return ItemInfo.AGE; } }, AVAILABLE { @Override public int getId() { return R.id.btn_sort_available; } @Override public Comparator<Item> getComparator() { return Item.SortByAvailable.INSTANCE; } @Override public ItemInfo getItemInfo() { return ItemInfo.AVAILABLE; } }, TOXICITY { @Override public int getId() { return R.id.btn_sort_toxicity; } @Override public Comparator<Item> getComparator() { return Item.SortByToxicity.INSTANCE; } @Override public ItemInfo getItemInfo() { return ItemInfo.ERRORS; } }, ERRORS { @Override public int getId() { return R.id.btn_sort_errors; } @Override public Comparator<Item> getComparator() { return Item.SortByErrors.INSTANCE; } @Override public ItemInfo getItemInfo() { return ItemInfo.ERRORS; } }, TYPE { @Override public int getId() { return R.id.btn_sort_type; } @Override public Comparator<Item> getComparator() { return Item.SortByType.INSTANCE; } @Override public ItemInfo getItemInfo() { return ItemInfo.AVAILABLE; } }; public abstract int getId(); public abstract Comparator<Item> getComparator(); public abstract ItemInfo getItemInfo(); public void apply(View parent, ItemListAdapter iad) { RadioGroup rg; rg = (RadioGroup) parent.findViewById(R.id.rg_order); rg.check(getId()); iad.setComparator(getComparator(), getItemInfo()); } } /** * The listener registered to the filter/sort radio group buttons. * When an item is clicked, it updates the list accordingly. */ class RadioGroupListener implements View.OnClickListener { public void onClick(View view) { View filterW, sortW; int id; filterW = parent.findViewById(R.id.menu_filter); sortW = parent.findViewById(R.id.menu_order); filterW.setVisibility(View.GONE); sortW.setVisibility(View.GONE); id = view.getId(); for (FilterType ft : EnumSet.allOf(FilterType.class)) if (id == ft.getId()) { setFilter(ft); break; } for (SortOrder so : EnumSet.allOf(SortOrder.class)) if (id == so.getId()) { setOrder(so); break; } } } /** * The listener registered to each item's hyperlink. It opens * the page, through @link {@link MainActivity#item()}, so * it uses the internal browser if the user chose so. * It is both a {@link HiPriorityScrollView#Callback}, to intercept * clicks on the scroll view, and a {@link View#OnClickListener}, * to intercept them when they fall out of the scroll view. * It also handles lock and unlock events. */ class ItemClickListener implements HiPriorityScrollView.Callback, View.OnClickListener { /// The list adapter, that will receive lock and unlock events ItemListAdapter ila; /// The URL to open private String url; /** * Constructor. * @param ila the item list adapter */ public ItemClickListener(ItemListAdapter ila) { this.ila = ila; } /** * Updates the URL to open * @param url the new URL to open */ public void setURL(String url) { this.url = url; } @Override public void onClick(View view) { if (url != null) main.item(url); } /** * Called when a motion on an item starts. If the item is actually * larger than the screen, we lock the tabs * @param hpsw the item's scroll view * @param childIsLarger set if the item is actually too large */ @Override public void down(HiPriorityScrollView hpsw, boolean childIsLarger) { ila.lock = childIsLarger; } /** * Called when a motion on an item stops. We unlock. * @param hpsw the item's scroll view * @param childIsLarger set if the item is actually too large */ @Override public void up(HiPriorityScrollView hpsw, boolean childIsLarger) { ila.lock = false; onClick(hpsw); } /** * Called when a motion on an item stops. We unlock. * @param hpsw the item's scroll view * @param childIsLarger set if the item is actually too large */ @Override public void cancel(HiPriorityScrollView hpsw, boolean childIsLarger) { ila.lock = false; } } /// The show all key private static final String KEY_SHOW_ANSWERS = "SHOW_ANSWERS"; /// The main activity MainActivity main; /// The root view of the fragment View parent; /// The periodical refresh alarm Alarm alarm; /// The refresh task RefreshTask refreshTask; /// The refresh period (must be one minute) private static final int REFRESH_DELAY = 60 * 1000; /* ---------- Levels stuff ---------- */ /// The list adapter of the levels' list LevelListAdapter lad; /// The levels' list view ListView lview; /// The levels' list click listener LevelClickListener lcl; /// The last level clicked so far int currentLevel; /// The number of levels int levels; /// Normal reading color int normalColor; /// Important reading color int importantColor; /// Selected level color int selectedColor; /// Unselected level color int unselectedColor; /* ---------- Items stuff ---------- */ /// The list adapter of the items' list ItemListAdapter iad; /// The items' list view ListView iview; /// The radical images cache RadicalImages rimg; /* ---------- Sort/filter stuff ---------- */ /// The popup menu listener MenuPopupListener mpl; /// The menu buttons' listener RadioGroupListener rgl; /// True if the "other filters" button is spinning boolean spinning; /// A SRS level to turtle icon map EnumMap<SRSLevel, Drawable> srsht; /// Show sensitive information that may break SRS boolean showAnswers; /// Item Search State ItemSearchDialog.State iss; /// Item Search dialog (null when detached) ItemSearchDialog isd; /* ---------- Filters ---------- */ /// The japanese typeface set private FontBox fbox; /// The current japanese typeface private Typeface jtf; /// Need to restart refresh private boolean resumeRefresh; /// Shall we use TLS? private boolean tls; /// Current Sort Order private SortOrder order; /// Current filter private FilterType filterType; /// Filter enum to implementation mapping private EnumMap<FilterType, Filter> fmap; private static final String PREFIX = "com.wanikani.androidnotifier.ItemsFragment."; private static final String PREF_ORDER = PREFIX + "order."; public ItemsFragment() { rimg = new RadicalImages(); alarm = new Alarm(); refreshTask = new RefreshTask(); order = SortOrder.TYPE; currentLevel = -1; } @Override public void onAttach(Activity main) { super.onAttach(main); this.main = (MainActivity) main; this.main.register(this); } /** * Creation of the fragment. We build up all the singletons, leaving the * bundle alone, because we set the retain instance flag to <code>true</code>. * @param bundle the saved instance state */ @Override public void onCreate(Bundle bundle) { Resources res; super.onCreate(bundle); setRetainInstance(true); fmap = new EnumMap<FilterType, Filter>(FilterType.class); fmap.put(FilterType.NONE, new NoFilter(this)); fmap.put(FilterType.LEVEL, new LevelFilter(this)); fmap.put(FilterType.CRITICAL, new CriticalFilter(this)); fmap.put(FilterType.TOXIC, new ToxicFilter(this)); fmap.put(FilterType.UNLOCKS, new UnlockFilter(this)); filterType = FilterType.LEVEL; lcl = new LevelClickListener(); mpl = new MenuPopupListener(); rgl = new RadioGroupListener(); res = getResources(); srsht = new EnumMap<SRSLevel, Drawable>(SRSLevel.class); srsht.put(SRSLevel.APPRENTICE, res.getDrawable(R.drawable.apprentice)); srsht.put(SRSLevel.GURU, res.getDrawable(R.drawable.guru)); srsht.put(SRSLevel.MASTER, res.getDrawable(R.drawable.master)); srsht.put(SRSLevel.ENLIGHTEN, res.getDrawable(R.drawable.enlighten)); srsht.put(SRSLevel.BURNED, res.getDrawable(R.drawable.burned)); normalColor = res.getColor(R.color.normal); importantColor = res.getColor(R.color.important); selectedColor = res.getColor(R.color.selected); unselectedColor = res.getColor(R.color.unselected); lad = new LevelListAdapter(); iad = new ItemListAdapter(Item.SortByType.INSTANCE, ItemInfo.AVAILABLE); iss = new ItemSearchDialog.State(); } /** * Builds the GUI and create the default item listing. Which is * by item type, sorted by time. * @param inflater the inflater * @param container the parent view * @param savedInstance an (unused) bundle */ @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle) { RadioGroup rg; ImageButton btn; View sview; int i; super.onCreateView(inflater, container, bundle); scheduleRefresh(); parent = inflater.inflate(R.layout.items, container, false); lview = (ListView) parent.findViewById(R.id.lv_levels); lview.setAdapter(lad); lview.setOnItemClickListener(lcl); iview = (ListView) parent.findViewById(R.id.lv_items); iview.setAdapter(iad); sview = parent.findViewById(R.id.it_search_win); isd = new ItemSearchDialog(sview, iss, fmap.get(filterType), iad); btn = (ImageButton) parent.findViewById(R.id.btn_item_filter); btn.setOnClickListener(mpl); btn = (ImageButton) parent.findViewById(R.id.btn_item_sort); btn.setOnClickListener(mpl); btn = (ImageButton) parent.findViewById(R.id.btn_item_view); btn.setOnClickListener(mpl); btn = (ImageButton) parent.findViewById(R.id.btn_item_search); btn.setOnClickListener(mpl); rg = (RadioGroup) parent.findViewById(R.id.rg_filter); for (i = 0; i < rg.getChildCount(); i++) rg.getChildAt(i).setOnClickListener(rgl); rg.check(R.id.btn_filter_none); rg = (RadioGroup) parent.findViewById(R.id.rg_order); for (i = 0; i < rg.getChildCount(); i++) rg.getChildAt(i).setOnClickListener(rgl); rg.check(R.id.btn_sort_type); enableSorting(true, true, true, true); return parent; } /** * Called when the app is resumed. We need to (re?)build the list views. */ public void onResume() { super.onResume(); SharedPreferences prefs; prefs = SettingsActivity.prefs(getActivity()); tls = SettingsActivity.getTLS(prefs); showAnswers = prefs.getBoolean(KEY_SHOW_ANSWERS, true); fbox = FontDatabase.getFontBox(getActivity()); jtf = fbox.nextFont(); alarm.screenOn(); /* Make sure that refreshCompleted has been called at least once */ if (levels > 0) redrawAll(); } /** * Redraws the entire GUI. Called when resuming, to build the new view. */ private void redrawAll() { boolean refresh; List<Level> l; Level level; int i; /* Very first resume. Need to set current level == to the user's level */ if (currentLevel < 0) currentLevel = levels; l = new Vector<Level>(levels); for (i = levels; i > 0; i--) { level = new Level(i); l.add(level); } refresh = lad.replace(l); if (refresh) lad.notifyDataSetChanged(); refresh |= iad.isEmpty(); refresh |= resumeRefresh; if (refresh) { resumeRefresh = false; setFilter(filterType); } } /** * Called when the internal refresh task must be rescheduled. */ protected void scheduleRefresh() { alarm.schedule(refreshTask, REFRESH_DELAY); } /** * Called when data has been refreshed. Actually the only field we * are intested in is the user's level, so we store it even if it * the view has not been created yet. */ public void refreshComplete(DashboardData dd) { levels = dd.level; if (currentLevel < 0 && isResumed()) redrawAll(); } /** * Called when the view is destroyed. We take this chance to stop all * the threads that may attempt to update a dead view. */ @Override public void onDestroyView() { super.onDetach(); alarm.cancel(); resumeRefresh = fmap.get(filterType).stopTask(); for (Filter f : fmap.values()) f.stopTask(); isd = null; } public void setOrder(SortOrder order) { SharedPreferences prefs; this.order = order; order.apply(parent, iad); try { prefs = PreferenceManager.getDefaultSharedPreferences(getActivity()); prefs.edit().putString(PREF_ORDER + filterType.name(), order.name()).commit(); } catch (Throwable t) { /* Should not happen, however this is just a secondary feature ... */ } } public void setLevelFilter(int level) { setFilter(FilterType.LEVEL, level); } public void setFilter(FilterType filter) { setFilter(filter, currentLevel); } public void setFilter(FilterType filter, int level) { SharedPreferences prefs; RadioButton btn; String s; filterType = filter; try { prefs = PreferenceManager.getDefaultSharedPreferences(getActivity()); s = prefs.getString(PREF_ORDER + filterType.name(), filterType.getDefaultOrder().name()); order = SortOrder.valueOf(s); } catch (Throwable t) { order = filterType.getDefaultOrder(); } if (parent != null) { order.apply(parent, iad); btn = (RadioButton) parent.findViewById(filter.getId()); btn.setChecked(true); fmap.get(filter).select(meter(), main.getConnection(), level); iview.setSelection(0); } filterChanged(); } /** * Called when the filter changes */ protected void filterChanged() { if (isd != null) isd.itemFilterChanged(fmap.get(filterType)); } /** * Toggle show answer flag */ protected void toggleShowAnswers() { SharedPreferences prefs; Context ctxt; ctxt = getActivity(); if (ctxt == null) return; showAnswers ^= true; prefs = SettingsActivity.prefs(ctxt); prefs.edit().putBoolean(KEY_SHOW_ANSWERS, showAnswers).commit(); if (iad != null) iad.filterChanged(); } /** * Replaces the contents of the items' list view contents with a new list. * @param sfilter the source filter * @param list the new list * @param ok if this is the complete list or something went wrong while * loading data */ @Override public void setData(Filter sfilter, List<Item> list, boolean ok) { if (sfilter != fmap.get(filterType)) return; if (fbox != null) jtf = fbox.nextFont(); iad.clear(); iad.addAll(list); iad.notifyDataSetChanged(); alert(ok); } /** * Updates the contents of the items' list view contents, by adding * new items. * @param sfilter the source filter * @param list the new list * @param ok if this is the complete list or something went wrong while * loading data */ @Override public void addData(Filter sfilter, List<Item> list) { if (sfilter != fmap.get(filterType)) return; iad.addAll(list); iad.notifyDataSetChanged(); } @Override public void clearData(Filter sfilter) { if (sfilter != fmap.get(filterType)) return; if (fbox != null) jtf = fbox.nextFont(); iad.clear(); iad.notifyDataSetChanged(); } /** * Called at the end of data retrieval. It shows or hides the alert info. * @param sfilter the filter which is publishing these items * @param ok tells if this is the complete list or something went wrong while * loading data */ @Override public void noMoreData(Filter sfilter, boolean ok) { if (sfilter != fmap.get(filterType)) return; alert(ok); } @Override public void loadRadicalImage(Radical r) throws IOException { rimg.getImage(getActivity(), r); } /** * Displays or hides an alert message. * @param ok if true, data retrieval was ok, otherwise is partial */ protected void alert(boolean ok) { TextView message; View panel; panel = parent.findViewById(R.id.it_lay_alert); if (!ok) { panel.setVisibility(View.VISIBLE); message = (TextView) parent.findViewById(R.id.it_alert); message.setText(getResources().getString(R.string.status_msg_partial)); } else panel.setVisibility(View.GONE); } /** * Show or hide the dashboard data spinner. * Needed to implement the @param Tab interface, but we actually ignore this. * @param enable true if should be shown */ public void spin(boolean enable) { /* empty */ } @Override public void selectLevel(Filter filter, int level, boolean spinning) { if (filter != fmap.get(filterType)) return; if (currentLevel != level && currentLevel > 0) selectLevel(currentLevel, false, false); currentLevel = level; this.spinning = spinning; selectLevel(level, true, spinning); } /** * Internal implementation of the {@link #selectLevel(Filter, int, boolean)} * logic. This gets called when it is certain that we are being called * by the correct filter, or no filter is involved, so checks are skipped. * In addition, this method may be called also to <i>unselect</i> a level. * @param level the level on which to act * @param select <code>true</code> if the level should be selected. * Otherwise it is unselected * @param spin <code>true</code> if the spinner should be displayed. * Otherwise it is hidden. Makes sense only if <code>select</code> * is <code>true</code> too */ protected void selectLevel(int level, boolean select, boolean spin) { View row; row = lad.getViewByLevel(level); selectLevel(row, select, spin); } /** * Internal implementation of the {@link #selectLevel(Filter, int, boolean)} * logic. This gets called when it is certain that we are being called * by the correct filter, or no filter is involved, so checks are skipped. * In addition, this method may be called also to <i>unselect</i> a level. * @param row the level row on which to act. This parameter may be <code>null</code>, * if the label is not visible. Calling the method is still important, to * show the levels list, if hidden * @param select <code>true</code> if the level should be selected. * Otherwise it is unselected * @param spin <code>true</code> if the spinner should be displayed. * Otherwise it is hidden. Makes sense only if <code>select</code> * is <code>true</code> too */ private void selectLevel(View row, boolean select, boolean spin) { TextView tw; View sw; selectOtherFilter(false, false); if (row == null) return; tw = (TextView) row.findViewById(R.id.tgr_level); sw = row.findViewById(R.id.pb_level); if (spin) { tw.setVisibility(View.GONE); sw.setVisibility(View.VISIBLE); } else { tw.setVisibility(View.VISIBLE); sw.setVisibility(View.GONE); } if (select) { tw.setTextColor(selectedColor); tw.setTypeface(null, Typeface.BOLD); } else { tw.setTextColor(unselectedColor); tw.setTypeface(null, Typeface.NORMAL); } } @Override public void selectOtherFilter(Filter filter, boolean spinning) { if (filter != fmap.get(filterType)) return; selectOtherFilter(true, spinning); } /** * Internal implementation of the {@link #selectOtherFilter(Filter, boolean)} * logic. This gets called when it is certain that we are being called * by the correct filter, or no filter is involved, so checks are skipped. * In addition, this method may be called also to <i>exit</i> filter mode. * When entering filter mode, the level list is hidden. * @param select <code>true</code> if the level should be selected. * Otherwise it is unselected * @param spin <code>true</code> if the spinner should be displayed. * Otherwise it is hidden. Makes sense only if <code>select</code> * is <code>true</code> too */ public void selectOtherFilter(boolean selected, boolean spinning) { View filterPB, levels, filler; filterPB = parent.findViewById(R.id.pb_item_filter); levels = parent.findViewById(R.id.lv_levels); filler = parent.findViewById(R.id.lv_filler); if (spinning) filterPB.setVisibility(View.VISIBLE); else filterPB.setVisibility(View.GONE); if (selected) { levels.setVisibility(View.GONE); filler.setVisibility(View.VISIBLE); } else { levels.setVisibility(View.VISIBLE); filler.setVisibility(View.GONE); } } @Override public void enableSorting(boolean errors, boolean unlock, boolean available, boolean mistakes) { View view; view = parent.findViewById(R.id.btn_sort_errors); view.setEnabled(errors); view = parent.findViewById(R.id.btn_sort_time); view.setEnabled(unlock); view = parent.findViewById(R.id.btn_sort_available); view.setEnabled(available); view = parent.findViewById(R.id.btn_sort_toxicity); view.setEnabled(mistakes); } /** * This methods is called by the levels' list adapter, right before * displaying a new row. It is needed to make sure that the spinner * is displayed on the newly created row if it should. * @param level the level being created * @param row the row being created */ public void levelAdded(int level, View row) { if (currentLevel == level) selectLevel(row, true, spinning); else selectLevel(row, false, false); } /** * Returns the tab name ID. * @param the <code>tag_items</code> ID */ public int getName() { return R.string.tag_items; } /** * Clears the cache and redisplays data. This may be called quite early, * so we check the null pointers. */ @Override public void flush(Tab.RefreshType rtype, boolean fg) { /* Might be called really early! */ if (fmap == null) return; switch (rtype) { case LIGHT: break; case FULL_IMPLICIT: case FULL_EXPLICIT: flush(FilterType.NONE); flush(FilterType.LEVEL); /* Fall through */ case MEDIUM: flush(FilterType.CRITICAL); flush(FilterType.TOXIC); flush(FilterType.UNLOCKS); } } private void flush(FilterType ftype) { Filter f; f = fmap.get(ftype); f.flush(); if (ftype == filterType && (currentLevel > 0 || ftype != FilterType.LEVEL)) f.select(meter(), main.getConnection(), currentLevel); } /** * Tells if we are interested in scroll events. We do, if the user is swiping * an item list * @return true if the user is swiping */ public boolean scrollLock() { return iad != null && iad.lock; } /** * Shows the search dialog. SRS filtering is enabled if supported by the current filter. * @param switching <tt>true</tt> if the activity was in background * @param level the level to be selected (or <tt>null</tt> if no SRS filter should be set) * @param type the item type to be selected (or <tt>null</tt> if all types should * be shown). */ public void showSearchDialog(boolean switching, SRSLevel level, Item.Type type) { showSearchDialog(switching, level, type, true, false); } /** * Shows the search dialog. * @param switching <tt>true</tt> if the activity was in background * @param level the level to be selected (or <tt>null</tt> if no SRS filter should be set) * @param type the item type to be selected (or <tt>null</tt> if all types should * be shown). * @param srs if srs filtering shall be enabled * @param invert inverts the srs filter (i.e. show all the objects not at the specified SRS level) */ public void showSearchDialog(boolean switching, SRSLevel level, Item.Type type, boolean srs, boolean invert) { if (level != null) iss.set(level, invert); if (type != null) iss.set(type); isd.show(!switching); isd.setSRSVisibility(srs); } /** * Hides the search dialog. */ public void hideSearchDialog() { isd.setVisibility(false); } /** * The back button is handled only if the search dialog is shown. * In that case, it is hidden * @return false */ @Override public boolean backButton() { return isd.setVisibility(false); } @Override public boolean contains(Contents c) { return c == Contents.ITEMS; } protected Connection.Meter meter() { return MeterSpec.T.ITEMS.get(main); } @Override public void flushDatabase() { /* empty */ } }