com.wanikani.androidnotifier.WebReviewActivity.java Source code

Java tutorial

Introduction

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

Source

package com.wanikani.androidnotifier;

import java.io.File;

import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.media.AudioManager;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.support.v4.app.ActivityCompat;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import android.webkit.CookieSyncManager;
import android.webkit.DownloadListener;
import android.webkit.JavascriptInterface;
import android.webkit.WebChromeClient;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.Button;
import android.widget.ImageButton;
import android.widget.ProgressBar;
import android.widget.ScrollView;
import android.widget.TextView;
import android.widget.Toast;

import com.wanikani.androidnotifier.notification.NotificationService;

/* 
 *  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 activity allows the user to perform its reviews through an integrated
 * browser. The only reason we need this (instead of just spawning an external
 * browser) is that we also display a minimal keyboard, that interacts with WK scripts
 * to compose kanas. Ordinarily, in fact, Android keyboards do not behave correctly.
 * <p>
 * The keyboard is displayed only when needed, so we need to check whether the
 * page contains a <code>user_response</code> text box, and it is enabled.
 * In addition, to submit the form, we simulate a click on the <code>option-submit</code>
 * button. Since the keyboard hides the standard controls (in particular the
 * info ("did you know...") balloons), we hide the keyboard when the user enters 
 * his/her response. 
 * <p>
 * To accomplish this, we register a JavascriptObject (<code>wknKeyboard</code>) and inject
 * a javascript to check how the page looks like. If the keyboard needs to be shown,
 * it calls its <code>show</code> (vs. <code>hide</code>) method.
 * The JavascriptObject is implemented by @link WebReviewActivity.WKNKeyboard.
 */
public class WebReviewActivity extends Activity {

    /**
     * This class is barely a container of all the strings that should match with the
     * WaniKani portal. Hopefully none of these will ever be changed, but in case
     * it does, here is where to look for.
     */
    public static class WKConfig {

        /** HTML id of the textbox the user types its answer in (reviews, client-side) */
        static final String ANSWER_BOX = "user-response";

        /** HTML id of the textbox the user types its answer in (lessons) */
        static final String LESSON_ANSWER_BOX_JP = "translit";

        /** HTML id of the textbox the user types its answer in (lessons) */
        static final String LESSON_ANSWER_BOX_EN = "lesson_user_response";

        /** HTML id of the submit button */
        static final String SUBMIT_BUTTON = "option-submit";

        /** HTML id of the lessons review form */
        static final String LESSONS_REVIEW_FORM = "new_lesson";

        /** HTML id of the lessons quiz */
        static final String QUIZ = "quiz";

        /** Any object on the lesson pages */
        static final String LESSONS_OBJ = "nav-lesson";

        /** Reviews div */
        static final String REVIEWS_DIV = "reviews";
    };

    /**
     * The listener attached to the ignore button tip message.
     * When the user taps the ok button, we write on the property
     * that it has been acknowleged, so it won't show up any more. 
     */
    private class OkListener implements DialogInterface.OnClickListener {

        @Override
        public void onClick(DialogInterface ifc, int which) {
            SettingsActivity.setIgnoreButtonMessage(prefs(), true);
        }
    }

    /**
     * The listener attached to the hw accel tip message.
     * When the user taps the ok button, we write on the property
     * that it has been acknowleged, so it won't show up any more. 
     */
    private class AccelOkListener implements DialogInterface.OnClickListener {

        @Override
        public void onClick(DialogInterface ifc, int which) {
            SettingsActivity.setHWAccelMessage(prefs(), true);
        }
    }

    /**
     * The listener that receives events from the mute buttons.
     */
    private class MuteListener implements View.OnClickListener {

        @Override
        public void onClick(View w) {
            SettingsActivity.toggleMute(prefs());
            applyMuteSettings();
        }
    }

    /**
     * The listener that receives events from the single buttons.
     */
    private class SingleListener implements View.OnClickListener {

        @Override
        public void onClick(View w) {
            single = !single;
            applySingleSettings();
        }
    }

    /**
     * Web view controller. This class is used by @link WebView to tell whether
     * a link should be opened inside of it, or an external browser needs to be invoked.
     * Currently, I will let all the pages inside the <code>/review</code> namespace
     * to be opened here. Theoretically, it could even be stricter, and use
     * <code>/review/session</code>, but that would be prevent the final summary
     * from being shown. That page is useful, albeit not as integrated with the app as
     * the other pages.
     */
    private class WebViewClientImpl extends WebViewClient {

        /**
         * Called to check whether a link should be opened in the view or not.
         * We also display the progress bar.
         *    @param view the web view
         *  @url the URL to be opened
         */
        @Override
        public boolean shouldOverrideUrlLoading(WebView view, String url) {
            Intent intent;

            if (shouldOpenExternal(url)) {
                intent = new Intent(Intent.ACTION_VIEW);
                intent.setData(Uri.parse(url));
                startActivity(intent);

                return true;
            }

            return false;
        }

        /**
         * Tells if we should spawn an external browser
         *  @param url the url we are opening
         *    @return true if we should
         */
        public boolean shouldOpenExternal(String url) {
            String curl;

            if (!url.contains("wanikani.com") && !download)
                return true;

            curl = wv.getUrl();
            if (curl == null)
                return false;

            /* Seems the only portable way to do this */
            if (curl.contains("www.wanikani.com/lesson") || curl.contains("www.wanikani.com/review")) {

                if ((url.contains("/kanji/") || url.contains("/radicals/"))
                        && SettingsActivity.getExternalItems(WebReviewActivity.this))
                    return true;

            }

            return false;
        }

        /**
         * Called when something bad happens while accessing the resource.
         * Show the splash screen and give some explanation (based on the <code>description</code>
         * string).
         *    @param view the web view
         *  @param errorCode HTTP error code
         *  @param description an error description
         *  @param failingUrl error
         */
        public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
            String s;

            s = getResources().getString(R.string.fmt_web_review_error, description);
            splashScreen(s);
            bar.setVisibility(View.GONE);
        }

        @Override
        public void onPageStarted(WebView view, String url, Bitmap favicon) {
            bar.setVisibility(View.VISIBLE);
            keyboard.reset();
        }

        /**
          * Called when a page finishes to be loaded. We hide the progress bar
          * and run the initialization javascript that shows the keyboard, if needed.
          * In addition, if this is the initial page, we check whether Viet has
          * deployed the client-side review system
          */
        @Override
        public void onPageFinished(WebView view, String url) {
            ExternalFramePlacer.Dictionary dict;
            boolean wasVisible;

            wasVisible = bar.getVisibility() != View.GONE;
            bar.setVisibility(View.GONE);

            /* wasVisible is just an hack because externalframeplacer 
             * causes a spurious call to onPageFinished on some devices */
            if (wasVisible && url.startsWith("http")) {

                wv.js(JS_INIT_KBD);
                if (SettingsActivity.getExternalFramePlacer(WebReviewActivity.this)) {
                    dict = SettingsActivity.getExternalFramePlacerDictionary(WebReviewActivity.this);
                    ExternalFramePlacer.run(wv, dict);
                }

                if (SettingsActivity.getPartOfSpeech(WebReviewActivity.this))
                    PartOfSpeech.enter(WebReviewActivity.this, wv, url);
            }
        }
    }

    /**
     * An additional webclient, that receives a few callbacks that a simple 
     * {@link WebChromeClient} does not intecept. 
     */
    private class WebChromeClientImpl extends WebChromeClient {

        /**
         * Called as the download progresses. We update the progress bar.
         * @param view the web view
         * @param progress progress percentage
         */
        @Override
        public void onProgressChanged(WebView view, int progress) {
            bar.setProgress(progress);
        }
    };

    /**
     * A small job that hides, shows or iconizes the keyboard. We need to implement this
     * here because {@link WebReviewActibity.WKNKeyboard} gets called from a
     * javascript thread, which is not necessarily an UI thread.
     * The constructor simply calls <code>runOnUIThread</code> to make sure
     * we hide/show the views from the correct context.
     */
    private class ShowHideKeyboard implements Runnable {

        /** New state to enter */
        KeyboardStatus kbstatus;

        /**
         * Constructor. It also takes care to schedule the invokation
         * on the UI thread, so all you have to do is just to create an
         * instance of this object
         * @param kbstatus the new keyboard status to enter
         */
        ShowHideKeyboard(KeyboardStatus kbstatus) {
            this.kbstatus = kbstatus;

            runOnUiThread(this);
        }

        /**
         * Hides/shows the keyboard. Invoked by the UI thread.
         * As a side effect, we update the {@link WebReviewActivity#flushCaches}
         * bit.
         */
        public void run() {
            kbstatus.apply(WebReviewActivity.this);
            if (kbstatus.isRelevantPage())
                reviewsSession();
        }

        private void reviewsSession() {
            flushCaches = true;
            CookieSyncManager.getInstance().sync();
        }

    }

    /**
     * This class implements the <code>wknKeyboard</code> javascript object.
     * It implements the @link {@link #show} and {@link #hide} methods. 
     */
    private class WKNKeyboard {

        /**
         * Called by javascript when the keyboard should be shown.
         */
        @JavascriptInterface
        public void show() {
            new ShowHideKeyboard(KeyboardStatus.REVIEWS_MAXIMIZED);
        }

        /**
         * Called by javascript when the keyboard should be shown, using
         * new lessons layout.
         */
        @JavascriptInterface
        public void showLessonsNew() {
            new ShowHideKeyboard(KeyboardStatus.LESSONS_MAXIMIZED_NEW);
        }

        /**
         * Called by javascript when the keyboard should be hidden.
         */
        @JavascriptInterface
        public void hide() {
            new ShowHideKeyboard(KeyboardStatus.INVISIBLE);
        }
    }

    /**
     * Our implementation of a menu listener. We listen for configuration changes. 
     */
    private class MenuListener extends MenuHandler.Listener {

        public MenuListener() {
            super(WebReviewActivity.this);
        }

        /**
         * The dashboard listener exits the activity
         */
        public void dashboard() {
            Intent i;

            i = new Intent(WebReviewActivity.this, MainActivity.class);
            i.setAction(Intent.ACTION_MAIN);
            i.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);

            startActivity(i);

            finish();
        }

        /**
         * Toggle override fonts
         */
        @Override
        public void fonts() {
            keyboard.overrideFonts();
            ActivityCompat.invalidateOptionsMenu(WebReviewActivity.this);
        }

        /**
         * Refresh the page, clearing the cache too.
         */
        @Override
        public void refresh() {
            wv.clearCache(true);
            wv.js("window.location.reload (true)");
        }
    }

    /**
     * Keyboard visiblity status.
     */
    enum KeyboardStatus {

        /** Keyboard visible, all keys visible */
        REVIEWS_MAXIMIZED {
            public void apply(WebReviewActivity wav) {
                wav.show(this);
            }

            public SettingsActivity.Keyboard getKeyboard(WebReviewActivity wav) {
                return SettingsActivity.getReviewsKeyboard(wav);
            }

            public boolean canMute() {
                return true;
            }

            public boolean canDoSingle() {
                return true;
            }
        },

        /** Keyboard visible, all keys but ENTER visible */
        LESSONS_MAXIMIZED_NEW {
            public void apply(WebReviewActivity wav) {
                wav.show(this);
            }

            public SettingsActivity.Keyboard getKeyboard(WebReviewActivity wav) {
                return SettingsActivity.getReviewsKeyboard(wav);
            }

            public boolean canMute() {
                return true;
            }
        },

        /** Keyboard invisible */
        INVISIBLE {
            public void apply(WebReviewActivity wav) {
                wav.hide(this);
            }

            public boolean isRelevantPage() {
                return false;
            }

            public SettingsActivity.Keyboard getKeyboard(WebReviewActivity wav) {
                return SettingsActivity.Keyboard.NATIVE;
            }

            public boolean backIsSafe() {
                return true;
            }
        };

        public abstract void apply(WebReviewActivity wav);

        public void maximize(WebReviewActivity wav) {
            /* empty */
        }

        public boolean isIconized() {
            return false;
        }

        public abstract SettingsActivity.Keyboard getKeyboard(WebReviewActivity wav);

        public boolean isRelevantPage() {
            return true;
        }

        public boolean canMute() {
            return false;
        }

        public boolean canDoSingle() {
            return false;
        }

        public boolean hasEnter(WebReviewActivity wav) {
            return false;
        }

        public boolean backIsSafe() {
            return false;
        }
    };

    private class ReaperTaskListener implements TimerThreadsReaper.ReaperTaskListener {

        public void reaped(int count, int total) {
            /* Here we could keep some stats. Currently unused */
        }
    }

    private class IgnoreButtonListener implements View.OnClickListener {

        @Override
        public void onClick(View view) {
            ignore();
        }

    }

    private class FileDownloader implements DownloadListener, FileDownloadTask.Listener {

        FileDownloadTask fdt;

        @Override
        public void onDownloadStart(String url, String userAgent, String contentDisposition, String mimetype,
                long contentLength) {
            dbar.setVisibility(View.VISIBLE);
            cancel();
            fdt = new FileDownloadTask(WebReviewActivity.this, downloadPrefix, this);
            fdt.execute(url);
        }

        private void cancel() {
            if (fdt != null)
                fdt.cancel();
        }

        @Override
        public void setProgress(int percentage) {
            dbar.setProgress(percentage);
        }

        @Override
        public void done(File file) {
            Intent results;

            dbar.setVisibility(View.GONE);
            if (file != null) {
                results = new Intent();
                results.putExtra(EXTRA_FILENAME, file.getAbsolutePath());
                setResult(RESULT_OK, results);
                finish();
            } else
                Toast.makeText(WebReviewActivity.this, getString(R.string.tag_download_failed), Toast.LENGTH_LONG)
                        .show();
        }
    }

    /** The web view, where the web contents are rendered */
    FocusWebView wv;

    /** The view containing a splash screen. Visible when we want to display 
     * some message to the user */
    View splashView;

    /**
     * The view contaning the ordinary content.
     */
    View contentView;

    /** A textview in the splash screen, where we can display some message */
    TextView msgw;

    /** The web progress bar */
    ProgressBar bar;

    /** The web download progress bar */
    ProgressBar dbar;

    /// Selected button color
    int selectedColor;

    /// Unselected button color
    int unselectedColor;

    /** The local prefix of this class */
    private static final String PREFIX = "com.wanikani.androidnotifier.WebReviewActivity.";

    /** Open action, invoked to start this action */
    public static final String OPEN_ACTION = PREFIX + "OPEN";

    /** Download action, invoked to download a file */
    public static final String DOWNLOAD_ACTION = PREFIX + "DOWNLOAD";

    public static final String EXTRA_DOWNLOAD_PREFIX = PREFIX + "download_prefix";

    public static final String EXTRA_FILENAME = PREFIX + "filename";

    /** Flush caches bundle key */
    private static final String KEY_FLUSH_CACHES = PREFIX + "flushCaches";

    /** Local preferences file. Need it because we access preferences from another file */
    private static final String PREFERENCES_FILE = "webview.xml";

    /** Javascript to be called each time an HTML page is loaded. It hides or shows the keyboard */
    private static final String JS_INIT_KBD = "var textbox, lessobj, ltextbox, reviews, style;"
            + "textbox = document.getElementById (\"" + WKConfig.ANSWER_BOX + "\"); "
            + "reviews = document.getElementById (\"" + WKConfig.REVIEWS_DIV + "\");"
            + "quiz = document.getElementById (\"" + WKConfig.QUIZ + "\");" + "if (quiz != null) {"
            + "   wknKeyboard.showLessonsNew ();" + "} else if (textbox != null && !textbox.disabled) {"
            + "   wknKeyboard.show (); " + "} else {" + "   wknKeyboard.hide ();" + "}" + "if (reviews != null) {"
            + "   reviews.style.overflow = \"visible\";" + "}" + "window.trueRandom = Math.random;"
            + "window.fakeRandom = function() { return 0;  };" + // @Ikalou's fix
            /* This fixes a bug that makes SRS indication slow */
            "style = document.createElement('style');" + "style.type = 'text/css';"
            + "style.innerHTML = '.animated { -webkit-animation-duration:0s; }';"
            + "document.getElementsByTagName('head')[0].appendChild(style);";

    private static final String JS_BULK_MODE = "if (window.trueRandom) Math.random=window.trueRandom;";
    private static final String JS_SINGLE_MODE = "if (window.fakeRandom) Math.random=window.fakeRandom;";

    /** The threads reaper */
    TimerThreadsReaper reaper;

    /** Thread reaper task */
    TimerThreadsReaper.ReaperTask rtask;

    /** The current keyboard status */
    protected KeyboardStatus kbstatus;

    /** The mute drawable */
    private Drawable muteDrawable;

    /** The sound drawable */
    private Drawable notMutedDrawable;

    /** The menu handler */
    private MenuHandler mh;

    /** The ignore button */
    private ImageButton ignbtn;

    /** Set if visible */
    boolean visible;

    /** Set if we have reviewed or had some lessons, so caches should be flushed */
    private boolean flushCaches;

    /** Is mute enabled */
    private boolean isMuted;

    /** The current keyboard */
    private Keyboard keyboard;

    /** The native keyboard */
    private Keyboard nativeKeyboard;

    /** The local IME keyboard */
    private Keyboard localIMEKeyboard;

    /** The mute button */
    private ImageButton muteH;

    /** The single button */
    private Button singleb;

    /** Single mode is on? */
    private boolean single;

    /** Shall we download a file? */
    private boolean download;

    /** Download prefix */
    private String downloadPrefix;

    /** The file downloader, if any */
    private FileDownloader fda;

    /**
     * Called when the action is initially displayed. It initializes the objects
     * and starts loading the review page.
     *    @param bundle the saved bundle
     */
    @Override
    public void onCreate(Bundle bundle) {
        super.onCreate(bundle);

        Resources res;

        CookieSyncManager.createInstance(this);
        setVolumeControlStream(AudioManager.STREAM_MUSIC);

        mh = new MenuHandler(this, new MenuListener());

        if (SettingsActivity.getFullscreen(this)) {
            getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
            requestWindowFeature(Window.FEATURE_NO_TITLE);
        }

        setContentView(R.layout.web_review);

        res = getResources();

        selectedColor = res.getColor(R.color.selected);
        unselectedColor = res.getColor(R.color.unselected);

        muteDrawable = res.getDrawable(R.drawable.ic_mute);
        notMutedDrawable = res.getDrawable(R.drawable.ic_not_muted);

        kbstatus = KeyboardStatus.INVISIBLE;

        bar = (ProgressBar) findViewById(R.id.pb_reviews);
        dbar = (ProgressBar) findViewById(R.id.pb_download);

        ignbtn = (ImageButton) findViewById(R.id.btn_ignore);
        ignbtn.setOnClickListener(new IgnoreButtonListener());

        /* First of all get references to views we'll need in the near future */
        splashView = findViewById(R.id.wv_splash);
        contentView = findViewById(R.id.wv_content);
        msgw = (TextView) findViewById(R.id.tv_message);
        wv = (FocusWebView) findViewById(R.id.wv_reviews);

        wv.getSettings().setJavaScriptEnabled(true);
        wv.getSettings().setJavaScriptCanOpenWindowsAutomatically(true);
        wv.getSettings().setSupportMultipleWindows(false);
        wv.getSettings().setUseWideViewPort(false);
        wv.getSettings().setDatabaseEnabled(true);
        wv.getSettings().setDomStorageEnabled(true);
        wv.getSettings().setDatabasePath(getFilesDir().getPath() + "/wv");
        wv.addJavascriptInterface(new WKNKeyboard(), "wknKeyboard");
        wv.setScrollBarStyle(ScrollView.SCROLLBARS_OUTSIDE_OVERLAY);
        wv.setWebViewClient(new WebViewClientImpl());
        wv.setWebChromeClient(new WebChromeClientImpl());

        download = getIntent().getAction().equals(DOWNLOAD_ACTION);
        if (download) {
            downloadPrefix = getIntent().getStringExtra(EXTRA_DOWNLOAD_PREFIX);
            wv.setDownloadListener(fda = new FileDownloader());
        }

        wv.loadUrl(getIntent().getData().toString());

        nativeKeyboard = new NativeKeyboard(this, wv);
        localIMEKeyboard = new LocalIMEKeyboard(this, wv);

        muteH = (ImageButton) findViewById(R.id.kb_mute_h);
        muteH.setOnClickListener(new MuteListener());

        singleb = (Button) findViewById(R.id.kb_single);
        singleb.setOnClickListener(new SingleListener());

        if (SettingsActivity.getTimerReaper(this)) {
            reaper = new TimerThreadsReaper();
            rtask = reaper.createTask(new Handler(), 2, 7000);
            rtask.setListener(new ReaperTaskListener());
        }
    }

    @Override
    public void onNewIntent(Intent intent) {
        String curl, nurl;

        super.onNewIntent(intent);
        curl = wv.getOriginalUrl();
        nurl = intent.getData().toString();
        if (curl == null || !curl.equals(nurl))
            wv.loadUrl(nurl);
    }

    @Override
    protected void onResume() {
        Window window;

        super.onResume();

        window = getWindow();
        if (SettingsActivity.getLockScreen(this))
            window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
        else
            window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);

        if (SettingsActivity.getResizeWebview(this))
            window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
        else
            window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN);

        visible = true;

        selectKeyboard();

        applyMuteSettings();
        applySingleSettings();

        showHWAccelMessage();

        wv.acquire();

        kbstatus.apply(this);

        if (rtask != null)
            rtask.resume();
    }

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

        mh.unregister(this);

        if (reaper != null)
            reaper.stopAll();

        if (fda != null)
            fda.cancel();

        if (SettingsActivity.getLeakKludge(this))
            System.exit(0);
    }

    @Override
    protected void onSaveInstanceState(Bundle bundle) {
        bundle.putBoolean(KEY_FLUSH_CACHES, flushCaches);
    }

    @Override
    protected void onRestoreInstanceState(Bundle bundle) {
        if (bundle != null && bundle.containsKey(KEY_FLUSH_CACHES))
            flushCaches = bundle.getBoolean(KEY_FLUSH_CACHES);
    }

    @Override
    protected void onPause() {
        Intent intent;

        visible = false;

        super.onPause();
        intent = new Intent(MainActivity.ACTION_REFRESH);
        intent.putExtra(MainActivity.E_FLUSH_CACHES, flushCaches);
        sendBroadcast(intent);

        /* Alert the notification service too (the main action may not be active) */
        intent = new Intent(this, NotificationService.class);
        intent.setAction(NotificationService.ACTION_NEW_DATA);
        startService(intent);

        setMute(false);

        wv.release();

        if (rtask != null)
            rtask.pause();

        keyboard.hide();
    }

    /**
     * Tells if calling {@link WebView#goBack()} is safe. On some WK pages we should not use it. 
     * @return <tt>true</tt> if it is safe.
     */
    protected boolean backIsSafe() {
        String lpage, rpage, url;

        url = wv.getUrl();
        lpage = "www.wanikani.com/lesson";
        rpage = "www.wanikani.com/review";

        return kbstatus.backIsSafe() &&
        /* Need this because the reviews summary page is dangerous */
                !(url.contains(rpage) || rpage.contains(url)) && !(url.contains(lpage) || lpage.contains(url));
    }

    @Override
    public void onBackPressed() {
        String url;

        url = wv.getUrl();

        if (url == null)
            super.onBackPressed();
        else if (url.contains("http://www.wanikani.com/quickview"))
            wv.loadUrl(SettingsActivity.getLessonURL(this));
        else if (wv.canGoBack() && backIsSafe())
            wv.goBack();
        else {
            // Dialog box added 25/6/2015 by Aralox, based on http://stackoverflow.com/a/9901871/1072869
            AlertDialog.Builder builder = new AlertDialog.Builder(this);
            builder.setMessage(R.string.back_msg).setCancelable(false)
                    .setPositiveButton(R.string.back_yes, new DialogInterface.OnClickListener() {
                        public void onClick(DialogInterface dialog, int id) {
                            WebReviewActivity.super.onBackPressed();
                        }
                    }).setNegativeButton(R.string.back_no, new DialogInterface.OnClickListener() {
                        public void onClick(DialogInterface dialog, int id) {
                            dialog.cancel();
                        }
                    });
            AlertDialog alert = builder.create();
            alert.show();
        }
    }

    /**
     * Associates the menu description to the menu key (or action bar).
     * The XML description is <code>review.xml</code>
     */
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.review, menu);
        return true;
    }

    @Override
    public boolean onPrepareOptionsMenu(Menu menu) {
        MenuItem mi;
        int i;

        for (i = 0; i < menu.size(); i++) {
            mi = menu.getItem(i);
            if (mi.getItemId() == R.id.em_fonts) {
                mi.setVisible(keyboard.canOverrideFonts());
                mi.setIcon(keyboard.getOverrideFonts() ? R.drawable.ic_menu_font_enabled : R.drawable.ic_menu_font);
            }
        }

        return true;
    }

    /**
     * Force menu invalidation.
     */
    public void invalidateMenu() {
        ActivityCompat.invalidateOptionsMenu(this);
    }

    /**
     * Menu handler. Relays the call to the common {@link MenuHandler}.
     *    @param item the selected menu item
     */
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        return mh.onOptionsItemSelected(item) || super.onOptionsItemSelected(item);
    }

    protected void selectKeyboard() {
        Keyboard oldk;

        oldk = keyboard;

        switch (kbstatus.getKeyboard(this)) {
        case LOCAL_IME:
            keyboard = localIMEKeyboard;
            break;

        case NATIVE:
            keyboard = nativeKeyboard;
            break;
        }

        if (keyboard != oldk && oldk != null)
            oldk.hide();

        updateCanIgnore();
    }

    private void applyMuteSettings() {
        boolean show;

        show = kbstatus.canMute() && SettingsActivity.getShowMute(this);
        muteH.setVisibility(show ? View.VISIBLE : View.GONE);

        setMute(show && SettingsActivity.getMute(prefs()));
    }

    private void applySingleSettings() {
        boolean show;

        show = kbstatus.canDoSingle() && SettingsActivity.getShowSingle(this);
        singleb.setVisibility(show ? View.VISIBLE : View.GONE);
        if (single) {
            singleb.setTextColor(selectedColor);
            singleb.setTypeface(null, Typeface.BOLD);
            wv.js(JS_SINGLE_MODE);
        } else {
            singleb.setTextColor(unselectedColor);
            singleb.setTypeface(null, Typeface.NORMAL);
            wv.js(JS_BULK_MODE);
        }
    }

    private void setMute(boolean m) {
        Drawable d;

        d = m ? muteDrawable : notMutedDrawable;
        muteH.setImageDrawable(d);

        if (isMuted != m && keyboard != null) {
            keyboard.setMute(m);
            isMuted = m;
        }
    }

    /**
     * Displays the splash screen, also providing a text message
     * @param msg the text message to display
     */
    protected void splashScreen(String msg) {
        msgw.setText(msg);
        contentView.setVisibility(View.GONE);
        splashView.setVisibility(View.VISIBLE);
    }

    /**
     * Hides the keyboard
     * @param kbstatus the new keyboard status
     */
    protected void hide(KeyboardStatus kbstatus) {
        this.kbstatus = kbstatus;

        applyMuteSettings();
        applySingleSettings();

        keyboard.hide();
    }

    protected void show(KeyboardStatus kbstatus) {
        this.kbstatus = kbstatus;

        selectKeyboard();

        applyMuteSettings();
        applySingleSettings();

        keyboard.show(kbstatus.hasEnter(this));
    }

    protected void iconize(KeyboardStatus kbs) {
        kbstatus = kbs;

        selectKeyboard();

        applyMuteSettings();
        applySingleSettings();

        keyboard.iconize(kbstatus.hasEnter(this));
    }

    public void updateCanIgnore() {
        ignbtn.setVisibility(keyboard.canIgnore() ? View.VISIBLE : View.GONE);
    }

    /**
     * Ignore button
     */
    public void ignore() {
        showIgnoreButtonMessage();
        keyboard.ignore();
    }

    protected void showIgnoreButtonMessage() {
        AlertDialog.Builder builder;
        Dialog dialog;

        if (!visible || SettingsActivity.getIgnoreButtonMessage(this, prefs()))
            return;

        builder = new AlertDialog.Builder(this);
        builder.setTitle(R.string.ignore_button_message_title);
        builder.setMessage(R.string.ignore_button_message_text);
        builder.setPositiveButton(R.string.ignore_button_message_ok, new OkListener());

        dialog = builder.create();
        SettingsActivity.setIgnoreButtonMessage(prefs(), true);

        dialog.show();
    }

    protected void showHWAccelMessage() {
        AlertDialog.Builder builder;
        Dialog dialog;

        if (!visible || SettingsActivity.getHWAccelMessage(prefs()))
            return;

        builder = new AlertDialog.Builder(this);
        builder.setTitle(R.string.hw_accel_message_title);
        builder.setMessage(R.string.hw_accel_message_text);
        builder.setPositiveButton(R.string.hw_accel_message_ok, new AccelOkListener());

        dialog = builder.create();
        SettingsActivity.setHWAccelMessage(prefs(), true);

        dialog.show();
    }

    protected SharedPreferences prefs() {
        return getSharedPreferences(PREFERENCES_FILE, Context.MODE_PRIVATE);
    }

}