com.wanikani.androidnotifier.DashboardFragment.java Source code

Java tutorial

Introduction

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

Source

package com.wanikani.androidnotifier;

import java.util.Date;
import java.util.List;
import java.util.Vector;

import android.app.Activity;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.text.Html;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.TextView;

import com.wanikani.androidnotifier.graph.ProgressChart;
import com.wanikani.androidnotifier.graph.ProgressPlot;
import com.wanikani.androidnotifier.graph.Pager.Marker;
import com.wanikani.androidnotifier.graph.ProgressChart.SubPlot;
import com.wanikani.androidnotifier.graph.ProgressPlot.DataSet;
import com.wanikani.wklib.Item;
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/>.
 */

/**
 * The home fragment, which is displayed on application launch.  We
 * display a simple GUI that show some stats, and allows to perform reviews
 * or study lessons, if some is pending.
 * <p> 
 * The stats are refreshed automatically, after a configurable timeout.  
 * This value should be kept quite large to avoid needless traffic and 
 * power consumption; if the user really needs to update the stats, we 
 * provide also a "refresh" menu.
 */
public class DashboardFragment extends Fragment implements Tab {

    /**
     * A listener that intercepts review button clicks.
     * It simply informs the main activity, which will choose what
     * to do next
     */
    private class ReviewClickListener implements View.OnClickListener {

        /**
         * Called when the button is clicked.
         * @param v the button 
         */
        @Override
        public void onClick(View v) {
            main.review();
        }
    }

    /**
     * A listener that intercepts lesson button clicks.
     * It simply informs the main activity, which will choose what
     * to do next
     */
    private class LessonsClickListener implements View.OnClickListener {

        /**
         * Called when the button is clicked.
         * @param v the button 
         */
        @Override
        public void onClick(View v) {
            main.lessons();
        }
    }

    /**
     * Listener to clicks on the forum link pages.
     */
    private class ChatClickListener implements View.OnClickListener {

        /**
         * Called when the button is clicked.
         * @param v the button 
         */
        @Override
        public void onClick(View v) {
            main.chat();
        }
    }

    /**
     * Listener to clicks on the review summary link pages.
     */
    private class ReviewSummaryClickListener implements View.OnClickListener {

        /**
         * Called when the button is clicked.
         * @param v the button 
         */
        @Override
        public void onClick(View v) {
            main.reviewSummary();
        }
    }

    /**
     * Listener for clicks on total items link. Causes the pager to 
     * switch to the item tab, and sets the item type filter.
     */
    private class TotalClickListener implements View.OnClickListener {

        /// The item type (typically radicals or kanji)
        private Item.Type type;

        /**
         * Constructor
         * @param type the item type (typically radicals or kanji) 
         */
        public TotalClickListener(Item.Type type) {
            this.type = type;
        }

        @Override
        public void onClick(View v) {
            main.showTotal(type);
        }
    }

    private static class ProgressionClickListener implements View.OnClickListener {

        /// Main activity
        private MainActivity main;

        /// The item type (typically radicals or kanji)
        private Item.Type type;

        /// The type of data to show      
        private ProgressionData pdata;

        public ProgressionClickListener(MainActivity main, Item.Type type, ProgressionData pdata) {
            this.main = main;
            this.type = type;
            this.pdata = pdata;
        }

        @Override
        public void onClick(View v) {
            pdata.show(main, type);
        }
    }

    private enum ProgressionData {

        LOCKED {
            @Override
            public String getDescription(Resources res) {
                return null; /* This means it won't be displayed */
            }

            @Override
            public int getColor() {
                return R.color.remaining;
            }

            @Override
            public int getValue(int apprentice, int guru, int total) {
                return total - guru - apprentice;
            }

            @Override
            public void show(MainActivity main, Item.Type type) {
                /* ignore, can't happen */
            }
        },

        APPRENTICE {
            @Override
            public String getDescription(Resources res) {
                return res.getString(R.string.tag_apprentice);
            }

            @Override
            public int getColor() {
                return R.color.apprentice;
            }

            @Override
            public int getValue(int apprentice, int guru, int total) {
                return apprentice;
            }

            @Override
            public void show(MainActivity main, Item.Type type) {
                main.showThisLevel(type, SRSLevel.APPRENTICE, false);
            }
        },

        GURU {
            @Override
            public String getDescription(Resources res) {
                return res.getString(R.string.tag_guru);
            }

            @Override
            public int getColor() {
                return R.color.guru;
            }

            @Override
            public int getValue(int apprentice, int guru, int total) {
                return guru;
            }

            @Override
            public void show(MainActivity main, Item.Type type) {
                main.showThisLevel(type, SRSLevel.APPRENTICE, true);
            }
        },

        REMAINING {
            @Override
            public String getDescription(Resources res) {
                return "    " + res.getString(R.string.tag_remaining);
            }

            @Override
            public boolean hasColor() {
                return false;
            }

            @Override
            public int getColor() {
                return -1; /* Doesn't matter */
            }

            @Override
            public int getValue(int apprentice, int guru, int total) {
                return total - (total / 10) - guru;
            }

            @Override
            public void show(MainActivity main, Item.Type type) {
                main.showRemaining(type);
            }
        };

        public DataSet getDataSet(MainActivity main, Resources res, Item.Type type, int apprentice, int guru,
                int total) {
            DataSet ans;

            if (hasColor())
                ans = new DataSet(getDescription(res), res.getColor(getColor()), getValue(apprentice, guru, total));
            else
                ans = new DataSet(getDescription(res), getValue(apprentice, guru, total));

            ans.listener = new ProgressionClickListener(main, type, this);

            return ans;
        }

        public abstract String getDescription(Resources res);

        public boolean hasColor() {
            return true;
        }

        public abstract int getColor();

        public abstract int getValue(int apprentice, int guru, int total);

        public abstract void show(MainActivity main, Item.Type type);

    }

    /**
     * Listener for clicks on critical items link. Causes the pager to 
     * switch to the item tab, and sets the critical items filter.
     */
    private class CriticalClickListener implements View.OnClickListener {

        @Override
        public void onClick(View v) {
            main.showCritical();
        }
    }

    /// The main activity
    MainActivity main;

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

    /// True if the spinner is (also virtually) visible
    boolean spinning;

    /// Number of reviews/lessons before switching to 42+ mode
    public static final int LESSONS_42P = 42;

    /// The Radicals progress subplot
    SubPlot radicalsProgress;

    /// The radicals progress chart
    View radicalsRow;

    /// The Kanji progress subplot
    SubPlot kanjiProgress;

    /// The kanji progress chart
    View kanjiRow;

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

        this.main = (MainActivity) main;

        this.main.register(this);
    }

    /**
     * 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);
    }

    /**
     * Registers all the click listeners.
     * Currently they are:
     * <ul>
     *    <li>The listener that handles "Available now" web link
     *  <li>The listener of the "Review button"
     *  <li>Kanji and radicals left "hyperlink"
     * </ul>
     */
    private void registerListeners() {
        View view;

        view = parent.findViewById(R.id.btn_review);
        view.setOnClickListener(new ReviewClickListener());

        view = parent.findViewById(R.id.btn_lessons_available);
        view.setOnClickListener(new LessonsClickListener());

        view = parent.findViewById(R.id.btn_view_critical);
        view.setOnClickListener(new CriticalClickListener());

        view = parent.findViewById(R.id.radicals_progression);
        view.setClickable(true);
        view.setOnClickListener(new TotalClickListener(Item.Type.RADICAL));

        view = parent.findViewById(R.id.kanji_progression);
        view.setClickable(true);
        view.setOnClickListener(new TotalClickListener(Item.Type.KANJI));

        view = parent.findViewById(R.id.btn_result);
        view.setOnClickListener(new ReviewSummaryClickListener());

        view = parent.findViewById(R.id.btn_chat);
        view.setOnClickListener(new ChatClickListener());
    }

    /**
     * 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);

        ProgressChart chart;

        parent = inflater.inflate(R.layout.dashboard, container, false);
        registerListeners();

        radicalsRow = parent.findViewById(R.id.row_radicals);
        chart = (ProgressChart) parent.findViewById(R.id.pb_radicals);
        radicalsProgress = chart.addData(parent.findViewById(R.id.rad_dropdown));

        kanjiRow = parent.findViewById(R.id.row_kanji);
        chart = (ProgressChart) parent.findViewById(R.id.pb_kanji);
        kanjiProgress = chart.addData(parent.findViewById(R.id.kanji_dropdown));

        return parent;
    }

    /**
     * Called when the view is resumed. We refresh the GUI, and start
     * the spinner, if it should be visible.
     */
    @Override
    public void onResume() {
        super.onResume();

        refreshComplete(main.getDashboardData());
        spin(spinning);
    }

    /**
     * Convenience method that changes the contents of a text view.
     * @param id the text view id
     * @param text the text to be displayed
     */
    protected void setText(int id, String text) {
        TextView view;

        view = (TextView) parent.findViewById(id);
        view.setText(text);
    }

    /**
     * Convenience method that changes the contents of a text view.
     * @param id the text view id
     * @param sid the string ID to be retrieved from the resources
     */
    protected void setText(int id, int sid) {
        TextView view;

        view = (TextView) parent.findViewById(id);
        view.setText(sid);
    }

    /**
     * Convenience method that changes the visibility of a view.
     * @param id the view id
     * @param flag any of {@link View#VISIBLE}, 
     * {@link View#INVISIBLE} or {@link View#GONE}
     */
    protected void setVisibility(int id, int flag) {
        View view;

        view = parent.findViewById(id);
        view.setVisibility(flag);
    }

    protected void setProgress(Item.Type type, int guru, int unlocked, int total) {
        List<DataSet> ddsets, ldsets;
        List<ProgressPlot.Marker> markers;
        DataSet gds, ads, tds, rds;
        int apprentice;
        View schart;
        SubPlot splot;
        Resources res;

        switch (type) {
        case RADICAL:
            splot = radicalsProgress;
            schart = radicalsRow;
            break;

        case KANJI:
            splot = kanjiProgress;
            schart = kanjiRow;
            break;

        case VOCABULARY:
        default:
            return;
        }

        schart.setVisibility(total > 0 ? View.VISIBLE : View.GONE);
        if (total <= 0)
            return;

        res = getResources();

        ddsets = new Vector<DataSet>();
        ldsets = new Vector<DataSet>();

        apprentice = unlocked - guru;

        tds = ProgressionData.LOCKED.getDataSet(main, res, type, apprentice, guru, total);
        ads = ProgressionData.APPRENTICE.getDataSet(main, res, type, apprentice, guru, total);
        gds = ProgressionData.GURU.getDataSet(main, res, type, apprentice, guru, total);
        rds = ProgressionData.REMAINING.getDataSet(main, res, type, apprentice, guru, total);

        /* Display data set: guru, apprentice, total */
        ddsets.add(gds);
        ddsets.add(ads);
        ddsets.add(tds);

        /* Legends data set: apprentice, guru, remaining */
        ldsets.add(ads);
        gds.showAlways = true;
        ldsets.add(gds);
        if (rds.value > 0)
            ldsets.add(rds);

        markers = new Vector<ProgressPlot.Marker>();
        if (guru == 0 && apprentice < total)
            markers.add(new ProgressPlot.Marker(Integer.toString(apprentice), Color.BLACK, apprentice));
        else if (apprentice == 0)
            markers.add(new ProgressPlot.Marker(Integer.toString(guru), Color.BLACK, guru));
        else {
            markers.add(new ProgressPlot.Marker(guru + "\u2194" + apprentice, Color.BLACK, guru));
            if (rds.value > 0)
                markers.add(new ProgressPlot.Marker("*", res.getColor(R.color.guru), total * 9f / 10, true));
        }
        markers.add(new ProgressPlot.Marker(Integer.toString(total), Color.BLACK, total));

        splot.setData(ddsets, ldsets, markers);
    }

    protected boolean setCurrentLevel(int rid, int vid, int number) {
        View row;
        TextView tv;

        row = parent.findViewById(rid);
        tv = (TextView) parent.findViewById(vid);

        if (number > 0) {
            tv.setText(Integer.toString(number));
            row.setVisibility(View.VISIBLE);
            return true;
        } else {
            row.setVisibility(View.GONE);
            return false;
        }
    }

    /**
     * Called by @link MainActivity when asynchronous data
     * retrieval is completed. If we already have a view on which
     * to display it, we update the GUI. Otherwise we cache the info
     * and display it when the fragment is resumed.
     * @param dd the retrieved data
     */
    public void refreshComplete(DashboardData dd) {
        Context ctxt;
        ImageView iw;
        String s;
        boolean show;

        ctxt = getActivity();

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

        iw = (ImageView) parent.findViewById(R.id.iv_gravatar);
        if (dd.gravatar != null)
            iw.setImageBitmap(mask(dd.gravatar));

        setText(R.id.tv_username, dd.username);
        setText(R.id.tv_level, getString(R.string.fmt_level, dd.level));
        setText(R.id.tv_title, getString(R.string.fmt_title, dd.title));

        if (SettingsActivity.get42plus(ctxt) && dd.reviewsAvailable > LESSONS_42P)
            setText(R.id.reviews_val, LESSONS_42P + "+");
        else
            setText(R.id.reviews_val, Integer.toString(dd.reviewsAvailable));
        setVisibility(R.id.tr_r_now, dd.reviewsAvailable > 0 ? View.VISIBLE : View.GONE);
        setText(R.id.tv_next_review, R.string.tag_next_review);

        if (dd.reviewsAvailable > 0) {
            setVisibility(R.id.tv_next_review, View.INVISIBLE);
            setVisibility(R.id.tv_next_review_val, View.GONE);
            setVisibility(R.id.btn_result, View.GONE);
            setVisibility(R.id.btn_review, View.VISIBLE);
        } else {
            setText(R.id.tv_next_review_val, niceInterval(getResources(), dd.nextReviewDate, dd.vacation));
            setVisibility(R.id.tv_next_review, View.VISIBLE);
            setVisibility(R.id.tv_next_review_val, View.VISIBLE);
            setVisibility(R.id.btn_result, View.VISIBLE);
            setVisibility(R.id.btn_review, View.GONE);
        }

        if (SettingsActivity.get42plus(ctxt) && dd.lessonsAvailable > LESSONS_42P)
            setText(R.id.lessons_available, getString(R.string.fmt_lessons_42p, LESSONS_42P));
        if (dd.lessonsAvailable > 1) {
            s = getString(R.string.fmt_lessons, dd.lessonsAvailable);
            setText(R.id.lessons_available, s);
        } else if (dd.lessonsAvailable == 1)
            setText(R.id.lessons_available, getString(R.string.fmt_one_lesson));

        /* If no more lessons, hide the message */
        setVisibility(R.id.lay_lessons_available, dd.lessonsAvailable > 0 ? View.VISIBLE : View.GONE);

        setText(R.id.next_hour_val, Integer.toString(dd.reviewsAvailableNextHour));
        setText(R.id.next_day_val, Integer.toString(dd.reviewsAvailableNextDay));

        /* Now the optional stuff */
        switch (dd.od.lpStatus) {
        case RETRIEVING:
            if (dd.od.lpStatus != DashboardData.OptionalDataStatus.RETRIEVING)
                setVisibility(R.id.pb_w_section, View.VISIBLE);
            break;

        case RETRIEVED:
            setVisibility(R.id.pb_w_section, View.GONE);
            setVisibility(R.id.lay_progress, View.VISIBLE);

            show = false;
            show |= setCurrentLevel(R.id.tr_cl_radicals, R.id.current_level_radicals_val,
                    dd.od.elp.currentLevelRadicalsAvailable);
            show |= setCurrentLevel(R.id.tr_cl_kanji, R.id.current_level_kanji_val,
                    dd.od.elp.currentLevelKanjiAvailable);
            setVisibility(R.id.tab_current_level, show ? View.VISIBLE : View.GONE);

            setProgress(Item.Type.RADICAL, dd.od.elp.radicalsProgress, dd.od.elp.radicalsUnlocked,
                    dd.od.elp.radicalsTotal);

            setProgress(Item.Type.KANJI, dd.od.elp.kanjiProgress, dd.od.elp.kanjiUnlocked, dd.od.elp.kanjiTotal);

            setVisibility(R.id.progress_section, View.VISIBLE);
            break;

        case FAILED:
            /* Just hide the spinner. 
             * If we already have some data, it is displayed anyway, otherwise hide it */
            if (dd.od.elp == null)
                setVisibility(R.id.lay_progress, View.GONE);
            setVisibility(R.id.pb_w_section, View.GONE);
        }

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

        case RETRIEVED:
            if (dd.od.criticalItems > 1) {
                s = getString(R.string.fmt_critical_items, dd.od.criticalItems);
                setText(R.id.critical_items, s);
            } else if (dd.od.criticalItems == 1)
                setText(R.id.critical_items, getString(R.string.fmt_one_critical_item));

            setVisibility(R.id.lay_critical_items, dd.od.criticalItems > 0 ? View.VISIBLE : View.GONE);

            break;

        case FAILED:
            break;
        }

        /* Show the alerts panel only if there are still alerts to be shown */
        showAlertsLayout(dd.lessonsAvailable > 0 || (dd.od != null && dd.od.criticalItems > 0));
    }

    /**
     * Shows or hides the alerts layout, depending on the state of the visibility
     * of its children
     *    @param show if it shall be shown  
     */
    protected void showAlertsLayout(boolean show) {
        ViewGroup lay;

        lay = (ViewGroup) parent.findViewById(R.id.lay_alerts);
        lay.setVisibility(show ? View.VISIBLE : View.GONE);
    }

    /**
     * Pretty-prints a date. This implementation tries to mimic the WaniKani website,
     * by returning an approximate interval. 
     * @param date the date to format
     * @return a string to be displayed
     */
    public static String niceInterval(Resources res, Date date, boolean vacation) {
        float days, hours, minutes;
        boolean forward;
        long delta;
        int x;

        if (vacation)
            return res.getString(R.string.fmt_vacation);
        if (date == null)
            return res.getString(R.string.fmt_no_reviews);

        delta = date.getTime() - new Date().getTime();
        forward = delta > 0;
        /* forward may be < 0 even if lessons are not available yet
         * (may due to clock disalignment)
         */
        if (!forward)
            delta = 1;

        minutes = delta / (60 * 1000);
        hours = minutes / 60;
        days = hours / 24;

        x = Math.round(days);
        if (x > 1)
            return res.getString(R.string.fmt_X_days, x);
        else if (x == 1)
            return res.getString(R.string.fmt_one_day);

        x = (int) Math.floor(hours);
        if (((long) minutes) % 60 < 2) {
            if (x > 1)
                return res.getString(R.string.fmt_X_hours, x);
            else if (x == 1 && hours >= 1)
                return res.getString(R.string.fmt_one_hour);
        } else {
            if (x > 1)
                return res.getString(R.string.fmt_X_hours_mins, x, ((long) minutes) % 60);
            else if (x == 1 && hours >= 1)
                return res.getString(R.string.fmt_one_hour_mins, ((long) minutes) % 60);
        }

        x = Math.round(minutes);
        if (x > 1)
            return res.getString(R.string.fmt_X_minutes, x);
        else if (x == 1)
            return res.getString(R.string.fmt_one_minute);

        return res.getString(R.string.fmt_seconds);
    }

    /**
     * Apply a circular mask on the given bitmap. This method is
     * used to display the avatar.
     * @param bmp an input bitmap
     * @param result the output (masked) bitmap
     */
    private Bitmap mask(Bitmap bmp) {
        Bitmap result, mask;
        Drawable dmask;
        Canvas canvas;
        Paint paint;

        result = Bitmap.createBitmap(bmp.getWidth(), bmp.getHeight(), Bitmap.Config.ARGB_8888);
        canvas = new Canvas(result);

        dmask = getResources().getDrawable(R.drawable.gravatar_mask);
        mask = ((BitmapDrawable) dmask).getBitmap();

        paint = new Paint(Paint.ANTI_ALIAS_FLAG);
        paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN));
        canvas.drawBitmap(bmp, 0, 0, null);
        canvas.drawBitmap(mask, 0, 0, paint);

        return result;
    }

    /**
     * Show or hide the spinner.
     * @param enable true if should be shown
     */
    public void spin(boolean enable) {
        ProgressBar pb;

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

    /**
     * Returns the tab name ID.
     * @param the <code>tag_dashboard</code> ID
     */
    public int getName() {
        return R.string.tag_dashboard;
    }

    /**
     * Does nothing. Needed just to implement the @link Tab interface, but
     * we don't keep any cache.
     */
    public void flush(Tab.RefreshType rtype, boolean fg) {
        /* empty */
    }

    /**
     * This item has no scroll view.
     * @return false
     */
    public boolean scrollLock() {
        return false;
    }

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

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

    @Override
    public void flushDatabase() {
        /* empty */
    }
}