org.liberty.android.fantastischmemo.ui.QACardActivity.java Source code

Java tutorial

Introduction

Here is the source code for org.liberty.android.fantastischmemo.ui.QACardActivity.java

Source

/*
    
Copyright (C) 2012 Haowen Ning
    
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 2
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, write to the Free Software
Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
    
*/
package org.liberty.android.fantastischmemo.ui;

import java.io.File;
import java.util.ArrayList;
import java.util.List;

import javax.inject.Inject;

import org.apache.commons.io.FilenameUtils;
import org.liberty.android.fantastischmemo.AMActivity;
import org.liberty.android.fantastischmemo.AMEnv;
import org.liberty.android.fantastischmemo.AnyMemoDBOpenHelper;
import org.liberty.android.fantastischmemo.AnyMemoDBOpenHelperManager;
import org.liberty.android.fantastischmemo.R;
import org.liberty.android.fantastischmemo.domain.Card;
import org.liberty.android.fantastischmemo.domain.Option;
import org.liberty.android.fantastischmemo.domain.Setting;
import org.liberty.android.fantastischmemo.service.AnyMemoService;
import org.liberty.android.fantastischmemo.ui.loader.CardTTSUtilLoader;
import org.liberty.android.fantastischmemo.ui.loader.MultipleLoaderManager;
import org.liberty.android.fantastischmemo.ui.loader.SettingLoader;
import org.liberty.android.fantastischmemo.utils.AMFileUtil;
import org.liberty.android.fantastischmemo.utils.AMStringUtils;
import org.liberty.android.fantastischmemo.utils.CardTTSUtil;
import org.liberty.android.fantastischmemo.widget.AnyMemoWidgetProvider;

import roboguice.util.Ln;

import android.content.Intent;
import android.gesture.Gesture;
import android.gesture.GestureLibraries;
import android.gesture.GestureLibrary;
import android.gesture.GestureOverlayView;
import android.gesture.Prediction;
import android.net.Uri;
import android.os.Bundle;
import android.support.v4.app.FragmentTransaction;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.Loader;
import android.text.ClipboardManager;
import android.util.Log;
import android.view.KeyEvent;
import android.view.View;
import android.widget.TextView;

import com.google.common.base.Strings;

/**
 * The base class for all activities that displays cards.
 *
 * To use this class, subclass it and call the startInit() in the onCreate() method.
 * onPostInit() is used for customized initialization. Additionally, the caller can register
 * a Loader that needs loading asynchronized. Call getMultipleLoaderManager() and register the loaders
 * before calling startInit().
 *
 * Override getContentView() for loading a customized layout that is compatible with qa_card_layout.
 */
@SuppressWarnings("deprecation")
public abstract class QACardActivity extends AMActivity {
    public static String EXTRA_DBPATH = "dbpath";

    private String dbPath;

    private String dbName;

    private AnyMemoDBOpenHelper dbOpenHelper;

    private Card currentCard;

    /**
     * The card may not be displayed immediate after setting.
     * The currentDisplayedCard stores the current displayed card in the view.
     * It is used for animation purpose.
     */
    private Card currentDisplayedCard = null;

    private static final int SETTING_LOADER_ID = 0;

    private static final int CARD_TTS_UTIL_LOADER_ID = 1;

    private Option option;

    private Setting setting;

    private boolean isAnswerShown = true;

    private TextView smallTitleBar;

    private CardTTSUtil cardTTSUtil;

    private AMFileUtil amFileUtil;

    private GestureLibrary gestureLibrary;

    /**
     * This needs to be defined before onCreate so in onCreate, all loaders will
     * be registered with the right manager.
     */
    private MultipleLoaderManager multipleLoaderManager;

    @Inject
    public void setOption(Option option) {
        this.option = option;
    }

    @Inject
    public void setMultipleLoaderManager(MultipleLoaderManager multipleLoaderManager) {
        this.multipleLoaderManager = multipleLoaderManager;
    }

    @Inject
    public void setAmFileUtil(AMFileUtil amFileUtil) {
        this.amFileUtil = amFileUtil;
    }

    /**
     * This is for testing only.
     */
    public CardTTSUtil getCardTTSUtil() {
        return cardTTSUtil;
    }

    /**
     * Subclasses should call this method instead of creating
     * a new instance of multipleLoaderManager.
     */
    public MultipleLoaderManager getMultipleLoaderManager() {
        return multipleLoaderManager;
    }

    @Override
    public void onCreate(Bundle bundle) {
        super.onCreate(bundle);
        setContentView(getContentView());
    }

    /**
     * Call this method to start the initialization process.
     * Must be called in UI thread.
     */
    public final void startInit() {
        Bundle extras = getIntent().getExtras();
        if (extras != null) {
            dbPath = extras.getString(EXTRA_DBPATH);
        }

        dbOpenHelper = AnyMemoDBOpenHelperManager.getHelper(this, dbPath);

        dbPath = extras.getString(EXTRA_DBPATH);

        dbName = FilenameUtils.getName(dbPath);

        // Load gestures
        loadGestures();

        multipleLoaderManager.registerLoaderCallbacks(SETTING_LOADER_ID, new SettingLoaderCallbacks(), false);
        multipleLoaderManager.registerLoaderCallbacks(CARD_TTS_UTIL_LOADER_ID, new CardTTSUtilLoaderCallbacks(),
                true);
        multipleLoaderManager.setOnAllLoaderCompletedRunnable(onPostInitRunnable);
        multipleLoaderManager.startLoading();
    }

    /**
     * Override to load customized layout.
     */
    protected int getContentView() {
        return R.layout.qa_card_layout;
    }

    protected void setCurrentCard(Card card) {
        currentCard = card;
    }

    protected Card getCurrentCard() {
        return currentCard;
    }

    protected String getDbPath() {
        return dbPath;
    }

    protected String getDbName() {
        return dbName;
    }

    // Important class that display the card using fragment
    // the showAnswer parameter is handled differently on single
    // sided card and double sided card.
    protected void displayCard(boolean showAnswer) {

        // First prepare the text to display

        String questionTypeface = setting.getQuestionFont();
        String answerTypeface = setting.getAnswerFont();

        Setting.Align questionAlign = setting.getQuestionTextAlign();
        Setting.Align answerAlign = setting.getAnswerTextAlign();

        String questionTypefaceValue = null;
        String answerTypefaceValue = null;
        /* Set the typeface of question and answer */
        if (!Strings.isNullOrEmpty(questionTypeface)) {
            questionTypefaceValue = questionTypeface;

        }
        if (!Strings.isNullOrEmpty(answerTypeface)) {
            answerTypefaceValue = answerTypeface;
        }

        final String[] imageSearchPaths = {
                /* Relative path */
                "",
                /* Relative path with db name */
                "" + FilenameUtils.getName(dbPath),
                /* Try the image in /sdcard/anymemo/images/dbname/ */
                AMEnv.DEFAULT_IMAGE_PATH + FilenameUtils.getName(dbPath),
                /* Try the image in /sdcard/anymemo/images/ */
                AMEnv.DEFAULT_IMAGE_PATH, };

        // Buttons view can be null if it is not decleared in the layout XML
        View buttonsView = findViewById(R.id.buttons_root);

        if (buttonsView != null) {
            // Make sure the buttons view are also handling the event for the answer view
            // e. g. clicking on the blank area of the buttons layout to reveal the answer
            // or flip the card.
            buttonsView.setOnClickListener(new View.OnClickListener() {
                public void onClick(View v) {
                    onQuestionViewClickListener.onClick(v);
                }
            });

            // Also the buttons should match the color of the view above.
            // It could be the question if it is the double sided card with only question shown
            // or answer view's color.
            if (!setting.isDefaultColor()) {
                if (setting.getCardStyle() == Setting.CardStyle.DOUBLE_SIDED && !showAnswer) {
                    buttonsView.setBackgroundColor(setting.getQuestionBackgroundColor());
                } else {
                    buttonsView.setBackgroundColor(setting.getAnswerBackgroundColor());
                }
            }
        }

        CardFragment.Builder questionFragmentBuilder = new CardFragment.Builder(getCurrentCard().getQuestion())
                .setTextAlignment(questionAlign).setTypefaceFromFile(questionTypefaceValue)
                .setTextOnClickListener(onQuestionTextClickListener)
                .setCardOnClickListener(onQuestionViewClickListener).setTextFontSize(setting.getQuestionFontSize())
                .setTypefaceFromFile(setting.getQuestionFont())
                .setDisplayInHtml(setting.getDisplayInHTMLEnum().contains(Setting.CardField.QUESTION))
                .setHtmlLinebreakConversion(setting.getHtmlLineBreakConversion())
                .setImageSearchPaths(imageSearchPaths);

        CardFragment.Builder answerFragmentBuilder = new CardFragment.Builder(getCurrentCard().getAnswer())
                .setTextAlignment(answerAlign).setTypefaceFromFile(answerTypefaceValue)
                .setTextOnClickListener(onAnswerTextClickListener).setCardOnClickListener(onAnswerViewClickListener)
                .setTextFontSize(setting.getAnswerFontSize()).setTypefaceFromFile(setting.getAnswerFont())
                .setDisplayInHtml(setting.getDisplayInHTMLEnum().contains(Setting.CardField.ANSWER))
                .setHtmlLinebreakConversion(setting.getHtmlLineBreakConversion())
                .setImageSearchPaths(imageSearchPaths);

        CardFragment.Builder showAnswerFragmentBuilder = new CardFragment.Builder(
                "?\n" + getString(R.string.memo_show_answer)).setTextAlignment(Setting.Align.CENTER)
                        .setTypefaceFromFile(answerTypefaceValue).setTextOnClickListener(onAnswerTextClickListener)
                        .setCardOnClickListener(onAnswerViewClickListener)
                        .setTextFontSize(setting.getAnswerFontSize()).setTypefaceFromFile(setting.getAnswerFont());

        // For default card colors, we will use the theme's color
        // so we do not set the colors here.
        if (!setting.isDefaultColor()) {
            questionFragmentBuilder.setTextColor(setting.getQuestionTextColor())
                    .setBackgroundColor(setting.getQuestionBackgroundColor());
            answerFragmentBuilder.setTextColor(setting.getAnswerTextColor())
                    .setBackgroundColor(setting.getAnswerBackgroundColor());
            showAnswerFragmentBuilder.setTextColor(setting.getAnswerTextColor())
                    .setBackgroundColor(setting.getAnswerBackgroundColor());
        }

        // Note is currently shared some settings with Answer
        CardFragment.Builder noteFragmentBuilder = new CardFragment.Builder(getCurrentCard().getNote())
                .setTextAlignment(answerAlign).setTypefaceFromFile(answerTypefaceValue)
                .setCardOnClickListener(onAnswerViewClickListener).setTextFontSize(setting.getAnswerFontSize())
                .setTypefaceFromFile(setting.getAnswerFont())
                .setDisplayInHtml(setting.getDisplayInHTMLEnum().contains(Setting.CardField.ANSWER))
                .setHtmlLinebreakConversion(setting.getHtmlLineBreakConversion())
                .setImageSearchPaths(imageSearchPaths);

        // Long click to launch image viewer if the card has an image
        questionFragmentBuilder.setTextOnLongClickListener(
                generateImageOnLongClickListener(getCurrentCard().getQuestion(), imageSearchPaths));
        answerFragmentBuilder.setTextOnLongClickListener(
                generateImageOnLongClickListener(getCurrentCard().getAnswer(), imageSearchPaths));

        FragmentTransaction ft = getSupportFragmentManager().beginTransaction();

        if (setting.getCardStyle() == Setting.CardStyle.SINGLE_SIDED) {
            TwoFieldsCardFragment fragment = new TwoFieldsCardFragment();
            Bundle b = new Bundle();

            // Handle card field setting.
            List<CardFragment.Builder> builders1List = new ArrayList<CardFragment.Builder>(4);
            if (setting.getQuestionFieldEnum().contains(Setting.CardField.QUESTION)) {
                builders1List.add(questionFragmentBuilder);
            }
            if (setting.getQuestionFieldEnum().contains(Setting.CardField.ANSWER)) {
                builders1List.add(answerFragmentBuilder);
            }
            if (setting.getQuestionFieldEnum().contains(Setting.CardField.NOTE)) {
                builders1List.add(noteFragmentBuilder);
            }

            List<CardFragment.Builder> builders2List = new ArrayList<CardFragment.Builder>(4);
            if (!showAnswer) {
                builders2List.add(showAnswerFragmentBuilder);
            }
            if (setting.getAnswerFieldEnum().contains(Setting.CardField.QUESTION)) {
                builders2List.add(questionFragmentBuilder);
            }
            if (setting.getAnswerFieldEnum().contains(Setting.CardField.ANSWER)) {
                builders2List.add(answerFragmentBuilder);
            }
            if (setting.getAnswerFieldEnum().contains(Setting.CardField.NOTE)) {
                builders2List.add(noteFragmentBuilder);
            }

            CardFragment.Builder[] builders1 = new CardFragment.Builder[builders1List.size()];
            builders1List.toArray(builders1);
            CardFragment.Builder[] builders2 = new CardFragment.Builder[builders2List.size()];
            builders2List.toArray(builders2);

            b.putSerializable(TwoFieldsCardFragment.EXTRA_FIELD1_CARD_FRAGMENT_BUILDERS, builders1);
            b.putSerializable(TwoFieldsCardFragment.EXTRA_FIELD2_CARD_FRAGMENT_BUILDERS, builders2);
            if (showAnswer) {
                b.putInt(TwoFieldsCardFragment.EXTRA_FIELD2_INITIAL_POSITION, 0);
            } else {
                b.putInt(TwoFieldsCardFragment.EXTRA_FIELD2_INITIAL_POSITION, 0);
            }
            b.putInt(TwoFieldsCardFragment.EXTRA_QA_RATIO, setting.getQaRatio());
            b.putInt(TwoFieldsCardFragment.EXTRA_SEPARATOR_COLOR, setting.getSeparatorColor());
            fragment.setArguments(b);

            configCardFragmentTransitionAnimation(ft);

            ft.replace(R.id.card_root, fragment);
            ft.commit();
        } else if (setting.getCardStyle() == Setting.CardStyle.DOUBLE_SIDED) {
            FlipableCardFragment fragment = new FlipableCardFragment();
            Bundle b = new Bundle(1);
            CardFragment.Builder[] builders = { questionFragmentBuilder, answerFragmentBuilder,
                    noteFragmentBuilder };
            b.putSerializable(FlipableCardFragment.EXTRA_CARD_FRAGMENT_BUILDERS, builders);
            if (showAnswer) {
                b.putInt(FlipableCardFragment.EXTRA_INITIAL_POSITION, 1);
            } else {
                b.putInt(FlipableCardFragment.EXTRA_INITIAL_POSITION, 0);
            }

            fragment.setArguments(b);

            configCardFragmentTransitionAnimation(ft);

            ft.replace(R.id.card_root, fragment);
            ft.commit();
        } else {
            assert false : "Card logic not implemented for style: " + setting.getCardStyle();
        }

        isAnswerShown = showAnswer;

        // Set up the small title bar
        // It is defualt "GONE" so it won't take any space
        // if there is no text
        smallTitleBar = (TextView) findViewById(R.id.small_title_bar);

        // Only copy to clipboard if answer is show
        // as a feature request:
        // http://code.google.com/p/anymemo/issues/detail?id=239
        if (showAnswer == true) {
            copyToClipboard();
        }

        currentDisplayedCard = getCurrentCard();

        onPostDisplayCard();
    }

    protected boolean isAnswerShown() {
        return isAnswerShown;
    }

    protected AnyMemoDBOpenHelper getDbOpenHelper() {
        return dbOpenHelper;
    }

    protected Setting getSetting() {
        return setting;
    }

    protected Option getOption() {
        return option;
    }

    /**
     * Called the loaders are done. Override it for customized initialization
     */
    protected void onPostInit() {
        View buttonsView = findViewById(R.id.buttons_root);
        if (buttonsView != null && !setting.isDefaultColor()) {
            buttonsView.setBackgroundColor(setting.getAnswerBackgroundColor());
        }

    }

    private class SettingLoaderCallbacks implements LoaderManager.LoaderCallbacks<Setting> {

        @Override
        public Loader<Setting> onCreateLoader(int arg0, Bundle arg1) {
            Loader<Setting> loader = new SettingLoader(QACardActivity.this, dbPath);
            loader.forceLoad();
            return loader;
        }

        @Override
        public void onLoadFinished(Loader<Setting> loader, Setting setting) {
            QACardActivity.this.setting = setting;
            multipleLoaderManager.checkAllLoadersCompleted();
        }

        @Override
        public void onLoaderReset(Loader<Setting> arg0) {
            // Do nothing now
        }
    }

    private class CardTTSUtilLoaderCallbacks implements LoaderManager.LoaderCallbacks<CardTTSUtil> {
        @Override
        public Loader<CardTTSUtil> onCreateLoader(int arg0, Bundle arg1) {
            Loader<CardTTSUtil> loader = new CardTTSUtilLoader(QACardActivity.this, dbPath);
            loader.forceLoad();
            return loader;
        }

        @Override
        public void onLoadFinished(Loader<CardTTSUtil> loader, CardTTSUtil cardTTSUtil) {
            QACardActivity.this.cardTTSUtil = cardTTSUtil;
            multipleLoaderManager.checkAllLoadersCompleted();
        }

        @Override
        public void onLoaderReset(Loader<CardTTSUtil> arg0) {
            // Do nothing now
        }
    }

    @Override
    public void onDestroy() {
        AnyMemoDBOpenHelperManager.releaseHelper(dbOpenHelper);

        if (cardTTSUtil != null) {
            cardTTSUtil.release();
        }

        multipleLoaderManager.destroy();

        /* Update the widget because StudyActivity can be accessed though widget*/
        Intent myIntent = new Intent(this, AnyMemoService.class);
        myIntent.putExtra("request_code", AnyMemoService.CANCEL_NOTIFICATION);
        startService(myIntent);

        AnyMemoWidgetProvider.updateWidget(this);
        super.onDestroy();
    }

    // Set the small title to display additional informaiton
    public void setSmallTitle(CharSequence text) {
        if (text != null && !Strings.isNullOrEmpty(text.toString())) {
            smallTitleBar.setText(text);
            smallTitleBar.setVisibility(View.VISIBLE);
        } else {
            smallTitleBar.setVisibility(View.GONE);
        }

    }

    /* Called when the card is displayed. */
    protected void onPostDisplayCard() {
        // Nothing
    }

    protected boolean speakQuestion() {
        cardTTSUtil.speakCardQuestion(getCurrentCard());
        return true;
    }

    protected boolean speakAnswer() {
        cardTTSUtil.speakCardAnswer(getCurrentCard());
        return true;
    }

    private void loadGestures() {
        gestureLibrary = GestureLibraries.fromRawResource(this, R.raw.gestures);
        if (!gestureLibrary.load()) {
            Log.e(TAG, "Gestures can not be load");
        }

        GestureOverlayView gestureOverlay = (GestureOverlayView) findViewById(R.id.gesture_overlay);
        gestureOverlay.addOnGesturePerformedListener(onGesturePerformedListener);

        // Set if gestures are enabled if set on preference
        gestureOverlay.setEnabled(option.getGestureEnabled());
    }

    // Default implementation is to handle the double sided card correctly.
    // Return true if the event is handled, else return false
    protected boolean onClickQuestionView() {
        if (setting.getCardStyle() == Setting.CardStyle.DOUBLE_SIDED) {
            displayCard(true);
            return true;
        }
        return false;
    }

    protected boolean onClickAnswerView() {
        if (setting.getCardStyle() == Setting.CardStyle.DOUBLE_SIDED) {
            displayCard(false);
            return true;
        }
        return false;
    }

    protected boolean onClickQuestionText() {
        if (!onClickQuestionView()) {
            speakQuestion();
        }
        return true;
    }

    protected boolean onClickAnswerText() {
        if (!onClickAnswerView()) {
            speakAnswer();
        }
        return true;
    }

    protected void onGestureDetected(GestureName gestureName) {
        // Nothing
    }

    // Return true if handled. Default not handle it.
    // This method will only be called if the volume key shortcut option is enabled.
    protected boolean onVolumeUpKeyPressed() {
        return false;
    }

    // Return true if handled. Default not handle it.
    protected boolean onVolumeDownKeyPressed() {
        return false;
    }

    // Do not handle the key down event. We handle it in onKeyUp
    // This method will only be called if the volume key shortcut option is enabled.
    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        if (option.getVolumeKeyShortcut()) {
            if (keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
                return true;
            }

            if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) {
                return true;
            }
        }

        return super.onKeyDown(keyCode, event);
    }

    // handle the key event
    @Override
    public boolean onKeyUp(int keyCode, KeyEvent event) {
        if (option.getVolumeKeyShortcut()) {
            if (keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
                return onVolumeUpKeyPressed();
            }

            if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) {
                return onVolumeDownKeyPressed();
            }
        }
        return super.onKeyUp(keyCode, event);
    }

    // Copy to clipboard
    protected void copyToClipboard() {
        String copiedText = "";
        switch (option.getCopyClipboard()) {
        case QUESTION:
            copiedText = "" + currentCard.getQuestion();
            break;
        case ANSWER:
            copiedText = "" + currentCard.getAnswer();
            break;
        case BOTH:
            copiedText = "" + currentCard.getQuestion() + " " + currentCard.getAnswer();
            break;
        default:
            copiedText = "";
        }
        if (!Strings.isNullOrEmpty(copiedText)) {
            ClipboardManager cm = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
            // Some Samsung device doesn't have ClipboardManager. So check
            // the null here to prevent crash.
            if (cm != null) {
                try {
                    cm.setText(copiedText);
                } catch (NullPointerException npe) {
                    // This is a Samsung clipboard bug. setPrimaryClip() can throw
                    // a NullPointerException if Samsung's /data/clipboard directory is full.
                    // Fortunately, the text is still successfully copied to the clipboard.
                    Ln.e(npe, "Got null pointer exception when copying text to clipboard");
                }
            }
        }
    }

    private Runnable onPostInitRunnable = new Runnable() {
        public void run() {
            onPostInit();
        }
    };

    /**
     * Configure the animation for the card transition in a fragment transaction.
     */
    private void configCardFragmentTransitionAnimation(FragmentTransaction ft) {
        if (option.getEnableAnimation()) {
            // The first card to display.
            if (currentDisplayedCard == null) {
                // Android support library bug prevent the exit animation displayed for nested fragment.
                // So the R.anim.slide_right_out is not actaully used.
                ft.setCustomAnimations(R.anim.slide_left_in, R.anim.slide_left_out);
            } else if (currentDisplayedCard.getOrdinal() > currentCard.getOrdinal()) {
                // Make sure the animation is the in the right direction.
                ft.setCustomAnimations(R.anim.slide_right_in, R.anim.slide_right_out);
            } else if (currentDisplayedCard.getOrdinal() < currentCard.getOrdinal()) {
                ft.setCustomAnimations(R.anim.slide_left_in, R.anim.slide_left_out);
            } else {
                // No animation for not changing the card.
            }
        }
    }

    /**
     * Generate the OnLongClickListener on the card fragment
     * @param cardText the text on the card.
     * @param imageSearchPaths the paths to search image
     * @return the OnLongClickListener.
     */
    private CardFragment.OnLongClickListener generateImageOnLongClickListener(final String cardText,
            final String[] imageSearchPaths) {
        return new CardFragment.OnLongClickListener() {
            @Override
            public boolean onLongClick(View arg0) {
                // Find the files in the card field
                List<String> filesFound = AMStringUtils.findFileInCardText(cardText,
                        new String[] { "jpg", "png", "bmp", "gif", "jpeg" });

                if (filesFound.size() == 0) {
                    Ln.v("No Images found for: " + cardText);
                    return false;
                }

                // Search the image file path for the file name found
                // Only the first image is used.
                List<File> imageFiles = amFileUtil.findFileInPaths(filesFound.get(0), imageSearchPaths);
                if (imageFiles.size() == 0) {
                    Ln.w("Image: " + filesFound.get(0) + "  not found for search paths.");
                    return false;
                }

                // Display the image in an image viewer
                Uri uri = Uri.parse("file://" + imageFiles.get(0).getAbsolutePath());
                Intent intent = new Intent(Intent.ACTION_VIEW);
                intent.setDataAndType(uri, "image/*");
                startActivity(intent);
                return true;
            }
        };
    }

    private CardFragment.OnClickListener onQuestionTextClickListener = new CardFragment.OnClickListener() {

        @Override
        public void onClick(View v) {
            onClickQuestionText();
        }
    };

    private CardFragment.OnClickListener onAnswerTextClickListener = new CardFragment.OnClickListener() {

        @Override
        public void onClick(View v) {
            onClickAnswerText();
        }
    };

    private CardFragment.OnClickListener onQuestionViewClickListener = new CardFragment.OnClickListener() {

        @Override
        public void onClick(View v) {
            onClickQuestionView();
        }
    };
    private CardFragment.OnClickListener onAnswerViewClickListener = new CardFragment.OnClickListener() {

        @Override
        public void onClick(View v) {
            onClickAnswerView();
        }
    };

    private GestureOverlayView.OnGesturePerformedListener onGesturePerformedListener = new GestureOverlayView.OnGesturePerformedListener() {

        @Override
        public void onGesturePerformed(GestureOverlayView overlay, Gesture gesture) {
            List<Prediction> predictions = gestureLibrary.recognize(gesture);
            if (predictions.size() > 0 && predictions.get(0).score > 3.0) {

                GestureName name = GestureName.parse(predictions.get(0).name);
                // Run the callback on the Activity.
                onGestureDetected(name);
            }

        }
    };
}